Spring Boot 2 + Spring Security 5 + JWT 的單頁應用 Restful 解決方案

此前我已經寫過一篇相似的教程,但那時候使用了投機的方法,沒有尊重 Spring Security 的官方設計,本身並不感到滿意。這段時間比較空,故從新研究了一遍。php

原文地址:www.inlighting.org/2019/spring…html

項目 GitHub:github.com/Smith-Cruis…前端

老版本:github.com/Smith-Cruis…java

特性

  • 使用 JWT 進行鑑權,支持 token 過時
  • 使用 Ehcache 進行緩存,減小每次鑑權對數據庫的壓力
  • 儘量貼合 Spring Security 的設計
  • 實現註解權限控制

準備

開始本教程的時候但願對下面知識點進行粗略的瞭解。git

  • 知道 JWT 的基本概念
  • 瞭解過 Spring Security

我以前寫過兩篇關於安全框架的問題,你們能夠大體看一看,打下基礎。github

Shiro+JWT+Spring Boot Restful簡易教程web

Spring Boot+Spring Security+Thymeleaf 簡單教程算法

本項目中 JWT 密鑰是使用用戶本身的登入密碼,這樣每個 token 的密鑰都不一樣,相對比較安全。spring

大致思路:

登入:數據庫

  1. POST 用戶名密碼到 \login
  2. 請求到達 JwtAuthenticationFilter 中的 attemptAuthentication() 方法,獲取 request 中的 POST 參數,包裝成一個 UsernamePasswordAuthenticationToken 交付給 AuthenticationManagerauthenticate() 方法進行鑑權。
  3. AuthenticationManager 會從 CachingUserDetailsService 中查找用戶信息,而且判斷帳號密碼是否正確。
  4. 若是帳號密碼正確跳轉到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法,咱們進行簽名,生成 token 返回給用戶。
  5. 帳號密碼錯誤則跳轉到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法,咱們返回錯誤信息讓用戶從新登入。

請求鑑權:

請求鑑權的主要思路是咱們會從請求中的 Authorization 字段拿取 token,若是不存在此字段的用戶,Spring Security 會默認會用 AnonymousAuthenticationToken() 包裝它,即表明匿名用戶。

  1. 任意請求發起
  2. 到達 JwtAuthorizationFilter 中的 doFilterInternal() 方法,進行鑑權。
  3. 若是鑑權成功咱們把生成的 AuthenticationSecurityContextHolder.getContext().setAuthentication() 放入 Security,即表明鑑權完成。此處如何鑑權由咱們本身代碼編寫,後序會詳細說明。

準備 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.inlighting</groupId>
    <artifactId>spring-boot-security-jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-security-jwt</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JWT 支持 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
        </dependency>

        <!-- cache 支持 -->
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- ehcache 讀取 xml 配置文件使用 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 讀取 xml 配置文件使用 -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 讀取 xml 配置文件使用 -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- ehcache 讀取 xml 配置文件使用 -->
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

複製代碼

pom.xml 配置文件這塊沒有什麼好說的,主要說明下面的幾個依賴:

<!-- ehcache 讀取 xml 配置文件使用 -->
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 讀取 xml 配置文件使用 -->
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 讀取 xml 配置文件使用 -->
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-core</artifactId>
  <version>2.3.0</version>
</dependency>

<!-- ehcache 讀取 xml 配置文件使用 -->
<dependency>
  <groupId>javax.activation</groupId>
  <artifactId>activation</artifactId>
  <version>1.1.1</version>
</dependency>
複製代碼

由於 ehcache 讀取 xml 配置文件時使用了這幾個依賴,而這幾個依賴從 JDK 9 開始時是選配模塊,因此高版本的用戶須要添加這幾個依賴才能正常使用。

基礎工做準備

接下來準備下幾個基礎工做,就是新建個實體、模擬個數據庫,寫個 JWT 工具類這種基礎操做。

UserEntity.java

關於 role 爲何使用 GrantedAuthority 說明下:實際上是爲了簡化代碼,直接用了 Security 現成的 role 類,實際項目中咱們確定要本身進行處理,將其轉換爲 Security 的 role 類。

