当前位置: 首页 > ai >正文

【Ruoyi解密-02.登录流程:】登录-找密码不抓瞎

探秘若依(RuoYi)框架登录流程:从前端到后端的完整链路

各位,咱们每天点登录按钮的时候,是不是觉得这就是 “输个账号密码点一下” 的事儿?但你想过没 —— 你敲的那串密码,是怎么跑到服务器里 “验明正身” 的?为啥输错三次就蹦验证码?“记住我” 又是靠啥让你下次不用再输密码?
在这里插入图片描述

今天咱们就把若依登录流程这只 “黑盒子” 拆开来看。可别觉得 “会用就行”,要是这儿的逻辑没吃透,下次线上突然报 “登录超时”,或者用户说 “密码对的登不进去”,你可能就得对着日志抓瞎。咱得打破砂锅问到底,不然同一个坑栽两次,那可就太亏了,真要是出了生产事故,加班排查的时间可比现在搞懂它多多了 —— 来吧,咱们从浏览器敲下回车的那一刻说起!

若依(RuoYi)是国内广受欢迎的开源后台管理框架,基于SpringBoot、Shiro、MyBatis等技术栈构建,其登录流程设计融合了安全性与易用性。本文将从前端页面交互、后端请求处理、Shiro认证核心到辅助功能(验证码、记住我、日志记录),全方位拆解若依框架的登录流程,帮助开发者深入理解其设计逻辑。

一、登录流程总览

若依的登录流程可分为前端交互层后端控制层安全认证层数据持久层四个核心环节,整体链路如下:

用户输入账号密码 → 前端表单验证 → 发送登录请求 → 后端接收参数 → Shiro认证 → 认证成功/失败 → 跳转首页/返回错误 → 记录登录日志

流程如下:

在这里插入图片描述
一句话拆解:
“浏览器先拿页面,再发 Ajax 把用户名、密码、验证码、记住我一并塞进 UsernamePasswordToken,Shiro 把认证委托给 UserRealm,UserRealm 做完查库、状态、密码三重校验后——成了就写登录日志、创建 Session、让前端跳首页;败了就抛异常,让前端弹错误并刷新验证码。”
至此,若依登录的前端交互 → 后端控制 → Shiro 安全 → 数据持久整条链路已完全打通。

时序流程如下:

在这里插入图片描述
一句话概括:
浏览器把账号、密码、验证码和“记住我”一次性 POST 到 /loginSysLoginController 立即把活儿甩给 Shiro;Shiro 让 UserRealm 去数据库查人、验状态、比密码——成了就写登录日志、发 Session、跳首页;败了直接返回 500,前端弹提示并刷新验证码。

接下来,我们将基于提供的源码,逐步剖析每个环节的实现细节。

是不是认为很简单?那看一下详细时序流程如下:
在这里插入图片描述
下面给出与上面 Mermaid 时序图 完全对应的 序号注释表。

序号动作(Actor → Participant)关键说明
用户 → 浏览器地址栏输入 /login 并回车,触发 GET 请求。
浏览器 → SysLoginController请求登录页;Controller 同时把“验证码开关/记住我开关”等配置塞进 Model。
SysLoginController → 浏览器返回渲染后的 login.html 页面。
浏览器 → 用户页面呈现账号、密码、验证码、记住我复选框。
用户 → 浏览器在表单中输入凭证并点击“登录”。
浏览器 → 浏览器jquery.validate 做字段合法性校验;若开启验证码还需校验长度。
浏览器 → SysLoginControllerAjax 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 → MySQLINSERT INTO sys_login_info (..., status = 0)
UserRealm → Shiro SecurityManager封装并返回 SimpleAuthenticationInfo
Shiro SecurityManager → SysLoginController无异常,认证通过。
SysLoginController → 浏览器返回 JSON {"code": 200}
浏览器 → 浏览器前端执行 location.href = '/index' 跳转首页。
24UserRealm → Shiro SecurityManager失败分支:抛 IncorrectCredentialsException
25Shiro SecurityManager → SysLoginController向上传递异常。
26SysLoginController → LoginInfoService记录登录失败日志。
27LoginInfoService → MySQLINSERT INTO sys_login_info (..., status = 1, msg = '密码错误')
28SysLoginController → 浏览器返回 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)验证码刷新:防缓存与状态重置

验证码刷新是保障安全的重要环节,源码中通过两种方式触发:

  1. 主动点击刷新(初始化时绑定):
    $('.imgcode').click(function() {var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();$(".imgcode").attr("src", url);
    });
    
  2. 登录失败自动刷新(在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);}
}

核心职责:

  • 接收前端传递的usernamepasswordrememberMe参数;
  • 创建UsernamePasswordToken(Shiro的令牌对象);
  • 调用subject.login(token)触发Shiro认证流程;
  • 封装认证结果为AjaxResult(若依统一响应格式)。

四、Shiro认证核心流程

Shiro是若依的安全框架,负责身份认证与授权,其认证流程是登录的核心。

1. Shiro认证入口:Subject.login()

subject.login(token)会触发Shiro的认证流程,核心逻辑如下:

  1. Subject(当前用户)将认证委托给SecurityManager
  2. SecurityManager进一步委托给Authenticator(认证器);
  3. 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. 验证码功能

验证码用于防止恶意暴力破解,实现流程:

  1. 前端请求/captcha/captchaImage接口获取验证码图片与uuid
  2. 后端生成随机验证码文本,存入Redis(键为uuid,值为验证码),并生成图片返回;
  3. 登录时前端传递uuid与用户输入的验证码;
  4. 后端通过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机制实现,用户下次访问无需重复登录:

  1. 前端传递rememberMe: true时,UsernamePasswordTokenrememberMe属性为true
  2. Shiro会将用户信息加密后存入Cookie(默认名为rememberMe);
  3. 下次访问时,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)提供了日志的搜索、删除、清空功能,方便管理员查看历史登录记录。

六、总结

若依框架的登录流程是一个"前端-后端-安全框架-数据层"紧密协作的过程,核心亮点包括:

  1. 安全性:通过验证码、密码加密(MD5+盐)、登录日志审计等机制防范风险;
  2. 灵活性:通过配置参数(如是否启用验证码、记住我)动态调整功能;
  3. 可扩展性:基于Shiro的认证流程易于扩展(如集成OAuth2、LDAP等)。

理解登录流程不仅有助于日常开发与问题排查,也能为自定义登录功能(如手机验证码登录、第三方登录)提供参考。如需进一步深入,可重点研究UserRealm的认证逻辑与Shiro的会话管理机制。

本篇文章不追求大而全,主打攻克一个流程,举一反三的效果。下一篇文章预告。
1.详细拆解validateKickout函数,是若依框架中处理 “用户被踢下线” 场景的核心逻辑,其实现原理可以概括为 “参数检测→→ 弹窗提示 → 页面重置” 三步,精准解决了用户在多端登录或会话失效时的体验问题。
2.白话讲解充当保安大叔的Shiro 验证身份的 “三步走”
3.若依框架中涉及用户账号密码的功能,除登录外主要包括:

  1. 密码重置:用户通过“忘记密码”功能,凭验证码或管理员操作重置密码,需验证身份后更新数据库密码(仍用MD5+盐加密存储)。
  2. 密码修改:已登录用户在个人中心自行修改密码,需校验原密码正确性,新密码加密后替换旧值。
  3. 账号创建/编辑:管理员在用户管理模块新增或编辑用户时,会设置初始密码(或由用户自行设置),并按加密规则处理后存储。
  4. 密码强度校验:在修改、重置密码时,前端通过正则判断密码复杂度(如长度、字符组合),确保符合安全规则。
  5. 会话管理:用户登录后,若管理员强制踢出在线用户,会失效其会话,用户需重新输入密码登录。
http://www.xdnf.cn/news/18319.html

相关文章:

  • selenium3.141.0执行JS无法传递element解决方法
  • Linux的奇妙冒险——进程间通信(管道、SystemV IPC)
  • 完全背包(模板)
  • webrtc中win端音频---windows Core Audio
  • 2025图表制作完全指南:设计规范、工具选型与行业案例
  • Chrome/360 浏览器扩展深度解析:内置扩展与普通扩展的实现机制对比
  • (栈)Leetcode155最小栈+739每日温度
  • 力扣 30 天 JavaScript 挑战 第37天 第九题笔记 知识点: 剩余参数,拓展运算符
  • Spring Boot集成腾讯云人脸识别实现智能小区门禁系统
  • 【C++去除整数某一位数字求新数和倍数保留2位小数控制】2022-10-22
  • 人工智能 -- 循环神经网络day1 -- 自然语言基础、NLP基础概率、NLP基本流程、NLP特征工程、NLP特征输入
  • 打造医疗新质生产力
  • 如何用算力魔方4060安装PaddleOCR MCP 服务器
  • visual studio更改git提交的用户名和邮件
  • Seaborn数据可视化实战:Seaborn基础与实践-数据可视化的艺术
  • 高效处理NetCDF文件经纬度转换:一个纯CDO驱动的Bash脚本详解
  • [大模型微调]基于llama_factory用 LoRA 高效微调 Qwen3 医疗大模型:从原理到实现
  • WPF中UI线程频繁操作造成卡顿的处理
  • 中文房间悖论:人工智能理解力的哲学拷问
  • 深度解析游戏引擎中的相机:视图矩阵
  • 小体积晶振1610/2016/3225选型参数
  • 小游戏AssetBundle加密方案解析
  • 5.Shell脚本修炼手册---Linux正则表达式(Shell三剑客准备启动阶段)
  • 电能质量监测装置 分布式光伏安全并网“准入证”
  • 8.21 随机森林
  • conda create 报错:Unable to read repodata JSON(镜像 pkgs/free 导致)
  • Neovim clangd LSP 配置出现 “attempt to call field ‘ge‘”
  • C# 13 与 .NET 9 跨平台开发实战(第一章:开发环境搭建与.NET概述-下篇)
  • 鸿蒙中基础耗时分析:Time分析
  • 音视频面试题集锦第 29 期