Spring Security Jwt Token 自動(dòng)刷新

功能需求

????????最近項(xiàng)目中有這么一個(gè)功能,用戶登錄系統(tǒng)后,需要給 用戶 頒發(fā)一個(gè) token ,后續(xù)訪問(wèn)系統(tǒng)的請(qǐng)求都需要帶上這個(gè) token ,如果請(qǐng)求沒(méi)有帶上這個(gè) token 或者 token 過(guò)期了,那么禁止訪問(wèn)系統(tǒng)。如果用戶一直訪問(wèn)系統(tǒng),那么還需要自動(dòng)延長(zhǎng) token 的過(guò)期時(shí)間。

注意

不推薦使用,存在 refreshToken 的泄漏風(fēng)險(xiǎn),應(yīng)該在 token 過(guò)期時(shí),再次發(fā)送請(qǐng)求使用 refreshToken 刷新 token。

功能分析

1、token 的生成

使用現(xiàn)在比較流行的 jwt 來(lái)生成。

2、token 的自動(dòng)延長(zhǎng)

要實(shí)現(xiàn) token 的自動(dòng)延長(zhǎng),系統(tǒng)給用戶 頒發(fā) 一個(gè) token 無(wú)法實(shí)現(xiàn),那么通過(guò)變通一個(gè),給用戶生成 2個(gè) token ,一個(gè)用于 api 訪問(wèn)的 token ,一個(gè) 用于在 token 過(guò)期的時(shí)候 用來(lái) 刷新 的 refreshToken。并且 refreshToken 的 生命周期要比 token 的生命周期長(zhǎng)。

3、系統(tǒng)資源的保護(hù)

可以使用Spring Security 來(lái)保護(hù)系統(tǒng)的各種資源。

4、用戶如何傳遞 token

系統(tǒng)中 tokenrefreshToken 的傳遞一律放在請(qǐng)求頭。

實(shí)現(xiàn)思路

1、生成 token 和 refreshToken

用戶登錄系統(tǒng)的時(shí)候,后臺(tái)給用戶生成 tokenrefreshToken 并放在響應(yīng)頭中返回

2、系統(tǒng) 判斷 token 是否合法

  1. token 未失效的時(shí)的處理

  2. token 失效 ,如何使用refreshToken來(lái)生成新的 token

token合法性判斷

核心代碼如下

1、過(guò)濾器代碼,token判斷和再次生成

