diff --git a/CHANGELOG.md b/CHANGELOG.md index 5221d1ce9..ceda3180f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.7.2 (2021-06-18) +# 5.7.2 (2021-06-19) ### 🐣新特性 * 【core 】 增加UserPassAuthenticator @@ -18,6 +18,7 @@ * 【core 】 修复ConcurrencyTester重复使用时开始测试未清空之前任务的问题(issue#I3VSDO@Gitee) * 【poi 】 修复使用BigWriter写出,ExcelWriter修改单元格值失败的问题(issue#I3VSDO@Gitee) * 【jwt 】 修复Hmac算法下生成签名是hex的问题(issue#I3W6IP@Gitee) +* 【jwt 】 修复TreeUtil.build中deep失效问题(issue#1661@Github) ------------------------------------------------------------------------------------------------------------- diff --git a/hutool-all/src/main/java/cn/hutool/Hutool.java b/hutool-all/src/main/java/cn/hutool/Hutool.java index 4c17cf16e..1b1b9749a 100644 --- a/hutool-all/src/main/java/cn/hutool/Hutool.java +++ b/hutool-all/src/main/java/cn/hutool/Hutool.java @@ -23,6 +23,11 @@ import cn.hutool.core.util.StrUtil; import java.util.Set; /** + *

+ * 秋千水,竹马道,一眼见你,万物不及。
+ * 春水生,春林初胜,春风十里不如你。 + *

+ * *

* Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 *

diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java index 6800a9213..2475cb022 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java @@ -118,6 +118,9 @@ public class ConverterRegistry implements Serializable { return SingletonHolder.INSTANCE; } + /** + * 构造 + */ public ConverterRegistry() { defaultConverter(); putCustomBySpi(); diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java b/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java index 190071050..29f06a07f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java @@ -724,8 +724,8 @@ public class DateTime extends Date { * 当前日期是否在日期指定范围内
* 起始日期和结束日期可以互换 * - * @param beginDate 起始日期 - * @param endDate 结束日期 + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) * @return 是否在范围内 * @since 3.0.8 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java index 40fb5f143..195fb831b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -1464,8 +1464,8 @@ public class DateUtil extends CalendarUtil { * 起始日期和结束日期可以互换 * * @param date 被检查的日期 - * @param beginDate 起始日期 - * @param endDate 结束日期 + * @param beginDate 起始日期(包含) + * @param endDate 结束日期(包含) * @return 是否在范围内 * @since 3.0.8 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java index c1aa96f51..e0d61aa72 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java @@ -88,7 +88,7 @@ public class PatternPool { * * @see 800 */ - public final static Pattern TEL_400_800 = Pattern.compile("(?:(?:0\\d{2,3}[\\- ]?[1-9]\\d{6,7})|(?:[48]00[\\- ]?[1-9]\\d{6}))"); + public final static Pattern TEL_400_800 = Pattern.compile("0\\d{2,3}[\\- ]?[1-9]\\d{6,7}|[48]00[\\- ]?[1-9]\\d{6}"); /** * 18位身份证号码 */ @@ -124,7 +124,7 @@ public class PatternPool { /** * MAC地址正则 */ - public static final Pattern MAC_ADDRESS = Pattern.compile("((?:[A-F0-9]{1,2}[:-]){5}[A-F0-9]{1,2})|(?:0x)(\\d{12})(?:.+ETHER)", Pattern.CASE_INSENSITIVE); + public static final Pattern MAC_ADDRESS = Pattern.compile("((?:[A-F0-9]{1,2}[:-]){5}[A-F0-9]{1,2})|0x(\\d{12}).+ETHER", Pattern.CASE_INSENSITIVE); /** * 16进制字符串 */ diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java index 101170556..1a6feb82b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java @@ -14,7 +14,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * 字段验证器 + * 字段验证器(验证器),分两种类型的验证: + * + * + * + * 主要验证字段非空、是否为满足指定格式等(如是否为Email、电话等) * * @author Looly */ diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/tree/Tree.java b/hutool-core/src/main/java/cn/hutool/core/lang/tree/Tree.java index 2ca102502..f97bb23ab 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/tree/Tree.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/tree/Tree.java @@ -164,11 +164,22 @@ public class Tree extends LinkedHashMap implements Node { return this; } + /** + * 获取所有子节点 + * + * @return 所有子节点 + */ @SuppressWarnings("unchecked") public List> getChildren() { return (List>) this.get(treeNodeConfig.getChildrenKey()); } + /** + * 设置子节点,设置后会覆盖所有原有子节点 + * + * @param children 子节点列表 + * @return this + */ public Tree setChildren(List> children) { this.put(treeNodeConfig.getChildrenKey(), children); return this; diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java b/hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java index b1e6a9814..de7e9f1ad 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/tree/TreeBuilder.java @@ -2,6 +2,7 @@ package cn.hutool.core.lang.tree; import cn.hutool.core.builder.Builder; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.tree.parser.NodeParser; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; @@ -21,6 +22,7 @@ public class TreeBuilder implements Builder> { private final Tree root; private final Map> idTreeMap; + private boolean isBuild; /** * 创建Tree构建器 @@ -64,6 +66,9 @@ public class TreeBuilder implements Builder> { * @return this */ public TreeBuilder append(Map> map) { + checkBuilt(); + + Assert.isFalse(isBuild, "Current tree has been built."); this.idTreeMap.putAll(map); return this; } @@ -75,6 +80,8 @@ public class TreeBuilder implements Builder> { * @return this */ public TreeBuilder append(Iterable> trees) { + checkBuilt(); + for (Tree tree : trees) { this.idTreeMap.put(tree.getId(), tree); } @@ -90,6 +97,8 @@ public class TreeBuilder implements Builder> { * @return this */ public TreeBuilder append(List list, NodeParser nodeParser) { + checkBuilt(); + final TreeNodeConfig config = this.root.getConfig(); final Map> map = new LinkedHashMap<>(list.size(), 1); Tree node; @@ -101,38 +110,69 @@ public class TreeBuilder implements Builder> { return append(map); } + + /** + * 重置Builder,实现复用 + * + * @return this + */ + public TreeBuilder reset() { + this.idTreeMap.clear(); + this.root.setChildren(null); + this.isBuild = false; + return this; + } + @Override public Tree build() { + checkBuilt(); + buildFromMap(); + cutTree(); + + this.isBuild = true; + this.idTreeMap.clear(); + return root; } /** - * 构建树列表,例如: + * 构建树列表,没有顶层节点,例如: * *
 	 * -用户管理
-	 *  --用户添加
-	 *  --用户管理
+	 *  -用户管理
+	 *    +用户添加
 	 * - 部门管理
-	 *  --部门添加
-	 *  --部门管理
+	 *  -部门管理
+	 *    +部门添加
 	 * 
* * @return 树列表 */ public List> buildList() { - return this.root.getChildren(); + if (isBuild) { + // 已经构建过了 + return this.root.getChildren(); + } + return build().getChildren(); } /** * 开始构建 */ private void buildFromMap() { + if (MapUtil.isEmpty(this.idTreeMap)) { + return; + } + final Map> eTreeMap = MapUtil.sortByValue(this.idTreeMap, false); List> rootTreeList = CollUtil.newArrayList(); E parentId; for (Tree node : eTreeMap.values()) { + if (null == node) { + continue; + } parentId = node.getParentId(); if (ObjectUtil.equals(this.root.getId(), parentId)) { this.root.addChildren(node); @@ -146,4 +186,48 @@ public class TreeBuilder implements Builder> { } } } + + /** + * 树剪枝 + */ + private void cutTree() { + final TreeNodeConfig config = this.root.getConfig(); + final Integer deep = config.getDeep(); + if (null == deep || deep < 0) { + return; + } + cutTree(this.root, 0, deep); + } + + /** + * 树剪枝叶 + * + * @param tree 节点 + * @param currentDepp 当前层级 + * @param maxDeep 最大层级 + */ + private void cutTree(Tree tree, int currentDepp, int maxDeep) { + if (null == tree) { + return; + } + if (currentDepp == maxDeep) { + // 剪枝 + tree.setChildren(null); + return; + } + + final List> children = tree.getChildren(); + if (CollUtil.isNotEmpty(children)) { + for (Tree child : children) { + cutTree(child, currentDepp + 1, maxDeep); + } + } + } + + /** + * 检查是否已经构建 + */ + private void checkBuilt() { + Assert.isFalse(isBuild, "Current tree has been built."); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java index 3d382f25d..397744d25 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -134,11 +134,11 @@ public class NumberUtil { } Number value = values[0]; - BigDecimal result = null == value ? BigDecimal.ZERO : new BigDecimal(value.toString()); + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (null != value) { - result = result.add(new BigDecimal(value.toString())); + result = result.add(toBigDecimal(value)); } } return result; @@ -158,11 +158,11 @@ public class NumberUtil { } String value = values[0]; - BigDecimal result = StrUtil.isBlank(value) ? BigDecimal.ZERO : new BigDecimal(value); + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (StrUtil.isNotBlank(value)) { - result = result.add(new BigDecimal(value)); + result = result.add(toBigDecimal(value)); } } return result; @@ -182,7 +182,7 @@ public class NumberUtil { } BigDecimal value = values[0]; - BigDecimal result = null == value ? BigDecimal.ZERO : value; + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (null != value) { @@ -274,11 +274,11 @@ public class NumberUtil { } Number value = values[0]; - BigDecimal result = null == value ? BigDecimal.ZERO : new BigDecimal(value.toString()); + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (null != value) { - result = result.subtract(new BigDecimal(value.toString())); + result = result.subtract(toBigDecimal(value)); } } return result; @@ -298,11 +298,11 @@ public class NumberUtil { } String value = values[0]; - BigDecimal result = StrUtil.isBlank(value) ? BigDecimal.ZERO : new BigDecimal(value); + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (StrUtil.isNotBlank(value)) { - result = result.subtract(new BigDecimal(value)); + result = result.subtract(toBigDecimal(value)); } } return result; @@ -322,7 +322,7 @@ public class NumberUtil { } BigDecimal value = values[0]; - BigDecimal result = null == value ? BigDecimal.ZERO : value; + BigDecimal result = toBigDecimal(value); for (int i = 1; i < values.length; i++) { value = values[i]; if (null != value) { @@ -729,9 +729,7 @@ public class NumberUtil { * @return 两个参数的商 */ public static BigDecimal div(String v1, String v2, int scale, RoundingMode roundingMode) { - final BigDecimal bd1 = StrUtil.isBlank(v1) ? BigDecimal.ZERO : new BigDecimal(v1); - final BigDecimal bd2 = StrUtil.isBlank(v2) ? BigDecimal.ZERO : new BigDecimal(v2); - return div(bd1, bd2, scale, roundingMode); + return div(toBigDecimal(v1), toBigDecimal(v2), scale, roundingMode); } /** diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeBuilderTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeBuilderTest.java new file mode 100644 index 000000000..4cc1d85d1 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeBuilderTest.java @@ -0,0 +1,16 @@ +package cn.hutool.core.lang.tree; + +import org.junit.Test; + +import java.util.ArrayList; + +public class TreeBuilderTest { + + @Test(expected = IllegalArgumentException.class) + public void checkIsBuiltTest(){ + final TreeBuilder of = TreeBuilder.of(0); + of.build(); + of.append(new ArrayList<>()); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeTest.java index 6202224bb..b376e17c3 100644 --- a/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/lang/tree/TreeTest.java @@ -50,7 +50,7 @@ public class TreeTest { // 自定义属性名 都要默认值的 treeNodeConfig.setWeightKey("order"); treeNodeConfig.setIdKey("rid"); - treeNodeConfig.setDeep(3); + treeNodeConfig.setDeep(2); //转换器 List> treeNodes = TreeUtil.build(nodeList, "0", treeNodeConfig, @@ -66,4 +66,5 @@ public class TreeTest { Assert.assertEquals(treeNodes.size(), 2); } + } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java index dc2186d93..cc9913c45 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java @@ -48,6 +48,12 @@ public class NumberUtilTest { Assert.assertEquals(new BigDecimal("464"), result); } + @Test + public void addBlankTest(){ + BigDecimal result = NumberUtil.add("123", " "); + Assert.assertEquals(new BigDecimal("123"), result); + } + @Test public void isIntegerTest() { Assert.assertTrue(NumberUtil.isInteger("-12")); diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/JWT.java b/hutool-jwt/src/main/java/cn/hutool/jwt/JWT.java index 57f4aa57d..40c4aea79 100644 --- a/hutool-jwt/src/main/java/cn/hutool/jwt/JWT.java +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/JWT.java @@ -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)用于在网络应用环境间传递声明。
*

- * 结构:xxxxx.yyyyy.zzzzz + * 结构:header.payload.signature *

    *
  • header:主要声明了JWT的签名算法
  • *
  • payload:主要承载了各种声明并传递明文数据
  • - *
  • signture:拥有该部分的JWT被称为JWS,也就是签了名的JWS
  • + *
  • signature:拥有该部分的JWT被称为JWS,也就是签了名的JWS
  • *
* *

@@ -32,8 +33,9 @@ import java.util.Map; *

* * @author looly + * @since 5.7.0 */ -public class JWT { +public class JWT implements RegisteredPayload{ 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 tokens = this.tokens; if (CollUtil.isEmpty(tokens)) { diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/JWTPayload.java b/hutool-jwt/src/main/java/cn/hutool/jwt/JWTPayload.java index fc0ebf3e7..52e716b44 100644 --- a/hutool-jwt/src/main/java/cn/hutool/jwt/JWTPayload.java +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/JWTPayload.java @@ -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{ 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; + } } diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/JWTValidator.java b/hutool-jwt/src/main/java/cn/hutool/jwt/JWTValidator.java new file mode 100644 index 000000000..c45fa73dc --- /dev/null +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/JWTValidator.java @@ -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数据校验器,用于校验包括: + *
    + *
  • 算法是否一致
  • + *
  • 算法签名是否正确
  • + *
  • 字段值是否有效(例如时间未过期等)
  • + *
+ * + * @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的以下三两个时间: + * + *
    + *
  • {@link JWTPayload#NOT_BEFORE}:被检查时间必须晚于生效时间
  • + *
  • {@link JWTPayload#EXPIRES_AT}:被检查时间必须早于失效时间
  • + *
  • {@link JWTPayload#ISSUED_AT}:签发时间必须早于失效时间
  • + *
+ *

+ * 如果某个时间没有设置,则不检查(表示无限制) + * + * @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的以下三两个时间: + * + *

    + *
  • {@link JWTPayload#NOT_BEFORE}:被检查时间必须晚于生效时间
  • + *
  • {@link JWTPayload#EXPIRES_AT}:被检查时间必须早于失效时间
  • + *
  • {@link JWTPayload#ISSUED_AT}:签发时间必须早于失效时间
  • + *
+ *

+ * 如果某个时间没有设置,则不检查(表示无限制) + * + * @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)); + } + } +} diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/RegisteredPayload.java b/hutool-jwt/src/main/java/cn/hutool/jwt/RegisteredPayload.java new file mode 100644 index 000000000..f0c0fc8e0 --- /dev/null +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/RegisteredPayload.java @@ -0,0 +1,122 @@ +package cn.hutool.jwt; + +import java.util.Date; + +/** + * 注册的标准载荷(Payload)声明 + * + * @param 实现此接口的类的类型 + * @author looly + * @since 5.7.2 + */ +public interface RegisteredPayload> { + + /** + * 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); +} diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSigner.java b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSigner.java index 722ad4d26..b6301a4bd 100644 --- a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSigner.java +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSigner.java @@ -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()); + } } diff --git a/hutool-jwt/src/test/java/cn/hutool/jwt/JWTValidatorTest.java b/hutool-jwt/src/test/java/cn/hutool/jwt/JWTValidatorTest.java new file mode 100644 index 000000000..88e7edb86 --- /dev/null +++ b/hutool-jwt/src/test/java/cn/hutool/jwt/JWTValidatorTest.java @@ -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())); + } +}