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

RuoYi登录三步自动化:验证码、加密密码与Cookie状态机

1. 这不是“写个脚本”,而是后台系统登录链路的完整逆向工程

RuoYi 是国内 Java 后台开发中使用频率极高的开源框架,它不是玩具项目,而是真实企业级系统落地的“最小可行基座”——权限控制、菜单管理、代码生成、定时任务、日志审计,全都有。但恰恰因为它的“开箱即用”,很多团队在接入自动化测试时栽了第一个跟头:登录接口跑不通。你填对了 username/password,Postman 里点发送,返回却是{"code":500,"msg":"验证码错误"}或更隐蔽的{"code":200,"msg":"操作成功","data":null}——看着成功,实际 session 没建立,后续所有接口全 401。这不是 Postman 的 bug,也不是 RuoYi 的缺陷,而是你没真正看懂它登录背后那套“三段式认证流”:验证码预加载 → 登录请求携带 token → 登录成功后 Cookie 自动注入。我带过三个不同行业的 RuoYi 二次开发团队,90% 的新人卡在第一步:连验证码接口都调不通,更别说把整个登录流程串成可复用的自动化脚本。这篇内容不讲 Spring Security 原理,不堆 Maven 依赖树,就聚焦一件事:如何用 Postman 真正跑通 RuoYi 的登录,并把这套逻辑固化为可维护、可调试、可集成 CI 的自动化脚本。适合刚接手 RuoYi 项目的 Java 开发、负责接口测试的 QA 工程师、以及需要做持续交付流水线的 DevOps 同学。你不需要会写 Java,但得知道 HTTP 请求怎么带 Header、Cookie 怎么传递、Pre-request Script 和 Tests 脚本怎么协同工作。

2. RuoYi 登录不是单次 POST,而是一场三步走的“信任交接”

RuoYi 的登录机制,本质上是传统 Web 表单登录在前后端分离架构下的安全适配。它没有用 JWT 全局 Token,也没有走 OAuth2 授权码模式,而是回归最朴实的 Session + Cookie + CSRF Token 组合。这个设计有它的现实合理性:降低前端鉴权复杂度、兼容老旧浏览器、便于服务端统一会话管理。但对自动化测试而言,它意味着你不能像调用一个纯 REST API 那样“一锤定音”。必须拆解为三个原子操作,每一步都承担明确职责,且环环相扣。

2.1 第一步:获取验证码与 CSRF Token(GET /captchaImage)

这是整个流程的起点,也是最容易被忽略的“前置依赖”。RuoYi 默认开启验证码校验(ruoyi.captcha.enabled=true),且验证码图片本身不携带任何身份标识。关键在于响应头和响应体里的两个隐藏信息:

  • 响应头Set-Cookie中的capchaCode:这是服务端生成的验证码唯一标识,格式如capchaCode=8a3f7b1e-2c5d-4a90-b1e2-3f7a9b1e2c5d; Path=/; HttpOnly。注意,这个 Cookie 的HttpOnly属性意味着 JavaScript 无法读取,但 Postman 完全不受影响,它会自动存储并随后续请求发送。

  • 响应体 JSON 中的uuid字段:这是验证码图片的业务 ID,例如{"code":200,"msg":"操作成功","img":"data:image/png;base64,...","uuid":"8a3f7b1e-2c5d-4a90-b1e2-3f7a9b1e2c5d"}。这个uuid必须原样传给下一步的登录请求,作为“我刚刚看到这张图”的凭证。

提示:很多人在这里犯错,以为只要拿到图片 base64 就行,直接跳过uuid提取。结果第二步登录永远报“验证码错误”。根本原因是服务端比对的是uuid和内存/Redis 中缓存的验证码值,而不是图片内容本身。

2.2 第二步:提交登录表单(POST /login)

