【Ruoyi解密-02.登录流程:】登录-找密码不抓瞎
探秘若依(RuoYi)框架登录流程:从前端到后端的完整链路
各位,咱们每天点登录按钮的时候,是不是觉得这就是 “输个账号密码点一下” 的事儿?但你想过没 —— 你敲的那串密码,是怎么跑到服务器里 “验明正身” 的?为啥输错三次就蹦验证码?“记住我” 又是靠啥让你下次不用再输密码?
今天咱们就把若依登录流程这只 “黑盒子” 拆开来看。可别觉得 “会用就行”,要是这儿的逻辑没吃透,下次线上突然报 “登录超时”,或者用户说 “密码对的登不进去”,你可能就得对着日志抓瞎。咱得打破砂锅问到底,不然同一个坑栽两次,那可就太亏了,真要是出了生产事故,加班排查的时间可比现在搞懂它多多了 —— 来吧,咱们从浏览器敲下回车的那一刻说起!
若依(RuoYi)是国内广受欢迎的开源后台管理框架,基于SpringBoot、Shiro、MyBatis等技术栈构建,其登录流程设计融合了安全性与易用性。本文将从前端页面交互、后端请求处理、Shiro认证核心到辅助功能(验证码、记住我、日志记录),全方位拆解若依框架的登录流程,帮助开发者深入理解其设计逻辑。
一、登录流程总览
若依的登录流程可分为前端交互层、后端控制层、安全认证层、数据持久层四个核心环节,整体链路如下:
用户输入账号密码 → 前端表单验证 → 发送登录请求 → 后端接收参数 → Shiro认证 → 认证成功/失败 → 跳转首页/返回错误 → 记录登录日志
流程如下:
一句话拆解:
“浏览器先拿页面,再发 Ajax 把用户名、密码、验证码、记住我一并塞进 UsernamePasswordToken,Shiro 把认证委托给 UserRealm,UserRealm 做完查库、状态、密码三重校验后——成了就写登录日志、创建 Session、让前端跳首页;败了就抛异常,让前端弹错误并刷新验证码。”
至此,若依登录的前端交互 → 后端控制 → Shiro 安全 → 数据持久整条链路已完全打通。
时序流程如下:
一句话概括:
浏览器把账号、密码、验证码和“记住我”一次性 POST 到 /login
,SysLoginController
立即把活儿甩给 Shiro;Shiro 让 UserRealm
去数据库查人、验状态、比密码——成了就写登录日志、发 Session、跳首页;败了直接返回 500,前端弹提示并刷新验证码。
接下来,我们将基于提供的源码,逐步剖析每个环节的实现细节。
是不是认为很简单?那看一下详细时序流程如下:
下面给出与上面 Mermaid 时序图 完全对应的 序号注释表。
序号 | 动作(Actor → Participant) | 关键说明 |
---|---|---|
① | 用户 → 浏览器 | 地址栏输入 /login 并回车,触发 GET 请求。 |
② | 浏览器 → SysLoginController | 请求登录页;Controller 同时把“验证码开关/记住我开关”等配置塞进 Model。 |
③ | SysLoginController → 浏览器 | 返回渲染后的 login.html 页面。 |
④ | 浏览器 → 用户 | 页面呈现账号、密码、验证码、记住我复选框。 |
⑤ | 用户 → 浏览器 | 在表单中输入凭证并点击“登录”。 |
⑥ | 浏览器 → 浏览器 | jquery.validate 做字段合法性校验;若开启验证码还需校验长度。 |
⑦ | 浏览器 → SysLoginController | Ajax POST /login ,携带 username, password, validateCode, rememberMe 。 |
⑧ | SysLoginController → SysLoginController | 根据参数创建 UsernamePasswordToken 对象。 |
⑨ | SysLoginController → Shiro SecurityManager | 调用 subject.login(token) 进入 Shiro 认证流程。 |
⑩ | Shiro SecurityManager → UserRealm | 回调 doGetAuthenticationInfo(token) 获取认证信息。 |
⑪ | UserRealm → UserService | 根据用户名查询数据库用户。 |
⑫ | UserService → MySQL | 执行 SELECT * FROM sys_user WHERE login_name = ? 。 |
⑬ | MySQL → UserService | 返回 SysUser 实体。 |
⑭ | UserService → UserRealm | 回传用户对象。 |
⑮ | UserRealm → UserRealm | 检查用户是否已删除、已禁用等状态。 |
⑯ | UserRealm → UserService | 成功分支:更新最后登录 IP、时间。 |
⑰ | UserService → MySQL | 执行 UPDATE sys_user SET login_ip = ?, login_date = ? WHERE user_id = ? 。 |
⑱ | UserRealm → LoginInfoService | 记录登录成功日志。 |
⑲ | LoginInfoService → MySQL | INSERT INTO sys_login_info (..., status = 0) 。 |
⑳ | UserRealm → Shiro SecurityManager | 封装并返回 SimpleAuthenticationInfo 。 |
㉑ | Shiro SecurityManager → SysLoginController | 无异常,认证通过。 |
㉒ | SysLoginController → 浏览器 | 返回 JSON {"code": 200} 。 |
㉓ | 浏览器 → 浏览器 | 前端执行 location.href = '/index' 跳转首页。 |
24 | UserRealm → Shiro SecurityManager | 失败分支:抛 IncorrectCredentialsException 。 |
25 | Shiro SecurityManager → SysLoginController | 向上传递异常。 |
26 | SysLoginController → LoginInfoService | 记录登录失败日志。 |
27 | LoginInfoService → MySQL | INSERT INTO sys_login_info (..., status = 1, msg = '密码错误') 。 |
28 | SysLoginController → 浏览器 | 返回 JSON {"code": 500, "msg": "密码错误"} 。 |
29 | 浏览器 → 浏览器 | 弹层提示错误,并调用 refreshCode() 刷新验证码。 |
直接对照上图中的 autonumber 数字即可秒懂每一步含义。
二、前端页面与交互逻辑
前端是登录流程的起点,负责用户输入收集、表单验证与请求发起。若依的登录页面(login.html
)与交互逻辑(login.js
)是这一环节的核心。
1. 登录页面结构(login.html)
登录页面采用Thymeleaf模板引擎渲染,主要包含以下核心元素:
<!-- 用户名输入框 -->
<input type="text" name="username" class="form-control uname" placeholder="用户名" value="admin" />
<!-- 密码输入框 -->
<input type="password" name="password" class="form-control pword" placeholder="密码" value="admin123" />
<!-- 验证码(条件渲染,由配置决定是否显示) -->
<div class="row m-t" th:if="${captchaEnabled==true}"><input type="text" name="validateCode" class="form-control code" placeholder="验证码" maxlength="5" /><img th:src="@{/captcha/captchaImage(type=${captchaType})}" class="imgcode" width="85%"/>
</div>
<!-- 记住我选项(由Shiro配置决定是否显示) -->
<div class="checkbox-custom" th:if="${isRemembered}" th:classappend="${captchaEnabled==false} ? 'm-t'"><input type="checkbox" id="rememberme" name="rememberme"> <label for="rememberme">记住我</label>
</div>
<!-- 登录按钮 -->
<button class="btn btn-success btn-block" id="btnSubmit" data-loading="正在验证登录,请稍候...">登录</button>
页面通过Thymeleaf的th:if
动态渲染功能:
- 验证码是否显示(
captchaEnabled
):由系统配置参数sys.account.captchaEnabled
控制; - 记住我选项是否显示(
isRemembered
):由Shiro配置shiro.rememberMe.enabled
控制。
2. 前端交互逻辑(login.js):从验证到请求的完整实现
结合提供的完整login.js
源码,若依框架的前端登录交互逻辑远比基础设计更细致,不仅包含表单验证、请求发送和验证码刷新,还增加了异常场景处理(如被踢下线提示)和用户体验优化(如加载状态)。以下是基于实际源码的深度解析:
(1)页面初始化:三重准备工作
页面加载完成后,$(function(){})
会自动执行三项核心初始化操作,为登录流程奠定基础:
$(function() {validateKickout(); // 检测是否被踢下线(异地登录场景)validateRule(); // 初始化表单验证规则// 绑定验证码点击刷新事件$('.imgcode').click(function() {var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();$(".imgcode").attr("src", url);});
});
validateKickout()
:通过URL参数kickout=1
检测用户是否因异地登录被强制下线,若存在则弹窗提示并重置页面状态。validateRule()
:使用jquery.validate
插件配置表单验证规则,确保关键参数合规。- 验证码刷新绑定:通过随机数
s=Math.random()
避免浏览器缓存,保证每次点击都能获取新验证码。
(2)表单验证:多层次校验机制
validateRule()
函数实现了严谨的前端表单校验,是拦截无效请求的第一道防线:
function validateRule() {var icon = "<i class='fa fa-times-circle'></i> ";$("#signupForm").validate({rules: {username: { required: true }, // 用户名必填password: { required: true } // 密码必填},messages: {username: { required: icon + "请输入您的用户名" },password: { required: icon + "请输入您的密码" }},submitHandler: function(form) {login(); // 验证通过后触发登录请求}});
}
- 校验规则:强制检查用户名和密码非空,减少无效后端请求。
- 错误提示:通过图标+文字组合提升提示直观性,比单纯文字更易引起用户注意。
- 提交处理:只有全部验证通过后,才会调用
login()
函数,避免不合规数据提交。
(3)登录请求:完整的前后端交互链路
login()
函数是前端登录的核心,负责参数处理、请求发送和结果反馈,细节设计尤为关键:
function login() {// 1. 提取并清洗用户输入(去空格处理避免误输入)var username = $.common.trim($("input[name='username']").val());var password = $.common.trim($("input[name='password']").val());var validateCode = $("input[name='validateCode']").val();var rememberMe = $("input[name='rememberme']").is(':checked');// 2. 验证码二次校验(若系统启用验证码)if($.common.isEmpty(validateCode) && captchaEnabled) {$.modal.msg("请输入验证码");return false;}// 3. 发送AJAX登录请求$.ajax({type: "post",url: ctx + "login",data: {"username": username,"password": password,"validateCode": validateCode,"rememberMe": rememberMe},beforeSend: function () {$.modal.loading($("#btnSubmit").data("loading")); // 显示加载动画},success: function(r) {if (r.code == web_status.SUCCESS) {location.href = ctx + 'index'; // 成功则跳转首页} else {$('.imgcode').click(); // 失败则刷新验证码$(".code").val(""); // 清空验证码输入框$.modal.msg(r.msg); // 显示错误信息}$.modal.closeLoading(); // 无论成败都关闭加载动画}});
}
- 参数处理:使用
$.common.trim()
去除用户名/密码前后空格,解决"输入正确却登录失败"的常见问题(用户可能误输入空格)。 - 条件校验:仅当系统启用验证码(
captchaEnabled=true
)时,才校验验证码非空,适配不同环境配置。 - 体验优化:
beforeSend
显示"正在登录"加载动画,避免用户因网络延迟重复点击;- 失败时自动刷新验证码并清空输入框,防止用户重复使用无效验证码;
- 统一在
success
中关闭加载动画,避免因异常导致加载状态卡死。
(4)验证码刷新:防缓存与状态重置
验证码刷新是保障安全的重要环节,源码中通过两种方式触发:
- 主动点击刷新(初始化时绑定):
$('.imgcode').click(function() {var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();$(".imgcode").attr("src", url); });
- 登录失败自动刷新(在
login()
的失败分支中):$('.imgcode').click(); // 直接触发点击事件
- 防缓存机制:通过
Math.random()
生成随机参数,确保浏览器不缓存旧验证码图片。 - 状态同步:刷新后清空输入框(
$(".code").val("")
),避免用户输入的旧验证码与新图片不匹配。
(5)异常场景处理:被踢下线检测
validateKickout()
函数专门处理"异地登录导致当前会话失效"的场景,体现系统的健壮性:
function validateKickout() {if (getParam("kickout") == 1) { // 检测URL中的kickout参数layer.alert("<font color='red'>您已在别处登录,请您修改密码或重新登录</font>", {icon: 0,title: "系统提示"}, function(index) {layer.close(index);// 处理嵌套页面场景,确保顶层页面同步刷新if (top != self) {top.location = self.location;} else {// 清除URL中的kickout参数,避免重复提示var oldUrl = window.location.href;var newUrl = oldUrl.substring(0, oldUrl.indexOf('?'));self.location = newUrl;}});}
}
- 参数检测:通过
getParam()
函数从URL中提取kickout
参数,判断是否为被踢下线状态。 - 页面重置:关闭弹窗后清除URL中的
kickout
参数,避免用户刷新页面时重复触发提示。 - 嵌套适配:针对
iframe
嵌套场景,强制顶层页面同步刷新,保证会话状态一致性。
总结:前端交互的设计亮点
若依的login.js
通过分层校验(表单验证→二次参数检查)、状态管理(加载动画→结果反馈)、异常适配(异地登录→缓存处理)三大维度,实现了既安全又友好的登录体验。这些细节(如去空格处理、加载状态闭环、嵌套页面适配)正是生产级代码与 demo 级代码的核心区别,也是我们需要深入理解并借鉴的设计思路——只有关注这些"不起眼"的逻辑,才能真正避免因前端交互疏漏导致的线上问题。
三、后端请求处理(SysLoginController)
后端由SysLoginController
接收登录请求,负责参数传递与认证结果封装,是连接前端与Shiro的桥梁。
1. GET请求:返回登录页面
当用户访问/login
时,触发GET请求处理:
@GetMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, ModelMap mmap) {// 如果是Ajax请求,返回JSON提示未登录if (ServletUtils.isAjaxRequest(request)) {return ServletUtils.renderString(response, "{\"code\":\"1\",\"msg\":\"未登录或登录超时。请重新登录\"}");}// 向页面传递参数mmap.put("isRemembered", rememberMe); // 记住我配置mmap.put("isAllowRegister", Convert.toBool(configService.getKey("sys.account.registerUser"), false)); // 允许注册配置return "login"; // 返回登录页面
}
关键逻辑:
- 通过
ServletUtils.isAjaxRequest
判断请求类型:Ajax请求返回JSON,非Ajax请求返回登录页面; ServletUtils.isAjaxRequest
的实现(见ServletUtils
类):通过Accept
头、X-Requested-With
头或URL后缀判断是否为Ajax请求。
2. POST请求:处理登录验证
用户提交表单后,触发POST请求处理,核心代码:
@PostMapping("/login")
@ResponseBody
public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe) {// 创建Shiro登录令牌(包含用户名、密码、记住我状态)UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);// 获取Shiro的Subject对象(当前用户)Subject subject = SecurityUtils.getSubject();try {// 调用Shiro的登录方法进行认证subject.login(token);return success(); // 认证成功,返回成功响应} catch (AuthenticationException e) {// 认证失败,返回错误信息String msg = "用户或密码错误";if (StringUtils.isNotEmpty(e.getMessage())) {msg = e.getMessage(); // 使用异常中携带的具体错误信息}return error(msg);}
}
核心职责:
- 接收前端传递的
username
、password
、rememberMe
参数; - 创建
UsernamePasswordToken
(Shiro的令牌对象); - 调用
subject.login(token)
触发Shiro认证流程; - 封装认证结果为
AjaxResult
(若依统一响应格式)。
四、Shiro认证核心流程
Shiro是若依的安全框架,负责身份认证与授权,其认证流程是登录的核心。
1. Shiro认证入口:Subject.login()
subject.login(token)
会触发Shiro的认证流程,核心逻辑如下:
Subject
(当前用户)将认证委托给SecurityManager
;SecurityManager
进一步委托给Authenticator
(认证器);Authenticator
调用Realm
(数据源)获取用户信息并验证。
2. Realm:用户信息获取与验证
若依自定义了UserRealm
(继承AuthorizingRealm
),实现doGetAuthenticationInfo
方法处理认证:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {UsernamePasswordToken upToken = (UsernamePasswordToken) token;String username = upToken.getUsername();String password = new String(upToken.getPassword());// 1. 查询用户信息SysUser user = userService.selectUserByLoginName(username);if (user == null) {throw new UnknownAccountException("用户名不存在");}if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {throw new LockedAccountException("用户已删除");}if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {throw new LockedAccountException("用户已禁用");}// 2. 验证密码(若依默认使用MD5加密,盐值为用户名+随机盐)if (!matchesPassword(password, user.getSalt(), user.getPassword())) {throw new IncorrectCredentialsException("密码错误");}// 3. 记录登录信息(更新最后登录时间、IP等)userService.recordLoginInfo(user);// 4. 返回认证信息return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(user.getSalt()), getName());
}
UserRealm
的核心作用:
- 用户查询:通过
userService.selectUserByLoginName
从数据库获取用户信息; - 状态校验:检查用户是否删除、禁用;
- 密码验证:若依默认将密码加密为
MD5(MD5(password) + salt)
,其中salt
为随机字符串; - 登录记录:更新用户最后登录时间、IP等信息。
3. 认证异常处理
Shiro通过抛出不同的AuthenticationException
子类区分错误类型,若依在SysLoginController
中捕获并返回给前端:
UnknownAccountException
:用户名不存在;LockedAccountException
:用户被锁定/禁用;IncorrectCredentialsException
:密码错误;- 其他异常(如验证码错误,由自定义过滤器抛出)。
五、辅助功能解析
若依的登录流程还包含多个辅助功能,提升安全性与用户体验。
1. 验证码功能
验证码用于防止恶意暴力破解,实现流程:
- 前端请求
/captcha/captchaImage
接口获取验证码图片与uuid
; - 后端生成随机验证码文本,存入Redis(键为
uuid
,值为验证码),并生成图片返回; - 登录时前端传递
uuid
与用户输入的验证码; - 后端通过
uuid
从Redis获取正确验证码,比对用户输入。
核心代码(验证码生成接口):
@GetMapping("/captcha/captchaImage")
public void getCode(HttpServletResponse response, String uuid) {// 生成验证码文本String code = RandomUtil.randomString(4);// 存入Redis(有效期2分钟)redisCache.setCacheObject("captcha:" + uuid, code, 2, TimeUnit.MINUTES);// 生成图片并写入响应CaptchaUtil.createLineCaptcha(120, 40, 4, 10).write(response.getOutputStream());
}
2. 记住我功能
通过Shiro的rememberMe
机制实现,用户下次访问无需重复登录:
- 前端传递
rememberMe: true
时,UsernamePasswordToken
的rememberMe
属性为true
; - Shiro会将用户信息加密后存入Cookie(默认名为
rememberMe
); - 下次访问时,Shiro自动从Cookie读取信息并完成认证。
配置项(application.yml
):
shiro:rememberMe:enabled: true # 启用记住我cookie:maxAge: 86400 # 有效期1天
3. 登录日志记录
登录成功或失败后,系统会记录登录日志(sys_login_info
表),用于审计与监控。
日志记录时机:
- 登录成功:在
UserRealm
的认证通过后,调用userService.recordLoginInfo
; - 登录失败:在
SysLoginController
的异常捕获块中,调用logininforService.recordLoginInfo
。
日志内容包括:
- 登录用户(
login_name
); - 登录IP(
ipaddr
); - 登录地点(通过IP解析,
login_location
); - 浏览器/操作系统(
browser
/os
); - 登录状态(
status
:0成功,1失败); - 错误信息(
msg
); - 登录时间(
login_time
)。
日志查询页面(logininfor.html
)提供了日志的搜索、删除、清空功能,方便管理员查看历史登录记录。
六、总结
若依框架的登录流程是一个"前端-后端-安全框架-数据层"紧密协作的过程,核心亮点包括:
- 安全性:通过验证码、密码加密(MD5+盐)、登录日志审计等机制防范风险;
- 灵活性:通过配置参数(如是否启用验证码、记住我)动态调整功能;
- 可扩展性:基于Shiro的认证流程易于扩展(如集成OAuth2、LDAP等)。
理解登录流程不仅有助于日常开发与问题排查,也能为自定义登录功能(如手机验证码登录、第三方登录)提供参考。如需进一步深入,可重点研究UserRealm
的认证逻辑与Shiro的会话管理机制。
本篇文章不追求大而全,主打攻克一个流程,举一反三的效果。下一篇文章预告。
1.详细拆解validateKickout函数,是若依框架中处理 “用户被踢下线” 场景的核心逻辑,其实现原理可以概括为 “参数检测→→ 弹窗提示 → 页面重置” 三步,精准解决了用户在多端登录或会话失效时的体验问题。
2.白话讲解充当保安大叔的Shiro 验证身份的 “三步走”
3.若依框架中涉及用户账号密码的功能,除登录外主要包括:
- 密码重置:用户通过“忘记密码”功能,凭验证码或管理员操作重置密码,需验证身份后更新数据库密码(仍用MD5+盐加密存储)。
- 密码修改:已登录用户在个人中心自行修改密码,需校验原密码正确性,新密码加密后替换旧值。
- 账号创建/编辑:管理员在用户管理模块新增或编辑用户时,会设置初始密码(或由用户自行设置),并按加密规则处理后存储。
- 密码强度校验:在修改、重置密码时,前端通过正则判断密码复杂度(如长度、字符组合),确保符合安全规则。
- 会话管理:用户登录后,若管理员强制踢出在线用户,会失效其会话,用户需重新输入密码登录。