SpringBoot 2整合SpringSecurity權限管理(六)實現基於JWT的訪問

前言
1. 什麼是JWT?

JWT,全稱是Json Web Token, 是一種JSON風格的輕量級的授權和身份認證規範,可實現無狀態、分佈式的Web應用授權。它是基於 RFC 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。

2. JWT的工作流程
  • 用戶進入登錄頁,輸入用戶名、密碼,進行登錄。
  • 服務器驗證登錄鑑權,如果改用戶合法,根據用戶的信息和服務器的規則生成 JWT Token。
  • 服務器將該 token 以 json 形式返回(不一定要json形式,這裏說的是一種常見的做法)。
  • 用戶得到 token,存在 localStorage、cookie 或其它數據存儲形式中。以後用戶請求 /protected 中的 API 時,在請求的 header 中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個7字符長度的 Bearer。
  • 服務器端對此 token 進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
  • 用戶取得結果。

來看一下 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

token 分成了三部分,頭部(header),荷載(Payload) 和 簽名(Signature),每部分用 . 分隔,其中頭部和荷載使用了base64編碼,分別解碼之後得到兩個JSON串:

第一部分-頭部:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg字段爲加密算法,這是告訴我們 HMAC 採用 HS512 算法對 JWT 進行的簽名。

第二部分-荷載:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

荷載的字段及含義:

  • iss: 該JWT的簽發者
  • sub: 該JWT所面向的用戶
  • aud: 接收該JWT的一方
  • exp(expires): 什麼時候過期,這裏是一個Unix時間戳
  • iat(issued at): 在什麼時候簽發的

這段告訴我們這個Token中含有的數據聲明(Claim),這個例子裏面有三個聲明:sub, name 和 iat。在我們這個例子中,分別代表着所面向的用戶、用戶名、創建時間,當然你可以把任意數據聲明在這裏。

第三部分-簽名:

第三部分簽名則不能使用base64解碼出來,該部分用於驗證頭部和荷載數據的完整性。

一、pom中引入JWT的依賴

<!-- jwt -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

同時在配置文件properties中加入:

# 密鑰
jwt.secret=mySecret
jwt.header=Authorization
# token 過期時間 2個小時
jwt.expiration=7200000

二、新增工具類JwtTokenUtil,定義關於JWT的一些方法

/**
 * @program sweet-dream
 * @description: JWT工具類
 * @author: zhangchao
 * @date: 2020/03/21 00:29
 * @since: 1.0.0
 */
@Slf4j
@Component
public class JwtTokenUtil implements Serializable {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.header}")
    private String header;

    @Value("${jwt.expiration}")
    private long expiration;


    /**
     * 獲取token中的信息
     * @param token 生成的token
     * @return 信息
     */
    public String getInfoFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    /**
     * 獲取token的生成時間
     * @param token 生成的token
     * @return token的生成時間
     */
    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    /**
     * 獲取token的過期時間
     * @param token 生成的token
     * @return token的過期時間
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    /**
     * 判斷token是否過期
     * @param token 生成的token
     * @return true:過期,false:失效
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(Date.from(Instant.now()));
    }

    public <T> T getClaimFromToken(String token, Function<Claims,T> claimsResolver){
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    private Boolean ignoreTokenExpiration(String token) {
        // here you specify tokens, for that the expiration is ignored
        return false;
    }

    /**
     * 生成令牌
     * @param userDetails
     * @return
     */
    public String makeToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    /**
     * 真正進行創建token的方法
     * @param claims
     * @param subject
     * @return
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = Date.from(Instant.now());
        final Date expirationDate = calculateExpirationDate(createdDate);

        return Jwts.builder()
                .setClaims(claims) /* 自定義屬性 */
                .setSubject(subject) /* 該JWT所面向的用戶 */
                .setIssuedAt(createdDate) /* 設置發放的時間,類型爲: Date*/
                .setExpiration(expirationDate) /* 設置過期時間 類型爲:Date */
                .signWith(SignatureAlgorithm.HS512, secret) /* jwt簽名算法和密鑰 */
                .compact(); /* 返回一個URL安全JWT字符串 */
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        final Date createdDate =  Date.from(Instant.now());
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUser user = (SecurityUser) userDetails;
        final Date created = getIssuedAtDateFromToken(token);
       /* final Date expiration = getExpirationDateFromToken(token);
        如果token存在,且token創建日期 > 最後修改密碼的日期 則代表token有效*/
        return (!isTokenExpired(token)
                /*&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())*/
        );
    }

    /**
     * 生成過期時間
     * @param createdDate 當前時間
     * @return 返回到期時間
     */
    private Date calculateExpirationDate(Date createdDate) {
        return Date.from(Instant.ofEpochMilli(createdDate.toInstant().toEpochMilli()+expiration));
    }
}

三、修改以前定義的登錄成功處理器LoginSuccessHandler,在登錄成功後生成和返回一個token給客戶端

 /**
 * @author ZHANGCHAO
 * @date 2020/3/13 9:35
 * @since 1.0.0
 */
@Slf4j
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        log.info("登錄成功!");
        /* 默認:會幫我們跳轉到上一次請求的頁面上 */
        //super.onAuthenticationSuccess(request, response, authentication);

        //生成token
        String token = jwtTokenUtil.makeToken((UserDetails) authentication.getPrincipal());
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter writer = response.getWriter();
        writer.write("{\"status\":\"ok\",\"msg\":\"登錄成功\",\"token\":\""+token+"\"}");
        writer.flush();
        writer.close();
    }
}

四、自定義一個JWT的過濾器

/**
 * @program sweet-dream
 * @description: JWT過濾器
 * @author: zhangchao
 * @date: 2020/03/21 00:48
 * @since: 1.0.0
 */
@Slf4j
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    @Value("${jwt.header}")
    private String header;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        final String header = request.getHeader(this.header);

        String authToken = null;
        String username = null;

        if (null != header && header.startsWith("Bearer")){
            authToken = header.replace("Bearer ","");
            log.info("獲取token: {}",authToken);
            username = jwtTokenUtil.getInfoFromToken(authToken);
        }

        if (null != username && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.customUserDetailsService.loadUserByUsername(username);
            if (jwtTokenUtil.validateToken(authToken,userDetails)){
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); /* 增加額外數據 */
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request,response);
    }
}

五、將自定義JWT過濾器加入Security配置文件SecurityConfig的過濾器鏈中

 // 禁用CSRF防護
http.csrf().disable();   http.addFilterBefore(jwtAuthorizationTokenFilter,UsernamePasswordAuthenticationFilter.class);

六、啓動項目測試

image-20200321011931037

輸入用戶名密碼和驗證碼,登錄成功後可以看到已經返回token了,以後每次請求都要在header裏帶上token。比如訪問/test接口:

image-20200321012308526

以上!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章