SpringBoot:基于JWT的token校驗(yàn)、單點(diǎn)登錄、刷新token


前言


用戶鑒權(quán)一直是我先前的一個(gè)問(wèn)題,以前我用戶接口鑒權(quán)是通過(guò)傳入?yún)?shù)進(jìn)行鑒權(quán),只要是驗(yàn)證用戶的地方就寫token驗(yàn)證,雖然后面也把token驗(yàn)證方法提取到基類中,但是整體來(lái)說(shuō)仍然不是太雅觀,當(dāng)時(shí)的接口如下所示.

    @RequestMapping(value = "like",method = RequestMethod.POST)
    public ResultMap userLikeOrDisLikeAction(@RequestParam(value = "shopId") String shopId,
                                             @RequestParam(value = "userId") String userId,
                                             @RequestParam(value = "islike") int islike,
                                             @RequestParam(value = "token") String token,
                                             @RequestParam(value = "timestamp") String timestamp
    )
    {

        ResultMap map = new ResultMap();

        if (!verifyTokenString(token,timestamp)){

            map.code = Constants.ERROR_CODE_TOKEN_NOT_EQUAL;
            map.msg = "token錯(cuò)誤";
            return map;
        }

        ....

    }

反正一句話來(lái)說(shuō),自己太菜了...

其實(shí)很久之前,就有了相應(yīng)的解決方案,那就是利用AOP在攔截器中統(tǒng)一處理token校驗(yàn)的問(wèn)題,那我們一起看看SpringBoot中如何使用JWT來(lái)做Token校驗(yàn)和單點(diǎn)登錄的.


JWT集成


項(xiàng)目是基于Maven來(lái)架構(gòu)的,所以我們先導(dǎo)入JWT的依賴.整體如下所示.

        <!-- JWT的用戶token相關(guān) -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <!-- JWT的用戶token相關(guān) -->

對(duì)于需要?jiǎng)?chuàng)建的類來(lái)說(shuō),主要有以下幾個(gè)類.

下面我們簡(jiǎn)單看一下各個(gè)文件的作用.

InterceptorConfig : Spring boot2.0 官方推薦實(shí)現(xiàn) WebMvcConfigurer 接口配置攔截器.
JwtConfig : token的相關(guān)方法工具類.
TokenInterceptor : 攔截器
PassToken 、UserLoginToken : 自定義注解,用于標(biāo)注接口或者類是否需要進(jìn)行token驗(yàn)證.


具體代碼


首先,我們對(duì)上面的類或者注解進(jìn)行一個(gè)詳細(xì)的說(shuō)明.

InterceptorConfig

該類主要是用來(lái)配置攔截器的,具體代碼如下所示.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Resource
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**");
    }
}

JwtConfig

該類主要是用來(lái)定義token的相關(guān)方法.例如,創(chuàng)建token,創(chuàng)建刷新token等等,驗(yàn)證token是否過(guò)期,獲取token中的用戶信息等等.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtConfig {
    private static final Log log = LogFactory.getLog(JwtConfig.class);

    private String secret = "秘鑰,請(qǐng)自己定義";

    // 外部http請(qǐng)求中 header中 token的 鍵值
    private String header = "token";

    private static Map<String, String> tokenMap = new HashMap<>();

    /**
     * 生成token
     *
     * @param subject
     * @return
     */
    public String createToken(String subject) {
        Date nowDate = new Date();

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(nowDate);
        calendar.add(Calendar.DAY_OF_MONTH, 10);
        Date expireDate = calendar.getTime();

        String userToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        // 把token添加到緩存中
        tokenMap.put(subject, userToken);
        return userToken;
    }

    public String createRefreshToken(String subject) {
        Date nowDate = new Date();

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 獲取token中注冊(cè)信息
     *
     * @param token
     * @return
     */
    public Claims getTokenClaim(String token) {
        try {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 驗(yàn)證token是否過(guò)期失效
     *
     * @param expirationTime
     * @return
     */
    public boolean isTokenExpired(Date expirationTime) {
        return expirationTime.before(new Date());
    }

    /**
     * 獲取token失效時(shí)間
     *
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token) {
        return getTokenClaim(token).getExpiration();
    }

    /**
     * 獲取用戶名從token中
     */
    public String getUsernameFromToken(String token) {
        return getTokenClaim(token).getSubject();
    }

    /**
     * 獲取jwt發(fā)布時(shí)間
     */
    public Date getIssuedAtDateFromToken(String token) {
        return getTokenClaim(token).getIssuedAt();
    }

    // --------------------- getter & setter ---------------------

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }

    public Map<String, String> getTokenMap() {
        return tokenMap;
    }
}

PassToken

定義一個(gè)哪些類或者接口跳過(guò)驗(yàn)證的注解,不添加也也判定是跳過(guò)驗(yàn)證.具體實(shí)現(xiàn)代碼如下所示.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

UserLoginToken

定義一個(gè)哪些類或者接口需要驗(yàn)證的注解,具體實(shí)現(xiàn)代碼如下所示.


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

TokenInterceptor

攔截器,繼承于 HandlerInterceptorAdapter 這個(gè)抽象類, 實(shí)現(xiàn)接口攔截驗(yàn)證功能,具體代碼如下所示.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {

    @Resource
    private JwtConfig jwtConfig;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws SignatureException, IOException {

        String uri = request.getRequestURI();
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        /** 檢查是否有passtoken注釋,有則跳過(guò)認(rèn)證 */
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }

        /** 檢查有沒(méi)有需要用戶權(quán)限的注解 */
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            /** Token 驗(yàn)證 */
            String token = request.getHeader(jwtConfig.getHeader());
            if (StringUtils.isEmpty(token)) {
                token = request.getParameter(jwtConfig.getHeader());
            }
            if (StringUtils.isEmpty(token)) {
                response.sendError(401, "token信息不能為空");
                return false;
            }
            String userName = jwtConfig.getUsernameFromToken(token);
            String compareToken = jwtConfig.getTokenMap().get(userName);
            if (compareToken != null && !compareToken.equals(token)) {
                response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
                return false;
            }

            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                Claims claims = null;
                try {
                    claims = jwtConfig.getTokenClaim(token);
                    if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
                        response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
                        return false;
                    }
                } catch (Exception e) {
                    response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
                    return false;
                }
                /** 設(shè)置 identityId 用戶身份ID */
                request.setAttribute("identityId", claims.getSubject());
                return true;
            }
            if (compareToken == null) {
                // 由于服務(wù)器war重新上傳導(dǎo)致臨時(shí)數(shù)據(jù)丟失,需要重新存儲(chǔ)
                jwtConfig.getTokenMap().put(userName, token);
            }
        }

        return true;
    }
}


