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 生成的  -keypass 值  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

相关文章
相关标签/搜索