面试官抛出这个问题时,不只是在考察你知道多少概念,更是想看你有没有真正处理过接口鉴权、登录态维持、并发刷新这类线上问题。很多测试同学在做接口自动化、测试平台集成、小程序/App兼容性测试时,都遇到过“Token失效”导致用例批量失败,但很少去深挖背后机制。本文就把 Token 过期判断、自动更新 Token、以及高频追问的“双 Token”方案完整拆开讲,既有原理也有可直接落地的代码,方便你用在自己的接口测试框架或学习笔记里。
说明一句:本文面向软件测试工程师、测试开发、以及准备面试的初级后端同学,重点是“测试视角”的完整理解,所以不会只贴八股文结论,而是尽量还原真实项目里的处理逻辑。
1. 先从面试场景聊起:这道题到底在考什么
1.1 面试官为什么爱问 Token 过期
面试官问“如何判断 Token 是否过期”,一般不是真让你背 JWT 里的 exp 字段,而是想确认三件事:
- 你是否理解 Token 的生命周期是谁定义的。
- 你是否知道前端和后端分别从哪里感知过期。
- 你是否考虑过 Token 过期后,用户体验如何保证。
例如:一个 App 登录之后,用户一直在浏览页面,没有关闭 App,第二天打开发现还在登录状态。这个“登录状态”是 Token 没变吗?不一定,很可能是前端在 Token 过期后自动刷新了,用户无感知。如果你能把这个过程讲清楚,面试官会认为你有真实项目经验。
1.2 常见答案和加分答案
很多人的第一反应是:
前端拿到 Token 后解析出过期时间,和当前时间比较,如果小于当前时间,就认为过期。这个答案没错,但太表面。加分回答会增加几层:
- 前端的判断只是“预防”,真正能判定 Token 是否有效的是后端。
- 因为 Token 有可能被服务端主动拉黑,比如用户被踢下线、密码修改后所有 Token 作废,此时 JWT 里的 exp 还没到,但 Token 已经不可用了。
- 所以更稳妥的做法是“本地预判断 + 接口请求后强制判断 + 自动续期兜底”。
1.3 测试工程师为什么要懂这个
对于做接口自动化的人来说,Token 失效是脚本失败的一大来源。比如你写好了 100 条用例,执行到第 47 条时登录态失效,后面全部报 401。如果不懂自动刷新机制,只能手动重新登录或重新抓包,效率很低。所以在测试框架里实现一个“统一 Token 管理模块”,往往比写一堆断言更能提升自动化稳定性。
2. 判断 Token 是否过期的三种常见方式
2.1 JWT Token:读取 exp 过期时间
JWT(JSON Web Token)是目前最常用的 Token 格式。它的结构是:
Header.Payload.Signature中间 Payload 部分可以携带很多标准字段,其中:
exp:过期时间,是一个 Unix 时间戳。iat:签发时间。nbf:生效时间,早于这个时间的 Token 不可用。
示例 Payload:
{ "sub": "10086", "name": "测试账号", "exp": 1717152000, "iat": 1717148400 }判断过期的逻辑就是拿当前时间戳和 exp 比较:
long currentTime = System.currentTimeMillis() / 1000; long expireTime = jwt.getExpiration().getTime() / 1000; if (currentTime >= expireTime) { // Token 已过期 }在 Java 的 jjwt 库中,可以直接用Jwts.parser()解析,如果 Token 过期会抛出ExpiredJwtException异常。
2.2 Redis 存储 Token 过期时间
不是所有系统都用 JWT,有些老项目用的是 Opaque Token,也就是一串随机字符串,本身不携带用户信息和过期时间。这种 Token 通常存在服务端的 Redis 中,结构可能是:
key: login:token:{tokenValue} value: { userId, loginTime, expireTime }这样判断过期就变成了查 Redis:
- 如果 key 不存在,说明 Token 不存在或已过期。
- 如果 key 存在,再比对
expireTime与当前时间。
这种方式的好处是服务端可以主动下线用户,删掉 Redis 里的 key 即可。缺点是多一次 Redis 查询,接口性能会比 JWT 本地解析稍差。
2.3 后端接口实时校验
还有一种“最笨但最准确”的方式,就是不发有效 Token,只把用户 ID 或 Token 标识传给后端,后端根据用户信息动态判断。
例如用户被封禁、被删除、密码被修改,这些情况下 JWT 本身还是“合法的”,但业务上已经不允许继续访问。所以真实项目往往用“组合校验”:
// 伪代码:先解析 JWT,再查用户状态 JwtClaims claims = jwtParser.parse(token); if (claims.isExpired()) { throw new TokenExpiredException(); } User user = userService.getById(claims.getUserId()); if (user == null || user.getStatus() != 1) { throw new UserDisabledException(); }这种方案在权限要求高的系统里非常常见。
为了直观对比,三种方式总结如下:
| 判断方式 | 判断时机 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JWT 解析 exp | 前端本地 / 后端解析 | 无状态、速度快 | 无法处理被服务端提前拉黑的情况 | 普通 Web 应用、移动端 API |
| Redis 查询 | 每次请求后端查询 | 可主动失效、可控性强 | 多一次网络开销 | 需要管理会话、踢人下线的系统 |
| 后端实时校验 | 每次请求后端组合校验 | 最准确、可结合用户状态 | 性能开销较大 | 权限要求高、账号状态经常变化 |
3. 自动更新 Token:从“被动过期”到“无感刷新”
3.1 为什么不能每次都重新登录
如果 Token 过期就要求用户重新输入用户名密码,体验太差。尤其移动端产品,不可能让用户每隔一小时就登录一次。所以常见的做法是引入“刷新 Token”机制:
- Access Token(访问令牌):有效期短,比如 30 分钟,用于调用业务接口。
- Refresh Token(刷新令牌):有效期长,比如 7 天或 30 天,专门用来换取新的 Access Token。
当 Access Token 过期时,前端不跳登录页,而是用 Refresh Token 去调用刷新接口,拿到新的 Access Token 后继续执行原请求。
3.2 双 Token 机制的工作流程
一个完整的双 Token 交互流程如下:
1. 用户登录成功 后端返回 accessToken 和 refreshToken 2. 前端调用业务接口 请求头携带 Authorization: Bearer accessToken 3. 后端校验 accessToken 如果有效,正常返回数据 4. 如果 accessToken 过期 后端返回 401 或者特定错误码 5. 前端收到 401 调用 /auth/refresh 接口,携带 refreshToken 6. 后端验证 refreshToken 如果有效,颁发新的 accessToken(并可选刷新 refreshToken) 7. 前端用新 accessToken 重放刚才失败的请求这个过程对用户完全透明,用户不会感受到“登录过期”。
3.3 Access Token 有效期设置建议
有效期不是越长越好。测试同学在做安全测试时经常会遇到这类问题:Token 有效期过长,泄露后危害大;有效期过短,用户体验差。
常见设置:
| 场景 | Access Token | Refresh Token |
|---|---|---|
| 普通 Web 后台 | 30 分钟 | 7 天 |
| 移动端 App | 2 小时 | 30 天 |
| 第三方开放平台 | 1 小时 | 90 天 |
注意:这些是常见经验值,具体要看业务安全需求。
4. 手把手实现一个自动续期 Token 模块
下面用 Java Spring Boot 为例,演示一个简化版的双 Token 实现。测试人员即使不写 Java 代码,也可以通过这个例子理解流程,方便后续阅读开发代码、设计测试用例。
4.1 项目结构
先看一下项目结构:
src/main/java/com/example/tokenrefresh/ ├── TokenRefreshApplication.java ├── controller/ │ └── AuthController.java ├── entity/ │ └── User.java ├── interceptor/ │ └── AuthInterceptor.java ├── service/ │ └── TokenService.java └── util/ └── JwtUtil.java4.2 添加依赖
在pom.xml中添加 JWT 相关依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>4.3 编写 JWT 工具类
这个工具类负责生成 Token、解析 Token、判断是否过期。
package com.example.tokenrefresh.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtUtil { // 生产环境不要写死在代码里,建议放到配置中心或环境变量 private static final String SECRET = "your-256-bit-secret-key-please-change-in-production"; private static final long ACCESS_TOKEN_EXPIRE = 30 * 60 * 1000L; // 30分钟 private static final long REFRESH_TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 7天 private static Key getKey() { return Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); } // 生成 accessToken public static String generateAccessToken(Long userId, String username) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); claims.put("tokenType", "access"); return Jwts.builder() .setClaims(claims) .setSubject(String.valueOf(userId)) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE)) .signWith(getKey(), SignatureAlgorithm.HS256) .compact(); } // 生成 refreshToken public static String generateRefreshToken(Long userId, String username) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); claims.put("tokenType", "refresh"); return Jwts.builder() .setClaims(claims) .setSubject(String.valueOf(userId)) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE)) .signWith(getKey(), SignatureAlgorithm.HS256) .compact(); } // 解析 Token,如果过期会抛出异常 public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getKey()) .build() .parseClaimsJws(token) .getBody(); } // 检查是否过期,内部已经通过 exp 判断 public static boolean isExpired(String token) { try { parseToken(token); return false; } catch (ExpiredJwtException e) { return true; } catch (Exception e) { return true; } } // 判断 Token 类型是否为 accessToken public static boolean isAccessToken(String token) { try { Claims claims = parseToken(token); return "access".equals(claims.get("tokenType")); } catch (Exception e) { return false; } } // 判断 Token 类型是否为 refreshToken public static boolean isRefreshToken(String token) { try { Claims claims = parseToken(token); return "refresh".equals(claims.get("tokenType")); } catch (Exception e) { return false; } } }这里的关键点是tokenType字段。如果 Access Token 过期后,有人拿着过期的 Access Token 去刷新接口,后端不仅不应该刷新成功,还应该拒绝。通过tokenType区分,能防止这种误用。
4.4 编写刷新接口
刷新接口的核心逻辑是:校验 Refresh Token 是否有效,有效则签发新的 Access Token。
package com.example.tokenrefresh.controller; import com.example.tokenrefresh.util.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/auth") public class AuthController { @PostMapping("/refresh") public Map<String, Object> refreshToken(@RequestHeader("refreshToken") String refreshToken) { Map<String, Object> result = new HashMap<>(); // 1. 刷新 Token 必须使用 refreshToken 去换取 if (!JwtUtil.isRefreshToken(refreshToken)) { result.put("code", 40001); result.put("message", "非法的刷新令牌"); return result; } // 2. 解析并判断是否过期 Claims claims; try { claims = JwtUtil.parseToken(refreshToken); } catch (Exception e) { result.put("code", 40002); result.put("message", "刷新令牌已过期,请重新登录"); return result; } // 3. 重新生成 accessToken(也可以同时刷新 refreshToken) Long userId = Long.valueOf(claims.get("userId").toString()); String username = claims.get("username").toString(); String newAccessToken = JwtUtil.generateAccessToken(userId, username); String newRefreshToken = JwtUtil.generateRefreshToken(userId, username); result.put("code", 0); result.put("accessToken", newAccessToken); result.put("refreshToken", newRefreshToken); return result; } }说明:这里的刷新逻辑为了方便演示,是根据旧 Refresh Token 直接生成新的。生产环境通常还会做 Refresh Token 轮换,也就是每次刷新后旧 Refresh Token 立即失效,防止被重放攻击。
4.5 编写登录接口
登录成功时同时返回两个 Token:
@PostMapping("/login") public Map<String, Object> login(@RequestBody Map<String, String> loginForm) { String username = loginForm.get("username"); String password = loginForm.get("password"); // 实际项目这里应该校验数据库用户信息 // 这里为了演示,直接假设用户存在,ID 为 1001 Long userId = 1001L; Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("accessToken", JwtUtil.generateAccessToken(userId, username)); result.put("refreshToken", JwtUtil.generateRefreshToken(userId, username)); return result; }4.6 拦截器里如何判断过期
拦截器这里只做“识别”,如果 Access Token 过期,就返回 401,让前端去调用刷新接口。
package com.example.tokenrefresh.interceptor; import com.example.tokenrefresh.util.JwtUtil; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录和刷新接口 String uri = request.getRequestURI(); if (uri.contains("/auth/login") || uri.contains("/auth/refresh")) { return true; } String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"token不存在\"}"); return false; } String token = authHeader.substring(7); if (JwtUtil.isExpired(token)) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"token已过期\"}"); return false; } if (!JwtUtil.isAccessToken(token)) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"非访问令牌\"}"); return false; } return true; } }注册拦截器:
package com.example.tokenrefresh.config; import com.example.tokenrefresh.interceptor.AuthInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; public WebConfig(AuthInterceptor authInterceptor) { this.authInterceptor = authInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**"); } }4.7 前端 Axios 拦截器自动续期
测试人员如果自己搭接口自动化框架,可以用 Python requests 或 Java HttpClient 写类似逻辑。这里以 Axios 为例,这是目前前端主流的自动续期写法。
// 文件路径:src/utils/request.js import axios from 'axios'; const request = axios.create({ baseURL: 'https://api.example.com', timeout: 10000 }); // 标记是否正在刷新 Token let isRefreshing = false; // 等待刷新期间,把原请求暂存起来 let pendingRequests = []; // 请求拦截:自动携带 Token request.interceptors.request.use(config => { const accessToken = localStorage.getItem('accessToken'); if (accessToken) { config.headers.Authorization = 'Bearer ' + accessToken; } return config; }); // 响应拦截:统一处理 401 request.interceptors.response.use( response => response.data, async error => { const { response, config } = error; if (!response) { return Promise.reject(error); } // 接口返回 401 if (response.status === 401) { // 防止刷新接口自己无限递归 if (config.url.includes('/auth/refresh')) { redirectToLogin(); return Promise.reject(error); } const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { redirectToLogin(); return Promise.reject(error); } // 如果正在刷新,先把请求加入队列 if (isRefreshing) { return new Promise((resolve, reject) => { pendingRequests.push({ config, resolve, reject }); }); } isRefreshing = true; try { // 用 refreshToken 换新 token const res = await axios.post('/auth/refresh', null, { headers: { refreshToken: refreshToken } }); const { accessToken, refreshToken: newRefreshToken } = res.data; localStorage.setItem('accessToken', accessToken); localStorage.setItem('refreshToken', newRefreshToken); // 重放等待队列中的请求 pendingRequests.forEach(item => { item.config.headers.Authorization = 'Bearer ' + accessToken; request(item.config).then(item.resolve).catch(item.reject); }); pendingRequests = []; // 重放当前请求 config.headers.Authorization = 'Bearer ' + accessToken; return request(config); } catch (refreshError) { redirectToLogin(); return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); function redirectToLogin() { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; } export default request;这段代码解决了一个非常经典的并发问题:当多个接口同时返回 401 时,不能每个接口都去调一次刷新接口,否则会造成 Refresh Token 并发刷新,旧 Token 失效导致全部失败。这里用isRefreshing标记,把其他请求先挂起,等刷新完成后再统一重放。
4.8 运行和验证
启动 Spring Boot 项目后,可以按以下步骤验证:
- 调用登录接口:
curl -X POST http://localhost:8080/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"123456"}'- 拿到返回的 accessToken 和 refreshToken 后,调用需要鉴权的接口:
curl -X GET http://localhost:8080/api/user/info \ -H "Authorization: Bearer {accessToken}"- 等 accessToken 过期后,再用 refreshToken 调用刷新接口,观察是否返回新的 accessToken。
5. 高频追问:并发刷新、Token 轮换与“记住我”
5.1 多个请求同时 401 怎么办
这是面试官非常喜欢的追问。如果前端同时发出 3 个请求,3 个请求都因为 Access Token 过期返回 401,可能会同时调用 3 次刷新接口。但 Refresh Token 如果做了轮换,第一个刷新成功后就作废了,后面两个刷新会失败,最终用户被强制登录。
解决办法就是前面 Axios 代码里展示的“请求队列 + 单一刷新标记”:
- 第一个 401 触发刷新逻辑。
- 后面进来的 401 不发起刷新,而是进入等待队列。
- 刷新完成后,用新 Token 重放等待队列里的所有请求。
- 如果刷新失败,所有等待请求一起失败并跳登录页。
5.2 Token 轮换是什么
Token 轮换的意思是每次使用 Refresh Token 刷新时,不仅返回新的 Access Token,还同时返回一个新的 Refresh Token,并且服务端把旧 Refresh Token 标记为失效。
这样做的好处是:
- 如果旧 Refresh Token 被窃取,只能使用一次。
- 服务端可以检测到 Refresh Token 异常重用。
- 更安全,但实现上需要把 Refresh Token 存储到 Redis 或者数据库,不能只靠 JWT 无状态解析。
5.3 刷新 Refresh Token 时谁去刷新
我们上面的实现里,/auth/refresh接口同时返回了新的 accessToken 和 refreshToken。这是推荐做法。
另一种做法是刷新接口只返回新的 accessToken,refreshToken 一直不变,直到它自身的有效期到期。这种方式实现简单,但安全性稍差,适合内部系统或低风险场景。
5.4 “记住我”功能如何实现
很多网站会有“记住我”复选项,勾选后一周内不用重新登录,不勾选则关闭浏览器就退出。
实现思路一般是:
- 未勾选:Refresh Token 有效期设为较短,比如 12 小时,并存放在 Cookie 或内存中。
- 勾选:Refresh Token 有效期设为 7 天或 30 天,并持久化到 localStorage 或数据库。
测试人员设计测试用例时,可以关注这样几个点:
- 勾选和不勾选,Token 的有效期是否有区别。
- 关闭浏览器后重新打开,是否还能自动登录。
- 修改密码后,旧的 Refresh Token 是否失效。
- 在多台设备登录,是否允许多人同时在线。
5.5 服务端主动下线如何实现
JWT 无状态,服务端不能直接把 JWT 作废。如果业务需要“用户改密码后强制下线”,通常用两个方案:
- 在 Redis 里存一个“用户 Token 版本号”,每次改密码 +1。JWT 解析通过后,再对比版本号。
- 把 JWT 的 jti(JWT ID)存入 Redis 黑名单,过期时间等于剩余有效期。
这也解释了为什么很多系统实际使用的是“JWT + Redis 组合”,而非纯无状态 JWT。
6. 常见问题排查:Token 失效相关的报错案例
实际开发中,Token 相关报错种类很多。下面整理几个典型场景,尤其结合项目里常见的接口对接问题。
6.1 接口请求返回 401 Unauthorized
| 可能原因 | 排查思路 |
|---|---|
| Token 过期 | 查看返回体中的错误码,是否为 token_expired |
| Token 被服务端主动拉黑 | 检查用户状态、密码是否修改、后台是否踢人 |
| Token 类型不对 | 是否将 Access Token 传给了刷新接口 |
| 请求头格式不对 | 检查是否漏掉了Bearer前缀 |
| 服务端公钥或签名密钥不一致 | 多环境部署时容易踩坑,检查配置 |
| 时间不同步 | JWT 的iat、exp依赖服务器时间,服务器时间不准会导致误判过期 |
6.2 调用第三方 API 报 token exchange failed
这类报错常见于 OAuth2/OIDC 授权码交换阶段,或者某些平台间的 Token 交换接口。我们经常在对接第三方平台时看到类似:
sign-in could not be completed token exchange failed token endpoint returned status 403 forbidden这种错误通常不是一个点,而是多个环节都可能触发,排查顺序建议如下:
| 排查步骤 | 检查内容 |
|---|---|
| 检查授权服务器配置 | 确认 client_id 和 client_secret 是否正确,回调地址是否白名单 |
| 检查网络策略 | 服务器出口 IP 是否在授权服务器允许范围内 |
| 检查请求参数 | grant_type 是否写成了authorization_code,code 是否过期 |
| 检查账号权限 | 当前账号在该租户或组织下是否有对应权限 |
| 检查地区限制 | 部分平台会根据 IP 或账号归属地限制 Token 交换 |
| 查看平台状态 | 确认对应 API 服务是否处于维护或故障状态 |
这里要特别强调一点:遇到 403 时不要只盯着客户端代码,还要检查服务端策略、IP 白名单、地区限制等。尤其是企业内网对接外部平台时,出口 IP 变化是最常见的坑。
6.3 刷新 Token 后原请求重放失败
如果实现了“401 后自动刷新再重放”,但重放仍然失败,通常原因有:
- 刷新成功后未及时更新内存中的 token,重放请求仍然携带旧 Token。
- 部分接口对幂等性要求较高,重复提交产生冲突。比如支付接口、创建订单接口,重放时可能返回“订单已存在”。
- 接口参数中绑定了旧 Token 的信息,比如签名串里包含旧 Token。
解决建议:自动化测试框架里,对 GET 等幂等接口可以做自动重放;对 POST 等非幂等接口,则要根据业务决定是否重放。另外,在做接口自动化时,最好在测试前置步骤中先确认 Token 有效,而不是等跑到中途再刷新。
7. Token 设计的最佳实践与工程建议
7.1 测试工程师眼中的 Token 测试点
如果你需要为公司设计 Token 相关测试用例,建议覆盖以下场景:
- 正常情况下获取 Token、携带 Token 访问接口。
- 不携带 Token、携带非法 Token、携带过期 Token。
- Token 有效性边界测试:正好在过期时间点前后访问。
- 并发请求时 Token 自动续期是否正常。
- 刷新 Token 后旧 Token 是否立即失效。
- 多设备登录、单端登录、强制下线。
- Token 在 Redis 中的存储时间与服务端配置时间是否一致。
- 接口超时、重试、幂等设计是否合理。
- 不同环境(测试环境、预发环境)的 Token 密钥是否隔离。
7.2 安全层面建议
- Token 不要放在 URL 参数中,优先使用
Authorization: Bearer请求头。 - Token 不要明文传输,生产环境必须启用 HTTPS。
- Refresh Token 过期时间尽量比 Access Token 长,但不能过长。
- 不要在日志中打印 Token 明文。
- JWT 的密钥长度必须足够,推荐使用 HS256 时密钥至少 256 位。
- 生产环境密钥不要提交到代码仓库,使用环境变量或配置中心管理。
- 如果用户投诉“自己的账号被异地登录”,设计时最好能记录 Token 签发设备和 IP。
7.3 性能层面建议
- JWT 本身无状态,适合分布式系统,但要避免把大量用户信息塞进 Payload。
- 每次请求都解析 JWT 有 CPU 开销,高并发场景下可以使用本地缓存或网关层统一鉴权。
- Redis 存储 Token 时,要给 key 设置合理的 TTL,避免 Redis 内存被无用的过期 Token 占满。
- 刷新 Token 接口要限流,防止恶意刷新。
8. 写在最后的面试回答参考
如果你正在准备软件测试面试,可以按下面这个框架组织回答,既有原理又有实战,听起来会更完整。
先说结论:Token 是否过期,可以从本地解析和服务端校验两个层面判断。如果用的是 JWT,本地解析 Payload 里的exp字段最方便;但服务端可能因为用户状态变化提前让 Token 失效,所以更稳妥的是服务端结合用户状态校验。
然后说自动更新:一般采用双 Token 机制,Access Token 短期有效,Refresh Token 长期有效。前端拦截所有接口返回的 401,当发现 Access Token 过期时,先尝试用 Refresh Token 调用刷新接口,拿到新 Token 后重放原请求,避免用户无感知地退出登录。设计时需要关注并发刷新问题,多个 401 同时到达时只发一次刷新请求,其他请求排队等待。
最后加一句自己在项目中的实践:我在接口自动化框架里做过一个统一的 Token 管理模块,登录后把 Token 放到全局变量,所有用例自动携带;用例执行过程中如果收到 401,会自动刷新并重放。这样既保证了用例稳定性,也把 Token 的过期处理变成了一次可复用的能力。
如果聊到这里,面试官基本可以确认你对 Token 的生命周期、刷新策略、并发处理都有比较完整的认知,这道题也就过关了。
如果你正在做测试框架,建议不要只停留在“会调接口”的层面,而是尝试在自己的框架中实现一个 Token 自动续期模块。真正把这个流程跑通之后,你对登录态、会话管理、前端拦截器这些概念的理解会明显上一个台阶,后续不管是接口测试、自动化测试还是性能测试,都多了一项很实用的底层能力。