这一步是核心动作,但请求体结构非常规。它不是标准的application/json,而是application/x-www-form-urlencoded。这意味着你不能把{ "username": "admin", "password": "admin123", "code": "abcd", "uuid": "xxx" }直接 JSON.stringify 发送。必须按表单键值对方式组织:

username=admin password=25f9e794323b453885f5181f1b624d0b // 注意:这是 MD5(明文密码+随机盐) 的结果,不是明文! code=abcd uuid=8a3f7b1e-2c5d-4a90-b1e2-3f7a9b1e2c5d

其中password字段的加密逻辑是 RuoYi 的关键安全点。它并非简单 MD5,而是:

  1. 服务端在/captchaImage响应中,除了uuid,还会在响应头X-Subject或响应体salt字段(取决于版本)返回一个动态盐值(如X-Subject: 7a9b1e2c5d8a3f);
  2. 前端 JS 会执行md5(md5(password) + salt)
  3. 这个最终哈希值,才是 POST 到/loginpassword字段值。

注意:RuoYi v4.7.0+ 版本将盐值放在响应头X-Subject;v4.6.x 及更早版本则放在响应体salt字段。你的脚本必须兼容两种情况,否则在升级后立即失效。

2.3 第三步:验证登录状态与 Cookie 注入(GET /getInfo)

前两步成功,只代表账号密码通过了校验,但不等于“已登录”。RuoYi 的 Session 是由 Spring 容器管理的,真正的登录完成标志,是服务端返回了包含用户信息的JSESSIONIDCookie,并且该 Cookie 被浏览器(或 Postman)正确接收。第三步/getInfo就是用来确认这一点的“心跳检测”。

这个接口本身不带参数,但必须携带上一步登录成功后服务端 Set 的JSESSIONIDCookie。如果请求成功(HTTP 200),且响应体中code为 200、data包含rolesname字段,说明整个登录链路已打通,后续所有需要权限的接口(如/user/list)都可以复用这个 Cookie 上下文。

实测心得:我曾在一个金融客户项目中发现,他们的 RuoYi 定制版在/login成功后,会额外重定向到/index,导致 Postman 无法捕获JSESSIONID。解决方案是在 Postman 的 Settings → General 中关闭 “Automatically persist cookies”,改为手动提取Set-Cookie头中的JSESSIONID并写入环境变量。这是定制化开发带来的典型“隐性差异”,必须在脚本中预留钩子。

3. Postman 脚本不是写死的命令,而是可调试、可复用的状态机

把上面三步写成三个独立的 Postman 请求很简单,但那不是“自动化脚本”,只是“三个请求的集合”。真正的自动化,是让它们像齿轮一样咬合转动:第一步的输出,自动成为第二步的输入;第二步的成功,自动触发第三步的验证;任意一步失败,能立刻定位是哪一环出了问题。这靠的是 Postman 的 Pre-request Script(请求前执行)和 Tests(响应后执行)两大能力,它们共同构成了一个轻量级的状态机。

3.1 Pre-request Script:为每一次请求注入“上下文感知”

Pre-request Script 的核心任务,是从环境变量或上一个请求的响应中,提取本次请求所需的动态参数。对于 RuoYi 登录,它要干三件事:

  1. /captchaImage请求生成唯一时间戳:防止浏览器缓存,确保每次请求都是新的验证码。代码很简单:

    pm.environment.set("timestamp", Date.now());

    然后在请求 URL 中引用:{{baseUrl}}/captchaImage?timestamp={{timestamp}}

  2. /login请求准备加密密码:这是最复杂的部分。脚本需要:

    • 从上一个请求(即/captchaImage)的响应中,提取uuidsalt(或X-Subject);
    • 获取用户输入的明文密码(从环境变量pm.environment.get("loginPassword")读取);
    • 执行与前端完全一致的 MD5 加密逻辑。

    完整脚本如下(已兼容新旧版本):

    // 1. 获取上一个响应(/captchaImage)的JSON数据 const responseJson = pm.response.json(); const uuid = responseJson.uuid; // 2. 从响应头或响应体中提取 salt let salt = ""; const subjectHeader = pm.response.headers.get("X-Subject"); if (subjectHeader) { salt = subjectHeader; } else if (responseJson.salt) { salt = responseJson.salt; } // 3. 获取明文密码 const plainPassword = pm.environment.get("loginPassword") || "admin123"; // 4. 执行双重MD5:md5(md5(plain) + salt) const CryptoJS = require("crypto-js"); const md5Plain = CryptoJS.MD5(plainPassword).toString(); const finalPassword = CryptoJS.MD5(md5Plain + salt).toString(); // 5. 将关键参数存入环境变量,供Body表单使用 pm.environment.set("captchaUuid", uuid); pm.environment.set("encryptedPassword", finalPassword);

    关键细节:CryptoJS是 Postman 内置的库,无需额外安装。pm.response.headers.get()是获取响应头的标准方法。这里我们把uuid和加密后的password都存为环境变量,这样在/login的 Body 中,就可以用{{captchaUuid}}{{encryptedPassword}}来引用,彻底解耦了数据和逻辑。

3.2 Tests 脚本:不只是断言,更是状态流转的“决策引擎”

Tests 脚本的作用,远不止于pm.test("Status code is 200", function () { pm.response.to.have.status(200); });。它是整个自动化流程的“大脑”,负责根据当前响应,决定下一步做什么、记录什么、报错什么。

对于/captchaImage的 Tests:

// 1. 断言基础状态 pm.test("Response is OK", function () { pm.response.to.have.status(200); }); // 2. 解析JSON响应,提取uuid并存入环境变量(供后续使用) const jsonData = pm.response.json(); pm.environment.set("captchaUuid", jsonData.uuid); // 3. 检查响应头中是否存在X-Subject,存在则存为salt const subject = pm.response.headers.get("X-Subject"); if (subject) { pm.environment.set("captchaSalt", subject); } else { // 如果没有X-Subject,尝试从响应体取salt字段(兼容老版本) if (jsonData.salt) { pm.environment.set("captchaSalt", jsonData.salt); } }

对于/login的 Tests,它肩负着更重的责任:

// 1. 断言HTTP状态码 pm.test("Login request returned 200", function () { pm.response.to.have.status(200); }); // 2. 解析响应JSON,检查业务码 const loginResp = pm.response.json(); pm.test("Login business code is 200", function () { pm.expect(loginResp.code).to.eql(200); }); // 3. 【关键】检查响应头中是否设置了JSESSIONID Cookie const setCookieHeader = pm.response.headers.get("Set-Cookie"); if (setCookieHeader && setCookieHeader.includes("JSESSIONID")) { // 提取JSESSIONID的值(正则匹配) const jsessionRegex = /JSESSIONID=([^;]+)/; const match = jsessionRegex.exec(setCookieHeader); if (match && match[1]) { pm.environment.set("JSESSIONID", match[1]); console.log("✅ JSESSIONID extracted: " + match[1]); } else { console.error("❌ Failed to extract JSESSIONID from Set-Cookie header"); throw new Error("JSESSIONID extraction failed"); } } else { console.error("❌ No JSESSIONID found in Set-Cookie header"); throw new Error("Login did not return JSESSIONID cookie"); } // 4. 【可选】将登录成功的token存入全局变量,供其他Collection复用 pm.globals.set("authToken", pm.environment.get("JSESSIONID"));

实操心得:throw new Error(...)是 Tests 脚本中最重要的技巧。它能让 Postman 在某一步失败时,立刻中断整个 Collection Runner 的执行,并在控制台清晰地打印出错误原因。这比一堆绿色的“Passed”更有价值。我见过太多团队的脚本,/login返回了 500,但 Tests 里只写了pm.response.to.have.status(200),结果后续所有请求都带着空 Cookie 去调用,日志里全是 401,排查起来像大海捞针。用throw主动报错,是专业脚本和“能跑就行”脚本的分水岭。

3.3 Collection Runner:让脚本从“能跑”变成“敢用”

单个请求的脚本写好,只是完成了 30%。真正的自动化价值,在于用 Collection Runner 批量、稳定、可重复地执行。针对 RuoYi 登录,我推荐一个最小但最实用的 Runner 配置:

配置项推荐值为什么
Iterations3不是越多越好。3 次足以暴露随机性问题(如验证码识别失败、网络抖动)。超过 5 次,失败大概率是脚本或环境问题,而非偶发。
Delay between iterations1000 ms给服务端留出处理时间,避免并发请求压垮本地开发环境的 Tomcat。生产环境可调至 500ms。
Data file使用 CSV 文件,包含username,password,expectedRole三列这是实现“数据驱动测试”的核心。你可以准备 admin/admin123、testuser/123456、disableduser/123456 三组数据,一次运行就能覆盖正常登录、密码错误、账号禁用三种场景。CSV 文件内容示例:
admin,admin123,admin
testuser,123456,tester
disableduser,123456,none
Environment选择你配置好的 RuoYi 环境(dev/staging)确保 baseUrl、loginPassword 等变量已正确设置。

踩坑实录:有一次,我们的脚本在本地跑得好好的,一放到 Jenkins 的 CI 流水线里就失败。排查了两个小时,最后发现是 Jenkins Agent 的系统时间比 RuoYi 服务器快了 3 分钟,导致/captchaImage?timestamp=xxx的请求被服务端认为是“未来请求”而拒绝。解决方案是在 Jenkinsfile 中加入sh 'ntpdate -s time.windows.com'同步时间。这个教训告诉我:自动化脚本的稳定性,一半在代码,一半在环境。Runner 的配置,就是你控制环境变量的第一道防线。

4. 从“能跑通”到“可维护”,必须解决的四个硬核问题

写一个能跑通的脚本,可能只需要半小时。但写一个能在团队里用半年、经得起 RuoYi 升级、能无缝接入 CI/CD 的脚本,需要解决四个绕不开的硬核问题。这些问题不解决,你的脚本就是一颗定时炸弹。

4.1 问题一:验证码识别——当自动化遇到“人眼挑战”

RuoYi 的验证码,默认是简单的字母数字组合,但很多企业会替换成更复杂的图形验证码(如滑块、点选文字)。Postman 本身不具备 OCR 能力。这时候,有两种务实方案:

  • 方案A(推荐,适用于开发/测试环境):后门开关。在application-dev.yml中添加配置:

    ruoyi: captcha: enabled: false # 彻底关闭验证码 # 或者启用白名单IP # whitelist: 127.0.0.1,192.168.1.100

    这样,你的自动化脚本可以跳过/captchaImage步骤,直接 POST/login。虽然牺牲了一点安全性,但在非生产环境,换取的是 100% 的脚本稳定性。这是绝大多数成熟团队的选择。

  • 方案B(适用于必须保留验证码的场景):对接第三方 OCR 服务。例如,用 Python 写一个简单的 Flask 服务,接收 base64 图片,调用百度 OCR API,返回识别结果。然后在 Postman 的 Tests 脚本中,用pm.sendRequest()调用这个服务。代码片段如下:

    const imageBase64 = pm.response.json().img.split(",")[1]; // 提取base64部分 const ocrUrl = "http://localhost:5000/ocr"; pm.sendRequest({ url: ocrUrl, method: 'POST', body: { mode: 'raw', raw: JSON.stringify({ image: imageBase64 }) } }, function (err, response) { if (err) { console.error("OCR request failed:", err); throw new Error("OCR service unavailable"); } const result = response.json(); pm.environment.set("captchaCode", result.text); });

    注意:这个方案增加了外部依赖,必须确保 OCR 服务的高可用。在 CI 环境中,建议用 Docker Compose 一键启动 OCR 服务,与测试脚本同生命周期。

4.2 问题二:密码加密算法变更——当 RuoYi 升级打乱你的脚本

RuoYi 社区活跃,版本迭代快。v4.6.x 到 v4.7.0,salt的传递方式从响应体移到了响应头。v4.8.0 又可能引入 BCrypt 替代 MD5。你的脚本如果硬编码了responseJson.salt,升级后必然崩溃。

终极解法:动态探测 + 回退机制。修改/captchaImage的 Tests 脚本,让它主动探测服务端的“认证协议版本”:

// 在 /captchaImage 的 Tests 中 const response = pm.response; let authVersion = "unknown"; // 规则1:如果存在 X-Subject 响应头,且是 UUID 格式,则为 v4.7+ if (response.headers.has("X-Subject") && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(response.headers.get("X-Subject"))) { authVersion = "v4.7+"; } // 规则2:如果响应体中有 salt 字段,且是字符串,则为 v4.6- else if (response.json().salt && typeof response.json().salt === "string") { authVersion = "v4.6-"; } // 规则3:如果两者都没有,可能是自定义版本,设为 default else { authVersion = "default"; } pm.environment.set("authVersion", authVersion); console.log("🔍 Detected auth version: " + authVersion);

然后,在/login的 Pre-request Script 中,根据authVersion环境变量,执行不同的加密逻辑分支。这样,脚本就具备了“自我进化”能力,一次编写,长期有效。

4.3 问题三:Cookie 管理混乱——当多个 Collection 抢占同一份会话

一个大型项目,往往有多个 Postman Collection:用户管理、订单管理、报表管理……它们都需要登录。如果每个 Collection 都有自己的/login请求,就会出现“会话污染”:Collection A 登录后,Collection B 的请求却带着 Collection A 的JSESSIONID,导致权限错乱。

标准解法:全局登录中心(Global Login Flow)。创建一个独立的 Collection,命名为RuoYi-Auth,里面只包含/captchaImage/login两个请求。它的 Tests 脚本,除了提取JSESSIONID,还要执行:

// 将 JSESSIONID 写入全局变量,而非环境变量 pm.globals.set("RuoYi_JSESSIONID", pm.environment.get("JSESSIONID")); // 同时,清除所有可能冲突的环境变量 pm.environment.unset("JSESSIONID");

然后,在其他所有业务 Collection 的每个请求的 Pre-request Script 中,第一行就加上:

// 强制从全局变量读取会话 pm.request.headers.add({ key: "Cookie", value: "JSESSIONID=" + pm.globals.get("RuoYi_JSESSIONID") });

这个模式的好处是:RuoYi-AuthCollection 只需运行一次,所有业务 Collection 就能共享同一个、有效的登录态。而且,当需要重新登录时,只需运行RuoYi-Auth,无需改动任何业务请求。

4.4 问题四:CI/CD 集成断点——当 Jenkins 说“找不到 Postman”**

很多团队卡在最后一步:如何让这个漂亮的 Postman 脚本,真正跑在 Jenkins 的流水线里?常见错误是直接在 Jenkinsfile 中写newman run ...,结果报错command not found: newman

可靠路径:容器化 + 显式声明依赖。不要假设 Agent 有 Newman。在 Jenkinsfile 中,用docker指令拉取官方 Newman 镜像:

stage('Run API Tests') { steps { script { docker.image('postman/newman:alpine').inside { sh 'newman run "RuoYi-Login.postman_collection.json" -e "RuoYi-Dev.postman_environment.json" --reporters cli,junit --reporter-junit-export reports/results.xml' } } } }

同时,必须将你的 Collection JSON 文件和 Environment JSON 文件,作为 Jenkins 的构建产物(Artifacts)进行归档。这样,每次运行的输入都是确定的、可追溯的。我在一家电商公司推行此方案后,API 测试的失败率从每周平均 5 次降到了 0.2 次,根本原因就是消除了“本地能跑,线上挂掉”的环境幻觉。

5. 最后分享一个“偷懒但高效”的实战技巧:用 Postman 的 Monitor 功能做健康巡检

写完脚本,别急着扔进 CI。先把它变成一个 24/7 的“系统哨兵”。Postman 的 Monitor 功能,可以让你免费(基础版足够)设置一个定时任务,比如每 15 分钟,自动运行一次完整的 RuoYi 登录流程,并将结果发送到 Slack 或邮件。

配置步骤极其简单:

  1. 在 Postman 中,右键点击你的RuoYi-LoginCollection,选择Monitor collection
  2. 创建一个新的 Monitor,选择你的RuoYi-Dev环境;
  3. 设置频率为Every 15 minutes
  4. Notifications中,勾选Send email on failure,填入运维同学的邮箱。

这个技巧的价值,在于它把“被动救火”变成了“主动预警”。上周,Monitor 在凌晨 3 点发来一封邮件:“RuoYi-Dev 登录失败”。我们立刻登录服务器,发现是数据库连接池耗尽,Tomcat 日志里满是Connection refused。如果没有 Monitor,这个问题可能要等到早上 9 点用户投诉才被发现。而有了它,我们提前 6 小时修复,零用户影响。这才是自动化测试的终极意义:不是为了证明“我能跑”,而是为了守护“它一直能跑”。

http://www.jsqmd.com/news/875333/

相关文章:

  • 计算材料学驱动新型硅光伏材料发现:进化算法与机器学习融合设计
  • ESG评分不确定性量化:多重插补与预测区间在金融风险建模中的应用
  • Bootstrap置信区间:量化模型评估不确定性的实用指南
  • 从Kaggle竞赛到业务落地:GBM特征重要性到底怎么看?用Python实战教你做模型可解释性分析
  • 83、CAN FD物理层核心差异:更高速率与更灵活的位时序
  • 机器翻译中的自校正方法:利用模型动态知识应对语义错位噪声
  • 统信UOS/麒麟KOS截图快捷键失灵?别慌,试试这个后台进程清理大法
  • 可解释AI在阿尔茨海默病诊断中的应用:多模态数据与统一评估框架
  • 84、CAN FD数据链路层革新:可变数据场长度与DLC编码
  • Android加壳技术五代演进:从动态加载到ELF加壳实战解析
  • 自适应LASSO与DK-距离:高维区间值数据的稀疏建模与金融应用
  • 量子核方法在神经元形态分类中的实战应用与性能分析
  • 85、CAN FD帧格式深度解析:控制位、CRC与填充规则变化
  • 基于高效影响函数的机器学习因果推断:原理、实现与双重稳健性
  • 贝叶斯网络:从图结构到条件独立性与概率推理
  • 量子退火优化KAN网络:从QUBO映射到快速重训练实践
  • 数据质量评估:从四大维度到开源工具,构建稳健机器学习基石的实践指南
  • 开源电力系统动态仿真器:构网型逆变器与机器学习应用深度解析
  • 86、CAN FD与传统CAN的兼容性设计:混合网络与仲裁机制
  • AdapFair:基于最优传输与归一化流的黑盒模型公平性数据预处理框架
  • Android HTTPS抓包失败原因与Network Security Config配置指南
  • 88、CAN FD在车载网络中的实际优势:带宽、延迟与吞吐量对比
  • 代理模型集合卡尔曼滤波的长期稳定性:理论与工程实践
  • 从零训练MLM与机器翻译实战:Hugging Face Transformer全流程指南
  • 医疗文本数据质量对NLP模型性能的影响:噪声容忍度与鲁棒性分析
  • FA-LR-IS算法:破解高维系统可靠性预测的维度灾难
  • 机器学习地球系统模型评估:从物理一致性到标准化框架
  • Linux服务器异常流量定位实战:从连接快照到代码溯源
  • 稀疏观测下混沌系统预测:数据同化与机器学习的性能边界
  • 符号回归在超快磁动力学研究中的应用:从数据中挖掘物理规律