51好读  ›  专栏  ›  码农小胖哥

Spring Security 实战干货:手把手教你实现JWT Token

码农小胖哥  · 掘金  ·  · 2019-10-27 16:23

正文

阅读 2

Spring Security 实战干货:手把手教你实现JWT Token

jwt.png

1. 前言

Json Web Token JWT ) 近几年是前后端分离常用的 Token 技术,是目前最流行的跨域身份验证解决方案。你可以通过文章 一文了解web无状态会话token技术JWT 来了解 JWT 。今天我们来手写一个通用的 JWT 服务。 DEMO 获取方式在文末,实现在 jwt 相关包下

2. spring-security-jwt

spring-security-jwt Spring Security Crypto 提供的 JWT 工具包 。

  <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的方法 需要指定 payload signer 签名算法。 payload 存放了一些可用的 不敏感 信息:

  • iss jwt签发者
  • sub jwt所面向的用户
  • aud 接收jwt的一方
  • iat jwt的签发时间
  • exp jwt的过期时间,这个过期时间必须要大于签发时间 iat
  • jti jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

除了以上提供的基本信息外,我们可以定义一些我们需要传递的信息,比如目标用户的权限集 等等。 切记不要传递密码等敏感信息 ,因为 JWT 的前两段都是用了 BASE64 编码,几乎算是明文了。

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 格式。

生成证书命令参考:

 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 我们要作为配置使用要记下来。我们要使用下面定义的这个类来读取证书

 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);
         }
 
     }
 }复制代码






请到「今天看啥」查看全文