4. 票据验证流程:请求拦截器设计、票据解析与校验、过期时间检查、黑名单机制
票据验证,说白了就是系统的一道安检门。
你想想看,用户拿着票据来访问你的服务,你不能直接放行吧?得先看看这票是真的假的,有没有过期,是不是已经被拉黑了。我见过不少团队,前期图省事,验证逻辑写得特别糙,结果线上出了安全问题,追悔莫及。
这一节,我们就来拆解一下完整的票据验证流程。我会结合我自己的实战经验,把每一步的关键细节都讲透。
4.1 请求拦截器:第一道防线
拦截器是验证的起点。它的职责很简单:在请求到达业务逻辑之前,把票据截住。
我个人习惯把拦截器设计成链式结构。什么意思呢?就是一条链上挂多个拦截器,每个只干一件事。比如:
- Token 提取器:从 Header、Cookie 或 URL 参数里把票据字符串捞出来。
- 黑名单检查器:先查一下这个票据是不是已经被拉黑了。如果是,直接返回 401。
- 票据解析器:把字符串解析成结构化对象。
- 过期检查器:看看时间戳是不是已经过了。
这样做的好处是,每个环节可以独立测试,出了问题也容易定位。我在项目中遇到过一种情况:某个拦截器里塞了太多逻辑,结果解析失败时,连黑名单都没来得及查,就把一个本该被拦截的请求放过去了。嗯,从那以后,我坚持每个拦截器只做一件事。
核心原则:拦截器只负责“拦截”和“放行”,不负责“处理”。验证通过后,把解析好的票据信息塞进请求上下文,交给后面的业务代码去用。
下面是一个简单的拦截器设计示例:
// 伪代码:请求拦截器链
public class TokenValidationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 提取票据
String token = extractToken(request);
if (token == null) {
response.setStatus(401);
return false;
}
// 2. 黑名单检查
if (blacklistService.isBlacklisted(token)) {
response.setStatus(401);
return false;
}
// 3. 解析票据
TokenPayload payload = tokenParser.parse(token);
if (payload == null) {
response.setStatus(401);
return false;
}
// 4. 过期检查
if (payload.getExpireAt() < System.currentTimeMillis()) {
response.setStatus(401);
return false;
}
// 5. 放行,并把票据信息存入上下文
request.setAttribute("currentUser", payload);
return true;
}
}
4.2 票据解析与校验:别信任何外部输入
票据解析,就是把字符串还原成有意义的数据结构。这一步最容易出问题。
我见过有人直接用 JSON.parse 去解析 JWT 的 Payload,连签名都不验证。这太危险了。你想想看,攻击者完全可以伪造一个 Payload,里面塞个 admin: true,你的系统就以为他是管理员了。
正确的做法是:
- 先验签名:用服务端的密钥去验证票据的签名。签名不对,直接拒绝。
- 再验结构:解析出来的字段,类型对不对?有没有缺失必填字段?
- 最后验内容:比如用户 ID 是不是合法的格式?角色字段是不是在允许的范围内?
注意:解析时一定要做异常捕获。我曾经遇到过一种情况,某个客户端传了一个格式错误的票据,解析器直接抛了空指针异常,导致整个请求处理流程崩溃。从那以后,我在解析代码外面包了一层 try-catch,任何解析异常都统一返回 401。
解析后的数据结构,我一般会设计成这样:
| 字段 | 类型 | 说明 |
|---|---|---|
| userId | String | 用户唯一标识 |
| role | String | 用户角色(如 admin, user) |
| issuedAt | long | 签发时间戳(毫秒) |
| expireAt | long | 过期时间戳(毫秒) |
| signature | String | 签名值(用于完整性校验) |
4.3 过期时间检查:别卡死在最后一秒
过期检查看起来简单,就是比一下当前时间和过期时间。但这里有个坑:时钟偏差。
你想想看,签发票据的服务器和验证票据的服务器,时间可能不完全一致。如果验证服务器的时间比签发服务器慢了几秒,那一个刚签发的票据可能就被误判为过期了。
我的做法是引入一个宽容窗口。比如,允许票据在过期后 30 秒内仍然有效。这个窗口不能太大,否则就失去了过期的意义;也不能太小,否则时钟偏差就会导致误杀。
经验值:宽容窗口一般设为 30 秒到 60 秒。对于跨机房部署的场景,可以适当放大到 120 秒。但记住,宽容窗口只用于“过期检查”,不能用于“签发”。签发的过期时间必须是精确的。
检查逻辑的伪代码:
// 过期检查,带宽容窗口
long gracePeriod = 30_000; // 30秒
long now = System.currentTimeMillis();
if (payload.getExpireAt() + gracePeriod < now) {
// 真的过期了
throw new TokenExpiredException("票据已过期");
}
// 还在宽容窗口内,放行
4.4 黑名单机制:让票据“死透”
黑名单是最后一道保险。有些场景下,票据还没过期,但用户已经登出了,或者管理员把某个用户封了。这时候,票据必须立即失效。
黑名单的实现方式有很多种:
- 内存 Set:最简单,但重启就丢了,不适合生产环境。
- Redis Set:常用方案。用票据的唯一标识(比如 JWT 的 jti)作为 key,过期时间设为票据的剩余有效期。
- 数据库表:适合需要持久化审计的场景,但性能不如 Redis。
我个人推荐用 Redis。原因很简单:快,而且可以自动过期。你想想看,一个票据的有效期可能是 2 小时,如果它被拉黑了,那 2 小时后它本来就该过期了,Redis 里的记录也可以自动删掉,省得我们手动清理。
关键点:黑名单检查一定要放在票据解析之前。为什么?因为解析本身可能消耗资源(比如解密、验签),如果一个票据已经在黑名单里了,就没必要浪费资源去解析它了。
下面是一个完整的票据验证流程图,我把整个流程串起来,方便你理解:
你看,整个流程是线性的,每一步都有明确的拒绝条件。这样做的好处是,逻辑清晰,容易排查问题。我在项目中遇到过一种情况:某个请求被拒绝了,但不知道是哪一步出的问题。后来我在每一步都加了日志,打印出拒绝原因,问题一下子就定位到了。
小技巧:在拦截器里,每一步拒绝时都返回不同的 HTTP 状态码或错误码。比如黑名单拒绝返回 40101,过期拒绝返回 40102。这样客户端可以根据不同的错误码做不同的处理,比如过期了就自动刷新票据,被拉黑了就强制用户重新登录。
好了,票据验证的四个核心环节就讲完了。记住,拦截器是骨架,解析是血肉,过期检查是时间线,黑名单是安全网。把这四块做好了,你的票据验证体系就稳了。