在Java后端、前端与测试面试中,“Token是什么?有什么用?怎么用?”是高频基础考题,尤其在登录、权限控制等场景下,Token是绕不开的核心组件。不少开发者仅知道“Token是登录后返回的一串字符串”,却难以阐明其本质、分类与工程实践细节。本文将从本质剖析、类型对比到实战编码,系统性地讲解Token。
一、Token的本质:不只是字符串
1. 核心定义
Token(令牌)本质上是一串经过加密的、具有唯一性的字符串,是服务器颁发给客户端的“数字身份凭证”。客户端在后续请求中携带此Token,服务器通过验证该Token即可确认客户端身份与权限,从而避免每次请求都校验用户名和密码。
2. 生活化类比:景区门票
- 购票(登录):你(客户端)用身份证和现金(账号密码)在售票处(登录接口)兑换一张门票(Token)。
- 入园(访问):之后进入景区内各个景点(业务接口),只需出示门票(Token),无需反复核验身份证。
- 门票规则(Token属性):门票上印有有效期(Token过期时间)和允许进入的区域(权限范围),过期或无效的门票将被拒绝使用。
3. Token解决的核心痛点
与传统基于Session的“账号密码”鉴权相比,Token方案针对以下痛点提供了更优解:
| 痛点 |
Session方案 |
Token方案 |
| 跨域问题 |
依赖Cookie,跨域访问需复杂CORS配置及Cookie共享 |
可置于请求头或参数中,天然支持跨域 |
| 分布式部署 |
需要Session共享(如用Redis),增加了系统复杂度和维护成本 |
Token自包含信息,服务器无状态,轻松支持分布式集群 |
| 多端适配 |
移动端(APP、小程序)处理Cookie较为麻烦 |
Token可统一存放于请求头,适配Web、APP、小程序等所有终端 |
二、主流Token类型详解
1. 自定义Token
适用于小型项目或内部系统,实现简单灵活。
- 特点:生成规则由开发者自定义(如
用户ID+时间戳+随机数+签名),服务器需存储Token(如存入Redis)以验证其有效性。
- 优点:完全可控,易于理解。
- 缺点:服务器需维护Token存储,有状态。
Java生成自定义Token示例:
// 生成自定义Token
public String generateToken(Long userId) {
// 1. 拼接核心信息
String baseStr = userId + "_" + System.currentTimeMillis() + "_" + RandomStringUtils.randomAlphanumeric(8);
// 2. 加盐签名(防止篡改)
String sign = DigestUtils.md5Hex(baseStr + "my_secret_key");
// 3. 拼接最终Token
String token = baseStr + "_" + sign;
// 4. 存入Redis,设置2小时过期
redisTemplate.opsForValue().set("token:" + token, userId, 2, TimeUnit.HOURS);
return token;
}
// 验证自定义Token
public boolean verifyToken(String token) {
// 1. 检查格式
if (token == null || !token.contains("_")) {
return false;
}
// 2. 拆分并验证签名
String[] parts = token.split("_");
String baseStr = String.join("_", Arrays.copyOf(parts, parts.length-1));
String sign = parts[parts.length-1];
String realSign = DigestUtils.md5Hex(baseStr + "my_secret_key");
if (!sign.equals(realSign)) {
return false;
}
// 3. 检查Redis中是否存在(未过期)
return redisTemplate.hasKey("token:" + token);
}
2. JWT (JSON Web Token)
当前最主流的无状态令牌标准,适合中大型分布式项目。
- 结构:
Header.Payload.Signature,三段之间用点(.)分隔。
- Header:声明令牌类型和签名算法,如
{"alg":"HS256","typ":"JWT"},经Base64编码形成第一段。
- Payload:携带声明(Claims),如用户ID、过期时间(
exp)、签发时间(iat),经Base64编码形成第二段。注意:Payload仅编码,未加密,切勿存放敏感信息。
- Signature:对前两段的签名,用于防篡改。例如使用HMACSHA256算法:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)。
示例JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImV4cCI6MTcyODA3MjAwMCwiaWF0IjoxNzI4MDY4NDAwfQ.5Z8n7k8X9m0P1s2l3k4j5h6g7f8d9s0a
Java生成与验证JWT示例(使用jjwt库):
// 生成JWT
public String generateJwt(Long userId) {
Date expireTime = new Date(System.currentTimeMillis() + 2 * 3600 * 1000); // 2小时后过期
String jwt = Jwts.builder()
.setSubject(userId.toString()) // 主题存放用户ID
.setExpiration(expireTime)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "my_jwt_secret") // 使用密钥签名
.compact();
return jwt;
}
// 验证JWT
public Long verifyJwt(String jwt) {
try {
Claims claims = Jwts.parser()
.setSigningKey("my_jwt_secret")
.parseClaimsJws(jwt) // 会自动验证签名和过期时间
.getBody();
return Long.parseLong(claims.getSubject()); // 解析出用户ID
} catch (Exception e) {
// 签名无效或过期
return null;
}
}
3. OAuth 2.0 Token
专为第三方应用授权设计,如实现“微信登录”。
- 核心令牌:
access_token:用于访问受保护资源,有效期较短(如2小时)。
refresh_token:用于在access_token过期后获取新的access_token,有效期较长(如30天),避免用户反复授权。
- 典型流程(以微信登录为例):
- 前端引导用户跳转至微信授权页。
- 用户授权后,微信重定向回应用并携带
code。
- 后端使用
code、AppID和AppSecret调用微信接口,换取access_token和openid。
- 后端根据
openid处理业务逻辑(查询或创建用户),并生成自有的系统Token(如JWT)返回给前端。
三、Token实战:前后端完整交互流程
1. 整体流程(以JWT为例)
前端 后端 存储/第三方
| | |
| 1. 登录请求(账号密码) | |
|--------------------------->| |
| | 2. 验证账号密码 |
| | 3. 生成JWT Token |
| 4. 返回JWT Token | |
|<---------------------------| |
| 5. 存储Token(localStorage) | |
| | |
| 6. 接口请求(携带Token) | |
|--------------------------->| 7. 验证Token有效性 |
| | 8. 处理业务逻辑 |
| 9. 返回接口数据 | |
|<---------------------------| |
| 10. Token过期 | |
|--------------------------->| 11. 返回401错误 |
|<---------------------------| |
| 12. 跳转到登录页 | |
2. 前端携带Token的三种方式
// 方式1:放在请求头(推荐,安全规范)
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token; // Bearer是标准前缀
}
return config;
});
// 方式2:放在URL参数(不推荐,易泄露于日志或浏览器历史)
axios.get('/api/user?token=' + token);
// 方式3:放在请求体(适用于POST/PUT等请求)
axios.post('/api/user', {
token: token,
name: '张三'
});
3. 后端统一拦截验证(Spring Boot示例)
在Spring Boot项目中,通常通过拦截器(Interceptor)统一处理Token验证。
定义Token拦截器:
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 排除登录接口等无需认证的路径
String requestURI = request.getRequestURI();
if (requestURI.contains("/api/login")) {
return true;
}
// 2. 从请求头获取Token
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(401);
response.getWriter().write("Token不存在");
return false;
}
token = token.substring(7); // 去除"Bearer "前缀
// 3. 验证Token (以JWT为例)
Long userId = verifyJwt(token);
if (userId == null) {
response.setStatus(401);
response.getWriter().write("Token无效或已过期");
return false;
}
// 4. 将用户ID存入请求属性,供后续Controller使用
request.setAttribute("userId", userId);
return true;
}
}
注册拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/api/**") // 拦截所有/api开头的请求
.excludePathPatterns("/api/login"); // 排除登录接口
}
}
四、Token安全防护八大措施
Token作为身份凭证,其安全性至关重要,必须实施多层防护。
- 传输加密:所有涉及Token传输的接口(登录、业务)必须使用HTTPS,防止网络嗅探。
- 存储安全:
- 前端:优先使用
localStorage或sessionStorage,避免使用易受CSRF攻击的Cookie。
- 后端:若需存储(如自定义Token),应在Redis等存储中为Key添加前缀(如
token:),并设置合理的过期时间。
- 防篡改:
- 自定义Token必须加盐签名。
- JWT必须使用足够强度的密钥进行签名。
- 切勿在Token的Payload中存储明文密码、手机号等敏感信息。
- 防御XSS:对用户输入进行严格过滤和转义,防止恶意脚本窃取
localStorage中的Token。
- 防御CSRF:验证请求头中的
Origin或Referer;对于敏感操作(如支付、改密),应增加二次验证(验证码、短信)。
- 缩短有效期:根据业务敏感度设置合理的过期时间(如普通业务2小时,支付操作15分钟)。提供“记住我”功能时,可使用
refresh_token机制。
- 支持主动注销:
- 自定义Token:退出登录时直接从存储中删除。
- JWT:可结合Redis维护一个短期的“黑名单”,注销时将JWT标识加入黑名单,验证时优先检查。
- 限流防护:对登录、Token验证等接口实施限流(如每分钟10次),防止暴力破解。
五、面试高频真题解析
真题1:Token 和 Session 的区别是什么?
核心区别在于“服务器是否有状态”。
- 存储位置:Session数据存储在服务器端(内存/Redis);Token由客户端存储和携带。
- 扩展性:Session在分布式环境中需要共享,增加复杂度;Token无状态,天然支持分布式。
- 跨域支持:Session基于Cookie,跨域限制多;Token可灵活置于请求头,跨域友好。
- 安全性:Session易受CSRF攻击;Token需重点防范XSS窃取和传输泄露。
真题2:JWT的优缺点是什么?如何解决JWT无法主动作废的问题?
- 优点:无状态、跨域/分布式友好、自包含信息、签发后无需存储。
- 缺点:一旦签发,在有效期内无法主动作废;Payload可解码,不能存敏感信息;体积相对较大。
- 解决作废问题:
- 维护黑名单:将需要作废的JWT ID(
jti)或整个签名存入Redis黑名单,验证时检查。
- 缩短有效期+刷新机制:使用短效
access_token配合长效refresh_token。
- 引入版本号:在Payload中加入用户信息版本号,用户关键信息变更时更新版本号,验证时比对。
真题3:Token的安全防护措施有哪些?
(参考答案可整合第四节内容,结构化列出)核心包括:HTTPS传输、安全存储(前端LocalStorage/后端Redis加过期)、添加签名防篡改、防御XSS/CSRF攻击、设置合理有效期、支持主动注销机制、对接口进行限流。
六、总结
- 本质:Token是服务端颁发的数字身份凭证,用以解决Session在跨域、分布式场景下的局限。
- 选型:小型项目可用自定义Token;中大型分布式项目推荐JWT;第三方授权集成需采用OAuth 2.0。
- 实践:前端妥善存储并规范携带Token;后端通过拦截器统一验证;安全防护需贯穿传输、存储、验证全流程。
- 安全:始终将安全性置于首位,综合运用加密、签名、防攻击、限流等多种手段构建防护体系。
深入理解Token的原理与实战,不仅能从容应对技术面试,更能为构建安全、可靠的现代应用鉴权体系打下坚实基础。