package com.huan.study.security.token;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.huan.study.security.configuration.TokenProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huan 2020-06-07 - 14:34
 */
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class TokenAuthenticateFilter extends OncePerRequestFilter {

    private final TokenProperties tokenProperties;
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 獲取 認(rèn)證頭
        String authorizationHeader = request.getHeader(tokenProperties.getAuthorizationHeaderName());
        if (!checkIsTokenAuthorizationHeader(authorizationHeader)) {
            log.debug("獲取到認(rèn)證頭Authorization的值:[{}]但不是我們系統(tǒng)中登錄后簽發(fā)的。", authorizationHeader);
            filterChain.doFilter(request, response);
            return;
        }
        // 獲取到真實(shí)的token
        String realToken = getRealAuthorizationToken(authorizationHeader);
        // 解析 jwt token
        Jws<Claims> jws = JwtUtils.parserAuthenticateToken(realToken, tokenProperties.getSecretKey());
        // token 不合法
        if (null == jws) {
            writeJson(response, "認(rèn)證token不合法");
            return;
        }
        // token 是否過(guò)期
        if (JwtUtils.isJwtExpired(jws)) {
            // 處理過(guò)期
            handleTokenExpired(response, request, filterChain);
            return;
        }

        // 構(gòu)建認(rèn)證對(duì)象
        JwtUtils.buildAuthentication(jws, tokenProperties.getUserId());

        filterChain.doFilter(request, response);
    }

    /**
     * 處理token過(guò)期情況
     *
     * @param response
     * @param request
     * @param filterChain
     * @return
     * @throws IOException
     */
    private void handleTokenExpired(HttpServletResponse response, HttpServletRequest request, FilterChain filterChain) throws IOException, ServletException {
        // 獲取刷新 token
        String refreshTokenHeader = request.getHeader(tokenProperties.getRefreshHeaderName());
        // 檢測(cè) refresh-token 是否是我們系統(tǒng)中簽發(fā)的
        if (!checkIsTokenAuthorizationHeader(refreshTokenHeader)) {
            log.debug("獲取到刷新認(rèn)證頭:[{}]的值:[{}]但不是我們系統(tǒng)中登錄后簽發(fā)的。", tokenProperties.getRefreshHeaderName(), refreshTokenHeader);
            writeJson(response, "token過(guò)期了,refresh token 不是我們系統(tǒng)簽發(fā)的");
            return;
        }
        // 解析 refresh-token
        Jws<Claims> refreshToken = JwtUtils.parserAuthenticateToken(getRealAuthorizationToken(refreshTokenHeader),
                tokenProperties.getSecretKey());
        // 判斷 refresh-token 是否不合法
        if (null == refreshToken) {
            writeJson(response, "refresh token不合法");
            return;
        }
        // 判斷 refresh-token 是否過(guò)期
        if (JwtUtils.isJwtExpired(refreshToken)) {
            writeJson(response, "refresh token 過(guò)期了");
            return;
        }
        // 重新簽發(fā) token

        String newToken = JwtUtils.generatorJwtToken(
                refreshToken.getBody().get(tokenProperties.getUserId()),
                tokenProperties.getUserId(),
                tokenProperties.getTokenExpireSecond(),
                tokenProperties.getSecretKey()
        );
        response.addHeader(tokenProperties.getAuthorizationHeaderName(), newToken);

        // 構(gòu)建認(rèn)證對(duì)象
        JwtUtils.buildAuthentication(JwtUtils.parserAuthenticateToken(newToken, tokenProperties.getSecretKey()), tokenProperties.getUserId());

        filterChain.doFilter(request, response);
    }

    /**
     * 寫(xiě) json 數(shù)據(jù)給前端
     *
     * @param response
     * @throws IOException
     */
    private void writeJson(HttpServletResponse response, String msg) throws IOException {
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        Map<String, String> params = new HashMap<>(4);
        params.put("msg", msg);
        response.getWriter().print(OBJECT_MAPPER.writeValueAsString(params));
    }

    /**
     * 獲取到真實(shí)的 token 串
     *
     * @param authorizationToken
     * @return
     */
    private String getRealAuthorizationToken(String authorizationToken) {
        return StringUtils.substring(authorizationToken, tokenProperties.getTokenHeaderPrefix().length()).trim();
    }

    /**
     * 判斷是否是系統(tǒng)中登錄后簽發(fā)的token
     *
     * @param authorizationHeader
     * @return
     */
    private boolean checkIsTokenAuthorizationHeader(String authorizationHeader) {
        if (StringUtils.isBlank(authorizationHeader)) {
            return false;
        }
        if (!StringUtils.startsWith(authorizationHeader, tokenProperties.getTokenHeaderPrefix())) {
            return false;
        }
        return true;
    }
}

2、jwt 工具類代碼

package com.huan.study.security.token;

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultJws;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;

/**
 * jwt 工具類
 *
 * @author huan
 * @date 2020-05-20 - 17:09
 */
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JwtUtils {

    /**
     * 解析 jwt token
     *
     * @param token     需要解析的json
     * @param secretKey 密鑰
     * @return
     */
    public static Jws<Claims> parserAuthenticateToken(String token, String secretKey) {
        try {
            final Jws<Claims> claimsJws = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            return claimsJws;
        } catch (ExpiredJwtException e) {
            return new DefaultJws<>(null, e.getClaims(), "");
        } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException | IncorrectClaimException e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 判斷 jwt 是否過(guò)期
     *
     * @param jws
     * @return true:過(guò)期 false:沒(méi)過(guò)期
     */
    public static boolean isJwtExpired(Jws<Claims> jws) {
        return jws.getBody().getExpiration().before(new Date());
    }

    /**
     * 構(gòu)建認(rèn)證過(guò)的認(rèn)證對(duì)象
     */
    public static Authentication buildAuthentication(Jws<Claims> jws, String userIdFieldName) {
        Object userId = jws.getBody().get(userIdFieldName);
        TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken(userId, null, new ArrayList<>(0));
        SecurityContextHolder.getContext().setAuthentication(testingAuthenticationToken);
        return SecurityContextHolder.getContext().getAuthentication();
    }

    /**
     * 生成 jwt token
     */
    public static String generatorJwtToken(Object loginUserId, String userIdFieldName, Long expireSecond, String secretKey) {
        Date expireTime = Date.from(LocalDateTime.now().plusSeconds(expireSecond).atZone(ZoneId.systemDefault()).toInstant());
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setIssuedAt(new Date())
                .setExpiration(expireTime)
                .claim(userIdFieldName, loginUserId)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
}

完整代碼

代碼 https://gitee.com/huan1993/Spring-Security/tree/master/spring-security-jwt

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容