
1. Spring Security JWT 认证方案概述在现代Web应用开发中安全认证是必不可少的一环。Spring Security作为Spring生态中的安全框架提供了全面的认证和授权功能。而JWT(JSON Web Token)则是一种轻量级的跨域认证解决方案特别适合前后端分离架构。本方案将Spring Security与JWT结合实现了以下核心功能基于用户名/密码的传统认证方式无状态的JWT令牌机制访问令牌(access token)和刷新令牌(refresh token)的双令牌体系细粒度的权限控制完整的异常处理机制这种组合既保留了Spring Security强大的安全特性又通过JWT实现了分布式系统的认证需求是当前企业级应用的主流选择。2. 环境准备与数据库设计2.1 数据库表结构首先需要设计用户存储相关的数据库表。我们使用MySQL作为持久化存储主要包含以下表-- 用户表 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(500) NOT NULL, enabled TINYINT(1) DEFAULT 1 NOT NULL, CONSTRAINT username UNIQUE (username) ); -- 用户权限表 CREATE TABLE user_authority ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, CONSTRAINT ix_auth_username UNIQUE (username, authority), CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES user (username) ); -- 角色权限表 CREATE TABLE role_authority ( role VARCHAR(50) NOT NULL, authority VARCHAR(255) NOT NULL, PRIMARY KEY (role, authority) );2.2 初始数据准备初始化两个测试用户密码都使用BCrypt加密存储INSERT INTO user (id, username, password, enabled) VALUES (1, admin, {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW, 1), (2, user, {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW, 1);密码中的{bcrypt}前缀表示使用的加密算法这种格式允许系统同时支持多种加密方式便于后续算法升级。2.3 数据库连接配置在application.yml中配置数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/learn_spring_security?useUnicodetruecharacterEncodingutf-8 username: root password: mysql driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Druid连接池专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 filters: stat,wall,log4j2这里使用了阿里巴巴的Druid连接池并开启了监控统计功能。3. Spring Security核心配置3.1 安全配置类创建SecurityConfig配置类这是Spring Security的核心配置Configuration EnableWebSecurity public class SecurityConfig { Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; Bean public SecurityFilterChain filterChain(HttpSecurity http, CustomAccessDeniedHandler customAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) throws Exception { http .csrf((csrf) - csrf.disable()) // 禁用CSRF .sessionManagement((session) - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话 .authorizeHttpRequests(authorize - authorize .requestMatchers(/api/auth/**,/error).permitAll() // 登录注册公开 .requestMatchers(/admin/**).hasRole(ADMIN) // 管理员可访问 .anyRequest().authenticated() // 其他请求需认证 ) .exceptionHandling((exception) - exception .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } Bean public AuthenticationManager authenticationManager( UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { DaoAuthenticationProvider authenticationProvider new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder); return new ProviderManager(authenticationProvider); } Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }关键配置说明禁用CSRF因为使用无状态的JWT不需要CSRF保护无状态会话服务端不保存会话状态路径权限控制/api/auth/**路径允许匿名访问/admin/**需要ADMIN角色异常处理自定义401和403异常处理器JWT过滤器在用户名密码认证前添加JWT认证过滤器3.2 密码编码器Spring Security提供了多种密码编码器Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }DelegatingPasswordEncoder可以支持多种编码算法它会根据密码前缀(如{bcrypt})自动选择对应的编码器进行验证。这种设计使得系统可以平滑升级加密算法。4. JWT实现细节4.1 JWT依赖配置使用JJWT库实现JWT功能添加以下依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.12.3/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.12.3/version scoperuntime/scope /dependency4.2 JWT工具类实现创建JwtTokenProvider处理JWT的生成和验证Component public class JwtTokenProvider { Autowired private JwtProperties jwtProperties; private final SecretKey key Jwts.SIG.HS512.key().build(); public String generateToken(UserDetails userDetails) { Date now new Date(); Date expiryDate new Date(now.getTime() jwtProperties.getExpirationMs()); return Jwts.builder() .subject(userDetails.getUsername()) .issuedAt(now) .expiration(expiryDate) .signWith(key) .compact(); } public String getUsernameFromJWT(String token) throws JwtException { JwsClaims claimsJws Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token); return claimsJws.getPayload().getSubject(); } }关键点使用HS512算法签名令牌包含用户名(subject)、签发时间(issuedAt)和过期时间(expiration)密钥在服务启动时随机生成确保安全性4.3 JWT认证过滤器创建JwtAuthenticationFilter处理请求中的JWT令牌Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtTokenProvider tokenProvider; Autowired private UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt getJwtFromRequest(request); if (StringUtils.hasText(jwt)) { String username tokenProvider.getUsernameFromJWT(jwt); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error(Could not set user authentication in security context, ex); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }过滤器从Authorization头中提取JWT令牌验证后设置认证信息到SecurityContext中。5. 认证接口实现5.1 登录接口实现/auth/login接口处理用户名密码认证RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthenticationManager authenticationManager; Autowired private JwtTokenService jwtTokenService; PostMapping(/login) public ResultLoginResponse login(RequestBody LoginRequest loginRequest) { Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.username(), loginRequest.password() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails (UserDetails) authentication.getPrincipal(); return Result.success(jwtTokenService.generateToken(userDetails)); } }登录流程使用AuthenticationManager验证用户名密码验证成功后生成JWT令牌返回令牌包含access token和refresh token5.2 令牌刷新接口实现/auth/refresh接口用于刷新令牌PostMapping(/refresh) public ResultLoginResponse refresh(RequestBody RefreshRequest refreshRequest) { LoginResponse loginResponse jwtTokenService.validateToken(refreshRequest.refreshToken()); return Result.success(loginResponse); }刷新流程验证refresh token有效性生成新的access token和refresh token旧的refresh token失效6. 权限控制实现6.1 自定义UserDetails创建自定义用户类实现UserDetails接口Data TableName(user) public class CustomUser implements UserDetails { private Integer id; private String username; private String password; private Boolean enabled; TableField(exist false) private SequencedSetGrantedAuthority grantedAuthorities; Override public Collection? extends GrantedAuthority getAuthorities() { if (grantedAuthorities null){ return List.of(); } return grantedAuthorities; } public void addRoleAuthority(String roleName){ addGrantedAuthority(ROLE_ roleName); } }6.2 权限加载逻辑在UserDetailsService中加载用户权限Override public CustomUser loadUserByUsername(String username) throws UsernameNotFoundException { CustomUser user userMapper.selectOne(new QueryWrapperUser() .eq(username, username)); if (user null) { throw new UsernameNotFoundException(用户不存在 username); } // 加载用户直接关联的权限 ListUserAuthority userAuthorities userAuthorityMapper.selectList( new QueryWrapperUserAuthority().eq(username, username)); // 加载角色关联的权限 userAuthorities.stream() .filter(auth - auth.getAuthority().startsWith(ROLE_)) .forEach(auth - { String roleName auth.getAuthority().substring(5); ListRoleAuthority roleAuths roleAuthorityMapper.selectList( new QueryWrapperRoleAuthority().eq(role, roleName)); roleAuths.forEach(ra - user.addNormalAuthority(ra.getAuthority())); }); return user; }6.3 方法级权限控制使用PreAuthorize注解实现方法级权限控制GetMapping(/admin) PreAuthorize(hasRole(ADMIN)) public ResultString adminOnly() { return Result.success(Admin area); } GetMapping(/user) PreAuthorize(hasAuthority(USER_READ)) public ResultString userRead() { return Result.success(User read permission required); }7. 常见问题与解决方案7.1 令牌过期处理当access token过期时客户端应该使用refresh token获取新的令牌。如果refresh token也过期了则需要重新登录。在JwtAuthenticationFilter中处理令牌过期异常try { username tokenProvider.getUsernameFromJWT(jwt); } catch (ExpiredJwtException e) { sendErrorResponse(response, HttpStatus.UNAUTHORIZED, AUTH_EXPIRED, 登录信息已过期请使用刷新令牌更新访问令牌); return; }7.2 权限不足处理自定义AccessDeniedHandler处理403错误Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpStatus.FORBIDDEN.value()); ResultVoid result Result.error(ACCESS_DENIED, 没有访问权限); new ObjectMapper().writeValue(response.getWriter(), result); } }7.3 多端登录处理如果需要限制用户只能在一个设备上登录可以在生成令牌时将设备信息存入Redis验证令牌时检查设备是否匹配。8. 安全最佳实践使用HTTPS所有认证相关的请求必须通过HTTPS传输设置合理的令牌有效期access token建议设置较短有效期(如30分钟)refresh token可以设置较长(如7天)妥善保管密钥JWT签名密钥应该定期更换建议使用配置中心管理实现令牌黑名单对于需要提前失效的令牌可以使用Redis维护黑名单限制刷新令牌使用每个refresh token只能使用一次刷新后旧的refresh token立即失效记录安全日志记录所有认证相关操作便于审计和问题排查9. 性能优化建议缓存用户权限将用户权限信息缓存到Redis减少数据库查询异步日志记录认证日志采用异步方式记录避免影响主流程性能令牌压缩对于包含大量信息的JWT可以考虑使用压缩算法减小体积批量验证对于批量请求可以实现批量令牌验证接口连接池优化合理配置数据库和Redis连接池参数10. 扩展思考多因素认证在用户名密码认证基础上增加短信/邮件验证码等二次验证OAuth2集成支持第三方登录如微信、GitHub等风险控制基于IP、设备指纹等信息实现风险控制权限动态加载支持权限的热更新无需重新登录微服务扩展将认证服务独立部署其他服务通过网关统一认证通过以上实现我们构建了一个完整的基于Spring Security和JWT的认证系统。这个系统具有良好的扩展性和安全性可以满足大多数企业级应用的需求。在实际项目中还需要根据具体业务场景进行调整和优化。