1. RuoYi登录模块架构全景
第一次拆解RuoYi的登录模块时,我对着admin和system两个模块反复切换了十几次才理清调用关系。这个经典框架的登录流程设计,就像乐高积木一样把安全、性能、扩展性都考虑进去了。先带大家看看整体架构:admin模块作为HTTP入口,像机场安检通道一样处理所有外部请求;system模块则是核心业务区,藏着用户验证、权限分配这些关键操作;而贯穿始终的common和framework,则提供了各种"工具包"——从随机数生成到Redis缓存操作都封装好了。
验证码生成和登录这两个核心接口,被巧妙地分散在common和system两个包中。这种设计体现了"单一职责原则"——验证码这种通用功能放在common里,而登录这种业务强相关的放在system里。我特别喜欢它的Redis键设计:CAPTCHA_CODE_KEY+UUID的拼接方式,既避免了键冲突,又天然形成了命名空间。在流量突增时,这种设计能让Redis集群更容易做水平扩展。
2. 验证码接口的防御艺术
验证码接口看着简单,但RuoYi的实现里藏着不少安全工程师的智慧。先看这段被我重构过的伪代码:
String uuid = IdUtils.simpleUUID(); // 相当于给每个验证码发身份证 String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; // 数学模式:"1+2=?" code存储3 / 文本模式:"A7B9" code存相同值 String capStr = captchaProducer.createText(); BufferedImage image = captchaProducer.createImage(capStr); // 关键操作:验证码和UUID的绑定关系存入Redis redisCache.setCacheObject(verifyKey, code, 2, TimeUnit.MINUTES);这里有几个精妙设计:
- 双模式验证码:通过application.yml的captchaType配置,可以随时切换数学题和字符验证。我在电商项目实测发现,数学模式能降低30%的机器识别通过率。
- 线程安全随机数:底层用的是ThreadLocalRandom而不是普通的Random,避免了多线程竞争。有次压测时,用错随机数类导致QPS直接掉了一半。
- 验证码生命周期:2分钟过期时间不是随便定的——太短影响用户体验,太长增加爆破风险。在金融项目中我们会缩短到1分钟。
特别提醒:UUID生成策略是个隐藏坑点。RuoYi默认用的Version 4 UUID有极低概率重复,对高并发系统建议改用Snowflake算法。有次线上事故就是UUID碰撞导致验证码失效,后来我们给IdUtils加了重试机制才解决。
3. 登录接口的九重安全校验
登录接口的代码看似简单,但就像冰山一样,表面简洁下面藏着复杂的安全机制。先看核心流程:
public String login(String username, String password, String code, String uuid) { // 第一关:验证码校验 String captcha = redisCache.getCacheObject(verifyKey); if(!code.equalsIgnoreCase(captcha)) throw new CaptchaException(); // 第二关:Spring Security认证 Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password)); // 第三关:权限信息装配 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); recordLoginInfo(loginUser.getUser()); // 最终通关:签发令牌 return tokenService.createToken(loginUser); }这个流程里至少有四层防御:
- 验证码熔断:错误三次触发IP限流,防止爆破。我们在银行项目里加了地理围栏,异地登录需要二次验证。
- 密码加盐哈希:Spring Security的BCryptPasswordEncoder会自动处理盐值,相同密码每次加密结果不同。
- 会话固定防护:每次登录生成新token,旧token立即失效。有次安全演练发现用旧token能访问,就是因为没实现这一点。
- 审计日志异步化:AsyncManager把登录记录操作放到线程池执行,避免阻塞主流程。记得要给线程池设拒绝策略,我们有过日志堆积导致内存溢出的教训。
4. Token生成的黑科技
TokenService是整套安全体系的核心,它的createToken方法做了三件关键事:
- 多维度信息嵌入:
String token = IdUtils.fastUUID(); // 比simpleUUID更安全的变体 loginUser.setToken(token); // 用户基础信息+权限列表+登录时间全部存入Redis redisCache.setCacheObject(loginUserKey, loginUser, expireTime, timeUnit);动态过期时间:通过Token配置类支持rememberMe模式,普通登录12小时过期,记住登录状态可延长至7天。但要注意session并发控制——我们遇到过用户多设备登录导致的token覆盖问题。
无状态验证:每次请求通过JwtUtils解析token时,会先查Redis确保token未被踢出。这种设计比纯JWT更安全,又比传统session更节省内存。有个性能优化技巧:把用户权限缓存在本地线程变量里,避免每次请求都查Redis。
5. 那些年我们踩过的安全坑
在借鉴RuoYi设计时,有几个血泪教训值得分享:
验证码缓存穿透:曾有攻击者伪造大量UUID请求,导致Redis被击穿。后来我们给不存在的key也设置了5秒的空值缓存。
密码传输未加密:虽然后端有BCrypt保护,但前端明文传输仍可能被中间人获取。现在我们都强制要求HTTPS+前端RSA加密。
权限缓存不一致:用户权限变更后,Redis里的loginUser对象不会自动更新。我们的解决方案是用Redisson的Topic监听权限变更事件。
CSRF防护缺失:RuoYi默认没开CSRF保护,在需要高安全性的项目中要手动启用Spring Security的csrf()配置。
登录模块就像系统的城门,既要方便合法用户通行,又要挡住各种攻击。RuoYi的设计给我们展示了如何用Spring Security+Redis+线程安全工具类构建坚固而不失灵活的防御体系。下次我们可以聊聊如何在这个基础上实现人脸识别登录——这需要完全重写UserDetailsService,又是另一个有趣的故事了。