Spring Security 實戰乾貨:手把手教你實現JWT Token

jwt.png

1. 前言

Json Web TokenJWT) 近幾年是先後端分離經常使用的 Token 技術,是目前最流行的跨域身份驗證解決方案。你能夠經過文章 一文了解web無狀態會話token技術JWT 來了解 JWT。今天咱們來手寫一個通用的 JWT 服務。DEMO 獲取方式在文末,實如今 jwt 相關包下java

2. spring-security-jwt

spring-security-jwtSpring Security Crypto 提供的 JWT 工具包 。web

<dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-jwt</artifactId>
       <version>${spring-security-jwt.version}</version>
 </dependency>

核心類只有一個: org.springframework.security.jwt.JwtHelper 。它提供了兩個很是有用的靜態方法。算法

3. JWT 編碼

JwtHelper 提供的第一個靜態方法就是 encode(CharSequence content, Signer signer) 這個是用來生成jwt的方法 須要指定 payloadsigner 簽名算法。payload 存放了一些可用的不敏感信息:spring

  • iss jwt簽發者
  • sub jwt所面向的用戶
  • aud 接收jwt的一方
  • iat jwt的簽發時間
  • exp jwt的過時時間,這個過時時間必需要大於簽發時間 iat
  • jti jwt的惟一身份標識,主要用來做爲一次性token,從而回避重放攻擊

除了以上提供的基本信息外,咱們能夠定義一些咱們須要傳遞的信息,好比目標用戶的權限集 等等。切記不要傳遞密碼等敏感信息 ,由於 JWT 的前兩段都是用了 BASE64 編碼,幾乎算是明文了。json

3.1 構建 JWT 中的 payload

咱們先來構建 payload :後端

/**
 * 構建 jwt payload
 *
 * @author Felordcn
 * @since 11:27 2019/10/25
 **/
public class JwtPayloadBuilder {

    private Map<String, String> payload = new HashMap<>();
    /**
     * 附加的屬性
     */
    private Map<String, String> additional;
    /**
     * jwt簽發者
     **/
    private String iss;
    /**
     * jwt所面向的用戶
     **/
    private String sub;
    /**
     * 接收jwt的一方
     **/
    private String aud;
    /**
     * jwt的過時時間,這個過時時間必需要大於簽發時間
     **/
    private LocalDateTime exp;
    /**
     * jwt的簽發時間
     **/
    private LocalDateTime iat = LocalDateTime.now();
    /**
     * 權限集
     */
    private Set<String> roles = new HashSet<>();
    /**
     * jwt的惟一身份標識,主要用來做爲一次性token,從而回避重放攻擊
     **/
    private String jti = IdUtil.simpleUUID();

    public JwtPayloadBuilder iss(String iss) {
        this.iss = iss;
        return this;
    }


    public JwtPayloadBuilder sub(String sub) {
        this.sub = sub;
        return this;
    }

    public JwtPayloadBuilder aud(String aud) {
        this.aud = aud;
        return this;
    }


    public JwtPayloadBuilder roles(Set<String> roles) {
        this.roles = roles;
        return this;
    }

    public JwtPayloadBuilder expDays(int days) {
        Assert.isTrue(days > 0, "jwt expireDate must after now");
        this.exp = this.iat.plusDays(days);
        return this;
    }

    public JwtPayloadBuilder additional(Map<String, String> additional) {
        this.additional = additional;
        return this;
    }

