SpringBoot+JWT完成token驗證

JWT官網(wǎng): https://jwt.io/
JWT(Java版)的github地址:https://github.com/jwtk/jjwt

什么是JWT

Json Web Token(JWT):JSON網(wǎng)絡(luò)令牌臭胜,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而制定的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519)哆档。JWT是一個輕便的安全跨平臺傳輸格式,定義了一個緊湊的自包含的方式用于通信雙方之間以 JSON 對象行使安全的傳遞信息庐冯。因為數(shù)字簽名的存在侣监,這些信息是可信的辜伟。

廣義上講JWT是一個標(biāo)準(zhǔn)的名稱弛随;狹義上講JWT指的就是用來傳遞的那個token字符串腮郊。

JWT的組成

JWT含有三個部分:

  • 頭部(header)
  • 載荷(payload)
  • 簽證(signature)

頭部(header)
頭部一般有兩部分信息:類型抄谐、加密的算法(通常使用HMAC SHA256)
頭部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解密后:

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

載荷(payload)
該部分一般存放一些有效的信息渺鹦。JWT的標(biāo)準(zhǔn)定義包含五個字段:

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

簽證(signature)
JWT最后一個部分蛹含。該部分是使用了HS256加密后的數(shù)據(jù)毅厚;包含三個部分:

  • header(base64后的)
  • payload(base64后的)
  • secret 私鑰

secret是保存在服務(wù)器端的,JWT的簽發(fā)生成也是在服務(wù)器端的浦箱,secret就是用來進(jìn)行JWT的簽發(fā)和JWT的驗證吸耿,所以祠锣,它就是你服務(wù)端的秘鑰,在任何場景都不應(yīng)該流露出去咽安。一旦客戶端得知這個secret伴网,那就意味著客戶端可以自我簽發(fā)JWT了。

JWT特點

  • 緊湊:意味著這個字符串很小妆棒,甚至可以放在URL參數(shù)澡腾,POST Parameter中以Http Header的方式傳輸。
  • 自包含:傳輸?shù)淖址芏嘈畔⒏馍海瑒e人拿到以后就不需要多次訪問數(shù)據(jù)庫獲取信息动分,而且通過其中的信息就可以知道加密類型和方式(當(dāng)然解密需要公鑰和密鑰)。

如何使用JWT红选?

在身份鑒定的實現(xiàn)中澜公,傳統(tǒng)的方法是在服務(wù)端存儲一個 session,給客戶端返回一個 cookie喇肋,而使用JWT之后坟乾,當(dāng)用戶使用它的認(rèn)證信息登錄系統(tǒng)之后,會返回給用戶一個JWT苟蹈, 用戶只需要本地保存該 token(通常使用localStorage糊渊,也可以使用cookie)即可。

當(dāng)用戶希望訪問一個受保護(hù)的路由或者資源的時候慧脱,通常應(yīng)該在 Authorization 頭部使用 Bearer 模式添加JWT渺绒,其內(nèi)容格式:

Authorization: Bearer <token>

因為用戶的狀態(tài)在服務(wù)端內(nèi)容中是不存儲的,所以這是一種無狀態(tài)的認(rèn)證機(jī)制菱鸥。服務(wù)端的保護(hù)路由將會檢查請求頭 Authorization 中的JWT信息宗兼,如果合法,則允許用戶的行為氮采。由于JWT是 自包含的殷绍,因此,減少了需要查詢數(shù)據(jù)庫的需要鹊漠。

JWT的這些特征使得我們可以完全依賴無狀態(tài)的特性提供數(shù)據(jù)API服務(wù)主到。因為JWT并不使用Cookie的,所以你可以在任何域名提供你的API服務(wù)而不需要擔(dān)心跨域資源共享問題(CORS)

下面的序列圖展示了該過程:

image

中文流程介紹:

  1. 用戶使用賬號和密碼發(fā)出POST登錄請求躯概;
  2. 服務(wù)器使用私鑰創(chuàng)建一個JWT登钥;
  3. 服務(wù)器返回這個JWT給瀏覽器;
  4. 瀏覽器將該JWT串放在請求頭中向服務(wù)器發(fā)送請求娶靡;
  5. 服務(wù)器驗證該JWT牧牢;
  6. 返回響應(yīng)的資源給瀏覽器。

說了這么多JWT到底如何應(yīng)用到我們的項目中,下面我們就使用SpringBoot 結(jié)合 JWT完成用戶的登錄驗證塔鳍。

