Spring Boot 2 + Spring Security 5 + JWT 的 Restful 簡易教程

準(zhǔn)備

開始本教程的時候希望對下面知識點進(jìn)行粗略的了解羡鸥。

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

本項目中 JWT 密鑰是使用用戶自己的登入密碼转质,這樣每一個 token 的密鑰都不同,相對比較安全偏瓤。

大體思路:

登入:

  1. POST 用戶名密碼到 \login
  2. 請求到達(dá) JwtAuthenticationFilter 中的 attemptAuthentication() 方法腿堤,獲取 request 中的 POST 參數(shù)排龄,包裝成一個 UsernamePasswordAuthenticationToken 交付給 AuthenticationManagerauthenticate() 方法進(jìn)行鑒權(quán)波势。
  3. AuthenticationManager 會從 CachingUserDetailsService 中查找用戶信息,并且判斷賬號密碼是否正確橄维。
  4. 如果賬號密碼正確跳轉(zhuǎn)到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法尺铣,我們進(jìn)行簽名,生成 token 返回給用戶争舞。
  5. 賬號密碼錯誤則跳轉(zhuǎn)到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法凛忿,我們返回錯誤信息讓用戶重新登入。

請求鑒權(quán):

請求鑒權(quán)的主要思路是我們會從請求中的 Authorization 字段拿取 token竞川,如果不存在此字段的用戶店溢,Spring Security 會默認(rèn)會用 AnonymousAuthenticationToken() 包裝它,即代表匿名用戶委乌。

  1. 任意請求發(fā)起
  2. 到達(dá) JwtAuthorizationFilter 中的 doFilterInternal() 方法床牧,進(jìn)行鑒權(quán)。
  3. 如果鑒權(quán)成功我們把生成的 AuthenticationSecurityContextHolder.getContext().setAuthentication() 放入 Security遭贸,即代表鑒權(quán)完成戈咳。此處如何鑒權(quán)由我們自己代碼編寫,后序會詳細(xì)說明壕吹。

準(zhǔn)備 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 開始時是選配模塊耳贬,所以高版本的用戶需要添加這幾個依賴才能正常使用踏堡。

基礎(chǔ)工作準(zhǔn)備

接下來準(zhǔn)備下幾個基礎(chǔ)工作,就是新建個實體咒劲、模擬個數(shù)據(jù)庫顷蟆,寫個 JWT 工具類這種基礎(chǔ)操作胖秒。

UserEntity.java

關(guān)于 role 為什么使用 GrantedAuthority 說明下:其實是為了簡化代碼,直接用了 Security 現(xiàn)成的 role 類慕的,實際項目中我們肯定要自己進(jìn)行處理阎肝,將其轉(zhuǎn)換為 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

前后端分離為了方便前端我們要統(tǒng)一 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 模擬了一個數(shù)據(jù)庫,密碼我已經(jīng)預(yù)先用 Bcrypt 加密過了嫉父,這也是 Spring Security 官方推薦的加密算法(MD5 加密已經(jīng)在 Spring Security 5 中被移除了沛硅,不安全)。

用戶名 密碼 權(quán)限
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绕辖,主要就是模仿數(shù)據(jù)庫的操作摇肌。

@Service
public class UserService {

    @Autowired
    private Database database;

    public UserEntity getUserByUsername(String username) {
        return database.getDatabase().get(username);
    }
}

JwtUtil.java

自己編寫的一個工具類,主要負(fù)責(zé) JWT 的簽名和鑒權(quán)仪际。

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 來進(jìn)行登入。

請求鑒權(quán)树碱,我們使用自定義的 JwtAuthorizationFilter 來處理肯适。

也許大家覺得兩個單詞長的有點像,??成榜。

UserDetailsServiceImpl.java

我們首先實現(xiàn)官方的 UserDetailsService 接口框舔,這里主要負(fù)責(zé)一個從數(shù)據(jù)庫拿數(shù)據(jù)的操作。

