
导读: 本文详解基于 JWT 和 Spring Security 6 的多终端认证系统设计与实现,包括 Token 设计、刷新机制、权限控制以及生产环境最佳实践。


问题:
•❌ Session 存储在服务器,占用内存•❌ 集群环境需要 Session 共享 (Redis)•❌ CSRF 攻击风险•❌ 跨域支持复杂•❌ 移动端适配困难

对比表:
特性 | Session 方案 | JWT 方案 | 优势 |
|---|---|---|---|
存储位置 | 服务器 + Redis | 客户端 | ✅ 节省资源 |
扩展性 | 需要 Session 共享 | 天然支持 | ✅ 易于扩展 |
性能 | 每次查询数据库 | 本地验证 | ✅ 性能提升 5 倍 |
跨域 | 复杂配置 | 原生支持 | ✅ 简单 |
安全性 | CSRF 风险 | 更安全 | ✅ 无 CSRF |
/**
* JWT Token 结构
*/
public class TokenInfo {
/**
* Header (Base64 编码)
* {
* "alg": "HS256",
* "typ": "JWT"
* }
*/
private String header;
/**
* Payload (Base64 编码)
* {
* "userId": 1001, // 用户 ID
* "userName": "admin", // 用户名
* "deviceId": "xxx", // 设备标识
* "iat": 1234567890, // 签发时间
* "exp": 1234571490 // 过期时间 (2 小时)
* }
*/
private String payload;
/**
* Signature (签名)
* HMACSHA256(
* base64UrlEncode(header) + "." +
* base64UrlEncode(payload),
* secret
* )
*/
private String signature;
}sequenceDiagram
participant Client as 客户端
participant Server as 服务端
Client->>Server: 登录 (用户名 + 密码)
Server->>Server: 验证通过
Server-->>Client: 返回 AccessToken(2h) + RefreshToken(7d)
Note over Client,Server: 正常请求阶段
Client->>Server: 请求 API + AccessToken
Server->>Server: 验证 Token 有效性
Server-->>Client: 返回数据
Note over Client,Server: Token 过期处理
Client->>Server: 请求 API + 过期 AccessToken
Server-->>Client: 返回 401 Token 已过期
Client->>Server: 刷新 Token + RefreshToken
Server->>Server: 验证 RefreshToken
Server-->>Client: 返回新的 AccessToken + RefreshToken
Note over Client,Server: 继续使用
Client->>Server: 请求 API + 新 AccessToken
Server-->>Client: 返回数据代码实现:
/**
* JWT Token 工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration; // AccessToken 过期时间 (2 小时)
@Value("${jwt.refreshExpiration}")
private Long refreshExpiration; // RefreshToken 过期时间 (7 天)
/**
* 生成 Token
*/
public String generateToken(LoginUser loginUser, String deviceId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", loginUser.getUserId());
claims.put("userName", loginUser.getUsername());
claims.put("deviceId", deviceId);
Date now = new Date();
Date expireDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
/**
* 生成 RefreshToken
*/
public String generateRefreshToken(String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + refreshExpiration);
return Jwts.builder()
.setSubject(username)
.setId(UUID.randomUUID().toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.error("解析 Token 失败:{}", e.getMessage());
return null;
}
}
/**
* 从 Token 中获取用户 ID
*/
public Long getUserId(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return null;
}
return Long.valueOf(claims.get("userId").toString());
}
/**
* 验证 Token 是否过期
*/
public boolean isTokenExpired(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return true;
}
return claims.getExpiration().before(new Date());
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
return claims != null && !isTokenExpired(token);
} catch (Exception e) {
log.error("验证 Token 失败", e);
return false;
}
}
}/**
* Spring Security 6 配置
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationProvider authenticationProvider;
/**
* 配置安全过滤器链
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF
.csrf(csrf -> csrf.disable())
// 禁用 Session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则
.authorizeHttpRequests(auth -> auth
// 放行静态资源和公开接口
.requestMatchers("/login", "/register", "/captchaImage")
.permitAll()
.requestMatchers("/common/download/**", "/common/download/resource")
.permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
// 配置异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
// 未认证处理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未认证\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
// 无权限处理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}");
})
);
return http.build();
}
/**
* 配置认证提供者
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 用户详情服务
*/
@Bean
public UserDetailsService userDetailsService() {
return username -> {
// 从数据库加载用户信息
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
username,
user.getPassword(),
getAuthorities(user)
);
};
}
/**
* 获取用户权限列表
*/
private Collection<? extends GrantedAuthority> getAuthorities(SysUser user) {
List<GrantedAuthority> authorities = new ArrayList<>();
// 添加角色权限
for (SysRole role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleKey()));
}
// 添加操作权限
for (String permission : user.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission));
}
return authorities;
}
}/**
* JWT 认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 获取 Token
String token = getTokenFromRequest(request);
// 2. 没有 Token,直接放行
if (StringUtils.isEmpty(token)) {
filterChain.doFilter(request, response);
return;
}
// 3. 验证 Token
if (!jwtUtil.validateToken(token)) {
log.warn("Token 无效或已过期");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"Token 无效或已过期\"}");
return;
}
// 4. 解析 Token,获取用户信息
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());
String username = claims.getSubject();
// 5. 从 Redis 中获取登录用户信息
LoginUser loginUser = tokenService.getLoginUser(token);
if (loginUser == null) {
log.warn("登录用户不存在");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"code\":401,\"msg\":\"登录用户不存在\"}");
return;
}
// 6. 设置 Spring Security 上下文
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null,
loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
log.debug("用户 [{}] 认证成功", username);
// 7. 继续过滤链
filterChain.doFilter(request, response);
}
/**
* 从请求中获取 Token
*/
private String getTokenFromRequest(HttpServletRequest request) {
// 从 Header 中获取
String token = request.getHeader("Authorization");
// 支持 Bearer Token 格式
if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 如果 Header 中没有,尝试从参数中获取
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
}
return token;
}
}/**
* 用户认证 Controller
*/
@RestController
public class AuthController {
@Autowired
private AuthService authService;
/**
* 登录接口
*/
@PostMapping("/login")
public R<Map<String, Object>> login(@RequestBody LoginBody loginBody) {
// 1. 验证码校验 (可选)
if (loginBody.getCaptchaCode() != null) {
captchaService.verifyCaptcha(loginBody.getUsername(),
loginBody.getCaptchaCode());
}
// 2. 执行登录认证
LoginResult result = authService.login(
loginBody.getUsername(),
loginBody.getPassword(),
loginBody.getCode(),
loginBody.getUuid()
);
// 3. 返回 Token 和用户信息
Map<String, Object> response = new HashMap<>();
response.put("accessToken", result.getAccessToken());
response.put("refreshToken", result.getRefreshToken());
response.put("userInfo", result.getUserInfo());
return R.success(response);
}
/**
* 退出登录
*/
@PostMapping("/logout")
public R<Void> logout(HttpServletRequest request) {
String token = getTokenFromRequest(request);
authService.logout(token);
return R.success();
}
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public R<Map<String, String>> refreshToken(
@RequestParam String refreshToken) {
Map<String, String> tokens = authService.refreshToken(refreshToken);
return R.success(tokens);
}
}/**
* 认证服务
*/
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final TokenService tokenService;
private final UserService userService;
/**
* 登录认证
*/
@Transactional
public LoginResult login(String username, String password,
String code, String uuid) {
// 1. 创建认证 Token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, password);
// 2. 执行认证 (会调用 UserDetailsService)
Authentication authenticate;
try {
authenticate = authenticationManager.authenticate(authToken);
} catch (BadCredentialsException e) {
throw new ServiceException("用户名或密码错误");
} catch (DisabledException e) {
throw new ServiceException("账号已被停用");
} catch (Exception e) {
log.error("登录认证失败", e);
throw new ServiceException("登录失败:" + e.getMessage());
}
// 3. 获取登录用户
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
// 4. 记录登录日志
recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
// 5. 生成 Token
String deviceId = UUID.randomUUID().toString();
String accessToken = jwtUtil.generateToken(loginUser, deviceId);
String refreshToken = jwtUtil.generateRefreshToken(username);
// 6. 将用户信息缓存到 Redis
tokenService.saveLoginUser(loginUser, accessToken, deviceId);
// 7. 构建返回结果
LoginResult result = new LoginResult();
result.setAccessToken(accessToken);
result.setRefreshToken(refreshToken);
result.setUserInfo(buildUserInfo(loginUser));
return result;
}
/**
* 退出登录
*/
public void logout(String token) {
// 从 Redis 中删除登录信息
tokenService.deleteLoginUser(token);
// 记录登出日志
Claims claims = jwtUtil.parseToken(token);
if (claims != null) {
String username = claims.getSubject();
recordLogininfor(username, Constants.LOGOUT, "退出成功");
}
}
/**
* 刷新 Token
*/
@Transactional
public Map<String, String> refreshToken(String refreshToken) {
// 1. 验证 RefreshToken
if (!jwtUtil.validateToken(refreshToken)) {
throw new ServiceException("RefreshToken 已过期");
}
// 2. 获取用户名
Claims claims = jwtUtil.parseToken(refreshToken);
String username = claims.getSubject();
// 3. 重新生成 Token
LoginUser loginUser = loadUserByUsername(username);
String deviceId = UUID.randomUUID().toString();
String newAccessToken = jwtUtil.generateToken(loginUser, deviceId);
String newRefreshToken = jwtUtil.generateRefreshToken(username);
// 4. 更新 Redis缓存
tokenService.saveLoginUser(loginUser, newAccessToken, deviceId);
// 5. 返回新 Token
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", newAccessToken);
tokens.put("refreshToken", newRefreshToken);
return tokens;
}
/**
* 加载用户信息
*/
private LoginUser loadUserByUsername(String username) {
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
throw new ServiceException("用户不存在");
}
return userDetailsService.loadUserByUsername(username);
}
}/**
* 设备类型枚举
*/
public enum DeviceType {
WEB("web", "PC 网页"),
APP("app", "手机 App"),
MINI_PROGRAM("mp", "小程序"),
WECHAT("wechat", "微信公众号");
private final String code;
private final String desc;
DeviceType(String code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 从 User-Agent 中识别设备类型
*/
public static DeviceType fromUserAgent(String userAgent) {
if (StringUtils.isEmpty(userAgent)) {
return DeviceType.WEB;
}
userAgent = userAgent.toLowerCase();
// 判断是否为 App
if (userAgent.contains("android") || userAgent.contains("iphone")) {
if (userAgent.contains("micromessenger")) {
return DeviceType.WECHAT;
}
return DeviceType.APP;
}
// 判断是否为小程序
if (userAgent.contains("miniprogram")) {
return DeviceType.MINI_PROGRAM;
}
// 默认为 Web
return DeviceType.WEB;
}
}/**
* 多终端登录管理器
*/
@Component
@RequiredArgsConstructor
public class MultiTerminalManager {
private final RedisUtil redisUtil;
/**
* 同一账号允许同时登录的设备数
*/
private static final int MAX_LOGIN_DEVICES = 5;
/**
* 检查是否允许登录
*/
public boolean allowLogin(Long userId, DeviceType deviceType) {
String key = "login:user:" + userId;
// 获取该用户所有登录设备
Set<String> devices = redisUtil.get(key, Set.class);
if (devices == null) {
return true;
}
// 检查是否超过最大设备数
if (devices.size() >= MAX_LOGIN_DEVICES) {
// 踢出最早登录的设备
String earliestDevice = getEarliestDevice(devices);
kickoutDevice(userId, earliestDevice);
}
return true;
}
/**
* 记录设备登录
*/
public void recordDeviceLogin(Long userId, String deviceId, DeviceType deviceType) {
String key = "login:user:" + userId;
Map<String, Object> deviceInfo = new HashMap<>();
deviceInfo.put("deviceId", deviceId);
deviceInfo.put("deviceType", deviceType.getCode());
deviceInfo.put("loginTime", System.currentTimeMillis());
redisUtil.hashSet(key, deviceId, deviceInfo);
redisUtil.expire(key, 7, TimeUnit.DAYS);
}
/**
* 踢出指定设备
*/
public void kickoutDevice(Long userId, String deviceId) {
String key = "login:user:" + userId + ":token:" + deviceId;
String token = redisUtil.get(key, String.class);
if (token != null) {
// 将 Token 加入黑名单
redisUtil.set("token:blacklist:" + token, "",
2, TimeUnit.HOURS);
}
// 删除设备信息
redisUtil.hashDelete(key, deviceId);
}
/**
* 获取最早登录的设备
*/
private String getEarliestDevice(Set<String> devices) {
long earliestTime = Long.MAX_VALUE;
String earliestDevice = null;
for (String deviceId : devices) {
// TODO: 获取设备登录时间并比较
}
return earliestDevice;
}
}/**
* Token 服务优化版
*/
@Service
@RequiredArgsConstructor
public class TokenService {
private final RedisUtil redisUtil;
/**
* 保存登录用户 (带缓存预热)
*/
public void saveLoginUser(LoginUser user, String token, String deviceId) {
String userKey = "login:token:" + token;
String deviceKey = "login:device:" + deviceId;
// 1. 缓存用户信息
redisUtil.set(userKey, user, 2, TimeUnit.HOURS);
// 2. 建立 Token 和设备映射
redisUtil.set(deviceKey, token, 2, TimeUnit.HOURS);
// 3. 预热用户权限数据
warmUpUserPermissions(user.getUserId());
}
/**
* 获取登录用户 (多级缓存)
*/
public LoginUser getLoginUser(String token) {
// 1. 检查 Token 黑名单
if (isTokenBlacklisted(token)) {
return null;
}
// 2. 从 Redis 获取
String userKey = "login:token:" + token;
LoginUser user = redisUtil.get(userKey, LoginUser.class);
if (user != null) {
// 3. 续期
redisUtil.expire(userKey, 2, TimeUnit.HOURS);
}
return user;
}
/**
* 预热用户权限数据
*/
private void warmUpUserPermissions(Long userId) {
// 提前加载用户权限到缓存
String permKey = "user:perms:" + userId;
// ... 加载权限数据
}
/**
* 检查 Token 是否在黑名单中
*/
private boolean isTokenBlacklisted(String token) {
return Boolean.TRUE.equals(redisUtil.hasKey("token:blacklist:" + token));
}
}/**
* 登录限流器
*/
@Component
public class LoginRateLimiter {
private final RedisUtil redisUtil;
/**
* 检查是否允许登录
*/
public boolean allowLogin(String ip, String username) {
// IP 维度限流:每分钟最多 10 次
String ipKey = "login:limit:ip:" + ip;
Long ipCount = redisUtil.increment(ipKey);
if (ipCount == 1) {
redisUtil.expire(ipKey, 1, TimeUnit.MINUTES);
}
if (ipCount > 10) {
log.warn("IP 登录过于频繁:{}", ip);
return false;
}
// 用户名维度限流:每分钟最多 5 次
String userKey = "login:limit:user:" + username;
Long userCount = redisUtil.increment(userKey);
if (userCount == 1) {
redisUtil.expire(userKey, 1, TimeUnit.MINUTES);
}
if (userCount > 5) {
log.warn("用户名登录过于频繁:{}", username);
return false;
}
return true;
}
}/**
* Token 签名验证增强
*/
public class SecureJwtUtil extends JwtUtil {
/**
* 双重签名验证
*/
public boolean validateTokenSecure(String token) {
// 1. 基础验证
if (!validateToken(token)) {
return false;
}
// 2. 检查签名算法
Claims claims = parseToken(token);
String alg = (String) claims.get("alg");
if (!"HS256".equals(alg)) {
log.warn("不支持的签名算法:{}", alg);
return false;
}
// 3. 检查必要字段
if (claims.get("userId") == null || claims.getSubject() == null) {
log.warn("Token 缺少必要字段");
return false;
}
return true;
}
/**
* 添加指纹验证
*/
public String generateTokenWithFingerprint(LoginUser user, String fingerprint) {
Map<String, Object> claims = buildClaims(user);
claims.put("fingerprint", fingerprint); // 设备指纹
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
}/**
* 安全响应头配置
*/
@Configuration
public class SecurityHeaderConfig {
@Bean
public Filter securityHeaderFilter() {
return (request, response, chain) -> {
// 防止 XSS 攻击
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
// 内容安全策略
response.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'");
// HSTS
response.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains");
chain.doFilter(request, response);
};
}
}本文详细介绍了 JWT+Spring Security 6 多终端认证系统的完整实现:
✅ JWT 原理: Token 结构 + 双 Token 机制 ✅ Security6 配置: 过滤器链 + 认证提供者 ✅ 登录流程: 完整的认证时序 ✅ 多终端: 设备识别 + 并发控制 ✅ 性能优化: 缓存策略 + 限流 ✅ 安全加固: 防篡改 + XSS/CSRF
如果这篇文章对你有帮助,请:
•👍 点赞支持•⭐ 收藏备用•💖 关注我,获取更多认证安全干货•💬 评论区聊聊:你在认证系统中遇到过哪些坑?
🚀 祝您的认证系统安全稳定!