JWT Token 认证
·
JWT Token 认证
学习目标
- 理解 JWT 的原理和结构
- 掌握 JWT Token 的生成和验证
- 实现基于 JWT 的认证系统
- 实现 Token 刷新机制
- 解决前后端分离的认证问题
一、为什么需要 JWT?
1.1 传统 Session 认证的问题
传统 Session 认证流程:
用户登录 → 服务器创建 Session → 返回 SessionID(Cookie)
↓
用户请求 → 携带 SessionID → 服务器查找 Session → 验证身份
问题:
├─ Session 存储在服务器内存,占用资源
├─ 分布式部署需要 Session 共享(Redis)
├─ 移动端不支持 Cookie
├─ 跨域问题复杂
└─ 扩展性差
1.2 JWT 的优势
JWT Token 认证流程:
用户登录 → 服务器生成 JWT Token → 返回 Token
↓
用户请求 → 携带 Token(Header) → 服务器验证 Token → 验证身份
优势:
✓ 无状态,服务器不存储 Token
✓ 适合分布式系统
✓ 支持移动端
✓ 跨域友好
✓ 扩展性好
1.3 适用场景
适合使用 JWT:
├─ 前后端分离项目
├─ 移动端 APP
├─ 微服务架构
├─ 单点登录(SSO)
└─ API 接口认证
不适合使用 JWT:
├─ 传统 Web 应用(Session 更简单)
├─ 需要立即撤销 Token 的场景
└─ Token 过期时间很长的场景
二、JWT 原理详解
2.1 JWT 结构
JWT Token 由三部分组成,用 . 分隔:
Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
↑ ↑ ↑
Header Payload Signature
2.2 Header(头部)
{
"alg": "HS256", // 签名算法
"typ": "JWT" // Token 类型
}
Base64 编码后:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.3 Payload(负载)
{
"sub": "1234567890", // 主题(用户ID)
"name": "John Doe", // 用户名
"iat": 1516239022, // 签发时间
"exp": 1516242622, // 过期时间
"roles": ["USER", "ADMIN"] // 自定义字段:角色
}
标准字段(Claims):
iss (issuer):签发者
sub (subject):主题(通常是用户ID)
aud (audience):接收方
exp (expiration time):过期时间
nbf (not before):生效时间
iat (issued at):签发时间
jti (JWT ID):唯一标识
Base64 编码后:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
2.4 Signature(签名)
签名算法:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
作用:
├─ 防止 Token 被篡改
├─ 验证 Token 的真实性
└─ 只有拥有密钥的服务器才能验证
签名结果:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2.5 工作流程
┌─────────────────────────────────────────────────────────┐
│ JWT 认证流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 用户登录 │
│ ├─ POST /api/auth/login │
│ ├─ {username: "admin", password: "123456"} │
│ └─ 服务器验证用户名密码 │
│ │
│ 2. 生成 JWT Token │
│ ├─ 创建 Payload(用户信息) │
│ ├─ 使用密钥签名 │
│ └─ 返回 Token 给客户端 │
│ │
│ 3. 客户端存储 Token │
│ ├─ LocalStorage / SessionStorage │
│ └─ 每次请求携带 Token │
│ │
│ 4. 服务器验证 Token │
│ ├─ 提取 Token(从 Header) │
│ ├─ 验证签名 │
│ ├─ 检查过期时间 │
│ └─ 提取用户信息 │
│ │
│ 5. 返回响应 │
│ ├─ Token 有效 → 处理请求 │
│ └─ Token 无效 → 返回 401 │
│ │
└─────────────────────────────────────────────────────────┘
三、JWT 实现
3.1 添加依赖
<!-- pom.xml -->
<dependencies>
<!-- JWT 库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
3.2 JWT 配置
# application.yml
jwt:
secret: mySecretKeyForJWTTokenGenerationAndValidation123456789 # 密钥(至少32字符)
expiration: 86400000 # Token 过期时间(毫秒,24小时)
refresh-expiration: 604800000 # 刷新 Token 过期时间(7天)
header: Authorization # Token 在 Header 中的名称
prefix: Bearer # Token 前缀
3.3 JWT 工具类
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.refresh-expiration}")
private Long refreshExpiration;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成访问 Token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("authorities", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return createToken(claims, userDetails.getUsername(), expiration);
}
/**
* 生成刷新 Token
*/
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
return createToken(claims, userDetails.getUsername(), refreshExpiration);
}
/**
* 创建 Token
*/
private String createToken(Map<String, Object> claims, String subject, Long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 从 Token 中提取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
/**
* 从 Token 中提取权限
*/
@SuppressWarnings("unchecked")
public List<String> getAuthoritiesFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return (List<String>) claims.get("authorities");
}
/**
* 从 Token 中提取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 解析 Token
*/
private Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token, UserDetails userDetails) {
try {
String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
/**
* 检查 Token 是否过期
*/
private boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
3.4 JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Value("${jwt.header}")
private String header;
@Value("${jwt.prefix}")
private String prefix;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 从请求头中获取 Token
String token = getTokenFromRequest(request);
if (token != null) {
// 2. 从 Token 中提取用户名
String username = tokenProvider.getUsernameFromToken(token);
// 3. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. 验证 Token
if (tokenProvider.validateToken(token, userDetails)) {
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 6. 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
logger.error("无法设置用户认证:{}", e.getMessage());
}
// 7. 继续过滤器链
filterChain.doFilter(request, response);
}
/**
* 从请求头中提取 Token
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(header);
if (bearerToken != null && bearerToken.startsWith(prefix + " ")) {
return bearerToken.substring(7); // 去掉 "Bearer " 前缀
}
return null;
}
}
3.5 Security 配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(JWT 不需要)
.csrf(csrf -> csrf.disable())
// 禁用 Session(使用 JWT,不需要 Session)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置授权规则
.authorizeHttpRequests(auth -> auth
// 公开接口
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
// 需要认证的接口
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
// 异常处理
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"未授权,请先登录\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\":\"权限不足\"}");
})
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
3.6 登录接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
/**
* 用户登录
*/
@PostMapping("/login")
public Result<LoginResponse> login(@RequestBody LoginRequest request) {
try {
// 1. 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 2. 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成 Token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = tokenProvider.generateToken(userDetails);
String refreshToken = tokenProvider.generateRefreshToken(userDetails);
// 4. 返回 Token
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setTokenType("Bearer");
response.setExpiresIn(86400L); // 24小时
return Result.success(response);
} catch (BadCredentialsException e) {
return Result.error("用户名或密码错误");
} catch (Exception e) {
return Result.error("登录失败:" + e.getMessage());
}
}
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public Result<LoginResponse> refresh(@RequestBody RefreshTokenRequest request) {
try {
String refreshToken = request.getRefreshToken();
// 1. 从刷新 Token 中提取用户名
String username = tokenProvider.getUsernameFromToken(refreshToken);
// 2. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 3. 验证刷新 Token
if (tokenProvider.validateToken(refreshToken, userDetails)) {
// 4. 生成新的访问 Token
String newAccessToken = tokenProvider.generateToken(userDetails);
LoginResponse response = new LoginResponse();
response.setAccessToken(newAccessToken);
response.setRefreshToken(refreshToken); // 刷新 Token 不变
response.setTokenType("Bearer");
response.setExpiresIn(86400L);
return Result.success(response);
} else {
return Result.error("刷新 Token 无效");
}
} catch (Exception e) {
return Result.error("刷新 Token 失败:" + e.getMessage());
}
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result<Void> logout() {
// JWT 是无状态的,客户端删除 Token 即可
// 如果需要立即失效,可以将 Token 加入黑名单
SecurityContextHolder.clearContext();
return Result.success("退出成功");
}
}
// 请求对象
@Data
class LoginRequest {
private String username;
private String password;
}
@Data
class RefreshTokenRequest {
private String refreshToken;
}
// 响应对象
@Data
class LoginResponse {
private String accessToken;
private String refreshToken;
private String tokenType;
private Long expiresIn;
}
四、Token 刷新机制
4.1 为什么需要刷新 Token?
问题:
├─ 访问 Token 过期时间短(如 1 小时)
├─ 用户频繁登录体验差
└─ 安全性和用户体验的平衡
解决方案:
├─ 访问 Token(Access Token):过期时间短(1小时)
└─ 刷新 Token(Refresh Token):过期时间长(7天)
流程:
访问 Token 过期 → 使用刷新 Token 获取新的访问 Token → 继续使用
4.2 双 Token 机制
┌─────────────────────────────────────────────────────────┐
│ 双 Token 机制 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 用户登录 │
│ └─ 返回:Access Token(1小时) + Refresh Token(7天)│
│ │
│ 2. 正常请求 │
│ ├─ 携带 Access Token │
│ └─ Token 有效 → 正常响应 │
│ │
│ 3. Access Token 过期 │
│ ├─ 请求返回 401 │
│ ├─ 前端自动调用刷新接口 │
│ ├─ 携带 Refresh Token │
│ └─ 获取新的 Access Token │
│ │
│ 4. Refresh Token 也过期 │
│ └─ 需要重新登录 │
│ │
└─────────────────────────────────────────────────────────┘
4.3 前端自动刷新
// Axios 拦截器实现自动刷新
import axios from 'axios';
// 创建 axios 实例
const api = axios.create({
baseURL: 'http://localhost:8080/api'
});
// 请求拦截器:添加 Token
api.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器:处理 Token 过期
api.interceptors.response.use(
response => {
return response;
},
async error => {
const originalRequest = error.config;
// 如果是 401 错误且没有重试过
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 调用刷新接口
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', {
refreshToken: refreshToken
});
// 保存新的 Token
const { accessToken } = response.data.data;
localStorage.setItem('accessToken', accessToken);
// 重新发送原请求
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转到登录页
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
五、Token 黑名单
5.1 为什么需要黑名单?
JWT 的问题:
├─ Token 一旦签发,无法撤销
├─ 即使用户退出登录,Token 在过期前仍然有效
└─ 存在安全风险
解决方案:
├─ 将已退出的 Token 加入黑名单
├─ 验证 Token 时检查黑名单
└─ 使用 Redis 存储黑名单(支持过期)
5.2 Redis 黑名单实现
@Service
public class TokenBlacklistService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private JwtTokenProvider tokenProvider;
private static final String BLACKLIST_PREFIX = "token:blacklist:";
/**
* 将 Token 加入黑名单
*/
public void addToBlacklist(String token) {
// 获取 Token 的过期时间
Date expiration = tokenProvider.getExpirationDateFromToken(token);
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
// 存入 Redis,过期时间与 Token 一致
String key = BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1", ttl, TimeUnit.MILLISECONDS);
}
}
/**
* 检查 Token 是否在黑名单中
*/
public boolean isBlacklisted(String token) {
String key = BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 从黑名单中移除 Token(一般不需要,会自动过期)
*/
public void removeFromBlacklist(String token) {
String key = BLACKLIST_PREFIX + token;
redisTemplate.delete(key);
}
}
// 修改 JWT 过滤器,检查黑名单
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenBlacklistService blacklistService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String token = getTokenFromRequest(request);
if (token != null) {
// 检查黑名单
if (blacklistService.isBlacklisted(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Token 已失效\"}");
return;
}
// ... 其他验证逻辑
}
} catch (Exception e) {
logger.error("认证失败:{}", e.getMessage());
}
filterChain.doFilter(request, response);
}
}
// 修改退出登录接口
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request) {
// 获取 Token
String token = getTokenFromRequest(request);
if (token != null) {
// 加入黑名单
blacklistService.addToBlacklist(token);
}
SecurityContextHolder.clearContext();
return Result.success("退出成功");
}
六、完整示例
6.1 前端登录
// 登录
async function login(username, password) {
try {
const response = await axios.post('/api/auth/login', {
username,
password
});
const { accessToken, refreshToken } = response.data.data;
// 保存 Token
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
// 跳转到首页
window.location.href = '/home';
} catch (error) {
alert('登录失败:' + error.response.data.message);
}
}
// 发送请求
async function getUserInfo() {
try {
const response = await api.get('/api/user/info');
console.log('用户信息:', response.data);
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
// 退出登录
async function logout() {
try {
await api.post('/api/auth/logout');
// 清除 Token
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// 跳转到登录页
window.location.href = '/login';
} catch (error) {
console.error('退出失败:', error);
}
}
6.2 测试接口
# 1. 登录获取 Token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}'
# 响应:
# {
# "code": 200,
# "data": {
# "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
# "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
# "tokenType": "Bearer",
# "expiresIn": 86400
# }
# }
# 2. 使用 Token 访问受保护接口
curl http://localhost:8080/api/user/info \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 3. 刷新 Token
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}'
# 4. 退出登录
curl -X POST http://localhost:8080/api/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
七、常见问题
7.1 Token 被盗用怎么办?
风险:
Token 被盗取 → 攻击者可以冒充用户 → 访问用户资源
防护措施:
// 1. 使用 HTTPS
// 防止 Token 在传输过程中被窃取
// 2. 设置较短的过期时间
jwt.expiration=3600000 // 1小时
// 3. 绑定 IP 地址
public String generateToken(UserDetails userDetails, String ipAddress) {
Map<String, Object> claims = new HashMap<>();
claims.put("ip", ipAddress);
// ...
}
// 验证时检查 IP
public boolean validateToken(String token, String currentIp) {
String tokenIp = getClaimFromToken(token, "ip");
return tokenIp.equals(currentIp) && !isTokenExpired(token);
}
// 4. 使用刷新 Token 机制
// 访问 Token 过期时间短,刷新 Token 过期时间长
// 5. 实现 Token 黑名单
// 用户退出或修改密码时,将旧 Token 加入黑名单
7.2 Token 过期后如何处理?
方案1:前端自动刷新(推荐)
// 使用 Axios 拦截器自动刷新
api.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 401 && !error.config._retry) {
error.config._retry = true;
const newToken = await refreshToken();
error.config.headers['Authorization'] = `Bearer ${newToken}`;
return api(error.config);
}
return Promise.reject(error);
}
);
方案2:后端自动续期
// 在过滤器中检查 Token 是否即将过期
if (isTokenExpiringSoon(token)) {
// 生成新 Token
String newToken = tokenProvider.generateToken(userDetails);
// 在响应头中返回新 Token
response.setHeader("New-Token", newToken);
}
7.3 如何立即撤销 Token?
问题: JWT 无法撤销,即使用户退出登录,Token 在过期前仍然有效
解决方案:
// 方案1:Token 黑名单(推荐)
// 将已退出的 Token 加入 Redis 黑名单
blacklistService.addToBlacklist(token);
// 方案2:版本号机制
// 在 Token 中加入版本号,用户修改密码时版本号+1
public String generateToken(UserDetails userDetails, Integer version) {
claims.put("version", version);
// ...
}
// 验证时检查版本号
public boolean validateToken(String token, User user) {
Integer tokenVersion = getVersionFromToken(token);
return tokenVersion.equals(user.getTokenVersion());
}
// 方案3:短过期时间 + 刷新 Token
// 访问 Token 过期时间设置为 15 分钟
// 用户退出时删除刷新 Token
7.4 Token 太长怎么办?
问题: JWT Token 包含用户信息和权限,可能很长
解决方案:
// 1. 减少 Payload 内容
// 只存储必要信息(用户ID),其他信息从数据库查询
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId()); // 只存储 ID
// 不要存储:用户名、邮箱、角色、权限等
// 2. 使用压缩
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setClaims(claims)
.compressWith(CompressionCodecs.GZIP) // 启用压缩
.signWith(key)
.compact();
}
// 3. 使用 Redis 存储 Token
// Token 只存储一个随机 ID,真实信息存在 Redis
String tokenId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("token:" + tokenId, userInfo, 1, TimeUnit.HOURS);
return tokenId;
7.5 跨域问题
问题: 前后端分离,跨域请求无法携带 Token
解决方案:
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许的源
config.addAllowedOrigin("http://localhost:3000");
// 允许的请求头
config.addAllowedHeader("*");
// 允许的方法
config.addAllowedMethod("*");
// 允许携带凭证
config.setAllowCredentials(true);
// 暴露的响应头(前端可以访问)
config.addExposedHeader("Authorization");
config.addExposedHeader("New-Token");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
八、JWT vs Session
8.1 对比表格
| 维度 | Session | JWT |
|---|---|---|
| 存储位置 | 服务器内存/Redis | 客户端 |
| 状态 | 有状态 | 无状态 |
| 扩展性 | 需要 Session 共享 | 天然支持分布式 |
| 性能 | 需要查询存储 | 无需查询,直接验证 |
| 安全性 | 较高(服务器控制) | 较低(无法撤销) |
| 跨域 | 复杂(Cookie 限制) | 简单(Header 传递) |
| 移动端 | 不友好 | 友好 |
| 撤销 | 容易(删除 Session) | 困难(需要黑名单) |
| 大小 | SessionID 很小 | Token 较大 |
| 适用场景 | 传统 Web 应用 | 前后端分离、微服务 |
8.2 选择建议
使用 Session:
├─ 传统 Web 应用
├─ 需要立即撤销会话
├─ 安全性要求高
└─ 单体应用
使用 JWT:
├─ 前后端分离
├─ 移动端 APP
├─ 微服务架构
├─ 单点登录(SSO)
└─ 第三方 API 认证
九、最佳实践
9.1 Token 设计
// ✓ 正确:只存储必要信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
// ✗ 错误:存储敏感信息
claims.put("password", user.getPassword()); // 永远不要存储密码
claims.put("creditCard", user.getCreditCard()); // 不要存储敏感信息
// ✓ 正确:设置合理的过期时间
.setExpiration(new Date(now.getTime() + 3600000)) // 1小时
// ✗ 错误:过期时间太长
.setExpiration(new Date(now.getTime() + 31536000000L)) // 1年
9.2 密钥管理
# ✓ 正确:使用强密钥
jwt:
secret: ${JWT_SECRET:myVeryLongAndSecureSecretKeyForJWTTokenGeneration123456789}
# 从环境变量读取,至少 32 字符
# ✗ 错误:使用弱密钥
jwt:
secret: 123456 # 太短,容易被破解
9.3 安全建议
1. 使用 HTTPS
└─ 防止 Token 在传输过程中被窃取
2. 设置合理的过期时间
├─ 访问 Token:15分钟 - 1小时
└─ 刷新 Token:7天 - 30天
3. 实现 Token 刷新机制
└─ 避免用户频繁登录
4. 使用黑名单
└─ 支持立即撤销 Token
5. 不要在 Token 中存储敏感信息
└─ Token 可以被解码(Base64)
6. 验证 Token 的完整性
└─ 检查签名、过期时间、发行者等
7. 限制 Token 的使用范围
└─ 绑定 IP、设备等
十、实战案例
10.1 完整的认证流程
/**
* 完整的 JWT 认证系统
*/
@Service
public class AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@Autowired
private TokenBlacklistService blacklistService;
/**
* 用户登录
*/
public LoginResponse login(LoginRequest request, String ipAddress) {
// 1. 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 2. 生成 Token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = tokenProvider.generateToken(userDetails, ipAddress);
String refreshToken = tokenProvider.generateRefreshToken(userDetails);
// 3. 记录登录日志
logLoginActivity(request.getUsername(), ipAddress, true);
// 4. 返回响应
return new LoginResponse(accessToken, refreshToken, "Bearer", 3600L);
}
/**
* 刷新 Token
*/
public LoginResponse refreshToken(String refreshToken, String ipAddress) {
// 1. 验证刷新 Token
String username = tokenProvider.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!tokenProvider.validateToken(refreshToken, userDetails)) {
throw new RuntimeException("刷新 Token 无效");
}
// 2. 生成新的访问 Token
String newAccessToken = tokenProvider.generateToken(userDetails, ipAddress);
return new LoginResponse(newAccessToken, refreshToken, "Bearer", 3600L);
}
/**
* 退出登录
*/
public void logout(String token) {
// 1. 加入黑名单
blacklistService.addToBlacklist(token);
// 2. 清除 SecurityContext
SecurityContextHolder.clearContext();
// 3. 记录退出日志
String username = tokenProvider.getUsernameFromToken(token);
logLoginActivity(username, null, false);
}
private void logLoginActivity(String username, String ipAddress, boolean isLogin) {
// 记录登录/退出日志
// ...
}
}
更多推荐




所有评论(0)