    public String builder() {
        payload.put("iss", this.iss);
        payload.put("sub", this.sub);
        payload.put("aud", this.aud);
        payload.put("exp", this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        payload.put("iat", this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        payload.put("jti", this.jti);

        if (!CollectionUtils.isEmpty(additional)) {
            payload.putAll(additional);
        }
        payload.put("roles", JSONUtil.toJsonStr(this.roles));
        return JSONUtil.toJsonStr(JSONUtil.parse(payload));

    }

}

經過建造類 JwtClaimsBuilder 咱們能夠很方便來構建 JWT 所須要的 payload json 字符串傳遞給 encode(CharSequence content, Signer signer) 中的 content跨域

3.2 生成 RSA 密鑰並進行簽名

爲了生成 JWT Token 咱們還須要使用 RSA 算法來進行簽名。 這裏咱們使用 JDK 提供的證書管理工具 Keytool 來生成 RSA 證書 ,格式爲 jks 格式。緩存

生成證書命令參考:springboot

keytool -genkey -alias felordcn -keypass felordcn -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore d:/keystores/felordcn.jks -storepass 123456  -dname "CN=(Felord), OU=(felordcn), O=(felordcn), L=(zz), ST=(hn), C=(cn)"

其中 -alias felordcn -storepass 123456 咱們要做爲配置使用要記下來。咱們要使用下面定義的這個類來讀取證書app

package cn.felord.spring.security.jwt;
 
 import org.springframework.core.io.ClassPathResource;
 
 import java.security.KeyFactory;
 import java.security.KeyPair;
 import java.security.KeyStore;
 import java.security.PublicKey;
 import java.security.interfaces.RSAPrivateCrtKey;
 import java.security.spec.RSAPublicKeySpec;
 
 /**
  * KeyPairFactory
  *
  * @author Felordcn
  * @since 13:41 2019/10/25
  **/
 class KeyPairFactory {
 
     private KeyStore store;
 
     private final Object lock = new Object();
 
     /**
      * 獲取公私鑰.
      *
      * @param keyPath  jks 文件在 resources 下的classpath
      * @param keyAlias  keytool 生成的 -alias 值  felordcn
      * @param keyPass  keytool 生成的  -storepass  值  felordcn  
      * @return the key pair 公私鑰對
      */
    KeyPair create(String keyPath, String keyAlias, String keyPass) {
         ClassPathResource resource = new ClassPathResource(keyPath);
         char[] pem = keyPass.toCharArray();
         try {
             synchronized (lock) {
                 if (store == null) {
                     synchronized (lock) {
                         store = KeyStore.getInstance("jks");
                         store.load(resource.getInputStream(), pem);
                     }
                 }
             }
             RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(keyAlias, pem);
             RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
             PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
             return new KeyPair(publicKey, key);
         } catch (Exception e) {
             throw new IllegalStateException("Cannot load keys from store: " + resource, e);
         }
 
     }
 }

獲取了 KeyPair 就能獲取公私鑰 生成 Jwt 的兩個要素就完成了。咱們能夠和以前定義的 JwtPayloadBuilder 一塊兒封裝出生成 Jwt Token 的方法:

private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) {
         String payload = jwtPayloadBuilder
                 .iss(jwtProperties.getIss())
                 .sub(jwtProperties.getSub())
                 .aud(aud)
                 .additional(additional)
                 .roles(roles)
                 .expDays(exp)
                 .builder();
         RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
 
         RsaSigner signer = new RsaSigner(privateKey);
         return JwtHelper.encode(payload, signer).getEncoded();
     }

一般狀況下 Jwt Token 都是成對出現的,一個爲日常請求攜帶的 accessToken, 另外一個只做爲刷新 accessToken 之用的 refreshToken 。並且 refreshToken 的過時時間要相對長一些。當 accessToken 失效而refreshToken 有效時,咱們能夠經過 refreshToken 來獲取新的 Jwt Token對 ;當兩個都失效就用戶就必須從新登陸了。

生成 Jwt Token對 的方法以下:

public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) {
         String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), roles, additional);
         String refreshToken = jwtToken(aud, jwtProperties.getRefreshExpDays(), roles, additional);
 
         JwtTokenPair jwtTokenPair = new JwtTokenPair();
         jwtTokenPair.setAccessToken(accessToken);
         jwtTokenPair.setRefreshToken(refreshToken);
         // 放入緩存
         jwtTokenStorage.put(jwtTokenPair, aud);
         return jwtTokenPair;
     }

