1. 为什么选择SpringSecurity+JWT做权限认证?
在分布式系统架构成为主流的今天,传统的Session认证方式逐渐暴露出扩展性差、服务器资源占用高等问题。我去年参与的一个电商平台项目就遇到了这样的困境:当用户量突破50万时,Session服务器频繁出现内存溢出,集群同步延迟导致用户频繁掉线。经过技术选型对比,我们最终采用SpringSecurity+JWT的方案重构了认证体系,不仅解决了性能瓶颈,还实现了更灵活的权限控制。
JWT(JSON Web Token)本质上是一种紧凑的URL安全令牌标准,由三部分组成:
- Header:声明令牌类型和签名算法(如HS256)
- Payload:存放实际传递的数据(如用户ID、角色)
- Signature:对前两部分的签名,防止篡改
与Session机制相比,JWT的核心优势在于:
- 无状态性:服务端不需要存储会话信息,所有必要数据都编码在Token中
- 跨域支持:天然适合微服务架构,各服务只需验证签名即可识别用户身份
- 移动端友好:对原生APP的支持比Cookie更优雅
- 性能优势:避免了Session集群同步的开销
SpringSecurity作为Spring生态的安全框架,提供了完整的认证和授权解决方案。其核心组件包括:
- AuthenticationManager:认证入口
- UserDetailsService:用户数据加载
- SecurityFilterChain:安全过滤链
- AccessDecisionManager:访问决策
当JWT与SpringSecurity结合时,能充分发挥两者的优势:
- JWT负责轻量级的身份凭证传递
- SpringSecurity提供强大的权限管控能力
- 两者通过自定义Filter实现无缝集成
2. 基础环境搭建与核心配置
2.1 项目初始化与依赖引入
使用Spring Initializr创建项目时,需要勾选以下依赖:
xml复制<dependencies>
<!-- Spring Security核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT支持库 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 其他必要依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2.2 JWT工具类实现
创建JwtTokenUtil工具类处理令牌的生成与验证:
java复制public class JwtTokenUtil {
// 签名密钥(实际项目应从配置读取)
private static final String SECRET_KEY = "your-256-bit-secret";
// 令牌有效期(单位:毫秒)
private static final long EXPIRATION_TIME = 86400000; // 24小时
// 生成令牌
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 验证令牌
public static boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 提取用户名
public static String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 提取过期时间
public static Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 通用提取方法
private static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 提取所有声明
private static Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
// 检查令牌是否过期
private static boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
2.3 SpringSecurity核心配置
创建SecurityConfig配置类:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 禁用CSRF保护(API服务通常不需要)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3. 认证流程深度实现
3.1 自定义UserDetailsService
实现从数据库加载用户信息的服务:
java复制@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
}
3.2 JWT认证过滤器实现
创建处理HTTP请求中JWT令牌的过滤器:
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
String jwtToken = requestTokenHeader.substring(7);
try {
String username = jwtTokenUtil.extractUsername(jwtToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
logger.error("无法获取JWT令牌: " + e);
}
}
chain.doFilter(request, response);
}
}
3.3 认证控制器实现
提供登录接口颁发JWT令牌:
java复制@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(
@RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(),
authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
} catch (DisabledException e) {
throw new Exception("用户已禁用", e);
} catch (BadCredentialsException e) {
throw new Exception("无效凭证", e);
}
}
}
4. 权限控制与RBAC模型实现
4.1 基于角色的访问控制(RBAC)
实现标准的RBAC模型需要以下数据库表:
- 用户表(users)
- 角色表(roles)
- 权限表(permissions)
- 用户角色关联表(user_roles)
- 角色权限关联表(role_permissions)
实体类示例:
java复制@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
}
@Entity
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name; // 如: user:read, user:write
private String description;
}
4.2 方法级权限控制
SpringSecurity支持多种粒度的权限控制方式:
- 注解方式:
java复制@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminOnly() {
return "管理员专属内容";
}
@PreAuthorize("hasAuthority('user:delete')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
// 删除用户逻辑
}
- 配置方式:
java复制@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
// 方法级安全配置
}
4.3 动态权限控制实现
对于需要从数据库动态加载权限的场景,可以自定义权限评估器:
java复制@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
// 实现自定义权限逻辑
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
// 实现基于ID的权限检查
}
}
然后在配置中注册:
java复制@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
@Bean
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
5. 安全加固与最佳实践
5.1 JWT安全防护措施
在实际项目中,必须注意以下安全问题:
-
密钥管理:
- 不要硬编码密钥,应从安全配置源获取
- 定期轮换签名密钥
- 生产环境使用至少256位的强密钥
-
令牌存储:
- 客户端使用HttpOnly的Cookie存储(防XSS)
- 或者使用localStorage但需配合CSRF防护
- 实现令牌黑名单机制(用于注销)
-
有效期限:
- 设置合理的短有效期(如30分钟)
- 实现刷新令牌机制
- 监控异常频繁的令牌刷新请求
-
敏感信息:
- 不要在Payload中存放密码等敏感信息
- 必要时对部分声明进行加密
5.2 防御常见攻击
- CSRF防护:
java复制@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/auth/login") // 登录接口排除
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
// 其他配置...
}
- 速率限制:
java复制@Bean
public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
FilterRegistrationBean<RateLimitFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RateLimitFilter());
registration.addUrlPatterns("/api/auth/login");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
- 输入验证:
java复制@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody UserDto userDto) {
// 自动验证输入
}
5.3 性能优化建议
-
JWT优化:
- 精简Payload体积
- 考虑使用无状态刷新令牌
- 对频繁访问的API实现本地缓存验证
-
数据库优化:
- 为权限相关表添加适当索引
- 实现权限缓存(如Redis)
- 批量加载用户权限避免N+1查询
-
日志监控:
- 记录异常认证尝试
- 监控令牌使用模式
- 设置可疑行为告警
6. 实战中的坑与解决方案
6.1 令牌失效问题
场景:用户修改密码后,原有令牌仍然有效
解决方案:
java复制public class JwtTokenUtil {
// 在Payload中加入用户版本号
public static String generateToken(UserDetails userDetails) {
claims.put("version", ((CustomUserDetails)userDetails).getVersion());
// ...其他代码
}
// 验证时检查版本号
public static boolean validateToken(String token, UserDetails userDetails) {
final int tokenVersion = (int) extractClaim(token, claims -> claims.get("version"));
final int userVersion = ((CustomUserDetails)userDetails).getVersion();
return tokenVersion == userVersion && ...;
}
}
6.2 权限缓存一致性问题
场景:管理员修改用户角色后,权限变更未及时生效
解决方案:
java复制@Service
public class CustomUserDetailsService implements UserDetailsService {
@Cacheable(value = "userDetails", key = "#username")
public UserDetails loadUserByUsername(String username) {
// 从数据库加载
}
@CacheEvict(value = "userDetails", key = "#username")
public void evictUserCache(String username) {
// 清除缓存
}
}
// 在角色变更时调用
userDetailsService.evictUserCache(username);
6.3 微服务间的令牌传递
场景:服务A调用服务B时如何传递用户身份
解决方案:
- 使用Feign拦截器自动传递令牌:
java复制@Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return requestTemplate -> {
String token = SecurityContextHolder.getContext()
.getAuthentication()
.getCredentials()
.toString();
requestTemplate.header("Authorization", "Bearer " + token);
};
}
- 或者使用Spring Cloud Gateway统一处理:
java复制public class TokenRelayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest()
.getHeaders()
.getFirst("Authorization");
if (token != null) {
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", extractUserId(token))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
return chain.filter(exchange);
}
}
7. 扩展功能实现
7.1 多因素认证集成
增强登录安全性:
java复制@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 第一阶段:验证用户名密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 生成临时令牌(有效期5分钟)
String tempToken = jwtTokenUtil.generateTempToken(authentication);
// 发送验证码到用户手机/邮箱
verificationService.sendCode(request.getUsername());
return ResponseEntity.ok(new TempTokenResponse(tempToken));
}
@PostMapping("/verify")
public ResponseEntity<?> verify(@RequestBody VerifyRequest request) {
// 验证临时令牌
if (!jwtTokenUtil.validateTempToken(request.getTempToken())) {
throw new BadCredentialsException("无效令牌");
}
// 验证验证码
if (!verificationService.verifyCode(
jwtTokenUtil.extractUsername(request.getTempToken()),
request.getCode())) {
throw new BadCredentialsException("验证码错误");
}
// 生成正式令牌
UserDetails userDetails = userDetailsService.loadUserByUsername(
jwtTokenUtil.extractUsername(request.getTempToken()));
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
7.2 社交登录集成
集成OAuth2社交登录:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oauth2SuccessHandler())
);
// 其他配置...
}
@Bean
public AuthenticationSuccessHandler oauth2SuccessHandler() {
return (request, response, authentication) -> {
CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();
// 查找或创建本地用户
User user = userService.processOAuth2User(oauthUser);
// 生成JWT
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
String token = jwtTokenUtil.generateToken(userDetails);
// 返回令牌
response.setContentType("application/json");
response.getWriter().write(
new ObjectMapper().writeValueAsString(new JwtResponse(token))
);
};
}
}
7.3 审计日志集成
记录用户操作:
java复制@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogService auditLogService;
@AfterReturning(pointcut = "@annotation(auditable)", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Auditable auditable, Object result) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
String operation = auditable.operation();
auditLogService.log(username, operation, joinPoint.getArgs(), result);
}
}
}
// 使用示例
@Auditable(operation = "删除用户")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
// 业务逻辑
}
