认证
这一课你将能做到
- 实现基于 JWT 的无状态认证,理解 Token 的生成、校验、刷新全流程
- 说清楚 OAuth2 的四种授权模式,知道什么场景用哪种
- 设计 API Key 机制,支持第三方系统安全调用
为什么要学
认证是 API 的大门。没有认证,任何人都能调你的接口;认证做得差,用户会被盗号、数据会泄露。面试必考:JWT 结构?OAuth2 流程?怎么防止 Token 泄露?
先建立图像
认证方式对比:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Session │ │ JWT │ │ API Key │
│ + Cookie │ │ (无状态) │ │ (长期有效) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
服务器存会话 自包含,不存 简单字符串
水平扩展需共享 可直接校验 需定期轮换
适合浏览器 适合 App/SPA 适合机器调用
JWT 结构:
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └── Signature(签名,防篡改)
│ └──────── Payload(载荷,用户ID、过期时间等)
└────────────── Header(算法、类型)
跟着做
步骤 1:引入 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>
步骤 2:JWT 工具类
@Component
public class JwtTokenProvider {
private final String secret;
private final long accessTokenValidity;
private final long refreshTokenValidity;
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-validity}") long accessTokenValidity,
@Value("${jwt.refresh-token-validity}") long refreshTokenValidity) {
this.secret = Base64.getEncoder().encodeToString(secret.getBytes());
this.accessTokenValidity = accessTokenValidity;
this.refreshTokenValidity = refreshTokenValidity;
}
public String createAccessToken(Long userId, String username, List<String> roles) {
Date now = new Date();
Date validity = new Date(now.getTime() + accessTokenValidity);
return Jwts.builder()
.setSubject(userId.toString())
.claim("username", username)
.claim("roles", roles)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public String createRefreshToken(Long userId) {
Date now = new Date();
Date validity = new Date(now.getTime() + refreshTokenValidity);
return Jwts.builder()
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(validity)
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public Long getUserId(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
}
步骤 3:登录接口颁发 Token
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final JwtTokenProvider tokenProvider;
private final UserService userService;
@PostMapping("/login")
public ApiResponse<TokenResponse> login(@RequestBody @Valid LoginRequest request) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
if (user == null) {
throw new BusinessException(401, "用户名或密码错误");
}
String accessToken = tokenProvider.createAccessToken(
user.getId(), user.getUsername(), user.getRoles());
String refreshToken = tokenProvider.createRefreshToken(user.getId());
return ApiResponse.success(new TokenResponse(accessToken, refreshToken, "Bearer"));
}
@PostMapping("/refresh")
public ApiResponse<TokenResponse> refresh(@RequestBody @Valid RefreshRequest request) {
if (!tokenProvider.validateToken(request.getRefreshToken())) {
throw new BusinessException(401, "Refresh Token 无效或已过期");
}
Long userId = tokenProvider.getUserId(request.getRefreshToken());
User user = userService.findById(userId);
String newAccessToken = tokenProvider.createAccessToken(
user.getId(), user.getUsername(), user.getRoles());
String newRefreshToken = tokenProvider.createRefreshToken(user.getId());
return ApiResponse.success(new TokenResponse(newAccessToken, newRefreshToken, "Bearer"));
}
}
步骤 4:JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && tokenProvider.validateToken(token)) {
Long userId = tokenProvider.getUserId(token);
// 加载用户详情,构建 Authentication
UserDetails userDetails = userDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
步骤 5:API Key 机制
@RestController
@RequestMapping("/api/v1")
public class ApiKeyController {
@GetMapping("/data")
public ApiResponse<Data> getData(
@RequestHeader("X-API-Key") String apiKey) {
ApiClient client = apiClientService.validate(apiKey);
if (client == null) {
throw new BusinessException(401, "无效的 API Key");
}
if (!client.hasPermission("read:data")) {
throw new BusinessException(403, "API Key 无此权限");
}
return ApiResponse.success(dataService.getData());
}
}
- 格式:
prefix_randomstring,如 cf_live_abc123...(便于识别环境)
- 存储:哈希存储(如 SHA-256),不存明文
- 权限:每个 Key 绑定权限范围(scope)
- 过期:支持设置有效期,强制轮换
- 限流:按 Key 限流,防止滥用
讲清楚要点
- JWT 是自包含的:服务器不存会话,Token 里包含用户信息和过期时间,验签即可。优点是水平扩展无压力,缺点是无法主动失效(除非加黑名单或缩短有效期)。
- Access Token + Refresh Token 是标配:Access Token 短期(15-30 分钟),用于接口调用;Refresh Token 长期(7-30 天),用于换新 Access Token。这样即使 Access Token 泄露,攻击窗口也很小。
- JWT 不加密,只签名:Payload 是 Base64 编码,任何人都能解码看内容。敏感信息(如密码)绝对不能放 JWT 里。签名只防篡改,不防窥探。
- OAuth2 是授权框架,不是认证协议:OAuth2 解决「第三方应用怎么安全地访问用户数据」。常见场景:微信登录、GitHub 登录、企业微信授权。
- API Key 适合机器对机器:没有用户交互的场景(如定时任务同步数据、第三方 SaaS 调用)。API Key 要配合 HTTPS、IP 白名单、限流使用。
你可能会懵的地方
-
JWT 和 Session 怎么选?
JWT:无状态、水平扩展好、适合 App/SPA/微服务;Session:可主动失效、适合传统 Web 应用、需要共享 Session 存储。现代架构推荐 JWT。
-
JWT 过期了怎么办?
Access Token 过期:用 Refresh Token 调 /api/auth/refresh 换新。Refresh Token 过期:用户重新登录。
-
怎么防止 JWT 被盗用?
HTTPS 传输、短有效期、HttpOnly Cookie 存储(防 XSS)、绑定 IP 或设备指纹(增加盗用难度)、异常检测(如同一账号多地登录)。
-
OAuth2 四种模式?
授权码模式(Authorization Code):最安全,适合有后端的 Web 应用。简化模式(Implicit):已废弃,被授权码 + PKCE 取代。密码模式(Resource Owner Password):用户把账号密码给第三方,仅限可信应用。客户端模式(Client Credentials):机器对机器,无用户参与。
-
API Key 和 JWT 能一起用吗?
可以。API Key 标识「哪个应用在调用」,JWT 标识「哪个用户在操作」。比如开放平台:先用 API Key 认证应用,再用 JWT 认证用户。
错法对照
| 错法 | 问题 | 正解 |
|---|
| JWT 里存用户密码 | Base64 可解码,密码泄露 | 只存非敏感信息(id、username、roles) |
| Access Token 有效期设 7 天 | 泄露后长期可用 | Access Token 15-30 分钟,Refresh Token 7-30 天 |
| API Key 明文存数据库 | 数据库泄露 = 所有 Key 泄露 | 哈希存储(SHA-256 + Salt) |
| 登录接口不限流 | 暴力破解密码 | 登录接口限流(如 5 次/分钟/账号) |
| Token 放 URL 参数 | 会被日志、浏览器历史记录 | 放 Authorization Header |
卡点急救
- Token 解析失败:检查 secret 是否一致(编码问题)、Token 是否过期、时钟是否同步(分布式系统)。
- 跨域时 Authorization Header 丢失:CORS 配置没允许
Authorization header,或预检请求没通过。
- Refresh Token 被重复使用:实现 Refresh Token 轮换(每次使用都换新,旧 Token 立即失效),检测到旧 Token 复用则全部作废(可能被盗)。
面试常问
-
JWT 的结构和原理?
Header(算法)、Payload(声明)、Signature(签名)。服务器用密钥签名,客户端带 Token 请求,服务器验签确认身份。能讲出优缺点:无状态、可扩展 vs 无法主动失效、体积较大。
-
OAuth2 授权码模式流程?
- 用户点击「微信登录」→ 跳转微信授权页;2. 用户同意 → 微信回调带 code;3. 后端用 code + secret 换 access_token;4. 用 access_token 拉取用户信息;5. 生成本系统 JWT 返回。能画时序图。
-
怎么设计一个安全的 API Key 系统?
生成:随机字符串 + 前缀;存储:哈希;权限:scope 控制;使用:HTTPS + Header;轮换:定期更换;监控:异常调用告警。
自测三问
- 如果 Access Token 被 XSS 攻击窃取,怎么把损失降到最低?
- 设计一个第三方天气 API 的认证方案,要求支持不同权限等级(免费/付费)。
- 为什么 OAuth2 的授权码模式比简化模式更安全?
小结一句话
认证的核心是「证明你是谁」:JWT 适合无状态分布式系统,OAuth2 解决第三方授权,API Key 服务机器调用;Access Token 短期 + Refresh Token 长期是行业标准。
延伸