SpringBoot整合JWT的實現示例

一. JWT簡介

1. 什麼是JWT?

JWT(JSON Web Token)是為瞭在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準。

它將用戶信息加密到token裡,服務器不保存任何用戶信息。服務器通過使用保存的密鑰驗證token的正確性,隻要正確即通過驗證;應用場景如用戶登錄。JWT詳細講解請見 github:https://github.com/jwtk/jjwt

2. 為什麼使用JWT?

隨著技術的發展,分佈式web應用的普及,通過session管理用戶登錄狀態成本越來越高,因此慢慢發展成為token的方式做登錄身份校驗,然後通過token去取redis中的緩存的用戶信息,隨著之後jwt的出現,校驗方式更加簡單便捷化,無需通過redis緩存,而是直接根據token取出保存的用戶信息,以及對token可用性校驗,單點登錄更為簡單。

3. 傳統Cookie+Session與JWT對比

① 在傳統的用戶登錄認證中,因為http是無狀態的,所以都是采用session方式。用戶登錄成功,服務端會保證一個session,當然會給客戶端一個sessionId,客戶端會把sessionId保存在cookie中,每次請求都會攜帶這個sessionId。

cookie+session這種模式通常是保存在內存中,而且服務從單服務到多服務會面臨的session共享問題,隨著用戶量的增多,開銷就會越大。而JWT不是這樣的,隻需要服務端生成token,客戶端保存這個token,每次請求攜帶這個token,服務端認證解析就可。

② JWT方式校驗方式更加簡單便捷化,無需通過redis緩存,而是直接根據token取出保存的用戶信息,以及對token可用性校驗,單點登錄,驗證token更為簡單。

4. JWT的組成(3部分)

第一部分為頭部(header),第二部分我們稱其為載荷(payload),第三部分是簽證(signature)。【中間用 . 分隔】

一個標準的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw

5. JWT驗證流程和特點

驗證流程:

① 在頭部信息中聲明加密算法和常量, 然後把header使用json轉化為字符串
② 在載荷中聲明用戶信息,同時還有一些其他的內容;再次使用json 把載荷部分進行轉化,轉化為字符串
③ 使用在header中聲明的加密算法和每個項目隨機生成的secret來進行加密, 把第一步分字符串和第二部分的字符串進行加密, 生成新的字符串。詞字符串是獨一無二的。
④ 解密的時候,隻要客戶端帶著JWT來發起請求,服務端就直接使用secret進行解密。

特點:

① 三部分組成,每一部分都進行字符串的轉化
② 解密的時候沒有使用數據庫,僅僅使用的是secret進行解密
③ JWT的secret千萬不能泄密!

6. JWT優缺點

優點:

①. 可擴展性好

應用程序分佈式部署的情況下,Session需要做多機數據共享,通常可以存在數據庫或者Redis裡面。而JWT不需要。

②. 無狀態

JWT不在服務端存儲任何狀態。RESTful API的原則之一是無狀態,發出請求時,總會返回帶有參數的響應,不會產生附加影響。用戶的認證狀態引入這種附加影響,這破壞瞭這一原則。另外JWT的載荷中可以存儲一些常用信息,用於交換信息,有效地使用 JWT,可以降低服務器查詢數據庫的次數。

缺點:

① 安全性:由於JWT的payload是使用Base64編碼的,並沒有加密,因此JWT中不能存儲敏感數據。而Session的信息是存在服務端的,相對來說更安全。

② 性能:JWT太長。由於是無狀態使用JWT,所有的數據都被放到JWT裡,如果還要進行一些數據交換,那載荷會更大,經過編碼之後導致JWT非常長,Cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在LocalStorage裡面。並且用戶在系統中的每一次Http請求都會把JWT攜帶在Header裡面,Http請求的Header可能比Body還要大。而SessionId隻是很短的一個字符串,因此使用JWT的Http請求比使用Session的開銷大得多。

