add JWTValidator

This commit is contained in:
Looly
2021-06-20 00:07:44 +08:00
parent a844a81782
commit 59275491de
19 changed files with 579 additions and 144 deletions

View File

@@ -10,6 +10,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.jwt.signers.AlgorithmUtil;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import cn.hutool.jwt.signers.NoneJWTSigner;
import java.nio.charset.Charset;
import java.security.Key;
@@ -20,11 +21,11 @@ import java.util.Map;
/**
* JSON Web Token (JWT)基于JSON的开放标准(RFC 7519)用于在网络应用环境间传递声明。<br>
* <p>
* 结构:xxxxx.yyyyy.zzzzz
* 结构:header.payload.signature
* <ul>
* <li>header主要声明了JWT的签名算法</li>
* <li>payload主要承载了各种声明并传递明文数据</li>
* <li>signture拥有该部分的JWT被称为JWS也就是签了名的JWS</li>
* <li>signature拥有该部分的JWT被称为JWS也就是签了名的JWS</li>
* </ul>
*
* <p>
@@ -32,8 +33,9 @@ import java.util.Map;
* </p>
*
* @author looly
* @since 5.7.0
*/
public class JWT {
public class JWT implements RegisteredPayload<JWT>{
private final JWTHeader header;
private final JWTPayload payload;
@@ -162,6 +164,15 @@ public class JWT {
return this;
}
/**
* 获取JWT算法签名器
*
* @return JWT算法签名器
*/
public JWTSigner getSigner(){
return this.signer;
}
/**
* 获取所有头信息
*
@@ -171,6 +182,16 @@ public class JWT {
return this.header.getClaimsJson();
}
/**
* 获取头
*
* @return 头信息
* @since 5.7.2
*/
public JWTHeader getHeader() {
return this.header;
}
/**
* 获取头信息
*
@@ -223,6 +244,16 @@ public class JWT {
return this.payload.getClaimsJson();
}
/**
* 获取载荷对象
*
* @return 载荷信息
* @since 5.7.2
*/
public JWTPayload getPayload() {
return this.payload;
}
/**
* 获取载荷信息
*
@@ -230,7 +261,7 @@ public class JWT {
* @return 载荷信息
*/
public Object getPayload(String name) {
return this.payload.getClaim(name);
return getPayload().getClaim(name);
}
/**
@@ -240,6 +271,7 @@ public class JWT {
* @param value 头
* @return this
*/
@Override
public JWT setPayload(String name, Object value) {
this.payload.setClaim(name, value);
return this;
@@ -304,7 +336,10 @@ public class JWT {
* @return 是否有效
*/
public boolean verify(JWTSigner signer) {
Assert.notNull(signer, () -> new JWTException("No Signer provided!"));
if(null == signer){
// 如果无签名器提供默认认为是无签名JWT信息
signer = NoneJWTSigner.NONE;
}
final List<String> tokens = this.tokens;
if (CollUtil.isEmpty(tokens)) {

View File

@@ -1,6 +1,5 @@
package cn.hutool.jwt;
import java.util.Date;
import java.util.Map;
/**
@@ -18,117 +17,9 @@ import java.util.Map;
* @author looly
* @since 5.7.0
*/
public class JWTPayload extends Claims {
public class JWTPayload extends Claims implements RegisteredPayload<JWTPayload>{
private static final long serialVersionUID = 1L;
/**
* jwt签发者
*/
public static String ISSUER = "iss";
/**
* jwt所面向的用户
*/
public static String SUBJECT = "sub";
/**
* 接收jwt的一方
*/
public static String AUDIENCE = "aud";
/**
* jwt的过期时间这个过期时间必须要大于签发时间
*/
public static String EXPIRES_AT = "exp";
/**
* 定义在什么时间之前该jwt都是不可用的.
*/
public static String NOT_BEFORE = "nbf";
/**
* jwt的签发时间
*/
public static String ISSUED_AT = "iat";
/**
* jwt的唯一身份标识主要用来作为一次性token,从而回避重放攻击。
*/
public static String JWT_ID = "jti";
/**
* 设置 jwt签发者("iss")的Payload值
*
* @param issuer jwt签发者
* @return this
*/
public JWTPayload setIssuer(String issuer) {
setClaim(ISSUER, issuer);
return this;
}
/**
* 设置jwt所面向的用户("sub")的Payload值
*
* @param subject jwt所面向的用户
* @return this
*/
public JWTPayload setSubject(String subject) {
setClaim(SUBJECT, subject);
return this;
}
/**
* 设置接收jwt的一方("aud")的Payload值
*
* @param audience 接收jwt的一方
* @return this
*/
public JWTPayload setAudience(String... audience) {
setClaim(AUDIENCE, audience);
return this;
}
/**
* Add a specific Expires At ("exp") claim to the Payload.
* 设置jwt的过期时间("exp")的Payload值这个过期时间必须要大于签发时间
*
* @param expiresAt jwt的过期时间
* @return this
* @see #setIssuedAt(Date)
*/
public JWTPayload setExpiresAt(Date expiresAt) {
setClaim(EXPIRES_AT, expiresAt);
return this;
}
/**
* 设置不可用时间点界限("nbf")的Payload值
*
* @param notBefore 不可用时间点界限在这个时间点之前jwt不可用
* @return this
*/
public JWTPayload setNotBefore(Date notBefore) {
setClaim(NOT_BEFORE, notBefore);
return this;
}
/**
* 设置jwt的签发时间("iat")
*
* @param issuedAt 签发时间
* @return this
*/
public JWTPayload setIssuedAt(Date issuedAt) {
setClaim(ISSUED_AT, issuedAt);
return this;
}
/**
* 设置jwt的唯一身份标识("jti")
*
* @param jwtId 唯一身份标识
* @return this
*/
public JWTPayload setJWTId(String jwtId) {
setClaim(JWT_ID, jwtId);
return this;
}
/**
* 增加自定义JWT认证载荷信息
*
@@ -139,4 +30,10 @@ public class JWTPayload extends Claims {
putAll(payloadClaims);
return this;
}
@Override
public JWTPayload setPayload(String name, Object value) {
setClaim(name, value);
return this;
}
}

View File

@@ -0,0 +1,176 @@
package cn.hutool.jwt;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.NoneJWTSigner;
import java.util.Date;
/**
* JWT数据校验器用于校验包括
* <ul>
* <li>算法是否一致</li>
* <li>算法签名是否正确</li>
* <li>字段值是否有效(例如时间未过期等)</li>
* </ul>
*
* @author looly
* @since 5.7.2
*/
public class JWTValidator {
private final JWT jwt;
/**
* 创建JWT验证器
*
* @param token JWT Token
* @return {@link JWTValidator}
*/
public static JWTValidator of(String token) {
return new JWTValidator(JWT.of(token));
}
/**
* 创建JWT验证器
*
* @param jwt JWT对象
* @return {@link JWTValidator}
*/
public static JWTValidator of(JWT jwt) {
return new JWTValidator(jwt);
}
/**
* 构造
*
* @param jwt JWT对象
*/
public JWTValidator(JWT jwt) {
this.jwt = jwt;
}
/**
* 验证算法使用JWT对象自带的{@link JWTSigner}
*
* @return this
* @throws ValidateException 验证失败的异常
*/
public JWTValidator validateAlgorithm() throws ValidateException {
return validateAlgorithm(null);
}
/**
* 验证算法,使用自定义的{@link JWTSigner}
*
* @param signer 用于验证算法的签名器
* @return this
* @throws ValidateException 验证失败的异常
*/
public JWTValidator validateAlgorithm(JWTSigner signer) throws ValidateException {
validateAlgorithm(this.jwt, signer);
return this;
}
/**
* 检查JWT的以下三两个时间
*
* <ul>
* <li>{@link JWTPayload#NOT_BEFORE}:被检查时间必须晚于生效时间</li>
* <li>{@link JWTPayload#EXPIRES_AT}:被检查时间必须早于失效时间</li>
* <li>{@link JWTPayload#ISSUED_AT}:签发时间必须早于失效时间</li>
* </ul>
* <p>
* 如果某个时间没有设置,则不检查(表示无限制)
*
* @param dateToCheck 被检查的时间,一般为当前时间
* @return this
* @throws ValidateException 验证失败的异常
*/
public JWTValidator validateDate(Date dateToCheck) throws ValidateException {
validateDate(this.jwt.getPayload(), dateToCheck);
return this;
}
/**
* 验证算法
*
* @param jwt {@link JWT}对象
* @param signer 用于验证的签名器
* @throws ValidateException 验证异常
*/
private static void validateAlgorithm(JWT jwt, JWTSigner signer) throws ValidateException {
final String algorithmId = jwt.getAlgorithm();
if (null == signer) {
signer = jwt.getSigner();
}
if (StrUtil.isEmpty(algorithmId)) {
// 可能无签名
if (null == signer || signer instanceof NoneJWTSigner) {
return;
}
throw new ValidateException("No algorithm defined in header!");
}
if (null == signer) {
throw new IllegalArgumentException("No Signer for validate algorithm!");
}
final String algorithmIdInSigner = signer.getAlgorithmId();
if (false == StrUtil.equals(algorithmId, algorithmIdInSigner)) {
throw new ValidateException("Algorithm [{}] defined in header doesn't match to [{}]!"
, algorithmId, algorithmIdInSigner);
}
// 通过算法验证签名是否正确
if (false == jwt.verify(signer)) {
throw new ValidateException("Signature verification failed!");
}
}
/**
* 检查JWT的以下三两个时间
*
* <ul>
* <li>{@link JWTPayload#NOT_BEFORE}:被检查时间必须晚于生效时间</li>
* <li>{@link JWTPayload#EXPIRES_AT}:被检查时间必须早于失效时间</li>
* <li>{@link JWTPayload#ISSUED_AT}:签发时间必须早于失效时间</li>
* </ul>
* <p>
* 如果某个时间没有设置,则不检查(表示无限制)
*
* @param payload {@link JWTPayload}
* @param dateToCheck 被检查的时间,一般为当前时间
* @throws ValidateException 验证异常
*/
private static void validateDate(JWTPayload payload, Date dateToCheck) throws ValidateException {
if (null == dateToCheck) {
// 默认当前时间
dateToCheck = DateUtil.date();
}
// 检查生效时间(被检查时间必须晚于生效时间)
final Date notBefore = payload.getClaimsJson().getDate(JWTPayload.NOT_BEFORE);
if (null != notBefore && dateToCheck.before(notBefore)) {
throw new ValidateException("Current date [{}] is before 'nbf' [{}]",
dateToCheck, DateUtil.date(notBefore));
}
// 检查失效时间(被检查时间必须早于失效时间)
final Date expiresAt = payload.getClaimsJson().getDate(JWTPayload.EXPIRES_AT);
if (null != expiresAt && dateToCheck.after(expiresAt)) {
throw new ValidateException("Current date [{}] is after 'exp' [{}]",
dateToCheck, DateUtil.date(expiresAt));
}
// 检查签发时间(被检查时间必须晚于签发时间)
final Date issueAt = payload.getClaimsJson().getDate(JWTPayload.ISSUED_AT);
if (null != issueAt && dateToCheck.before(issueAt)) {
throw new ValidateException("Current date [{}] is before 'iat' [{}]",
dateToCheck, DateUtil.date(issueAt));
}
}
}

View File

@@ -0,0 +1,122 @@
package cn.hutool.jwt;
import java.util.Date;
/**
* 注册的标准载荷Payload声明
*
* @param <T> 实现此接口的类的类型
* @author looly
* @since 5.7.2
*/
public interface RegisteredPayload<T extends RegisteredPayload<T>> {
/**
* jwt签发者
*/
String ISSUER = "iss";
/**
* jwt所面向的用户
*/
String SUBJECT = "sub";
/**
* 接收jwt的一方
*/
String AUDIENCE = "aud";
/**
* jwt的过期时间这个过期时间必须要大于签发时间
*/
String EXPIRES_AT = "exp";
/**
* 生效时间定义在什么时间之前该jwt都是不可用的.
*/
String NOT_BEFORE = "nbf";
/**
* jwt的签发时间
*/
String ISSUED_AT = "iat";
/**
* jwt的唯一身份标识主要用来作为一次性token,从而回避重放攻击。
*/
String JWT_ID = "jti";
/**
* 设置 jwt签发者("iss")的Payload值
*
* @param issuer jwt签发者
* @return this
*/
default T setIssuer(String issuer) {
return setPayload(ISSUER, issuer);
}
/**
* 设置jwt所面向的用户("sub")的Payload值
*
* @param subject jwt所面向的用户
* @return this
*/
default T setSubject(String subject) {
return setPayload(SUBJECT, subject);
}
/**
* 设置接收jwt的一方("aud")的Payload值
*
* @param audience 接收jwt的一方
* @return this
*/
default T setAudience(String... audience) {
return setPayload(AUDIENCE, audience);
}
/**
* 设置jwt的过期时间("exp")的Payload值这个过期时间必须要大于签发时间
*
* @param expiresAt jwt的过期时间
* @return this
* @see #setIssuedAt(Date)
*/
default T setExpiresAt(Date expiresAt) {
return setPayload(EXPIRES_AT, expiresAt);
}
/**
* 设置不可用时间点界限("nbf")的Payload值
*
* @param notBefore 不可用时间点界限在这个时间点之前jwt不可用
* @return this
*/
default T setNotBefore(Date notBefore) {
return setPayload(NOT_BEFORE, notBefore);
}
/**
* 设置jwt的签发时间("iat")
*
* @param issuedAt 签发时间
* @return this
*/
default T setIssuedAt(Date issuedAt) {
return setPayload(ISSUED_AT, issuedAt);
}
/**
* 设置jwt的唯一身份标识("jti")
*
* @param jwtId 唯一身份标识
* @return this
*/
default T setJWTId(String jwtId) {
return setPayload(JWT_ID, jwtId);
}
/**
* 设置Payload值
*
* @param name payload名
* @param value payload值
* @return this
*/
T setPayload(String name, Object value);
}

View File

@@ -21,7 +21,7 @@ public interface JWTSigner {
*
* @param headerBase64 JWT头的JSON字符串Base64表示
* @param payloadBase64 JWT载荷的JSON字符串Base64表示
* @param signBase64 被验证的签名Base64表示
* @param signBase64 被验证的签名Base64表示
* @return 签名是否一致
*/
boolean verify(String headerBase64, String payloadBase64, String signBase64);
@@ -32,4 +32,14 @@ public interface JWTSigner {
* @return 算法
*/
String getAlgorithm();
/**
* 获取算法ID即算法的简写形式如HS256
*
* @return 算法ID
* @since 5.7.2
*/
default String getAlgorithmId() {
return AlgorithmUtil.getId(getAlgorithm());
}
}

View File

@@ -0,0 +1,63 @@
package cn.hutool.jwt;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.jwt.signers.JWTSignerUtil;
import org.junit.Test;
public class JWTValidatorTest {
@Test(expected = ValidateException.class)
public void expiredAtTest(){
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo";
JWTValidator.of(token).validateDate(DateUtil.date());
}
@Test(expected = ValidateException.class)
public void issueAtTest(){
final String token = JWT.create()
.setIssuedAt(DateUtil.date())
.setKey("123456".getBytes())
.sign();
// 签发时间早于被检查的时间
JWTValidator.of(token).validateDate(DateUtil.yesterday());
}
@Test
public void issueAtPassTest(){
final String token = JWT.create()
.setIssuedAt(DateUtil.date())
.setKey("123456".getBytes())
.sign();
// 签发时间早于被检查的时间
JWTValidator.of(token).validateDate(DateUtil.date());
}
@Test(expected = ValidateException.class)
public void notBeforeTest(){
final JWT jwt = JWT.create()
.setNotBefore(DateUtil.date());
JWTValidator.of(jwt).validateDate(DateUtil.yesterday());
}
@Test
public void notBeforePassTest(){
final JWT jwt = JWT.create()
.setNotBefore(DateUtil.date());
JWTValidator.of(jwt).validateDate(DateUtil.date());
}
@Test
public void validateAlgorithmTest(){
final String token = JWT.create()
.setNotBefore(DateUtil.date())
.setKey("123456".getBytes())
.sign();
// 验证算法
JWTValidator.of(token).validateAlgorithm(JWTSignerUtil.hs256("123456".getBytes()));
}
}