JWT Token实现方法及步骤详解

网友投稿 492 2022-11-23


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 工具包 。

org.springframework.security

spring-security-jwt

${spring-security-jwt.version}

核心类只有一个: 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 payload = new HashMap<>();

/**

* 附加的属性

*/

private Map 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 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 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 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)"

其中dPpaAFiYg `-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 roles, Map 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 roles, Map 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";dPpaAFiYg

/**

* 是否可用

*/

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 都十分有借鉴意义。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:JAVA如何读取Excel数据
下一篇:详解java 对象锁与类锁
相关文章

 发表评论

暂时没有评论,来抢沙发吧~