一般 Jwt Token對 會在返回給前臺的同時放入緩存中。過時策略你能夠選擇分開處理,也能夠選擇以refreshToken 的過時時間爲準。

4. JWT 解碼以及驗證

JwtHelper 提供的第二個靜態方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier) 用來 驗證和解碼 Jwt Token 。咱們獲取到請求中的token後會解析出用戶的一些信息。經過這些信息去緩存中對應的token ,而後比對並驗證是否有效(包括是否過時)。

/**
      * 解碼 並校驗簽名 過時不予解析
      *
      * @param jwtToken the jwt token
      * @return the jwt claims
      */
     public JSONObject decodeAndVerify(String jwtToken) {
         Assert.hasText(jwtToken, "jwt token must not be bank");
         RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic();
         SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey);
         Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier);
         String claims = jwt.getClaims();
         JSONObject jsonObject = JSONUtil.parseObj(claims);
         String exp = jsonObject.getStr(JWT_EXP_KEY);
        // 是否過時
         if (isExpired(exp)) {
             throw new IllegalStateException("jwt token is expired");
         }
         return jsonObject;
     }

上面咱們將有效的 Jwt Token 中的 payload 解析爲 JSON對象 ,方便後續的操做。

## 5. 配置

咱們將 JWT 的可配置項抽出來放入 JwtProperties 以下:

/**
 * Jwt 在 springboot application.yml 中的配置文件
 *
 * @author Felordcn
 * @since 15 :06 2019/10/25
 */
@Data
@ConfigurationProperties(prefix=JWT_PREFIX)
public class JwtProperties {
    static final String JWT_PREFIX= "jwt.config";
    /**
     * 是否可用
     */
    private boolean enabled;
    /**
     * jks 路徑
     */
    private String keyLocation;
    /**
     * key alias
     */
    private String keyAlias;
    /**
     * key store pass
     */
    private String keyPass;
    /**
     * jwt簽發者
     **/
    private String iss;
    /**
     * jwt所面向的用戶
     **/
    private String sub;
    /**
     * access jwt token 有效天數
     */
    private int accessExpDays;
    /**
     * refresh jwt token 有效天數
     */
    private int refreshExpDays;
}

而後咱們就能夠配置 JWTjavaConfig 以下:

/**
  * JwtConfiguration
  *
  * @author Felordcn
  * @since 16 :54 2019/10/25
  */
 @EnableConfigurationProperties(JwtProperties.class)
 @ConditionalOnProperty(prefix = "jwt.config",name = "enabled")
 @Configuration
 public class JwtConfiguration {
 
 
     /**
      * Jwt token storage .
      *
      * @return the jwt token storage
      */
     @Bean
     public JwtTokenStorage jwtTokenStorage() {
         return new JwtTokenCacheStorage();
     }
 
 
     /**
      * Jwt token generator.
      *
      * @param jwtTokenStorage the jwt token storage
      * @param jwtProperties   the jwt properties
      * @return the jwt token generator
      */
     @Bean
     public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) {
         return new JwtTokenGenerator(jwtTokenStorage, jwtProperties);
     }
 
 }

而後你就能夠經過 JwtTokenGenerator 編碼/解碼驗證 Jwt Token 對 ,經過 JwtTokenStorage 來處理 Jwt Token 緩存。緩存這裏我用了Spring Cache Ehcache 來實現,你也能夠切換到 Redis 。相關單元測試參見 DEMO

6. 總結

今天咱們利用 spring-security-jwt 手寫了一套 JWT 邏輯。不管對你後續結合 Spring Security 仍是 Shiro 都十分有借鑑意義。下一篇咱們會講解 JWT 結合Spring Security ,敬請關注公衆號:Felordcn 來及時獲取資料。

本次的 DEMO 也可經過關注公衆號回覆 day05 獲取。

關注公衆號:Felordcn獲取更多資訊

我的博客:https://felord.cn

相關文章
相關標籤/搜索