public class UserEntity {

    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    private String username;

    private String password;

    private Collection<? extends GrantedAuthority> role;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Collection<? extends GrantedAuthority> getRole() {
        return role;
    }

    public void setRole(Collection<? extends GrantedAuthority> role) {
        this.role = role;
    }
}
複製代碼

ResponseEntity.java

先後端分離爲了方便前端咱們要統一 json 的返回格式,因此自定義一個 ResponseEntity.java。

public class ResponseEntity {

    public ResponseEntity() {
    }

    public ResponseEntity(int status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    private int status;

    private String msg;

    private Object data;

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}
複製代碼

Database.java

這裏咱們使用一個 HashMap 模擬了一個數據庫,密碼我已經預先用 Bcrypt 加密過了,這也是 Spring Security 官方推薦的加密算法(MD5 加密已經在 Spring Security 5 中被移除了,不安全)。

用戶名 密碼 權限
jack jack123 存 Bcrypt 加密後 ROLE_USER
danny danny123 存 Bcrypt 加密後 ROLE_EDITOR
smith smith123 存 Bcrypt 加密後 ROLE_ADMIN
@Component
public class Database {
    private Map<String, UserEntity> data = null;
    
    public Map<String, UserEntity> getDatabase() {
        if (data == null) {
            data = new HashMap<>();

            UserEntity jack = new UserEntity(
                    "jack",
                    "$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",
                    getGrants("ROLE_USER"));
            UserEntity danny = new UserEntity(
                    "danny",
                    "$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12",
                    getGrants("ROLE_EDITOR"));
            UserEntity smith = new UserEntity(
                    "smith",
                    "$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi",
                    getGrants("ROLE_ADMIN"));
            data.put("jack", jack);
            data.put("danny", danny);
            data.put("smith", smith);
        }
        return data;
    }
    
    private Collection<GrantedAuthority> getGrants(String role) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
    }
}
複製代碼

UserService.java

這裏再模擬一個 service,主要就是模仿數據庫的操做。

@Service
public class UserService {

    @Autowired
    private Database database;

    public UserEntity getUserByUsername(String username) {
        return database.getDatabase().get(username);
    }
}
複製代碼

JwtUtil.java

本身編寫的一個工具類,主要負責 JWT 的簽名和鑑權。

public class JwtUtil {

    // 過時時間5分鐘
    private final static long EXPIRE_TIME = 5 * 60 * 1000;

    /** * 生成簽名,5min後過時 * @param username 用戶名 * @param secret 用戶的密碼 * @return 加密的token */
    public static String sign(String username, String secret) {
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        } catch (Exception e) {
            return null;
        }
    }

    /** * 校驗token是否正確 * @param token 密鑰 * @param secret 用戶的密碼 * @return 是否正確 */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /** * 得到token中的信息無需secret解密也能得到 * @return token中包含的用戶名 */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}
複製代碼

Spring Security 改造

登入這塊,咱們使用自定義的 JwtAuthenticationFilter 來進行登入。

請求鑑權,咱們使用自定義的 JwtAuthorizationFilter 來處理。

也許你們以爲兩個單詞長的有點像,😜。

UserDetailsServiceImpl.java

咱們首先實現官方的 UserDetailsService 接口,這裏主要負責一個從數據庫拿數據的操做。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userService.getUserByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("This username didn't exist.");
        }
        return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());
    }
}
複製代碼

後序咱們還須要對其進行緩存改造,否則每次請求都要從數據庫拿一次數據鑑權,對數據庫壓力太大了。

JwtAuthenticationFilter.java