Token驗(yàn)證


Token驗(yàn)證的過(guò)程主要是在攔截器中,用戶在登錄過(guò)程中,我們需要把生成好的token 、refreshToken(刷新token)、expirationDate(過(guò)期時(shí)間)發(fā)送給用戶.然后再需要的接口的header中傳入token信息用于驗(yàn)證.

驗(yàn)證過(guò)程主要是在 preHandle 方法中實(shí)現(xiàn)的.

首先我們驗(yàn)證是否含有 @PassToken 這個(gè)注解,如果有,那么直接跳過(guò)驗(yàn)證.

      if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }

然后只有含有 @UserLoginToken 的接口中才去驗(yàn)證token.驗(yàn)證Token主要是驗(yàn)證它的過(guò)期時(shí)間.代碼如下所示.

            if (userLoginToken.required()) {
                Claims claims = null;
                try {
                    claims = jwtConfig.getTokenClaim(token);
                    if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
                        response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
                        return false;
                    }
                } catch (Exception e) {
                    response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
                    return false;
                }
                /** 設(shè)置 identityId 用戶身份ID */
                request.setAttribute("identityId", claims.getSubject());
                return true;
            }


單點(diǎn)登錄


如何簡(jiǎn)單實(shí)現(xiàn)一個(gè)單點(diǎn)登錄呢?我們需要維護(hù)一個(gè)全局的HaspMap,以 Token中的 subject (這里我使用的不會(huì)重復(fù)的username) 作為鍵值,以token為value存儲(chǔ). Map定義在 JwtConfig 中,代碼如下所示.

    private static Map<String, String> tokenMap = new HashMap<>();

在創(chuàng)建token的方法中,我們認(rèn)定前面的token都失效了,所以我們直接添加即可,如果存在舊的token就進(jìn)行覆蓋操作,如果沒(méi)有就進(jìn)行添加.代碼如下所示.

    public String createToken(String subject) {

        ....

        String userToken = ....

        tokenMap.put(subject, userToken);

        ....
    }

在攔截器中的攔截方法中我們需要去驗(yàn)證 傳入的token是否是我們存儲(chǔ)中的token,如果不是,那么就直接返回token過(guò)期.

    String userName = jwtConfig.getUsernameFromToken(token);
    String compareToken = jwtConfig.getTokenMap().get(userName);
    if (compareToken != null && !compareToken.equals(token)) {
        response.sendError(400, "token已經(jīng)失效,請(qǐng)重新登錄");
        return false;
    }

由于HashMap存儲(chǔ)在緩存中,當(dāng)下次服務(wù)重啟的時(shí)候,HashMap所有值就會(huì)失效.這時(shí)候我們?cè)撊绾巫瞿?我們需要在攔截方法最后把當(dāng)前驗(yàn)證完畢的token 重新填入 Map中即可.

    if (compareToken == null) {
        // 由于服務(wù)器war重新上傳導(dǎo)致臨時(shí)數(shù)據(jù)丟失,需要重新存儲(chǔ)
        jwtConfig.getTokenMap().put(userName, token);
    }


刷新token


當(dāng)token過(guò)期之后,我們?cè)试S用戶進(jìn)行token的刷新.這時(shí)候我們需要定義一個(gè)生成刷新token的方法,如下所示.

    public String createRefreshToken(String subject) {
        Date nowDate = new Date();

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

我們已經(jīng)在登錄之時(shí)把該refreshToken 返回給用戶,只要我們定義接口實(shí)現(xiàn)新token的創(chuàng)建即可.這樣就完成token的刷新了.


結(jié)語(yǔ)


基于JWT的token校驗(yàn)、單點(diǎn)登錄、刷新token整體來(lái)說(shuō)還是比較簡(jiǎn)單的,如果有問(wèn)題,歡迎各位大佬在評(píng)論區(qū)指導(dǎo)批評(píng),謝謝啦~OK,今天就到這里了.....


?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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