③ 一次性:無狀態是JWT的特點,但也導致瞭這個問題,JWT是一次性的。想修改裡面的內容,就必須簽發一個新的JWT。即缺陷是一旦下發,服務後臺無法拒絕攜帶該jwt的請求(如踢除用戶)

(1)無法廢棄:通過JWT的驗證機制可以看出來,一旦簽發一個JWT,在到期之前就會始終有效,無法中途廢棄。例如你在payload中存儲瞭一些信息,當信息需要更新時,則重新簽發一個JWT,但是由於舊的jwt還沒過期,拿著這個舊的JWT依舊可以登錄,那登錄後服務端從JWT中拿到的信息就是過時的。為瞭解決這個問題,我們就需要在服務端部署額外的邏輯,例如設置一個黑名單,一旦簽發瞭新的JWT,那麼舊的就加入黑名單(比如存到redis裡面),避免被再次使用。

(2)續簽:如果你使用jwt做會話管理,傳統的Cookie續簽方案一般都是框架自帶的,Session有效期30分鐘,30分鐘內如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時間,就要簽發新的JWT。最簡單的一種方式是每次請求刷新JWT,即每個HTTP請求都返回一個新的JWT。這個方法不僅暴力不優雅,而且每次請求都要做JWT的加密解密,會帶來性能問題。另一種方法是在Redis中單獨為每個JWT設置過期時間,每次訪問時刷新JWT的過期時間。

可以看出想要破解JWT一次性的特性,就需要在服務端存儲jwt的狀態。但是引入 redis 之後,就把無狀態的jwt硬生生變成瞭有狀態瞭,違背瞭JWT的初衷。而且這個方案和Session都差不多瞭。

二. Java實現JWT(SpringBoot方式整合)

 1. Maven依賴與application.yml配置

<!-- JWT依賴 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
server:
  port: 8080
spring:
  application:
    name: springboot-jwt
config:
  jwt:
    # 加密密鑰
    secret: abcdefg1234567
    # token有效時長
    expire: 3600
    # header 名稱
    header: token

 2. 編寫JwtConfig

package com.example.config;
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
 
/**
 * JWT的token,區分大小寫
 */
@ConfigurationProperties(prefix = "config.jwt")
@Component
public class JwtConfig {
 
    private String secret;
    private long expire;
    private String header;
 
    /**
     * 生成token
     * @param subject
     * @return
     */
    public String createToken (String subject){
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);//過期時間
 
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .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){
//            e.printStackTrace();
            return null;
        }
    }
    /**
     * 驗證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發佈時間
     */
    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 long getExpire() {
        return expire;
    }
    public void setExpire(long expire) {
        this.expire = expire;
    }
    public String getHeader() {
        return header;
    }
    public void setHeader(String header) {
        this.header = header;
    }
}

 3. 配置攔截器

package com.example.interceptor;
 
import com.example.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
 
    @Resource
    private JwtConfig jwtConfig ;
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws SignatureException {
        /** 地址過濾 */
        String uri = request.getRequestURI() ;
        if (uri.contains("/login")){
            return true ;
        }
        /** Token 驗證 */
        String token = request.getHeader(jwtConfig.getHeader());
        if(StringUtils.isEmpty(token)){
            token = request.getParameter(jwtConfig.getHeader());
        }
        if(StringUtils.isEmpty(token)){
            throw new SignatureException(jwtConfig.getHeader()+ "不能為空");
        }
 
        Claims claims = null;
        try{
            claims = jwtConfig.getTokenClaim(token);
            if(claims == null || jwtConfig.isTokenExpired(claims.getExpiration())){
                throw new SignatureException(jwtConfig.getHeader() + "失效,請重新登錄。");
            }
        }catch (Exception e){
            throw new SignatureException(jwtConfig.getHeader() + "失效,請重新登錄。");
        }
 
        /** 設置 identityId 用戶身份ID */
        request.setAttribute("identityId", claims.getSubject());
        return true;
    }
}

  註冊攔截器到SpringMvc