這個過濾器主要處理登入操做,咱們繼承了 UsernamePasswordAuthenticationFilter,這樣能大大簡化咱們的工做量。

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /* 過濾器必定要設置 AuthenticationManager,因此此處咱們這麼編寫,這裏的 AuthenticationManager 我會從 Security 配置的時候傳入 */
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        /* 運行父類 UsernamePasswordAuthenticationFilter 的構造方法,可以設置此濾器指定 方法爲 POST [\login] */
        super();
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 從請求的 POST 中拿取 username 和 password 兩個字段進行登入
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        // 設置一些客戶 IP 啥信息,後面想用的話能夠用,雖然沒啥用
        setDetails(request, token);
        // 交給 AuthenticationManager 進行鑑權
        return getAuthenticationManager().authenticate(token);
    }

    /* 鑑權成功進行的操做,咱們這裏設置返回加密後的 token */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        handleResponse(request, response, authResult, null);
    }

    /* 鑑權失敗進行的操做,咱們這裏就返回 用戶名或密碼錯誤 的信息 */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        handleResponse(request, response, null, failed);
    }

    private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        ResponseEntity responseEntity = new ResponseEntity();
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        if (authResult != null) {
            // 處理登入成功請求
            User user = (User) authResult.getPrincipal();
            String token = JwtUtil.sign(user.getUsername(), user.getPassword());
            responseEntity.setStatus(HttpStatus.OK.value());
            responseEntity.setMsg("登入成功");
            responseEntity.setData("Bearer " + token);
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().write(mapper.writeValueAsString(responseEntity));
        } else {
            // 處理登入失敗請求
            responseEntity.setStatus(HttpStatus.BAD_REQUEST.value());
            responseEntity.setMsg("用戶名或密碼錯誤");
            responseEntity.setData(null);
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().write(mapper.writeValueAsString(responseEntity));
        }
    }
}
複製代碼

private void handleResponse() 此到處理的方法不是很好,個人想法是跳轉到控制器中進行處理,可是這樣鑑權成功的 token 帶不過去,因此先這麼寫了,有點複雜。

JwtAuthorizationFilter.java

這個過濾器處理每一個請求鑑權,咱們選擇繼承 BasicAuthenticationFilter ,考慮到 Basic 認證和 JWT 比較像,就選擇了它。

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserDetailsService userDetailsService;

    // 會從 Spring Security 配置文件那裏傳過來
    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 判斷是否有 token,而且進行認證
        Authentication token = getAuthentication(request);
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }
        // 認證成功
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header == null || ! header.startsWith("Bearer ")) {
            return null;
        }

        String token = header.split(" ")[1];
        String username = JwtUtil.getUsername(token);
        UserDetails userDetails = null;
        try {
            userDetails = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException e) {
            return null;
        }
        if (! JwtUtil.verify(token, username, userDetails.getPassword())) {
            return null;
        }
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}
複製代碼

SecurityConfiguration.java

此處咱們進行 Security 的配置,而且實現緩存功能。緩存這塊咱們使用官方現成的 CachingUserDetailsService ,惟獨的缺點就是它沒有 public 方法,咱們不能正常實例化,須要曲線救國,下面代碼也有詳細說明。

// 開啓 Security
@EnableWebSecurity
// 開啓註解配置支持
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    // Spring Boot 的 CacheManager,這裏咱們使用 JCache
    @Autowired
    private CacheManager cacheManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 開啓跨域
        http.cors()
                .and()
                // security 默認 csrf 是開啓的,咱們使用了 token ,這個也沒有什麼必要了
                .csrf().disable()
                .authorizeRequests()
                // 默認全部請求經過,可是咱們要在須要權限的方法加上安全註解,這樣比寫死配置靈活不少
                .anyRequest().permitAll()
                .and()
                // 添加本身編寫的兩個過濾器
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
                // 先後端分離是 STATELESS,故 session 使用該策略
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    // 此處配置 AuthenticationManager,而且實現緩存
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 對本身編寫的 UserDetailsServiceImpl 進一步包裝,實現緩存
        CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
        // jwt-cache 咱們在 ehcache.xml 配置文件中有聲明
        UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache"));
        cachingUserDetailsService.setUserCache(userCache);
        /* security 默認鑑權完成後會把密碼抹除,可是這裏咱們使用用戶的密碼來做爲 JWT 的生成密鑰, 若是被抹除了,在對 JWT 進行簽名的時候就拿不到用戶密碼了,故此處關閉了自動抹除密碼。 */
        auth.eraseCredentials(false);
        auth.userDetailsService(cachingUserDetailsService);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /* 此處咱們實現緩存的時候,咱們使用了官方現成的 CachingUserDetailsService ,可是這個類的構造方法不是 public 的, 咱們不可以正常實例化,因此在這裏進行曲線救國。 */
    private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {

        Constructor<CachingUserDetailsService> ctor = null;
        try {
            ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
        ctor.setAccessible(true);
        return BeanUtils.instantiateClass(ctor, delegate);
    }
}
複製代碼

