摘要:微服务架构下,认证(Authentication)与授权(Authorization)是系统安全的核心。本文基于 Spring Boot 3.x + Spring Security 6,深入讲解如何构建一套完整的 JWT 无状态认证体系,并结合 Redis 实现 Token 管理(黑名单/刷新/单点登录),最终与 Spring Cloud Gateway 打通,实现微服务统一鉴权入口。
目录
- 技术选型与架构设计
- Spring Security 6 核心变化
- 项目结构与依赖
- 用户认证模块实现
- JWT 工具类封装
- Redis Token 管理策略
- Spring Security 6 配置详解
- JWT 过滤器实现
- RBAC 权限模型集成
- Gateway 网关统一鉴权
- Token 刷新与单点登录
- 安全加固建议
- 完整流程测试
1. 技术选型与架构设计
1.1 为什么选择 JWT + Redis?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Session(有状态) | 简单,天然支持强制踢线 | 水平扩展难,需共享存储 |
| 纯 JWT(无状态) | 无需服务端存储,扩展性好 | 无法主动失效,安全性弱 |
| JWT + Redis(混合) | 扩展性好 + 可主动失效 + 支持刷新 | 多一次 Redis 查询 |
结论:生产环境推荐 JWT + Redis 混合模式:JWT 携带用户基本信息,Redis 存储 Token 状态(有效性、黑名单、刷新令牌)。
1.2 整体架构
客户端
│
▼
Spring Cloud Gateway(统一鉴权入口)
│ ├─ 白名单路由直接放行
│ ├─ 请求携带 Authorization: Bearer <token>
│ └─ 调用认证服务验证 Token / 解析用户信息
│
▼
认证服务(Auth Service)
│ ├─ /auth/login → 登录,颁发 AccessToken + RefreshToken
│ ├─ /auth/logout → 登出,Token 加入 Redis 黑名单
│ └─ /auth/refresh → 刷新,颁发新 AccessToken
│
▼
业务微服务(User/Order/Product...)
│ └─ 从 Gateway 转发的请求头中读取用户信息(无需再验证 Token)
│
▼
Redis(Token 存储)
│ ├─ auth:token:{userId} → 当前有效 AccessToken(支持单点登录)
│ ├─ auth:refresh:{userId} → RefreshToken
│ └─ auth:blacklist:{jti} → 黑名单(TTL = Token 剩余有效期)
2. Spring Security 6 核心变化
Spring Boot 3.x 使用的是 Spring Security 6.x,相比 5.x 有以下重要变化:
2.1 废弃 WebSecurityConfigurerAdapter
Spring Security 5.x:
// ❌ 已废弃
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception { ... }
}
Spring Security 6.x:
// ✅ 推荐:直接注入 SecurityFilterChain Bean
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
}
2.2 Lambda DSL 成为主流
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable());
2.3 AuthorizationManager 替代 AccessDecisionManager
Spring Security 6 引入 AuthorizationManager<RequestAuthorizationContext> 接口,更加灵活,可完全自定义鉴权逻辑。
2.4 路径匹配器变化
AntPathMatcher 仍可用,但推荐使用 PathPatternParser(默认),性能更好。
3. 项目结构与依赖
3.1 Maven 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT(推荐使用 jjwt 0.12.x) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus(用户数据) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
3.2 项目结构
auth-service/
├── src/main/java/com/example/auth/
│ ├── config/
│ │ ├── SecurityConfig.java # Spring Security 主配置
│ │ └── RedisConfig.java # Redis 序列化配置
│ ├── controller/
│ │ └── AuthController.java # 登录/登出/刷新接口
│ ├── filter/
│ │ └── JwtAuthenticationFilter.java # JWT 验证过滤器
│ ├── handler/
│ │ ├── LoginSuccessHandler.java # 登录成功处理器
│ │ └── LoginFailureHandler.java # 登录失败处理器
│ ├── model/
│ │ ├── entity/User.java
│ │ └── dto/LoginRequest.java
│ ├── service/
│ │ ├── AuthService.java
│ │ └── UserDetailsServiceImpl.java
│ └── util/
│ ├── JwtUtil.java # JWT 工具类
│ └── RedisTokenStore.java # Redis Token 管理
└── src/main/resources/
└── application.yml
4. 用户认证模块实现
4.1 用户实体与 UserDetails 实现
// User.java
@Data
@TableName("sys_user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String password;
private Integer status; // 0禁用 1启用
private String roles; // 角色(逗号分隔,如 ROLE_ADMIN,ROLE_USER)
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// LoginUser.java —— 实现 UserDetails
@Data
public class LoginUser implements UserDetails {
private User user;
private List<GrantedAuthority> authorities;
public LoginUser(User user) {
this.user = user;
// 解析角色
this.authorities = Arrays.stream(user.getRoles().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() { return user.getPassword(); }
@Override
public String getUsername() { return user.getUsername(); }
@Override
public boolean isEnabled() { return user.getStatus() == 1; }
// 账号未过期、未锁定、凭证未过期均返回 true(简化处理)
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
}
4.2 UserDetailsService 实现
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
);
if (user == null) {
throw new UsernameNotFoundException("用户不存在:" + username);
}
return new LoginUser(user);
}
}
5. JWT 工具类封装
5.1 配置参数
# application.yml
jwt:
secret: "your-256-bit-secret-key-at-least-32-characters-long"
access-token-expire: 1800 # AccessToken 有效期(秒),30 分钟
refresh-token-expire: 604800 # RefreshToken 有效期(秒),7 天
issuer: "92yangyi.top"
5.2 JwtUtil 实现(基于 jjwt 0.12.x)
@Component
@Slf4j
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expire}")
private long accessTokenExpire;
@Value("${jwt.refresh-token-expire}")
private long refreshTokenExpire;
@Value("${jwt.issuer}")
private String issuer;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(
Base64.getEncoder().encodeToString(secret.getBytes(StandardCharsets.UTF_8))
));
}
/**
* 生成 AccessToken
*/
public String generateAccessToken(LoginUser loginUser) {
return buildToken(loginUser, accessTokenExpire, "access");
}
/**
* 生成 RefreshToken
*/
public String generateRefreshToken(LoginUser loginUser) {
return buildToken(loginUser, refreshTokenExpire, "refresh");
}
private String buildToken(LoginUser loginUser, long expireSeconds, String tokenType) {
User user = loginUser.getUser();
Date now = new Date();
Date expiry = new Date(now.getTime() + expireSeconds * 1000);
List<String> roles = loginUser.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return Jwts.builder()
.id(UUID.randomUUID().toString()) // jti,唯一标识,用于黑名单
.subject(String.valueOf(user.getId())) // sub:用户 ID
.issuer(issuer)
.issuedAt(now)
.expiration(expiry)
.claim("username", user.getUsername())
.claim("roles", roles)
.claim("tokenType", tokenType)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 解析 Token,返回 Claims
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取用户 ID
*/
public Long getUserId(String token) {
return Long.valueOf(parseToken(token).getSubject());
}
/**
* 获取 jti(Token 唯一标识)
*/
public String getJti(String token) {
return parseToken(token).getId();
}
/**
* 获取 Token 过期时间(毫秒时间戳)
*/
public long getExpiration(String token) {
return parseToken(token).getExpiration().getTime();
}
/**
* 验证 Token 是否有效(签名合法 + 未过期)
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.warn("Token 验证失败:{}", e.getMessage());
return false;
}
}
public long getAccessTokenExpire() { return accessTokenExpire; }
public long getRefreshTokenExpire() { return refreshTokenExpire; }
}
6. Redis Token 管理策略
6.1 Redis 配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 Jackson JSON 序列化
Jackson2JsonRedisSerializer<Object> jackson2 =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(jackson2);
template.setHashValueSerializer(jackson2);
template.afterPropertiesSet();
return template;
}
}
6.2 RedisTokenStore 实现
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisTokenStore {
private final RedisTemplate<String, Object> redisTemplate;
private final JwtUtil jwtUtil;
// Key 前缀
private static final String ACCESS_TOKEN_KEY = "auth:token:";
private static final String REFRESH_TOKEN_KEY = "auth:refresh:";
private static final String BLACKLIST_KEY = "auth:blacklist:";
/**
* 保存 AccessToken(同一用户只保留最新,实现单设备登录)
*/
public void saveAccessToken(Long userId, String token) {
String key = ACCESS_TOKEN_KEY + userId;
long expire = jwtUtil.getAccessTokenExpire();
redisTemplate.opsForValue().set(key, token, expire, TimeUnit.SECONDS);
log.info("保存 AccessToken,userId={},expire={}s", userId, expire);
}
/**
* 保存 RefreshToken
*/
public void saveRefreshToken(Long userId, String refreshToken) {
String key = REFRESH_TOKEN_KEY + userId;
long expire = jwtUtil.getRefreshTokenExpire();
redisTemplate.opsForValue().set(key, refreshToken, expire, TimeUnit.SECONDS);
}
/**
* 获取当前有效 AccessToken(用于单点登录校验)
*/
public String getAccessToken(Long userId) {
return (String) redisTemplate.opsForValue().get(ACCESS_TOKEN_KEY + userId);
}
/**
* 获取 RefreshToken
*/
public String getRefreshToken(Long userId) {
return (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_KEY + userId);
}
/**
* Token 加入黑名单(用户登出时调用)
* TTL = Token 剩余有效期,保证黑名单自动清理
*/
public void addToBlacklist(String token) {
String jti = jwtUtil.getJti(token);
long expiration = jwtUtil.getExpiration(token);
long ttl = expiration - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
BLACKLIST_KEY + jti, "1", ttl, TimeUnit.MILLISECONDS
);
log.info("Token 加入黑名单,jti={},ttl={}ms", jti, ttl);
}
}
/**
* 检查 Token 是否在黑名单中
*/
public boolean isBlacklisted(String token) {
String jti = jwtUtil.getJti(token);
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_KEY + jti));
}
/**
* 删除 Token(登出时清理用户的所有 Token)
*/
public void deleteUserTokens(Long userId) {
redisTemplate.delete(ACCESS_TOKEN_KEY + userId);
redisTemplate.delete(REFRESH_TOKEN_KEY + userId);
}
}
7. Spring Security 6 配置详解
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 启用方法级鉴权 @PreAuthorize
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final JwtAuthenticationFilter jwtAuthFilter;
/**
* 密码编码器(BCrypt)
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* AuthenticationManager(登录时手动调用)
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
/**
* 核心安全过滤链配置
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(JWT 无状态模式不需要)
.csrf(csrf -> csrf.disable())
// 禁用 Session(纯无状态)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 路由权限配置
.authorizeHttpRequests(auth -> auth
// 认证接口放行
.requestMatchers(
"/auth/login",
"/auth/refresh",
"/actuator/health"
).permitAll()
// 管理员接口
.requestMatchers("/admin/**").hasRole("ADMIN")
// 其他请求需要认证
.anyRequest().authenticated()
)
// 自定义异常处理
.exceptionHandling(ex -> ex
// 未认证(401)
.authenticationEntryPoint((request, response, e) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}");
})
// 无权限(403)
.accessDeniedHandler((request, response, e) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}");
})
)
// 在 UsernamePasswordAuthenticationFilter 之前插入 JWT 过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
8. JWT 过滤器实现
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final RedisTokenStore redisTokenStore;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 从请求头提取 Token
String token = extractToken(request);
if (token == null) {
filterChain.doFilter(request, response);
return;
}
// 2. 验证 Token 签名和有效期
if (!jwtUtil.validateToken(token)) {
filterChain.doFilter(request, response);
return;
}
// 3. 检查黑名单(是否已登出)
if (redisTokenStore.isBlacklisted(token)) {
log.warn("Token 已在黑名单中,拒绝访问");
filterChain.doFilter(request, response);
return;
}
// 4. 单点登录校验:比对 Redis 中最新 Token
Long userId = jwtUtil.getUserId(token);
String storedToken = redisTokenStore.getAccessToken(userId);
if (!token.equals(storedToken)) {
log.warn("Token 与 Redis 不一致,可能已在其他设备登录,userId={}", userId);
filterChain.doFilter(request, response);
return;
}
// 5. 加载用户信息,构建认证对象
Claims claims = jwtUtil.parseToken(token);
String username = claims.get("username", String.class);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 6. 存入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
/**
* 从 Authorization 头提取 Bearer Token
*/
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
9. RBAC 权限模型集成
9.1 数据库表设计
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(128) NOT NULL,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL UNIQUE COMMENT '如 ROLE_ADMIN',
role_name VARCHAR(64) NOT NULL,
status TINYINT DEFAULT 1
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY,
perm_code VARCHAR(128) NOT NULL UNIQUE COMMENT '如 user:read',
perm_name VARCHAR(64) NOT NULL,
resource_url VARCHAR(256)
);
-- 用户-角色关联
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限关联
CREATE TABLE sys_role_permission (
role_id BIGINT NOT NULL,
perm_id BIGINT NOT NULL,
PRIMARY KEY (role_id, perm_id)
);
9.2 方法级鉴权使用
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@GetMapping("/list")
@PreAuthorize("hasRole('ADMIN')") // 需要管理员角色
public List<User> listUsers() { ... }
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('user:delete')") // 需要具体权限
public void deleteUser(@PathVariable Long id) { ... }
@GetMapping("/profile")
@PreAuthorize("isAuthenticated()") // 仅需登录
public User getProfile() { ... }
}
10. Gateway 网关统一鉴权
在 Spring Cloud Gateway 中,通过全局过滤器实现 Token 预验证,下游服务无需再次验证。
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtUtil jwtUtil;
private final RedisTokenStore redisTokenStore;
// 白名单路径(不需要鉴权)
private static final List<String> WHITE_LIST = Arrays.asList(
"/auth/login", "/auth/refresh", "/actuator/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单直接放行
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 提取 Token
String token = extractToken(exchange.getRequest());
if (token == null) {
return unauthorized(exchange, "缺少认证 Token");
}
// 验证 Token
if (!jwtUtil.validateToken(token) || redisTokenStore.isBlacklisted(token)) {
return unauthorized(exchange, "Token 无效或已过期");
}
// 解析用户信息,注入下游请求头(避免下游重复解析 JWT)
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.getSubject());
String username = claims.get("username", String.class);
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", String.valueOf(userId))
.header("X-Username", username)
.header("X-User-Roles", claims.get("roles", List.class).toString())
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = String.format("{\"code\":401,\"msg\":\"%s\"}", msg);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
private boolean isWhiteList(String path) {
return WHITE_LIST.stream().anyMatch(p ->
new AntPathMatcher().match(p, path)
);
}
private String extractToken(ServerHttpRequest request) {
List<String> headers = request.getHeaders().get("Authorization");
if (headers != null && !headers.isEmpty()) {
String header = headers.get(0);
if (header.startsWith("Bearer ")) {
return header.substring(7);
}
}
return null;
}
@Override
public int getOrder() { return -100; } // 高优先级
}
11. Token 刷新与单点登录
11.1 认证控制器
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final RedisTokenStore redisTokenStore;
private final UserDetailsServiceImpl userDetailsService;
/**
* 登录接口
*/
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest request) {
// 1. Spring Security 认证(内部调用 UserDetailsService.loadUserByUsername)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
// 2. 生成 Token
String accessToken = jwtUtil.generateAccessToken(loginUser);
String refreshToken = jwtUtil.generateRefreshToken(loginUser);
// 3. 保存到 Redis(覆盖旧 Token,实现单点登录)
redisTokenStore.saveAccessToken(userId, accessToken);
redisTokenStore.saveRefreshToken(userId, refreshToken);
log.info("用户登录成功,userId={}", userId);
return ResponseEntity.ok(Map.of(
"accessToken", accessToken,
"refreshToken", refreshToken,
"expiresIn", jwtUtil.getAccessTokenExpire()
));
}
/**
* 登出接口
*/
@PostMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request) {
String token = extractToken(request);
if (token != null && jwtUtil.validateToken(token)) {
Long userId = jwtUtil.getUserId(token);
// 加入黑名单
redisTokenStore.addToBlacklist(token);
// 清理 Redis 中的 Token(可选:多设备模式不清理,单设备登录则清理)
redisTokenStore.deleteUserTokens(userId);
log.info("用户登出,userId={}", userId);
}
return ResponseEntity.ok("登出成功");
}
/**
* 刷新 Token 接口
* 使用 RefreshToken 换取新的 AccessToken
*/
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestHeader("X-Refresh-Token") String refreshToken) {
// 1. 验证 RefreshToken
if (!jwtUtil.validateToken(refreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("msg", "RefreshToken 无效或已过期,请重新登录"));
}
// 2. 检查 Redis 中的 RefreshToken
Long userId = jwtUtil.getUserId(refreshToken);
String storedRefreshToken = redisTokenStore.getRefreshToken(userId);
if (!refreshToken.equals(storedRefreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("msg", "RefreshToken 已失效,请重新登录"));
}
// 3. 加载用户信息
Claims claims = jwtUtil.parseToken(refreshToken);
String username = claims.get("username", String.class);
LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
// 4. 颁发新的 AccessToken
String newAccessToken = jwtUtil.generateAccessToken(loginUser);
redisTokenStore.saveAccessToken(userId, newAccessToken);
return ResponseEntity.ok(Map.of(
"accessToken", newAccessToken,
"expiresIn", jwtUtil.getAccessTokenExpire()
));
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
11.2 单点登录(SSO)原理
用户 A 在设备1登录 → Redis 保存 AccessToken1
用户 A 在设备2登录 → Redis 保存 AccessToken2(覆盖 AccessToken1)
设备1 再次请求 → 过滤器比对 Redis 中 Token != AccessToken1 → 拒绝 → 强制重新登录
12. 安全加固建议
12.1 HTTPS 强制
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: yourpassword
key-store-type: PKCS12
12.2 防止暴力破解(登录限流)
@Service
@RequiredArgsConstructor
public class LoginAttemptService {
private final RedisTemplate<String, Object> redisTemplate;
private static final int MAX_ATTEMPTS = 5;
private static final long LOCK_DURATION = 15 * 60; // 锁定 15 分钟
private String getKey(String username) {
return "auth:attempts:" + username;
}
public void recordFailure(String username) {
String key = getKey(username);
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, LOCK_DURATION, TimeUnit.SECONDS);
}
public boolean isLocked(String username) {
Object count = redisTemplate.opsForValue().get(getKey(username));
return count != null && Integer.parseInt(count.toString()) >= MAX_ATTEMPTS;
}
public void resetAttempts(String username) {
redisTemplate.delete(getKey(username));
}
}
12.3 Token 密钥轮换
生产环境建议使用 RSA 非对称加密(私钥签名、公钥验证),避免密钥泄露导致全局风险:
// 使用 RSA 密钥对(Spring Security OAuth2 Resource Server 原生支持)
@Bean
public JwtDecoder jwtDecoder() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) loadPublicKey();
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
12.4 CORS 配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://92yangyi.top", "http://localhost:*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
13. 完整流程测试
13.1 登录获取 Token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin@123"}'
# 响应
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 1800
}
13.2 访问受保护接口
curl -X GET http://localhost:8080/user/list \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 无权限时返回
{"code": 403, "msg": "权限不足"}
13.3 刷新 Token
curl -X POST http://localhost:8080/auth/refresh \
-H "X-Refresh-Token: eyJhbGciOiJIUzI1NiJ9..."
# 响应
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...(新的)",
"expiresIn": 1800
}
13.4 登出
curl -X POST http://localhost:8080/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 响应
"登出成功"
# 再次使用该 Token 访问 → 401(已在黑名单中)
13.5 Redis 数据查看
redis-cli
KEYS auth:*
# auth:token:1001 → AccessToken
# auth:refresh:1001 → RefreshToken
# auth:blacklist:xxx → 黑名单(已登出的 Token jti)
# auth:attempts:admin → 登录失败次数
TTL auth:token:1001 → 剩余有效时间(秒)
GET auth:token:1001 → 查看 Token 内容
总结
本文系统讲解了基于 Spring Boot 3.x + Spring Security 6 + JWT + Redis 的微服务统一认证授权方案:
| 模块 | 核心技术 | 关键点 |
|---|---|---|
| 认证 | Spring Security 6 + AuthenticationManager | SecurityFilterChain Lambda DSL |
| Token | jjwt 0.12.x | AccessToken + RefreshToken 双 Token 机制 |
| 状态管理 | Redis | 黑名单、单点登录、登录失败限流 |
| 鉴权 | RBAC + @PreAuthorize | 方法级权限控制 |
| 网关集成 | Spring Cloud Gateway + GlobalFilter | 统一鉴权入口,下游透传用户信息 |
| 安全加固 | HTTPS + 限流 + CORS | 生产环境必备 |
与博客已有内容的衔接:
- 搭配 Spring Cloud Gateway 网关鉴权(见 Gateway 实战文章)
- 结合 Redis 实现 Token 管理(见 Redis 系列)
- 作为 微服务架构安全层的核心组件,与 Docker 部署、K8s 集群方案完美融合
参考资料:
评论区