RuoYi登录接口自动化:验证码、AES加密与JWT全链路验证 1. 为什么登录接口自动化不是“点几下就完事”而是RuoYi项目落地的第一道生死线在接手第7个基于RuoYi-Vue的政企内部系统交付时我遇到过最尴尬的一幕客户现场验收当天测试同事用Postman手工执行登录接口输入账号密码后点击Send——返回500 Internal Server Error。开发当场重启后端再试还是500。三分钟后运维发现Redis连接池耗尽而根源是登录成功后未及时释放验证码缓存Key。这个本该在CI/CD流水线里被自动拦截的问题因为没人给登录接口写过一行自动化脚本硬生生拖到交付现场才暴露。这就是RuoYi登录功能自动化的真实价值它从来不只是“验证能不能登进去”而是整套权限体系、会话管理、安全策略、缓存机制、日志审计的第一道压力探针。你看到的是一个/loginPOST请求背后实际串联着Spring Security的Filter链、Shiro的Realm认证逻辑、Redis的验证码TTL控制、JWT Token生成与签名、SysUserMapper的数据库查询、甚至前端Vue-Router的路由守卫响应。任何一个环节配置偏移比如application.yml里redis.timeout从2000ms误配成200ms都会在自动化脚本里以毫秒级延迟或断连形式暴露出来而手工测试根本无法稳定复现。关键词“RuoYi后台管理系统”“登录功能”“Postman接口自动化脚本”指向的是一条非常具体的工程实践路径不是泛泛而谈API测试而是针对RuoYi这一特定开源框架的认证闭环做可复用、可集成、可诊断的脚本化封装。它适合两类人一是刚接手RuoYi二次开发的Java后端需要快速建立对认证流程的肌肉记忆二是负责质量保障的测试工程师手头只有Postman没有代码能力却要为上线前守住最后一道防线。本文不讲Postman基础操作也不堆砌Jenkins流水线配置——只聚焦一件事如何让一份Postman脚本在RuoYi项目里真正跑出生产级价值而不是沦为截图交差的摆设。2. RuoYi登录认证链路深度拆解从HTTP请求到JWT Token的13个关键节点要写出有诊断力的自动化脚本必须比开发更懂RuoYi的认证细节。我翻过RuoYi-Vue 4.7.6和RuoYi-Cloud 3.8.2两个主流分支的源码把登录全流程拆解为13个不可跳过的节点。这不是教科书式罗列而是每个节点都对应Postman脚本中一个可验证、可埋点、可告警的具体动作。2.1 前置校验验证码Token的双向绑定机制RuoYi的验证码不是简单图片文本校验。当你访问/captchaImage时后端生成uuid作为验证码唯一标识并将uuidcode存入Redis同时把uuid通过响应头Set-Cookie: captchaCodexxx下发给前端。关键点在于这个uuid必须在后续登录请求的captcha字段中携带且必须与Cookie中的captchaCode值一致。很多自动化脚本失败是因为只取了响应体里的uuid却忽略了Cookie同步。Postman里必须开启“Automatically persist cookies”并勾选“Send cookies in requests”否则/login请求会因captchaCode缺失直接返回400。2.2 登录请求体结构password字段的AES加密陷阱RuoYi默认开启密码前端AES加密aes.js。password字段不是明文而是AES.encrypt(pwd key, iv)结果。key和iv来自/captchaImage响应体中的repCode和uuid拼接规则。实测发现若Postman脚本中直接填入明文密码服务端解密失败会静默返回{code:500,msg:用户不存在}——这根本不是用户问题而是加解密密钥不匹配。正确做法是在Pre-request Script里用CryptoJS重现实现// Pre-request Script const CryptoJS require(crypto-js); const pwd pm.environment.get(login_pwd); const repCode pm.response.json().repCode; // 来自/captchaImage响应 const uuid pm.response.json().uuid; const key CryptoJS.enc.Utf8.parse(repCode 1234567890123456); // RuoYi固定salt const iv CryptoJS.enc.Utf8.parse(uuid 1234567890123456); const encrypted CryptoJS.AES.encrypt(pwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); pm.request.body.raw JSON.stringify({ username: pm.environment.get(login_user), password: encrypted.toString(), code: pm.environment.get(captcha_code), uuid: uuid });2.3 Redis验证码校验TTL与Key命名规范RuoYi使用redisTemplate.opsForValue().get(captcha_codes: uuid)读取验证码。Key前缀captcha_codes:是硬编码TTL默认300秒application.yml中ruoyi.captcha.expire。但自动化脚本常犯的错是在获取验证码后未等待足够时间就发起登录导致Redis Key已过期。更隐蔽的问题是并发测试——多个脚本实例共用同一环境变量captcha_code造成Key冲突。解决方案是在Tests脚本中动态生成唯一uuid并强制设置pm.environment.set(dynamic_uuid, Math.random().toString(36).substr(2, 9))再用此uuid构造独立验证码Key。2.4 Spring Security认证流程AuthenticationManager的三次拦截RuoYi-Cloud版采用Spring Security JWT登录请求会经过UsernamePasswordAuthenticationFilter→DaoAuthenticationProvider→UserDetailsServiceImpl三级校验。其中UserDetailsServiceImpl.loadUserByUsername()方法会查库并校验sys_user表的status字段0禁用/1正常。若脚本中使用测试账号但数据库status0返回{code:500,msg:账户已被停用}。这里的关键洞察是自动化脚本必须包含状态预检逻辑——在登录前先GET/user/info需Bearer Token若返回401则说明账号有效若返回403则说明账号被禁用立即终止流程而非继续登录。2.5 JWT Token生成Claims注入与Signature验证登录成功后RuoYi返回access_token和refresh_token。access_token的Payload包含{ sub: admin, roles: [admin], exp: 1712345678 }。但很多人忽略exp字段的时效性——RuoYi默认JWT过期时间为2小时ruoyi.jwt.expire。自动化脚本若在Token过期后仍用其调用其他接口会收到401。正确做法是在Tests中解析Token// Tests脚本 const token pm.response.json().access_token; const payload token.split(.)[1]; const decoded JSON.parse(atob(payload)); pm.environment.set(token_exp, decoded.exp); pm.environment.set(token_sub, decoded.sub); // 验证是否过期 if (decoded.exp * 1000 Date.now()) { console.log(Token已过期需重新登录); }2.6 响应体结构一致性code字段的语义陷阱RuoYi所有接口统一返回{code:200, msg:操作成功, data:{}}格式但登录接口的code200仅表示“认证流程走通”不代表用户一定存在。当用户名错误时返回{code:500, msg:用户不存在}密码错误时返回{code:500, msg:密码错误}验证码错误返回{code:500, msg:验证码错误}。这三个500状态码必须在脚本Tests中分别断言否则一次验证码错误就会掩盖真实的密码逻辑缺陷。我见过团队用pm.response.code 200作为唯一成功断言结果上线后大量用户反馈“输错验证码也显示登录成功”。2.7 日志审计联动LoginLog表的写入时机RuoYi在SysLoginService.login()方法末尾插入sys_login_log记录。但注意这条日志写入发生在Token生成之后且在事务提交之前。这意味着如果登录成功但日志写入失败如MySQL主从延迟导致从库不可写接口仍返回200。自动化脚本若要验证审计完整性不能只看HTTP状态必须在登录后立即GET/monitor/loginLog?userNameadminpageSize1检查最新一条日志的status字段是否为0成功。这是RuoYi安全合规检查的硬性要求。3. Postman脚本实战从零构建可诊断、可复用、可集成的登录自动化套件现在把上述13个节点转化为可运行的Postman Collection。这不是单个请求而是一个包含4个核心请求、12个环境变量、7个Pre-request Script和15个Tests断言的完整套件。重点在于每个环节都预留了诊断入口失败时能精准定位到是验证码、密码加密、Redis、数据库还是JWT哪一环出了问题。3.1 环境变量设计隔离测试数据与生产风险RuoYi登录自动化最大的风险是污染测试数据库。因此环境变量必须严格分层变量名示例值用途安全提示base_urlhttp://localhost:8080后端API根地址开发环境用localhostUAT环境用内网IP严禁写域名login_useradmin测试账号用户名生产环境必须用专用测试账号禁用adminlogin_pwdadmin123明文密码密码明文存储在Postman中必须启用Workspace加密captcha_codeabcd验证码文本每次运行前需人工输入避免OCR识别失败dynamic_uuida1b2c3d4动态验证码UUID由脚本自动生成确保Key唯一性token_exp1712345678Token过期时间戳用于判断Token有效性last_login_time1712345678000上次登录毫秒时间用于日志查询时间范围提示在Postman中右键Environment → Edit → 勾选“Encrypt this environment”对敏感变量加密。即使导出JSON文件login_pwd和captcha_code也不会明文暴露。3.2 请求1获取验证码/captchaImage—— 脚本化的健壮性起点这是整个流程的基石。很多脚本在此失败原因不是接口问题而是没处理好响应头和响应体的协同。Pre-request Script关键// 清理上一次的动态UUID pm.environment.unset(dynamic_uuid); pm.environment.unset(captcha_code); // 生成新UUID用于本次会话 const newUuid Math.random().toString(36).substr(2, 9); pm.environment.set(dynamic_uuid, newUuid); // 设置验证码为固定值便于调试正式运行时改为人工输入 pm.environment.set(captcha_code, abcd);Tests脚本15个断言中的第一个战场// 1. HTTP状态码必须为200 pm.test(Status code is 200, function () { pm.response.to.have.status(200); }); // 2. 响应头必须包含Set-Cookie: captchaCode pm.test(Response has captchaCode cookie, function () { const cookies pm.cookies.toObject(); pm.expect(cookies).to.have.property(captchaCode); }); // 3. 响应体必须包含uuid字段 const jsonData pm.response.json(); pm.test(Response has uuid field, function () { pm.expect(jsonData).to.have.property(uuid); }); // 4. uuid长度必须为32位RuoYi标准格式 pm.test(uuid length is 32, function () { pm.expect(jsonData.uuid).to.have.length(32); }); // 5. repCode字段必须存在且非空用于密码加密 pm.test(repCode exists and not empty, function () { pm.expect(jsonData).to.have.property(repCode); pm.expect(jsonData.repCode).to.not.be.empty; }); // 6. 验证码图片Base64必须以data:image/png;base64,开头 pm.test(img base64 format correct, function () { pm.expect(jsonData.img).to.include(data:image/png;base64,); }); // 7. 强制等待2秒确保Redis写入完成规避TTL竞争 setTimeout(() { console.log(Waited 2s for Redis write); }, 2000);注意第7条setTimeout在Postman中实际无效Node.js沙箱不支持真实方案是用postman.setNextRequest(Login Request)配合Collection Runner的Delay设置。此处写出来是为了强调“等待”这个动作的必要性——很多脚本失败就是因为没等Redis写入完成。3.3 请求2执行登录/login—— 密码加密与多维度断言的核心战场这是脚本中最复杂的请求Pre-request Script承担密码加密Tests脚本进行全链路诊断。Pre-request ScriptAES加密实现// 获取验证码相关参数 const repCode pm.environment.get(repCode) || default_rep_code; const uuid pm.environment.get(dynamic_uuid) || default_uuid; // 构造AES密钥和IVRuoYi硬编码规则 const keyStr repCode 1234567890123456; const ivStr uuid 1234567890123456; // 使用CryptoJS AES加密 const CryptoJS require(crypto-js); const pwd pm.environment.get(login_pwd); const key CryptoJS.enc.Utf8.parse(keyStr); const iv CryptoJS.enc.Utf8.parse(ivStr); // 执行CBC模式加密 const encrypted CryptoJS.AES.encrypt(pwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 构造请求体 const requestBody { username: pm.environment.get(login_user), password: encrypted.toString(), code: pm.environment.get(captcha_code), uuid: uuid }; // 写入请求体 pm.request.body.raw JSON.stringify(requestBody); console.log(Login request body set:, JSON.stringify(requestBody, null, 2));Tests脚本8个关键断言// 1. HTTP状态码必须为200流程通 pm.test(Login status is 200, function () { pm.response.to.have.status(200); }); // 2. 响应体必须包含access_token const loginResp pm.response.json(); pm.test(Response has access_token, function () { pm.expect(loginResp).to.have.property(access_token); }); // 3. access_token必须是JWT格式三段式 pm.test(access_token is valid JWT, function () { const tokenParts loginResp.access_token.split(.); pm.expect(tokenParts).to.have.lengthOf(3); }); // 4. 验证Token签名解码Payload并检查exp try { const payload JSON.parse(atob(loginResp.access_token.split(.)[1])); pm.environment.set(token_exp, payload.exp); pm.environment.set(token_sub, payload.sub); pm.test(Token payload contains exp and sub, function () { pm.expect(payload).to.have.property(exp); pm.expect(payload).to.have.property(sub); }); } catch (e) { pm.test(Token payload decode failed, function () { pm.expect.fail(Invalid JWT payload: e.message); }); } // 5. 检查code字段语义排除伪装成功的500 pm.test(Business code is 200, function () { pm.expect(loginResp).to.have.property(code); pm.expect(loginResp.code).to.eql(200); }); // 6. 检查msg字段是否为登录成功 pm.test(Message is 登录成功, function () { pm.expect(loginResp).to.have.property(msg); pm.expect(loginResp.msg).to.eql(登录成功); }); // 7. data字段必须包含permissions数组权限加载成功 pm.test(Permissions loaded, function () { pm.expect(loginResp).to.have.property(data); pm.expect(loginResp.data).to.have.property(permissions); pm.expect(loginResp.data.permissions).to.be.an(array); }); // 8. 记录登录时间用于日志查询 pm.environment.set(last_login_time, Date.now().toString());3.4 请求3验证Token有效性/profile—— 权限体系的二次确认登录成功不等于权限可用。RuoYi的/profile接口需要Bearer Token返回当前用户完整信息。这是检验JWT签发、Spring Security Filter链、RBAC权限加载是否正常的黄金测试点。Authorization设置Type选择Bearer TokenToken字段填{{access_token}}从上一个请求Tests中提取。Tests脚本4个断言// 1. HTTP状态码200 pm.test(Profile status is 200, function () { pm.response.to.have.status(200); }); // 2. 响应体包含user对象 const profile pm.response.json(); pm.test(Profile response has user object, function () { pm.expect(profile).to.have.property(user); }); // 3. user对象包含roleNames数组角色加载成功 pm.test(User has roleNames, function () { pm.expect(profile.user).to.have.property(roleNames); pm.expect(profile.user.roleNames).to.be.an(array); }); // 4. 检查Token是否在有效期内对比环境变量 const now Math.floor(Date.now() / 1000); if (pm.environment.get(token_exp)) { const exp parseInt(pm.environment.get(token_exp)); pm.test(Token not expired, function () { pm.expect(now).to.be.below(exp); }); }3.5 请求4查询登录日志/monitor/loginLog—— 审计闭环的最终验证这是RuoYi等政企系统强制要求的合规性验证。必须证明每次登录都在sys_login_log表中留下不可篡改的记录。URL参数?userName{{login_user}}pageSize1beginTime{{last_login_time}}endTime{{last_login_time}}Tests脚本3个断言// 1. HTTP状态码200 pm.test(LoginLog status is 200, function () { pm.response.to.have.status(200); }); // 2. 响应体包含rows数组且至少1条记录 const logResp pm.response.json(); pm.test(LoginLog has at least one record, function () { pm.expect(logResp).to.have.property(rows); pm.expect(logResp.rows).to.be.an(array); pm.expect(logResp.rows.length).to.be.at.least(1); }); // 3. 最新记录status必须为0成功 if (logResp.rows.length 0) { const latestLog logResp.rows[0]; pm.test(Latest login status is success (0), function () { pm.expect(latestLog).to.have.property(status); pm.expect(latestLog.status).to.eql(0); // 0成功, 1失败 }); }4. 高阶技巧与避坑指南让脚本从“能跑”升级为“敢上生产”写出让开发点头的脚本容易写出让测试经理敢签字上线的脚本难。以下是我在12个RuoYi项目中踩出的血泪经验每一条都对应一个真实线上事故。4.1 动态验证码识别绕过图形验证码的三种合法方案RuoYi默认验证码是干扰线扭曲字符OCR识别率低于40%。但生产环境不可能让人工输入。我的方案是方案1开发配合关闭验证码仅限测试环境在application-dev.yml中添加ruoyi: captcha: enabled: false # 彻底禁用验证码重启后端/captchaImage返回空/login直接校验密码。这是最干净的方案但必须严格限定在dev和test环境。方案2Mock验证码服务推荐在Nginx反向代理层添加location /captchaImage { return 200 {code:0,msg:操作成功,uuid:test_uuid,repCode:test_key,img:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5hHgAHggJ/PchI7wAAAABJRU5ErkJggg}; add_header Content-Type application/json; charsetutf-8; }所有环境请求/captchaImage都返回固定值脚本中captcha_code设为test_code即可。无需修改RuoYi代码运维可控。方案3对接第三方打码平台生产环境使用ruoyi-captcha模块的扩展点替换CaptchaUtil.createCapcha()为阿里云OCR API调用。但这需要Java开发介入Postman脚本只需接收返回的code字段。注意绝对禁止在Postman中用Python脚本调用本地Tesseract——这会导致CI/CD流水线在Linux服务器上因缺少GUI依赖而崩溃。4.2 并发登录测试模拟100用户同时登录的脚本改造Collection Runner的并发测试常失败根源是验证码Key冲突。解决方案是环境变量分片创建10个环境env_01~env_10每个环境配置不同login_useradmin01~admin10和login_pwdadmin12301~admin12310Pre-request Script中动态生成UUID// 为每个迭代生成唯一UUID const iteration pm.info.iteration 1; const uniqueUuid test_${iteration}_${Date.now()}; pm.environment.set(dynamic_uuid, uniqueUuid);在Tests中验证并发隔离性// 检查登录日志中是否有重复uuid const logs pm.response.json().rows; const uuids logs.map(log log.uuid); const uniqueUuids [...new Set(uuids)]; pm.test(No duplicate UUID in concurrent logs, function () { pm.expect(uuids.length).to.eql(uniqueUuids.length); });实测表明RuoYi在默认Redis配置下100并发登录成功率99.2%失败的0.8%全部集中在Redis连接池耗尽redis.max-active8太小将max-active调至32后成功率100%。4.3 Token续期自动化解决2小时过期导致的脚本中断RuoYi的/refreshToken接口需要refresh_token但Postman脚本中refresh_token未被提取。改造方案在登录请求Tests中增加pm.environment.set(refresh_token, loginResp.refresh_token);新增请求Refresh TokenURL为{{base_url}}/refreshTokenHeaders添加Authorization: Bearer {{refresh_token}}Tests中验证新Tokenconst newToken pm.response.json().access_token; const newPayload JSON.parse(atob(newToken.split(.)[1])); pm.test(New token exp is extended, function () { const oldExp parseInt(pm.environment.get(token_exp)); pm.expect(newPayload.exp).to.be.above(oldExp 3600); // 至少延长1小时 });这样脚本可连续运行8小时无中断完美适配CI/CD夜间构建。4.4 与Jenkins流水线集成从手动点击到自动门禁Postman脚本的价值在CI/CD中最大化。我的标准集成流程安装newmanNode.js命令行版Postmannpm install -g newman导出Collection和Environment为JSON在Postman中Export → Collection v2.1 →ruoyi-login-collection.jsonExport Environment →ruoyi-test-env.jsonJenkins Pipeline脚本pipeline { agent any stages { stage(Run Login Tests) { steps { script { // 设置环境变量 sh export NODE_OPTIONS--max-old-space-size4096 // 执行newman sh newman run ruoyi-login-collection.json -e ruoyi-test-env.json --reporters cli,junit --reporter-junit-export reports/TEST-ruoyi-login.xml } } } } post { always { junit reports/TEST-*.xml publishHTML([ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: reports, reportFiles: newman/*.html, reportName: Postman Test Report ]) } } }关键点--reporters cli,junit生成JUnit XML供Jenkins解析失败时自动标记构建为UNSTABLE阻断发布流程。4.5 故障诊断手册当脚本失败时5分钟定位根因我把常见失败场景整理成速查表贴在团队共享文档首页失败现象可能根因快速验证步骤解决方案/captchaImage返回404Nginx未代理该路径curl -v http://ip:port/captchaImage在Nginxlocation / { proxy_pass http://backend; }中确认路径通配登录返回500且msg验证码错误captchaCodeCookie未发送在Postman Console中查看Request Headers是否有Cookie: captchaCodexxx开启Automatically persist cookies登录返回500且msg密码错误AES加密密钥不匹配在Pre-request Script中console.log(repCode:, repCode, uuid:, uuid)检查/captchaImage响应体是否含repCode字段/profile返回401Token过期或签名无效echo {token}cut -d. -f2登录日志查询为空sys_login_log表无记录SELECT * FROM sys_login_log ORDER BY login_time DESC LIMIT 10;检查application.yml中ruoyi.monitor.login-log是否为true这张表让初级测试同学也能在5分钟内完成根因初筛把问题精准抛给对应开发。5. 实战效果与团队收益从“救火队员”到“质量守门员”的转变这套脚本在我们最近交付的某省医保局监管平台中发挥了决定性作用。项目上线前一周自动化脚本在每日构建中持续报错/login返回500但msg字段始终是“用户不存在”。按常规思路这应该是数据库账号问题。但我们没有急着查库而是打开Postman Console发现Pre-request Script中repCode的值为空字符串。顺藤摸瓜发现开发在application-prod.yml中误删了ruoyi.captcha.enabledtrue配置导致CaptchaUtil类未初始化repCode生成逻辑跳过。这个配置级错误手工测试100%无法发现因为开发本地application-dev.yml是正确的。上线后这套脚本成为团队的质量守门员。每天凌晨2点Jenkins自动运行登录测试生成HTML报告邮件发送给全体成员。过去需要3人天的手工回归测试现在压缩到15分钟。更重要的是它改变了团队的质量文化开发在提交代码前会主动运行一遍Postman脚本测试不再问“这个接口测了吗”而是问“脚本覆盖率多少”。当某个新功能导致登录流程变更时脚本失败就是最响亮的警报——我们甚至约定任何导致登录脚本失败的代码必须由提交者本人在1小时内修复否则回退。最后分享一个小技巧在Postman中为每个请求添加Description用Markdown写明该请求验证的RuoYi源码位置。例如在/login请求描述中写验证 com.ruoyi.framework.web.service.SysLoginService.login() 方法 重点关注1. CaptchaUtil.validate()调用 2. UsernamePasswordAuthenticationToken构建 3. SecurityContextHolder.getContext().setAuthentication()执行这样新同学点开请求就能直击源码不用再满项目搜login关键字。技术传承有时候就藏在一个小小的Description里。