Ehcache 配置

Ehcache 3 開始,統一使用了 JCache,就是 JSR107 標準,網上不少教程都是基於 Ehcache 2 的,因此你們可能在參照網上的教程會遇到不少坑。

JSR107:emm,其實 JSR107 是一種緩存標準,各個框架只要遵照這個標準,就是現實大一統。差很少就是我不須要更改系統代碼,也能隨意更換底層的緩存系統。

在 resources 目錄下建立 ehcache.xml 文件:

<ehcache:config xmlns:ehcache="http://www.ehcache.org/v3" xmlns:jcache="http://www.ehcache.org/v3/jsr107">

    <ehcache:cache alias="jwt-cache">
        <!-- 咱們使用用戶名做爲緩存的 key,故使用 String -->
        <ehcache:key-type>java.lang.String</ehcache:key-type>
        <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>
        <ehcache:expiry>
            <ehcache:ttl unit="days">1</ehcache:ttl>
        </ehcache:expiry>
        <!-- 緩存實體的數量 -->
        <ehcache:heap unit="entries">2000</ehcache:heap>
    </ehcache:cache>

</ehcache:config>
複製代碼

application.properties 中開啓緩存支持:

spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml
複製代碼

統一全局異常

咱們要把異常的返回形式也統一了,這樣才能方便前端的調用。

咱們日常會使用 @RestControllerAdvice 來統一異常,可是它只能管理 Controller 層面拋出的異常。Security 中拋出的異常不會抵達 Controller,沒法被 @RestControllerAdvice 捕獲,故咱們還要改造 ErrorController

@RestController
public class CustomErrorController implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("/error")
    public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {
        return new ResponseEntity(response.getStatus(), (String) request.getAttribute("javax.servlet.error.message"), null);
    }
}
複製代碼

測試

寫個控制器試試,你們也能夠參考我控制器裏面獲取用戶信息的方式,推薦使用 @AuthenticationPrincipal 這個註解!!!

@RestController
public class MainController {

    // 任何人均可以訪問,在方法中判斷用戶是否合法
    @GetMapping("everyone")
    public ResponseEntity everyone() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (! (authentication instanceof AnonymousAuthenticationToken)) {
            // 登入用戶
            return new ResponseEntity(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal());
        } else {
            return new ResponseEntity(HttpStatus.OK.value(), "You are anonymous", null);
        }
    }

    @GetMapping("user")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
        return new ResponseEntity(HttpStatus.OK.value(), "You are user", token);
    }

    @GetMapping("admin")
    @IsAdmin
    public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
        return new ResponseEntity(HttpStatus.OK.value(), "You are admin", token);
    }
}
複製代碼

我這裏還使用了 @IsAdmin 註解,@IsAdmin 註解以下:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}
複製代碼

這樣能省去每次編寫一長串的 @PreAuthorize() ,並且更加直觀。

FAQ

如何解決JWT過時問題?

咱們能夠在 JwtAuthorizationFilter 中加點料,若是用戶快過時了,返回個特別的狀態碼,前端收到此狀態碼去訪問 GET /re_authentication 攜帶老的 token 從新拿一個新的 token 便可。

如何做廢已頒發未過時的 token?

我我的的想法是把每次生成的 token 放入緩存中,每次請求都從緩存裏拿,若是沒有則表明此緩存報廢。

相關文章
相關標籤/搜索