前言
若依的登录体系用一句话概括就是「一次签发,每次校验」:
登录时:认证通过后生成令牌,把登录用户写入 Redis;
登录后:每个请求由过滤器从 Redis 取出登录用户完成鉴权。
它没有使用自解释型 JWT,而是「UUID 令牌 + Redis 存储」——服务端不解析令牌内容,Redis 中的login_tokens:{token}才是会话真相。本文按真实代码链路走一遍完整流程。
一、涉及的核心类
类 / 组件 | 职责 |
|---|---|
| 登录入口 |
| 登录主流程编排:验证码 → 前置校验 → 调 Spring Security 认证 → 记录日志 |
| Spring Security 回调,按用户名查库并组装 LoginUser |
| 密码错误次数计数(错 10 次锁 10 分钟)与 BCrypt 比对 |
| 生成 UUID 令牌,LoginUser 写入 Redis(30 分钟),续期与删除 |
| 每个请求的鉴权过滤器:取 token → 查 Redis → 写入 SecurityContext |
| Redis 操作封装,验证码与登录令牌的存取都经过它 |
二、阶段 0:验证码(可配置关闭)
登录之前,前端先请求GET /captchaImage:后端生成一道算术题,把答案以captcha_codes:{uuid}为键写入 Redis(有效期 2 分钟),将图片 Base64 与 uuid 返回前端。
参数sys.account.captchaEnabled=false时跳过整个验证码校验(本地测试常用)。
三、阶段 1:登录与令牌签发
3.1 入口:Controller 只做转发
@PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { AjaxResult ajax = AjaxResult.success(); // 生成令牌 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid()); ajax.put(Constants.TOKEN, token); return ajax; }3.2 验证码校验
SysLoginService.validateCaptcha:按 uuid 从 Redis 取答案比对——取不到报「验证码已失效」,不匹配报「验证码错误」,且无论对错用后即删。
3.3 前置校验
IP 黑名单校验、用户名密码长度边界检查。
3.4 委托 Spring Security 认证
authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password));DaoAuthenticationProvider回调UserDetailsServiceImpl.loadUserByUsername:查询sys_user(含部门、角色),依次校验用户不存在 / 已删除 / 已停用 / IP 锁定等状态。
3.5 密码校验
SysPasswordService:错误次数写入 Redis(pwd_err_cnt:{username}),连续错 10 次锁定 10 分钟;密码使用 BCrypt 不可逆比对。
3.6 认证成功收尾
异步记录登录日志sys_logininfor,更新用户登录 IP 与时间,清空密码错误计数。
3.7 签发令牌
TokenService.createToken(loginUser):生成 UUID 作为 token,将 LoginUser(用户 + 角色 + 权限)以login_tokens:{token}为键写入 Redis,有效期 30 分钟。最终返回:
{ "code": 200, "token": "xxxxxxxx-xxxx-xxxx" }四、阶段 2:登录后的每次请求鉴权
JwtAuthenticationTokenFilter(OncePerRequestFilter)拦截请求,从请求头Authorization: Bearer {token}取出令牌;TokenService.getLoginUser按login_tokens:{token}查 Redis,查不到则最终返回401 认证失败;剩余有效期不足 20 分钟时自动
refreshToken续期;将 LoginUser 写入
SecurityContextHolder,之后 Controller 上的@PreAuthorize("@ss.hasPermi('xxx')")据此做权限判断。
五、阶段 3:getInfo 与退出
GET /getInfo:从 SecurityContext 取 LoginUser,返回 user、roles、permissions 三组数据;权限集合有变化时同步刷新 Redis 缓存,前端据此渲染菜单与按钮。
GET /getRouters:按角色查询动态路由树,生成左侧菜单。
POST /logout:删除 Redis 中的
login_tokens:{token}并记录退出日志,令牌即刻失效——会话状态在 Redis 而非客户端,所以服务端可以主动踢人。
六、全流程泳道图
登录 → 首页数据加载 → 退出 的完整调用链路如下:
七、设计总结
为什么用 UUID + Redis 而不是纯 JWT:牺牲一次 Redis 查询,换来服务端主动失效、在线用户管理、单点登录控制三大能力;
两条时间线是两套代码:登录瞬间走认证链(入口
SysLoginController),登录之后走过滤器鉴权(入口JwtAuthenticationTokenFilter);四层安全防线各司其职:验证码防机器人 → 错误次数锁定防爆破 → BCrypt 防拖库 → @PreAuthorize 防越权;
两个高频报错的区分:「401 认证失败」发生在过滤器阶段(token 缺失/过期);「验证码已失效」发生在登录阶段(Redis 键过期)——排查时先定位阶段。