一、常规校验
在 JWT 的使用中,基于 JWT 的 SDK 可以做基础的有效性校验(不包含业务规则)。
这些校验一般包括:
- 生效时间(Payload 中的
nbf
字段,Not Before) - 过期时间(Payload 中的
exp
字段,Expiration Time) - 签名合法性
二、iat
字段的坑
在 Payload 部分,有个 iat
字段(Issued At:签发时间)。在我们的理解中,这个字段只表示签发时间,是个标记,并不参与校验。但实际上并不一定是这样的。
1、RFC 文档中的描述
4.1.6. "iat" (Issued At) Claim
4.1.6. "iat" (Issued At) Claim
The "iat" (issued at) claim identifies the time at which the JWT was
issued. This claim can be used to determine the age of the JWT. Its
value MUST be a number containing a NumericDate value. Use of this
claim is OPTIONAL.
这里表述了如下几个意思:
- 字段作用:表示 JWT 的颁发时间
- 可以用于确定 JWT 的时效性
- 此字段可选
但是有一点文档中没有明确:即当这个字段有值时,其值是否会影响 JWT 的有效性。
2、JWT 库的实现
Java 项目中,使用较多的 JWT 库是 auth0/java-jwt。其最新版本(4.4)的校验实现部分是这样的:
private void addMandatoryClaimChecks() {
long expiresAtLeeway = this.getLeewayFor("exp");
long notBeforeLeeway = this.getLeewayFor("nbf");
long issuedAtLeeway = this.getLeewayFor("iat");
this.expectedChecks.add(this.constructExpectedCheck("exp", (claim, decodedJWT) -> {
return this.assertValidInstantClaim("exp", claim, expiresAtLeeway, true);
}));
this.expectedChecks.add(this.constructExpectedCheck("nbf", (claim, decodedJWT) -> {
return this.assertValidInstantClaim("nbf", claim, notBeforeLeeway, false);
}));
if (!this.ignoreIssuedAt) {
this.expectedChecks.add(this.constructExpectedCheck("iat", (claim, decodedJWT) -> {
return this.assertValidInstantClaim("iat", claim, issuedAtLeeway, false);
}));
}
}
从这里可以看到:
-
iat
字段通过配置来决定是否参与到校验,默认是参与校验的(我估计是为了与旧版兼容,稍后贴旧版代码) - 如果
iat
字段参与校验,其内容不允许是当前时间的未来时间
但是在旧版(3.x)版本中,iat
字段是必须参与校验的:
private void verifyClaims(DecodedJWT jwt, Map<String, Object> claims) throws TokenExpiredException, InvalidClaimException {
for (Map.Entry<String, Object> entry : claims.entrySet()) {
switch (entry.getKey()) {
case PublicClaims.AUDIENCE:
//noinspection unchecked
assertValidAudienceClaim(jwt.getAudience(), (List<String>) entry.getValue());
break;
case PublicClaims.EXPIRES_AT:
assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true);
break;
case PublicClaims.ISSUED_AT:
assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false);
break;
case PublicClaims.NOT_BEFORE:
assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false);
break;
case PublicClaims.ISSUER:
assertValidStringClaim(entry.getKey(), jwt.getIssuer(), (String) entry.getValue());
break;
case PublicClaims.JWT_ID:
assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue());
break;
case PublicClaims.SUBJECT:
assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue());
break;
default:
assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue());
break;
}
}
}
三、iat
字段参与校验带来的影响
我们一般习惯添加 iat
字段来记录颁发时间,并且这个时间基本都会取当前时间。
假想以下场景:
A、B 两台主机上的服务交互协作
A 主机上的服务颁发 JWT,由 B 主机上的服务校验
B 主机的时钟比 A 主机慢一点
如果 A 颁发 JWT 时以自己当前的时间写入了 iat
字段,本意只是标识颁发时间。但 B 校验时,iat
字段参与了校验,由于 B 的时钟慢,对于 B 来说,iat
字段中的时间是个未来时间,所以会认为 JWT 不合法,从而导致校验失败。
这问题排查起来简直是觉得无厘头。
四、解决方案
1、没有特别需要,尽量不写入 iat
字段,以规避不确定性。因为一般 JWT 的 SDK 文档中,也不会写这么细。没必要找麻烦;
2、如果自己想做个标识记录下颁发时间,可以使用一个自定义字段记录;
3、如果使用的 SDK 支持指定 iat
字段是否参加校验(如 auth0/java-jwt ),那么尽量在执行校验时,忽略这个字段,不对这个字段进行校验。如果使用的 SDK 不支持自己定义这个选项(如:Auth0 低版本的 JWT 库或者其他某些库),就只能在颁发 JWT 时,通过避免写入 iat
字段来规避这个问题了。
另:这里没有广泛对各种库的实现进行验证,只是发现这个坑先抛出来,大家有验证结果可以补充。
(完)