@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());
    }
}

后序我們還需要對其進(jìn)行緩存改造赎婚,不然每次請求都要從數(shù)據(jù)庫拿一次數(shù)據(jù)鑒權(quán)菜枷,對數(shù)據(jù)庫壓力太大了团驱。

JwtAuthenticationFilter.java

這個過濾器主要處理登入操作轿亮,我們繼承了 UsernamePasswordAuthenticationFilter捐寥,這樣能大大簡化我們的工作量。

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /*
    過濾器一定要設(shè)置 AuthenticationManager歧焦,所以此處我們這么編寫移斩,這里的 AuthenticationManager
    我會從 Security 配置的時候傳入
    */
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        /*
        運(yùn)行父類 UsernamePasswordAuthenticationFilter 的構(gòu)造方法,能夠設(shè)置此濾器指定
        方法為 POST [\login]
        */
        super();
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 從請求的 POST 中拿取 username 和 password 兩個字段進(jìn)行登入
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        // 設(shè)置一些客戶 IP 啥信息绢馍,后面想用的話可以用向瓷,雖然沒啥用
        setDetails(request, token);
        // 交給 AuthenticationManager 進(jìn)行鑒權(quán)
        return getAuthenticationManager().authenticate(token);
    }

    /*
    鑒權(quán)成功進(jìn)行的操作,我們這里設(shè)置返回加密后的 token
    */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        handleResponse(request, response, authResult, null);
    }

    /*
    鑒權(quán)失敗進(jìn)行的操作舰涌,我們這里就返回 用戶名或密碼錯誤 的信息
    */
    @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() 此處處理的方法不是很好猖任,我的想法是跳轉(zhuǎn)到控制器中進(jìn)行處理,但是這樣鑒權(quán)成功的 token 帶不過去瓷耙,所以先這么寫了朱躺,有點復(fù)雜刁赖。

JwtAuthorizationFilter.java

這個過濾器處理每個請求鑒權(quán),我們選擇繼承 BasicAuthenticationFilter 长搀,考慮到 Basic 認(rèn)證和 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源请,并且進(jìn)行認(rèn)證
        Authentication token = getAuthentication(request);
        if (token == null) {
            chain.doFilter(request, response);
            return;
        }
        // 認(rèn)證成功
        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

此處我們進(jìn)行 Security 的配置枪芒,并且實現(xiàn)緩存功能。緩存這塊我們使用官方現(xiàn)成的 CachingUserDetailsService 谁尸,唯獨(dú)的缺點就是它沒有 public 方法舅踪,我們不能正常實例化,需要曲線救國良蛮,下面代碼也有詳細(xì)說明抽碌。

// 開啟 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 默認(rèn) csrf 是開啟的决瞳,我們使用了 token 货徙,這個也沒有什么必要了
                .csrf().disable()
                .authorizeRequests()
                // 默認(rèn)所有請求通過,但是我們要在需要權(quán)限的方法加上安全注解瞒斩,這樣比寫死配置靈活很多
                .anyRequest().permitAll()
                .and()
                // 添加自己編寫的兩個過濾器
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
                // 前后端分離是 STATELESS破婆,故 session 使用該策略
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    // 此處配置 AuthenticationManager涮总,并且實現(xiàn)緩存
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 對自己編寫的 UserDetailsServiceImpl 進(jìn)一步包裝胸囱,實現(xiàn)緩存
        CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
        // jwt-cache 我們在 ehcache.xml 配置文件中有聲明
        UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache"));
        cachingUserDetailsService.setUserCache(userCache);
        /*
        security 默認(rèn)鑒權(quán)完成后會把密碼抹除,但是這里我們使用用戶的密碼來作為 JWT 的生成密鑰瀑梗,
        如果被抹除了烹笔,在對 JWT 進(jìn)行簽名的時候就拿不到用戶密碼了,故此處關(guān)閉了自動抹除密碼抛丽。
         */
        auth.eraseCredentials(false);
        auth.userDetailsService(cachingUserDetailsService);
    }

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

    /*
    此處我們實現(xiàn)緩存的時候谤职,我們使用了官方現(xiàn)成的 CachingUserDetailsService ,但是這個類的構(gòu)造方法不是 public 的亿鲜,
    我們不能夠正常實例化允蜈,所以在這里進(jìn)行曲線救國。
     */
    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 開始蒿柳,統(tǒng)一使用了 JCache饶套,就是 JSR107 標(biāo)準(zhǔn),網(wǎng)上很多教程都是基于 Ehcache 2 的垒探,所以大家可能在參照網(wǎng)上的教程會遇到很多坑妓蛮。

