ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Security与JWT在若依框架中的登录认证实践

Spring Security与JWT在若依框架中的登录认证实践 1. 若依框架登录认证体系概览若依RuoYi作为国内流行的开源后台管理系统其登录认证模块采用了Spring Security与JWT的组合方案。这套方案完美解决了传统Session认证在分布式环境下的扩展性问题同时兼顾了前后端分离架构的安全需求。在实际项目中我遇到过不少团队直接照搬若依的认证代码却不知其所以然。比如有个电商项目组把JWT有效期设为7天却未实现续签机制导致用户频繁重新登录。理解这套认证机制的工作原理才能根据业务需求进行合理定制。Spring Security负责整个认证流程的管控而JWT则作为无状态令牌承载身份信息。这种组合有三大优势服务端无需存储会话状态天然适合微服务架构令牌自带用户信息和权限数据减少数据库查询签名机制防止篡改配合HTTPS可保证传输安全2. Spring Security核心配置解析2.1 安全过滤器链配置若依在SecurityConfig类中通过EnableWebSecurity注解启用了安全配置。最关键的configure(HttpSecurity http)方法定义了过滤器链Override protected void configure(HttpSecurity http) throws Exception { http // CSRF禁用因使用JWT .csrf().disable() // 认证异常处理 .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() // 不创建session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 过滤请求 .authorizeRequests() // 静态资源放行 .antMatchers( HttpMethod.GET, /, /*.html, /**/*.html, /**/*.css, /**/*.js, /profile/** ).permitAll() // 登录URL放行 .antMatchers(/login).permitAll() // 验证码放行 .antMatchers(/captchaImage).permitAll() // 其他请求需要认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // 添加JWT过滤器 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }关键配置点解析CSRF禁用传统Web应用需要防止CSRF攻击但REST APIJWT的方案本身不受此影响无状态会话STATELESS策略声明不创建HttpSession放行规则静态资源、登录接口、验证码接口允许匿名访问JWT过滤器自定义的JwtAuthenticationTokenFilter被添加到用户名密码过滤器之前2.2 自定义用户详情服务若依通过实现UserDetailsService接口加载用户详情Service public class UserDetailsServiceImpl implements UserDetailsService { Autowired private ISysUserService userService; Autowired private SysPermissionService permissionService; Override public UserDetails loadUserByUsername(String username) { // 查询用户信息 SysUser user userService.selectUserByUserName(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } // 权限检查 if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { throw new BaseException(账号已被删除); } if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { throw new BaseException(账号已停用); } // 封装权限集合 SetString permissions permissionService.getMenuPermission(user); return new LoginUser(user, permissions); } }这里有几个值得注意的实现细节用户状态检查删除/禁用在认证阶段完成权限数据通过SysPermissionService获取自定义的LoginUser实现了UserDetails接口扩展了若依特有的用户字段3. JWT实现深度剖析3.1 令牌生成与校验若依的JWT工具类JwtUtils封装了令牌操作public class JwtUtils { // 令牌密钥从配置读取 private static String secret RuoYiConfig.getSecret(); // 令牌有效期默认30分钟 private static Long expireTime RuoYiConfig.getExpireTime(); public static String createToken(LoginUser loginUser) { String token Jwts.builder() // 主题设置为用户ID .setSubject(loginUser.getUser().getUserId().toString()) // 签发时间 .setIssuedAt(new Date()) // 签发者 .setIssuer(ruoyi) // 自定义声明 .claim(user_key, loginUser.getUser().getUserKey()) // 过期时间 .setExpiration(new Date(System.currentTimeMillis() expireTime * 60 * 1000)) // 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, secret) .compact(); return token; } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }安全注意事项密钥管理实际项目应将secret存储在配置中心或KMS中避免硬编码令牌有效期生产环境建议2-4小时敏感操作可缩短至30分钟自定义声明避免存储敏感信息用户关键数据应加密处理3.2 令牌刷新机制若依默认未实现自动刷新机制但可通过以下方式扩展public class JwtRefreshFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String token getToken(request); if (StringUtils.isNotEmpty(token)) { try { Claims claims JwtUtils.parseToken(token); // 令牌剩余时间小于阈值时刷新 Date expiration claims.getExpiration(); long remainTime expiration.getTime() - System.currentTimeMillis(); if (remainTime REFRESH_THRESHOLD) { String newToken JwtUtils.refreshToken(claims); response.setHeader(New-Token, newToken); } } catch (Exception e) { // 解析异常不中断流程 } } chain.doFilter(request, response); } }实现要点设置合理的刷新阈值如剩余15分钟新令牌通过响应头返回前端需要处理更新避免频繁刷新可结合Redis记录最后刷新时间4. 认证过滤器工作流程4.1 令牌认证过滤器核心过滤器JwtAuthenticationTokenFilter的处理逻辑Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // 从请求头获取token String token getToken(request); if (StringUtils.isNotEmpty(token)) { try { // 解析令牌 Claims claims JwtUtils.parseToken(token); // 获取用户标识 String userId claims.getSubject(); // 从Redis获取用户信息 String userKey (String) claims.get(user_key); String redisKey login: userKey; LoginUser loginUser redisCache.getCacheObject(redisKey); if (loginUser ! null) { // 构建Authentication对象 UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( loginUser, null, loginUser.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); // 设置到安全上下文 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // 异常处理... } } chain.doFilter(request, response); }关键设计决策Redis缓存用户信息避免每次请求都查询数据库双因素校验既验证JWT有效性又检查Redis中的用户状态上下文传递通过SecurityContextHolder实现线程内共享认证信息4.2 异常处理机制若依通过AuthenticationEntryPoint处理认证异常Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { int code HttpStatus.UNAUTHORIZED; String msg 请求访问 request.getRequestURI() 认证失败; if (e instanceof BadCredentialsException) { msg 用户名或密码错误; } else if (e instanceof InsufficientAuthenticationException) { msg 缺少认证信息; } // 封装响应结果 ServletUtils.renderString(response, JSON.toJSONString( AjaxResult.error(code, msg))); } }最佳实践建议区分不同类型的认证错误令牌过期/格式错误/用户不存在生产环境应记录异常日志但不要返回敏感信息前端应根据不同错误码展示友好提示5. 实战问题与解决方案5.1 分布式会话一致性在若依微服务版中需要额外处理# application.yml配置 spring: session: store-type: redis redis: namespace: ruoyi:session常见问题处理序列化异常确保LoginUser实现Serializable网络分区配置合理的Redis超时和重试策略性能瓶颈对权限数据做本地缓存需考虑一致性5.2 权限缓存优化原始方案每次请求都查询Redis可改进为public class CachingPermissionService implements SysPermissionService { Cacheable(value user:perms, key #user.userId) public SetString getMenuPermission(LoginUser user) { // 原始数据库查询逻辑 } }缓存策略建议设置合理的TTL如12小时用户权限变更时清除缓存高频访问用户可做本地缓存5.3 安全增强措施生产环境必备加固方案令牌绑定String deviceId request.getHeader(Device-ID); claims.put(did, deviceId); // 校验时检查设备ID是否匹配动态密钥轮换// 使用KMS获取当前有效的密钥 String currentSecret kmsClient.getSecret(jwt-key-v1);令牌撤销列表// 登出时将令牌加入黑名单 redisTemplate.opsForValue().set( jwt:blacklist:token, 1, expireTime, TimeUnit.MILLISECONDS);6. 性能监控与调优6.1 认证链路监控通过Spring Actuator添加自定义指标Bean public MeterRegistryCustomizerMeterRegistry jwtMetrics() { return registry - { Counter.builder(jwt.requests) .description(JWT认证请求计数) .tag(type, total) .register(registry); }; }关键监控指标认证请求QPS令牌解析耗时P99Redis查询延迟异常比例统计6.2 压力测试建议使用JMeter测试策略基准测试100并发持续5分钟混合读写请求3:1比例观察错误率和响应时间峰值测试逐步增加并发至系统极限重点关注认证服务降级方案耐久测试24小时持续中等压力监控内存泄漏和GC情况测试数据准备预生成10万用户测试账号使用不同权限组合包含异常token测试用例7. 扩展开发指南7.1 多因素认证集成以短信验证码为例public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response) { String phone obtainPhone(request); String code obtainCode(request); // 验证短信码 if (!smsService.verifyCode(phone, code)) { throw new BadCredentialsException(验证码错误); } // 后续认证流程... } }集成步骤添加新的过滤器到安全链配置URL路由规则前端适配认证流程7.2 OAuth2.0集成若依兼容Spring Security OAuth2Configuration EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(ruoyi-app) .secret(passwordEncoder.encode(secret)) .authorizedGrantTypes(password, refresh_token) .scopes(all); } }注意事项生产环境应使用JdbcClientDetailsService合理设置access_token有效期实现自定义的UserDetailsService7.3 微服务间认证在Spring Cloud Gateway中统一处理public class JwtGlobalFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token extractToken(exchange.getRequest()); if (StringUtils.isBlank(token)) { return chain.filter(exchange); } // 验证令牌并转发用户信息 return Mono.fromCallable(() - { Claims claims JwtUtils.parseToken(token); exchange.getRequest().mutate() .header(X-User-Id, claims.getSubject()) .build(); return chain.filter(exchange); }); } }转发策略建议只传递必要的最小用户信息敏感字段应加密处理记录完整的审计日志8. 最佳实践总结经过多个项目的实战验证我总结了以下经验密钥管理三原则开发/测试/生产环境使用不同密钥定期轮换密钥如每季度使用HS512等强签名算法令牌设计规范标准声明iss/sub/exp等必须完整自定义声明前缀避免冲突如com_xxx_单个令牌大小控制在4KB以内性能优化要点Redis使用Pipeline批量操作权限数据本地缓存事件通知更新避免在JWT中存储过大用户对象监控报警策略异常认证请求阈值报警令牌刷新频率监控定期审计令牌使用情况前端配合建议实现无感刷新令牌机制401错误自动跳转登录页敏感操作前主动检查令牌有效期这套认证方案在笔者参与的政务云项目中支撑了日均100万的认证请求通过合理的缓存设计和集群部署平均响应时间控制在50ms以内。关键在于根据业务特点调整参数并建立完善的监控体系。
返回列表