zoukankan      html  css  js  c++  java
  • 手把手教你实现JWT Token

    640?wx_fmt=gif

    1. 前言

    Json Web Token (JWT) 近几年是前后端分离常用的 Token 技术,是目前最流行的跨域身份验证解决方案。你可以通过文章 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 生成的  -storepass 值  123456
          * @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;
     }
    

    然后我们就可以配置 JWT 的 javaConfig 如下:

     /**
      * 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 获取。

    640?wx_fmt=png

    640?wx_fmt=gif

  • 相关阅读:
    三方协议,档案,工龄,保险,户口,
    老爸-军事
    反思,关于问 问题,
    计算机组成原理,
    c语言中定义函数和调用函数(在函数中调用其他函数,计算四个数中的最大值)
    c语言中定义函数和调用函数(计算三个数中的最大值)
    c语言中函数的定义和调用(计算1到n之间的所有整数的和)
    c语言中函数的定义和调用(值传递,计算x的n次幂)
    c语言中定义函数和调用函数(将函数的返回值作为参数传递给其他函数,计算平方差)
    c语言中定义函数和调用函数(在函数中调用其他函数,计算int型整数的4次幂)
  • 原文地址:https://www.cnblogs.com/felordcn/p/12142528.html
Copyright © 2011-2022 走看看