JSR107:emm,其實 JSR107 是一種緩存標(biāo)準(zhǔn)圾叼,各個框架只要遵守這個標(biāo)準(zhǔn)蛤克,就是現(xiàn)實大一統(tǒng)捺癞。差不多就是我不需要更改系統(tǒng)代碼,也能隨意更換底層的緩存系統(tǒng)构挤。

在 resources 目錄下創(chuàng)建 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>
        <!-- 緩存實體的數(shù)量 -->
        <ehcache:heap unit="entries">2000</ehcache:heap>
    </ehcache:cache>

</ehcache:config>

application.properties 中開啟緩存支持:

spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml

統(tǒng)一全局異常

我們要把異常的返回形式也統(tǒng)一了,這樣才能方便前端的調(diào)用筋现。

我們平常會使用 @RestControllerAdvice 來統(tǒng)一異常版保,但是它只能管理 Controller 層面拋出的異常。Security 中拋出的異常不會抵達(dá) 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 這個注解!N⑽健森篷!

@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 中加點料钓辆,如果用戶快過期了,返回個特別的狀態(tài)碼肴焊,前端收到此狀態(tài)碼去訪問 GET /re_authentication 攜帶老的 token 重新拿一個新的 token 即可前联。

如何作廢已頒發(fā)未過期的 token?

我個人的想法是把每次生成的 token 放入緩存中娶眷,每次請求都從緩存里拿似嗤,如果沒有則代表此緩存報廢。

項目地址:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA

本文首發(fā)于公眾號:Java版web項目届宠,歡迎關(guān)注獲取更多精彩內(nèi)容

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烁落,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子豌注,更是在濱河造成了極大的恐慌伤塌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幌羞,死亡現(xiàn)場離奇詭異寸谜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門熊痴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來他爸,“玉大人,你說我怎么就攤上這事果善≌矬裕” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵巾陕,是天一觀的道長讨跟。 經(jīng)常有香客問我,道長鄙煤,這世上最難降的妖魔是什么晾匠? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮梯刚,結(jié)果婚禮上凉馆,老公的妹妹穿的比我還像新娘。我一直安慰自己亡资,他們只是感情好澜共,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锥腻,像睡著了一般嗦董。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瘦黑,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天京革,我揣著相機(jī)與錄音,去河邊找鬼供璧。 笑死存崖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睡毒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼冗栗,長吁一口氣:“原來是場噩夢啊……” “哼演顾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起隅居,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钠至,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后胎源,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棉钧,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年涕蚤,在試婚紗的時候發(fā)現(xiàn)自己被綠了宪卿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片的诵。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖佑钾,靈堂內(nèi)的尸體忽然破棺而出西疤,到底是詐尸還是另有隱情,我是刑警寧澤休溶,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布代赁,位于F島的核電站,受9級特大地震影響兽掰,放射性物質(zhì)發(fā)生泄漏芭碍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一孽尽、第九天 我趴在偏房一處隱蔽的房頂上張望豁跑。 院中可真熱鬧,春花似錦泻云、人聲如沸艇拍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卸夕。三九已至,卻和暖如春婆瓜,著一層夾襖步出監(jiān)牢的瞬間快集,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工廉白, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留个初,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓猴蹂,卻偏偏與公主長得像院溺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子磅轻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容