前言
"JWT 权限管理不是玄学,是架构!"
在微服务架构盛行的今天,无状态认证已成为企业级应用的标准配置。而 JWT(JSON Web Token)凭借其自包含、可验证、跨语言的特性,成为了认证领域的事实标准。
然而,很多开发者对 JWT 的理解仍停留在"复制粘贴代码"的层面:
- ❌ Token 被篡改了怎么办?
- ❌ Token 如何优雅续期?
- ❌ 用户注销后 Token 为何还能用?
- ❌ 多设备登录如何管理?
- ❌ 权限变更如何实时生效?
本文将基于 Spring Boot 3.2 + Spring Security 6.2 的最新特性,带你从零构建一个生产级 JWT 认证系统,涵盖从原理剖析、代码实现到安全加固的完整链路。
一、为什么选择 JWT?
1.1 传统 Session vs JWT
┌─────────────────────────────────────────────────────────────┐
│ 传统 Session 认证 │
├─────────────────────────────────────────────────────────────┤
│ 客户端 服务端 存储层 │
│ │ │ │ │
│ │──登录请求──> │ │ │
│ │ │──创建 Session─>│ │
│ │<─Set-Cookie──│ │ │
│ │ │ │ │
│ │──请求 +Cookie─>│ │ ← 性能瓶颈! │
│ │ │──查询 Session─>│ │
│ │<─响应────────│ │ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ JWT 认证 │
├─────────────────────────────────────────────────────────────┤
│ 客户端 服务端 存储层 │
│ │ │ │ │
│ │──登录请求──> │ │ │
│ │ │──生成 JWT────>│ (可选存储) │
│ │<─Token───────│ │ │
│ │ │ │ │
│ │──请求 +Token─> │ │ │
│ │ │──验证签名───>│ ← 无状态!无需查询! │
│ │<─响应────────│ │ │
└─────────────────────────────────────────────────────────────┘
1.2 JWT 的核心优势
| 特性 |
说明 |
适用场景 |
| 无状态 |
服务端无需存储 Session |
微服务、分布式系统 |
| 跨域支持 |
天然支持 CORS |
前后端分离、多端应用 |
| 自包含 |
Claims 携带用户信息 |
减少数据库查询 |
| 可验证 |
签名防篡改 |
高安全要求场景 |
| 标准化 |
RFC 7519 标准 |
跨语言、跨平台 |
1.3 JWT 结构解析
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
┌─────────────┐ ┌─────────────────────────────────────┐ ┌──────────────────┐
│ Header │ │ Payload │ Signature │
│ (头部) │ │ (载荷) │ (签名) │
├─────────────┤ ├─────────────────────────────────────┤ ├──────────────────┤
│ alg: HS256 │ │ sub: "1234567890" │ HMACSHA256( │
│ typ: JWT │ │ name: "John Doe" │ base64UrlEncode( │
│ │ │ iat: 1516239022 ← 过期时间 │ header) + "." │
│ │ │ exp: 1516242622 ← 过期时间 │ + base64UrlEncode│
│ │ │ roles: ["USER","ADMIN"] ← 权限 │ (payload), │
│ │ │ deviceId: "xxx" ← 设备标识 │ secretKey) │
└─────────────┘ └─────────────────────────────────────┘ └──────────────────┘
二、项目架构设计
2.1 技术栈选型
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot 3.2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Spring Security 6.2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.0</version>
</dependency>
<!-- 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>
<!-- Redis 缓存(Token 黑名单) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
2.2 系统架构图
┌─────────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Web │ │ Mobile │ │ App │ │ 第三方 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────┼──────────────────────┘
│ │ │ │
└─────────────┴──────┬──────┴─────────────┘
│ JWT Token
┌────────────────────────────┼────────────────────────────────────────────┐
│ API 网关层 │
│ ┌───────────────────────────────┴───────────────────────────────────┐ │
│ │ AuthenticationFilter │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Token 解析 │ │ 签名验证 │ │ 权限校验 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────────────┼────────────────────────────────────────┐
│ 业务服务层 │
│ ┌─────────────┐ ┌─────────────┴─────────────┐ ┌─────────────┐ │
│ │ 用户服务 │ │ 认证服务 │ │ 权限服务 │ │
│ │ UserService│ │ AuthService │ │ RoleService │ │
│ └─────────────┘ └───────────────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
┌────────────────────────────────┼────────────────────────────────────────┐
│ 数据访问层 │
│ ┌─────────────┐ ┌────────────┴──────────────┐ ┌─────────────┐ │
│ │ MySQL │ │ Redis │ │ MongoDB │ │
│ │ 用户数据 │ │ Token 黑名单/刷新 Token │ │ 日志 │ │
│ └─────────────┘ └───────────────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
2.3 核心模块划分
auth-system/
├── src/main/java/com/example/auth/
│ ├── config/ # 配置类
│ │ ├── SecurityConfig.java # Security 配置
│ │ ├── JwtConfig.java # JWT 配置
│ │ └── RedisConfig.java # Redis 配置
│ ├── filter/ # 过滤器
│ │ └── JwtAuthenticationFilter.java
│ ├── handler/ # 处理器
│ │ ├── AuthSuccessHandler.java
│ │ └── AuthFailureHandler.java
│ ├── controller/ # 控制器
│ │ ├── AuthController.java
│ │ └── UserController.java
│ ├── service/ # 服务层
│ │ ├── AuthService.java
│ │ ├── UserService.java
│ │ └── JwtService.java
│ ├── entity/ # 实体类
│ │ ├── User.java
│ │ └── Role.java
│ ├── dto/ # 数据传输对象
│ │ ├── LoginRequest.java
│ │ ├── RegisterRequest.java
│ │ └── AuthResponse.java
│ ├── security/ # 安全相关
│ │ ├── UserDetailsImpl.java
│ │ └── JwtTokenProvider.java
│ └── exception/ # 异常处理
│ └── GlobalExceptionHandler.java
└── src/main/resources/
├── application.yml
└── schema.sql
三、核心代码实现
3.1 数据库设计
-- 用户表
CREATE TABLE `users` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(100),
`phone` VARCHAR(20),
`status` TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_username` (`username`),
INDEX `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色表
CREATE TABLE `roles` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL UNIQUE COMMENT '角色标识',
`description` VARCHAR(255),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户角色关联表
CREATE TABLE `user_roles` (
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`, `role_id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 权限表
CREATE TABLE `permissions` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL UNIQUE COMMENT '权限标识',
`description` VARCHAR(255),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色权限关联表
CREATE TABLE `role_permissions` (
`role_id` BIGINT NOT NULL,
`permission_id` BIGINT NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`),
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`),
FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始化数据
INSERT INTO `roles` (`name`, `description`) VALUES
('ROLE_USER', '普通用户'),
('ROLE_ADMIN', '管理员'),
('ROLE_SUPER_ADMIN', '超级管理员');
INSERT INTO `permissions` (`name`, `description`) VALUES
('user:read', '查看用户'),
('user:write', '编辑用户'),
('user:delete', '删除用户'),
('order:read', '查看订单'),
('order:write', '编辑订单');
INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
(1, 1), (1, 4), -- USER: user:read, order:read
(2, 1), (2, 2), (2, 4), (2, 5); -- ADMIN: user:read, user:write, order:read, order:write
3.2 实体类定义
// entity/User.java
package com.example.auth.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Accessors(chain = true)
@TableName("users")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableField(exist = false)
private List<Role> roles;
}
// entity/Role.java
package com.example.auth.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("roles")
public class Role {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private LocalDateTime createdAt;
@TableField(exist = false)
private List<Permission> permissions;
}
// entity/Permission.java
package com.example.auth.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("permissions")
public class Permission {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private LocalDateTime createdAt;
}
3.3 JWT 配置类
// config/JwtConfig.java
package com.example.auth.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
/**
* JWT 密钥(生产环境应从环境变量读取)
*/
private String secret = "your-256-bit-secret-key-for-jwt-signing-and-verification";
/**
* Access Token 过期时间
*/
private Duration accessTokenExpiration = Duration.ofMinutes(30);
/**
* Refresh Token 过期时间
*/
private Duration refreshTokenExpiration = Duration.ofDays(7);
/**
* Token 前缀
*/
private String tokenPrefix = "Bearer ";
/**
* Token Header 名称
*/
private String tokenHeader = "Authorization";
}
3.4 JWT 工具类(核心)
// security/JwtTokenProvider.java
package com.example.auth.security;
import com.example.auth.config.JwtConfig;
import com.example.auth.entity.User;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final JwtConfig jwtConfig;
/**
* 生成 SecretKey
*/
private SecretKey getSigningKey() {
byte[] keyBytes = jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成 Access Token
*/
public String generateAccessToken(Authentication authentication) {
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
return generateToken(userDetails, jwtConfig.getAccessTokenExpiration());
}
/**
* 生成 Refresh Token
*/
public String generateRefreshToken(Authentication authentication) {
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
return generateToken(userDetails, jwtConfig.getRefreshTokenExpiration());
}
/**
* 生成 Token(通用)
*/
private String generateToken(UserDetailsImpl userDetails, Duration expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration.toMillis());
// 提取权限字符串
String authorities = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.subject(String.valueOf(userDetails.getId()))
.claim("username", userDetails.getUsername())
.claim("roles", authorities)
.claim("deviceId", userDetails.getDeviceId()) // 设备标识
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 从 Token 中提取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseClaims(token);
return claims.get("username", String.class);
}
/**
* 从 Token 中提取用户 ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
return Long.parseLong(claims.getSubject());
}
/**
* 从 Token 中提取权限
*/
public String getRolesFromToken(String token) {
Claims claims = parseClaims(token);
return claims.get("roles", String.class);
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
parseClaims(token);
return true;
} catch (SecurityException ex) {
log.error("Invalid JWT signature: {}", ex.getMessage());
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token: {}", ex.getMessage());
} catch (ExpiredJwtException ex) {
log.error("JWT token is expired: {}", ex.getMessage());
} catch (UnsupportedJwtException ex) {
log.error("JWT token is unsupported: {}", ex.getMessage());
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty: {}", ex.getMessage());
}
return false;
}
/**
* 检查 Token 是否过期
*/
public boolean isTokenExpired(String token) {
try {
Claims claims = parseClaims(token);
return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
/**
* 解析 Claims
*/
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 刷新 Token(在 Token 即将过期时)
*/
public String refreshToken(String token) {
Claims claims = parseClaims(token);
// 如果 Token 已过期,不允许刷新
if (claims.getExpiration().before(new Date())) {
throw new ExpiredJwtException(null, claims, "Token is expired");
}
Date now = new Date();
Date newExpiryDate = new Date(now.getTime() + jwtConfig.getAccessTokenExpiration().toMillis());
return Jwts.builder()
.claims(claims)
.issuedAt(now)
.expiration(newExpiryDate)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
}
3.5 UserDetailsService 实现
// security/UserDetailsImpl.java
package com.example.auth.security;
import com.example.auth.entity.User;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
@Data
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final String deviceId;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean enabled;
public static UserDetailsImpl fromUser(User user, Collection<GrantedAuthority> authorities) {
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getPassword(),
null, // deviceId 可在登录时传入
authorities,
user.getStatus() == 1
);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
// service/CustomUserDetailsService.java
package com.example.auth.service;
import com.example.auth.entity.User;
import com.example.auth.security.UserDetailsImpl;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
if (user.getStatus() != 1) {
throw new UsernameNotFoundException("User account is disabled: " + username);
}
// 加载用户角色和权限
List<SimpleGrantedAuthority> authorities = roleRepository
.findByUserId(user.getId())
.stream()
.flatMap(role -> role.getPermissions().stream())
.map(permission -> new SimpleGrantedAuthority(permission.getName()))
.collect(Collectors.toList());
// 添加角色
roleRepository.findByUserId(user.getId())
.forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getName())));
return UserDetailsImpl.fromUser(user, authorities);
}
}
3.6 Security 配置(Spring Security 6 新特性)
// config/SecurityConfig.java
package com.example.auth.config;
import com.example.auth.filter.JwtAuthenticationFilter;
import com.example.auth.handler.AuthFailureHandler;
import com.example.auth.handler.AuthSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthSuccessHandler authSuccessHandler;
private final AuthFailureHandler authFailureHandler;
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
/**
* 认证提供者
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* Security 过滤链配置
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(使用 Token 无需 CSRF)
.csrf(AbstractHttpConfigurer::disable)
// 配置 CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 配置授权规则
.authorizeHttpRequests(auth -> auth
// 公开接口
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 静态资源
.requestMatchers(HttpMethod.GET, "/static/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 无状态 Session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置登录处理
.formLogin(form -> form
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
)
// 配置异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未授权\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
})
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* CORS 配置
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
return request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://yourdomain.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
return config;
};
}
}
3.7 JWT 认证过滤器
// filter/JwtAuthenticationFilter.java
package com.example.auth.filter;
import com.example.auth.security.JwtTokenProvider;
import com.example.auth.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final CustomUserDetailsService userDetailsService;
private final TokenBlacklistService tokenBlacklistService;
// 不需要 Token 验证的路径
private static final List<String> WHITELIST = Arrays.asList(
"/api/auth/login",
"/api/auth/register",
"/api/auth/refresh",
"/swagger-ui",
"/v3/api-docs"
);
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String requestUri = request.getRequestURI();
// 白名单路径直接放行
if (WHITELIST.stream().anyMatch(requestUri::startsWith)) {
filterChain.doFilter(request, response);
return;
}
// 从请求头获取 Token
String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token)) {
// 检查 Token 是否在黑名单中(已注销)
if (tokenBlacklistService.isBlacklisted(token)) {
log.warn("Token is blacklisted: {}", token.substring(0, 20));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token 已失效,请重新登录\"}");
return;
}
// 验证 Token
if (jwtTokenProvider.validateToken(token)) {
// 提取用户名
String username = jwtTokenProvider.getUsernameFromToken(token);
// 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Set authentication for user: {}", username);
}
}
filterChain.doFilter(request, response);
}
/**
* 从请求头提取 Token
*/
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3.8 认证服务层
// service/AuthService.java
package com.example.auth.service;
import com.example.auth.config.JwtConfig;
import com.example.auth.dto.*;
import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import com.example.auth.security.JwtTokenProvider;
import com.example.auth.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final TokenBlacklistService tokenBlacklistService;
private final JwtConfig jwtConfig;
/**
* 用户登录
*/
@Transactional
public AuthResponse login(LoginRequest request) {
log.info("User login attempt: username={}", request.getUsername());
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()));
// 2. 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成 Token
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
String accessToken = jwtTokenProvider.generateAccessToken(authentication);
String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);
// 4. 保存 Refresh Token(用于后续刷新和注销)
tokenBlacklistService.saveRefreshToken(userDetails.getId(), refreshToken);
log.info("User login successful: username={}, userId={}",
request.getUsername(), userDetails.getId());
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType(jwtConfig.getTokenPrefix())
.expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
.username(userDetails.getUsername())
.build();
}
/**
* 用户注册
*/
@Transactional
public AuthResponse register(RegisterRequest request) {
log.info("User registration attempt: username={}", request.getUsername());
// 1. 检查用户名是否已存在
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("用户名已存在");
}
// 2. 检查邮箱是否已存在
if (request.getEmail() != null && userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("邮箱已被注册");
}
// 3. 创建用户
User user = new User()
.setUsername(request.getUsername())
.setPassword(passwordEncoder.encode(request.getPassword()))
.setEmail(request.getEmail())
.setPhone(request.getPhone())
.setStatus(1);
userRepository.save(user);
// 4. 分配默认角色(ROLE_USER)
// roleRepository.assignRoleToUser(user.getId(), "ROLE_USER");
log.info("User registration successful: userId={}", user.getId());
// 5. 自动登录
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String accessToken = jwtTokenProvider.generateAccessToken(authentication);
String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);
tokenBlacklistService.saveRefreshToken(user.getId(), refreshToken);
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType(jwtConfig.getTokenPrefix())
.expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
.username(user.getUsername())
.build();
}
/**
* 刷新 Token
*/
@Transactional
public AuthResponse refreshToken(RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
// 1. 验证 Refresh Token
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new BusinessException("Refresh Token 无效");
}
// 2. 检查是否在黑名单中
if (tokenBlacklistService.isBlacklisted(refreshToken)) {
throw new BusinessException("Refresh Token 已失效");
}
// 3. 提取用户信息
Long userId = jwtTokenProvider.getUserIdFromToken(refreshToken);
String username = jwtTokenProvider.getUsernameFromToken(refreshToken);
// 4. 重新认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, null));
// 5. 生成新 Token
String newAccessToken = jwtTokenProvider.generateAccessToken(authentication);
String newRefreshToken = jwtTokenProvider.generateRefreshToken(authentication);
// 6. 将旧 Refresh Token 加入黑名单
tokenBlacklistService.blacklist(refreshToken);
tokenBlacklistService.saveRefreshToken(userId, newRefreshToken);
log.info("Token refreshed for user: {}", username);
return AuthResponse.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.tokenType(jwtConfig.getTokenPrefix())
.expiresIn(jwtConfig.getAccessTokenExpiration().getSeconds())
.username(username)
.build();
}
/**
* 注销登录
*/
@Transactional
public void logout(String accessToken, String refreshToken) {
// 1. 将 Access Token 加入黑名单
if (accessToken != null) {
tokenBlacklistService.blacklist(accessToken);
}
// 2. 将 Refresh Token 加入黑名单
if (refreshToken != null) {
tokenBlacklistService.blacklist(refreshToken);
}
// 3. 清除 SecurityContext
SecurityContextHolder.clearContext();
log.info("User logout successful");
}
}
3.9 Token 黑名单服务(Redis)
// service/TokenBlacklistService.java
package com.example.auth.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "token:blacklist:";
private static final String REFRESH_TOKEN_PREFIX = "token:refresh:";
/**
* 将 Token 加入黑名单
*/
public void blacklist(String token) {
// 解析 Token 获取过期时间
long expiration = getExpirationFromToken(token);
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + token,
"blacklisted",
expiration,
TimeUnit.SECONDS
);
log.debug("Token added to blacklist: {}", token.substring(0, 20));
}
/**
* 检查 Token 是否在黑名单中
*/
public boolean isBlacklisted(String token) {
Boolean exists = redisTemplate.hasKey(BLACKLIST_PREFIX + token);
return Boolean.TRUE.equals(exists);
}
/**
* 保存 Refresh Token
*/
public void saveRefreshToken(Long userId, String refreshToken) {
long expiration = 7 * 24 * 60 * 60; // 7 天
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + userId,
refreshToken,
expiration,
TimeUnit.SECONDS
);
}
/**
* 获取 Refresh Token
*/
public String getRefreshToken(Long userId) {
return redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
}
/**
* 删除 Refresh Token
*/
public void deleteRefreshToken(Long userId) {
redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
}
/**
* 从 Token 中提取剩余有效期(秒)
*/
private long getExpirationFromToken(String token) {
// 简化实现,实际应从 Token 中解析
return 1800; // 30 分钟
}
}
3.10 控制器层
// controller/AuthController.java
package com.example.auth.controller;
import com.example.auth.dto.*;
import com.example.auth.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<ApiResponse<AuthResponse>> login(
@Valid @RequestBody LoginRequest request) {
log.info("Login request: username={}", request.getUsername());
AuthResponse response = authService.login(request);
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 用户注册
*/
@PostMapping("/register")
public ResponseEntity<ApiResponse<AuthResponse>> register(
@Valid @RequestBody RegisterRequest request) {
log.info("Register request: username={}", request.getUsername());
AuthResponse response = authService.register(request);
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<AuthResponse>> refreshToken(
@Valid @RequestBody RefreshTokenRequest request) {
AuthResponse response = authService.refreshToken(request);
return ResponseEntity.ok(ApiResponse.success(response));
}
/**
* 注销登录
*/
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Void>> logout(
@RequestBody LogoutRequest request,
HttpServletRequest httpRequest) {
String accessToken = extractTokenFromRequest(httpRequest);
authService.logout(accessToken, request.getRefreshToken());
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 获取当前用户信息
*/
@GetMapping("/me")
public ResponseEntity<ApiResponse<UserInfoResponse>> getCurrentUser(
HttpServletRequest httpRequest) {
Long userId = (Long) httpRequest.getAttribute("userId");
UserInfoResponse userInfo = authService.getUserInfo(userId);
return ResponseEntity.ok(ApiResponse.success(userInfo));
}
private String extractTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3.11 全局异常处理
// exception/GlobalExceptionHandler.java
package com.example.auth.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiResponse<Void>> handleBadCredentials(BadCredentialsException ex) {
log.warn("Bad credentials: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "用户名或密码错误"));
}
@ExceptionHandler(DisabledException.class)
public ResponseEntity<ApiResponse<Void>> handleDisabledAccount(DisabledException ex) {
log.warn("Disabled account: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "账户已被禁用"));
}
@ExceptionHandler(LockedException.class)
public ResponseEntity<ApiResponse<Void>> handleLockedAccount(LockedException ex) {
log.warn("Locked account: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "账户已被锁定"));
}
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleUserNotFound(UsernameNotFoundException ex) {
log.warn("User not found: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "用户不存在"));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(400, "参数验证失败", errors));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
log.error("Business exception: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(400, ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
log.error("Unexpected exception: ", ex);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "服务器内部错误"));
}
}
四、高级特性实现
4.1 基于注解的权限控制
// controller/UserController.java
package com.example.auth.controller;
import com.example.auth.dto.ApiResponse;
import com.example.auth.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 查看用户列表(需要 user:read 权限)
*/
@GetMapping
@PreAuthorize("hasAuthority('user:read')")
public ResponseEntity<ApiResponse<?>> listUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(ApiResponse.success(userService.listUsers(page, size)));
}
/**
* 查看用户详情(需要 user:read 权限)
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('user:read')")
public ResponseEntity<ApiResponse<?>> getUser(@PathVariable Long id) {
return ResponseEntity.ok(ApiResponse.success(userService.getUser(id)));
}
/**
* 创建用户(需要 user:write 权限)
*/
@PostMapping
@PreAuthorize("hasAuthority('user:write')")
public ResponseEntity<ApiResponse<?>> createUser(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.ok(ApiResponse.success(userService.createUser(request)));
}
/**
* 删除用户(需要 user:delete 权限)
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('user:delete')")
public ResponseEntity<ApiResponse<?>> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 仅管理员可访问
*/
@GetMapping("/admin/stats")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<?>> getAdminStats() {
return ResponseEntity.ok(ApiResponse.success(userService.getAdminStats()));
}
}
4.2 动态权限刷新
// service/PermissionRefreshService.java
package com.example.auth.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class PermissionRefreshService {
private final RedisTemplate<String, String> redisTemplate;
private final SimpMessagingTemplate messagingTemplate;
private static final String PERMISSION_UPDATE_PREFIX = "permission:update:";
/**
* 通知用户权限已更新
*/
public void notifyPermissionUpdate(Long userId) {
// 1. Redis 发布消息
redisTemplate.convertAndSend(
"permission:update:" + userId,
"permission_updated");
// 2. WebSocket 推送消息
messagingTemplate.convertAndSend(
"/topic/user/" + userId + "/permission",
new PermissionUpdateMessage(userId));
log.info("Permission update notified for user: {}", userId);
}
/**
* 检查权限是否需要刷新
*/
public boolean shouldRefreshPermissions(Long userId) {
String lastUpdate = redisTemplate.opsForValue()
.get("permission:lastupdate:" + userId);
if (lastUpdate == null) {
return false;
}
long lastUpdateTime = Long.parseLong(lastUpdate);
long currentTime = System.currentTimeMillis();
// 如果权限在 Token 签发后更新过,需要刷新
return currentTime > lastUpdateTime;
}
}
4.3 多设备登录管理
// service/DeviceTokenService.java
package com.example.auth.service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@RequiredArgsConstructor
public class DeviceTokenService {
private final RedisTemplate<String, String> redisTemplate;
private static final String DEVICE_TOKEN_PREFIX = "device:token:";
private static final int MAX_DEVICES_PER_USER = 5;
/**
* 绑定设备 Token
*/
public void bindDeviceToken(Long userId, String deviceId, String token) {
String key = DEVICE_TOKEN_PREFIX + userId;
// 获取现有设备列表
Map<String, String> devices = redisTemplate.opsForHash().entries(key);
// 如果设备数已达上限,删除最早的设备
if (devices.size() >= MAX_DEVICES_PER_USER) {
String oldestDevice = Collections.min(devices.keySet());
redisTemplate.opsForHash().delete(key, oldestDevice);
}
// 绑定新设备
redisTemplate.opsForHash().put(key, deviceId, token);
redisTemplate.expire(key, 30, TimeUnit.DAYS);
}
/**
* 验证设备 Token
*/
public boolean validateDeviceToken(Long userId, String deviceId, String token) {
String key = DEVICE_TOKEN_PREFIX + userId;
String storedToken = (String) redisTemplate.opsForHash().get(key, deviceId);
return token.equals(storedToken);
}
/**
* 注销指定设备
*/
public void logoutDevice(Long userId, String deviceId) {
String key = DEVICE_TOKEN_PREFIX + userId;
redisTemplate.opsForHash().delete(key, deviceId);
}
/**
* 注销所有设备
*/
public void logoutAllDevices(Long userId) {
String key = DEVICE_TOKEN_PREFIX + userId;
redisTemplate.delete(key);
}
/**
* 获取用户所有设备
*/
public Map<String, String> getUserDevices(Long userId) {
String key = DEVICE_TOKEN_PREFIX + userId;
return redisTemplate.opsForHash().entries(key);
}
}
五、配置与部署
5.1 应用配置
# application.yml
spring:
application:
name: auth-service
datasource:
url: jdbc:mysql://localhost:3306/auth_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD:root}
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
# JWT 配置
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-for-jwt-signing-and-verification}
access-token-expiration: 30m
refresh-token-expiration: 7d
token-prefix: "Bearer "
token-header: "Authorization"
# 服务器配置
server:
port: 8080
servlet:
context-path: /
# 日志配置
logging:
level:
root: INFO
com.example.auth: DEBUG
org.springframework.security: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
5.2 Docker 部署
# Dockerfile
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/auth-service-*.jar app.jar
EXPOSE 8080
ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
version: '3.8'
services:
auth-service:
build: .
ports:
- "8080:8080"
environment:
- DB_PASSWORD=your_password
- REDIS_PASSWORD=your_password
- JWT_SECRET=your-secret-key
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: auth_db
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
六、安全加固建议
6.1 JWT 安全最佳实践
| 风险 |
防护措施 |
| Token 泄露 |
使用 HTTPS、设置 HttpOnly Cookie |
| Token 篡改 |
使用强密钥(256 位+)、定期轮换 |
| Token 重放 |
加入 jti 唯一标识、使用黑名单 |
| 暴力破解 |
限制登录尝试次数、添加验证码 |
| 权限提升 |
服务端验证权限、不信任客户端数据 |
6.2 密钥管理
// config/KeyRotationService.java
@Service
public class KeyRotationService {
// 使用密钥 ID 支持密钥轮换
private Map<String, SecretKey> keyRing = new ConcurrentHashMap<>();
private String currentKeyId = "key-1";
public String signWithKeyRotation(Claims claims) {
return Jwts.builder()
.claims(claims)
.header().add("kid", currentKeyId) // 密钥 ID
.and()
.signWith(getKey(currentKeyId))
.compact();
}
public Claims verifyWithKeyRotation(String token) {
String kid = Jwts.parser().build().parseUnsignedClaims(token).getHeader().get("kid", String.class);
return Jwts.parser()
.verifyWith(getKey(kid))
.build()
.parseSignedClaims(token)
.getPayload();
}
}
总结
通过本文的完整实战,我们构建了基于 Spring Boot 3 + Spring Security 6 的企业级 JWT 认证系统,涵盖了:
- 核心认证流程:登录、注册、刷新 Token、注销
- 权限管理:基于角色的访问控制(RBAC)、注解式权限校验
- 安全加固:Token 黑名单、设备管理、密钥轮换
- 生产部署:Docker 容器化、配置管理
关键要点回顾:
- ✅ 使用 Spring Security 6 的新配置方式(
SecurityFilterChain)
- ✅ JWT Token 包含用户信息和权限,减少数据库查询
- ✅ Refresh Token 机制实现无感续期
- ✅ Redis 黑名单处理 Token 注销
- ✅ 多设备登录管理和权限动态刷新
JWT 权限管理不是玄学,而是需要深入理解的架构设计。希望本文能帮助你在实际项目中构建安全、高效的认证系统。
如需进一步学习 Java 生态下的安全实践,可参考云栈社区提供的 后端 & 架构 专题资源。