應(yīng)用流程

  • 初次登錄生成JWT流程圖

    image
  • 用戶訪問資源流程圖

    image

搭建SpringBoot + JWT工程

下面通過代碼來實現(xiàn)用戶認(rèn)證的功能伯铣,博主這里主要采用Spring Boot與JWT整合的方式實現(xiàn)。關(guān)于Spring Boot項目如何搭建與使用本章不做詳細(xì)介紹轮纫。

  1. 首先引入JWT依賴:
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

  1. 在工程 application.yml 配置文件中添加JWT的配置信息:
##jwt配置
audience:
  # 代表這個JWT的接收對象,存入audience
  clientId: 098f6bcd4621d373cade4e832627b4f6
  # 密鑰, 經(jīng)過Base64加密, 可自行替換
  base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
  # JWT的簽發(fā)主體腔寡,存入issuer
  name: restapiuser
  # 過期時間,時間戳
  expiresSecond: 172800

  1. 新建配置信息的實體類蜡感,以便獲取JWT配置:
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {

    private String clientId;
    private String base64Secret;
    private String name;
    private int expiresSecond;

}

JWT驗證主要是通過過濾器驗證蹬蚁,所以我們需要添加一個攔截器來演請求頭中是否包含有后臺頒發(fā)的 token,這里請求頭的格式:

Authorization: Bearer <token>

  1. 創(chuàng)建JWT工具類:
package com.thtf.util;

import com.thtf.common.exception.CustomException;
import com.thtf.common.response.ResultCode;
import com.thtf.model.Audience;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/17 17:24
 * Version: v1.0
 * ========================
 */
public class JwtTokenUtil {

    private static Logger log = LoggerFactory.getLogger(JwtTokenUtil.class);

    public static final String AUTH_HEADER_KEY = "Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * 解析jwt
     * @param jsonWebToken
     * @param base64Security
     * @return
     */
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (ExpiredJwtException  eje) {
            log.error("===== Token過期 =====", eje);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
        } catch (Exception e){
            log.error("===== token解析異常 =====", e);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
        }
    }

    /**
     * 構(gòu)建jwt
     * @param userId
     * @param username
     * @param role
     * @param audience
     * @return
     */
    public static String createJWT(String userId, String username, String role, Audience audience) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //生成簽名密鑰
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //userId是重要信息郑兴,進(jìn)行加密下
            String encryId = Base64Util.encode(userId);

            //添加構(gòu)成JWT的參數(shù)
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以將基本不重要的對象信息放到claims
                    .claim("role", role)
                    .claim("userId", userId)
                    .setSubject(username)           // 代表這個JWT的主體犀斋,即它的所有人
                    .setIssuer(audience.getClientId())              // 代表這個JWT的簽發(fā)主體;
                    .setIssuedAt(new Date())        // 是一個時間戳情连,代表這個JWT的簽發(fā)時間叽粹;
                    .setAudience(audience.getName())          // 代表這個JWT的接收對象;
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token過期時間
            int TTLMillis = audience.getExpiresSecond();
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // 是一個時間戳却舀,代表這個JWT的過期時間虫几;
                        .setNotBefore(now); // 是一個時間戳,代表這個JWT生效的開始時間挽拔,意味著在這個時間之前驗證JWT是會失敗的
            }

            //生成JWT
            return builder.compact();
        } catch (Exception e) {
            log.error("簽名失敗", e);
            throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
        }
    }

    /**
     * 從token中獲取用戶名
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUsername(String token, String base64Security){
        return parseJWT(token, base64Security).getSubject();
    }

    /**
     * 從token中獲取用戶ID
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUserId(String token, String base64Security){
        String userId = parseJWT(token, base64Security).get("userId", String.class);
        return Base64Util.decode(userId);
    }

    /**
     * 是否已過期
     * @param token
     * @param base64Security
     * @return
     */
    public static boolean isExpiration(String token, String base64Security) {
        return parseJWT(token, base64Security).getExpiration().before(new Date());
    }
}

  1. 創(chuàng)建JWT驗證攔截器:
package com.thtf.interceptor;