package com.example.config;
 
import com.example.interceptor.TokenInterceptor;
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 WebConfig implements WebMvcConfigurer {
    @Resource
    private TokenInterceptor tokenInterceptor ;
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

 4. 編寫統一異常處理類

package com.example.config;
import io.jsonwebtoken.SignatureException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.yuyi.full.handler.exception.ExceptionInfoBO;
import org.yuyi.full.handler.exception.ResultBO;
import org.yuyi.full.handler.exception.ResultTool;
 
@RestControllerAdvice
public class PermissionHandler {
    @ExceptionHandler(value = { SignatureException.class })
    @ResponseBody
    public ResultBO<?> authorizationException(SignatureException e){
        return ResultTool.error(new ExceptionInfoBO(1008,e.getMessage()));
    }
}

 5.編寫測試接口

package com.example.controller;
 
import com.alibaba.fastjson.JSONObject;
import com.example.config.JwtConfig;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yuyi.full.handler.exception.ResultBO;
import org.yuyi.full.handler.exception.ResultTool;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
 
@RestController
public class TokenController {
 
    @Resource
    private JwtConfig jwtConfig ;
 
    /**
     * 用戶登錄接口
     * @param userName
     * @param passWord
     * @return
     */
    @PostMapping("/login")
    public ResultBO<?> login (@RequestParam("userName") String userName,
                           @RequestParam("passWord") String passWord){
        JSONObject json = new JSONObject();
 
        /** 驗證userName,passWord和數據庫中是否一致,如不一致,直接return ResultTool.errer(); 【這裡省略該步驟】*/
 
        // 這裡模擬通過用戶名和密碼,從數據庫查詢userId
        // 這裡把userId轉為String類型,實際開發中如果subject需要存userId,則可以JwtConfig的createToken方法的參數設置為Long類型
        String userId = 5 + "";
        String token = jwtConfig.createToken(userId) ;
        if (!StringUtils.isEmpty(token)) {
            json.put("token",token) ;
        }
        return ResultTool.success(json) ;
    }
 
    /**
     * 需要 Token 驗證的接口
     */
    @PostMapping("/info")
    public ResultBO<?> info (){
        return ResultTool.success("info") ;
    }
 
    /**
     * 根據請求頭的token獲取userId
     * @param request
     * @return
     */
    @GetMapping("/getUserInfo")
    public ResultBO<?> getUserInfo(HttpServletRequest request){
        String usernameFromToken = jwtConfig.getUsernameFromToken(request.getHeader("token"));
        return ResultTool.success(usernameFromToken) ;
    }
 
    /*
        為什麼項目重啟後,帶著之前的token還可以訪問到需要info等需要token驗證的接口?
        答案:隻要不過期,會一直存在,類似於redis
     */
 
}

用PostMan測試工具測試一下,訪問登錄接口,當對賬號密碼驗證通過時,則返回一個token給客戶端:

 當直接去訪問info接口時,會返回token為空的自定義異常:

 當在請求頭加上正確token時,則攔截器驗證通過,可以正常訪問到接口:

 當在請求頭加入一個錯誤token,則會返回token失效的自定義異常:

 接下來測試一下獲取用戶信息,因為這裡存的subject為userId,所以直接返回上面寫死的假數據5:

JWT總結

1. 基於JSON,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,Node.JS,PHP等很多語言都可以使用。

2. payload部分,需要時JWT可以存儲一些其他業務邏輯所必要的非敏感信息。

3. 體積小巧,便於傳輸;JWT的構成非常簡單,字節占用很小,所以它是非常便於傳輸的。它不需要在服務端保存會話信息, 所以它易於應用的擴展。

到此這篇關於SpringBoot整合JWT的實現示例的文章就介紹到這瞭,更多相關SpringBoot整合JWT內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: