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


前言


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

    @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錯誤";
            return map;
        }

        ....

    }

反正一句話來說,自己太菜了...

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


JWT集成


項(xiàng)目是基于Maven來架構(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) -->

對于需要創(chuàng)建的類來說,主要有以下幾個類.

下面我們簡單看一下各個文件的作用.

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


具體代碼


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

InterceptorConfig

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

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

該類主要是用來定義token的相關(guān)方法.例如,創(chuàng)建token,創(chuàng)建刷新token等等,驗(yàn)證token是否過期,獲取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 = "秘鑰,請自己定義";

    // 外部http請求中 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中注冊信息
     *
     * @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是否過期失效
     *
     * @param expirationTime
     * @return
     */
    public boolean isTokenExpired(Date expirationTime) {
        return expirationTime.before(new Date());
    }

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

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

    /**
     * 獲取jwt發(fā)布時間
     */
    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

定義一個哪些類或者接口跳過驗(yàn)證的注解,不添加也也判定是跳過驗(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

定義一個哪些類或者接口需要驗(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 這個抽象類, 實(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注釋好爬,有則跳過認(rèn)證 */
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }

        /** 檢查有沒有需要用戶權(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)失效,請重新登錄");
                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)失效,請重新登錄");
                        return false;
                    }
                } catch (Exception e) {
                    response.sendError(400, "token已經(jīng)失效,請重新登錄");
                    return false;
                }
                /** 設(shè)置 identityId 用戶身份ID */
                request.setAttribute("identityId", claims.getSubject());
                return true;
            }
            if (compareToken == null) {
                // 由于服務(wù)器war重新上傳導(dǎo)致臨時數(shù)據(jù)丟失,需要重新存儲
                jwtConfig.getTokenMap().put(userName, token);
            }
        }

        return true;
    }
}


Token驗(yàn)證


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

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

首先我們驗(yàn)證是否含有 @PassToken 這個注解,如果有,那么直接跳過驗(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)證它的過期時間.代碼如下所示.

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


單點(diǎn)登錄


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

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

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

    public String createToken(String subject) {

        ....

        String userToken = ....

        tokenMap.put(subject, userToken);

        ....
    }

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

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

由于HashMap存儲在緩存中,當(dāng)下次服務(wù)重啟的時候,HashMap所有值就會失效.這時候我們該如何做呢?我們需要在攔截方法最后把當(dāng)前驗(yàn)證完畢的token 重新填入 Map中即可.

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


刷新token


當(dāng)token過期之后,我們允許用戶進(jìn)行token的刷新.這時候我們需要定義一個生成刷新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)在登錄之時把該refreshToken 返回給用戶,只要我們定義接口實(shí)現(xiàn)新token的創(chuàng)建即可.這樣就完成token的刷新了.


結(jié)語


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


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市穆桂,隨后出現(xiàn)的幾起案子宫盔,更是在濱河造成了極大的恐慌,老刑警劉巖充尉,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飘言,死亡現(xiàn)場離奇詭異,居然都是意外死亡驼侠,警方通過查閱死者的電腦和手機(jī)姿鸿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來倒源,“玉大人苛预,你說我怎么就攤上這事∷癜荆” “怎么了热某?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胳螟。 經(jīng)常有香客問我昔馋,道長,這世上最難降的妖魔是什么糖耸? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任秘遏,我火速辦了婚禮,結(jié)果婚禮上嘉竟,老公的妹妹穿的比我還像新娘邦危。我一直安慰自己洋侨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布倦蚪。 她就那樣靜靜地躺著希坚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陵且。 梳的紋絲不亂的頭發(fā)上裁僧,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機(jī)與錄音滩报,去河邊找鬼锅知。 笑死播急,一個胖子當(dāng)著我的面吹牛脓钾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播桩警,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼可训,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捶枢?” 一聲冷哼從身側(cè)響起握截,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎烂叔,沒想到半個月后谨胞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒜鸡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年胯努,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逢防。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡叶沛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出忘朝,到底是詐尸還是另有隱情灰署,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布局嘁,位于F島的核電站溉箕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悦昵。R本人自食惡果不足惜肴茄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旱捧。 院中可真熱鬧独郎,春花似錦踩麦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贪婉,卻和暖如春反粥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疲迂。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工才顿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尤蒿。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓郑气,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腰池。 傳聞我的和親對象是個殘疾皇子尾组,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359