import com.thtf.annotation.JwtIgnore;
import com.thtf.common.exception.CustomException;
import com.thtf.common.response.ResultCode;
import com.thtf.model.Audience;
import com.thtf.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * ========================
 * token驗證攔截器
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/18 9:46
 * Version: v1.0
 * ========================
 */
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter{

    @Autowired
    private Audience audience;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 忽略帶JwtIgnore注解的請求, 不做后續(xù)token認(rèn)證校驗
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 獲取請求頭信息authorization信息
        final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        log.info("## authHeader= {}", authHeader);

        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
            log.info("### 用戶未登錄辆脸,請先登錄 ###");
            throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
        }

        // 獲取token
        final String token = authHeader.substring(7);

        if(audience == null){
            BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
            audience = (Audience) factory.getBean("audience");
        }

        // 驗證token是否有效--無效已做異常拋出,由全局異常處理后返回對應(yīng)信息
        JwtTokenUtil.parseJWT(token, audience.getBase64Secret());

        return true;
    }

}

  1. 配置攔截器:
package com.thtf.config;

import com.thtf.interceptor.JwtInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/18 10:37
 * Version: v1.0
 * ========================
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * 添加攔截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //攔截路徑可自行配置多個 可用 螃诅,分隔開
        registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
    }

    /**
     * 跨域支持
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }

}

這里JWT可能會有跨域問題啡氢,配置跨域支持。

  1. 編寫測試Controller接口:
package com.thtf.controller;

import com.alibaba.fastjson.JSONObject;
import com.thtf.annotation.JwtIgnore;
import com.thtf.common.response.Result;
import com.thtf.model.Audience;
import com.thtf.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * ========================
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/7/18 10:41
 * Version: v1.0
 * ========================
 */
@Slf4j
@RestController
public class AdminUserController {

    @Autowired
    private Audience audience;

    @PostMapping("/login")
    @JwtIgnore
    public Result adminLogin(HttpServletResponse response, String username,String password) {
        // 這里模擬測試, 默認(rèn)登錄成功术裸,返回用戶ID和角色信息
        String userId = UUID.randomUUID().toString();
        String role = "admin";

        // 創(chuàng)建token
        String token = JwtTokenUtil.createJWT(userId, username, role, audience);
        log.info("### 登錄成功, token={} ###", token);

        // 將token放在響應(yīng)頭
        response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
        // 將token響應(yīng)給客戶端
        JSONObject result = new JSONObject();
        result.put("token", token);
        return Result.SUCCESS(result);
    }

    @GetMapping("/users")
    public Result userList() {
        log.info("### 查詢所有用戶列表 ###");
        return Result.SUCCESS();
    }
}

  1. 接下來我們使用PostMan工具進(jìn)行測試:

沒有登錄時候直接訪問:http://localhost:8080/users 接口:

image

執(zhí)行登錄:

image

攜帶生成token再次訪問:http://localhost:8080/users 接口

image

注意:這里選擇 Bearer Token類型倘是,就把不要在 Token中手動Bearer,postman會自動拼接袭艺。

源碼下載地址https://github.com/pyygithub/JWT-DEMO.git

作者:一行代碼一首詩
鏈接:http://www.reibang.com/p/430cd44a2796
來源:簡書
簡書著作權(quán)歸作者所有搀崭,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猾编,一起剝皮案震驚了整個濱河市瘤睹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌答倡,老刑警劉巖默蚌,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異苇羡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門设江,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锦茁,“玉大人,你說我怎么就攤上這事叉存÷肓” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵歼捏,是天一觀的道長稿存。 經(jīng)常有香客問我,道長瞳秽,這世上最難降的妖魔是什么瓣履? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮练俐,結(jié)果婚禮上袖迎,老公的妹妹穿的比我還像新娘。我一直安慰自己腺晾,他們只是感情好燕锥,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悯蝉,像睡著了一般归形。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鼻由,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天暇榴,我揣著相機(jī)與錄音,去河邊找鬼嗡靡。 笑死跺撼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讨彼。 我是一名探鬼主播歉井,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哈误!你這毒婦竟也來了哩至?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蜜自,失蹤者是張志新(化名)和其女友劉穎菩貌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體重荠,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡箭阶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仇参。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘹叫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诈乒,到底是詐尸還是另有隱情罩扇,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布怕磨,位于F島的核電站喂饥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肠鲫。R本人自食惡果不足惜员帮,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滩届。 院中可真熱鬧集侯,春花似錦、人聲如沸帜消。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泡挺。三九已至辈讶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娄猫,已是汗流浹背贱除。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留媳溺,地道東北人月幌。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像悬蔽,于是被迫代替她去往敵國和親扯躺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

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