JWT Token实现方法及步骤详解

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

生成证书命令参考:

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

```java
 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;
 }

然后我们就可以配置 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 都十分有借鉴意义。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Django JWT Token RestfulAPI用户认证详解

    一般情况下我们Django默认的用户系统是满足不了我们的需求的,那么我们会对他做一定的扩展 创建用户项目 python manage.py startapp users 添加项目apps settings.py INSTALLED_APPS = [ ... 'users.apps.UsersConfig', ] 添加AUTH_USRE_MODEL 替换默认的user AUTH_USER_MODEL = 'users.UserProfile' 如果说想用全局认证需要在配置文件中添加 # 全局认证f

  • koa+jwt实现token验证与刷新功能

    JWT JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的.自包含的方式,用于作为JSON对象在各方之间安全地传输信息.该信息可以被验证和信任,因为它是数字签名的. 本文只讲Koa2 + jwt的使用,不了解JWT的话请到这里)进行了解. koa环境 要使用koa2+jwt需要先有个koa的空环境,搭环境比较麻烦,我直接使用koa起手式,这是我使用koa+typescript搭建的空环境,如果你也经常用koa写写小demo,可以点个star,方便~ 安

  • 基于Token的身份验证之JWT基础教程

    前言 初次了解JWT,很基础,高手勿喷. 基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session. token应用流程为: 1.初次登录:用户初次登录,输入用户名密码. 2.密码验证:服务器从数据库取出用户名和密码进行验证. 3.生成JWT:服务器端验证通过,根据从数据库返回的信息,以及预设规则,生成JWT. 4.返还JWT:服务器的HTTP RESPONSE中将JWT返还. 5.带JWT的请求:以后客户端发起请求,HTTP REQUEST HEADER

  • 详解JWT token心得与使用实例

    本文你能学到什么? token的组成 token串的生成流程. token在客户端与服务器端的交互流程 Token的优点和思考 参考代码:核心代码使用参考,不是全部代码 JWT token的组成 头部(Header),格式如下: { "typ": "JWT", "alg": "HS256" } 由上可知,该token使用HS256加密算法,将头部使用Base64编码可得到如下个格式的字符串: eyJhbGciOiJIUzI1N

  • spring boot+jwt实现api的token认证详解

    前言 本篇和大家分享jwt(json web token)的使用,她主要用来生成接口访问的token和验证,其单独结合springboot来开发api接口token验证很是方便,由于jwt的token中存储有用户的信息并且有加密,所以适用于分布式,这样直接吧信息存储在用户本地减速了服务端存储sessiion或token的压力: 如下快速使用: <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId>

  • thinkphp框架使用JWTtoken的方法详解

    本文实例讲述了thinkphp框架使用JWTtoken的方法.分享给大家供大家参考,具体如下: 简介 一:JWT介绍:全称JSON Web Token,基于JSON的开放标准((RFC 7519) ,以token的方式代替传统的Cookie-Session模式,用于各服务器.客户端传递信息签名验证. 二:JWT优点: 1:服务端不需要保存传统会话信息,没有跨域传输问题,减小服务器开销. 2:jwt构成简单,占用很少的字节,便于传输. 3:json格式通用,不同语言之间都可以使用. 三:JWT组成

  • Java中使用JWT生成Token进行接口鉴权实现方法

    先介绍下利用JWT进行鉴权的思路: 1.用户发起登录请求. 2.服务端创建一个加密后的JWT信息,作为Token返回. 3.在后续请求中JWT信息作为请求头,发给服务端. 4.服务端拿到JWT之后进行解密,正确解密表示此次请求合法,验证通过:解密失败说明Token无效或者已过期. 流程图如下: 一.用户发起登录请求 二.服务端创建一个加密后的JWT信息,作为Token返回 1.用户登录之后把生成的Token返回给前端 @Authorization @ResponseBody @GetMappin

  • Laravel (Lumen) 解决JWT-Auth刷新token的问题

    Laravel(Lumen)中使用JWT-Auth遇到一个问题,即token如何刷新. 一开始不太理解作者的设计思想,看了很多issue之后,慢慢明白jwt-refresh如何使用. 建一个路由,比如"auth/refresh-token" ,可以指向某个方法,也可以直接写个匿名函数. $app->post('auth/refresh-token', ['middleware' => 'jwt.refresh', function() { try { $old_token

  • php实现JWT(json web token)鉴权实例详解

    JWT是什么 JWT是json web token缩写.它将用户信息加密到token里,服务器不保存任何用户信息.服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证.基于token的身份验证可以替代传统的cookie+session身份验证方法. JWT由三个部分组成:header.payload.signature 以下示例以JWT官网为例 header部分: { "alg": "HS256", "typ": "JWT

  • SpringBoot集成JWT实现token验证的流程

    JWT官网: https://jwt.io/ JWT(Java版)的github地址:https://github.com/jwtk/jjwt 什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息.因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名. JWT请求流程 1. 用户使

随机推荐