# 登录图形验证码重构实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan. **Goal:** 将登录页滑块验证改为数字+字母图形验证码,前端注释保留原滑块代码不删除。 **Architecture:** - 后端新增 `/verify/captcha` 接口生成验证码图片(base64)和uuid,存入Redis - 登录接口新增 `captchaCode` 参数,与uuid联合校验 - 前端新增验证码图片展示和输入框,注释掉滑块相关代码 **Tech Stack:** Hutool CaptchaUtil (已有 hutool 依赖), Redis, Vue.js --- ## 文件改动总览 | 文件 | 操作 | |------|------| | `VerifyService.java` | 修改:新增 `getCaptcha()` 和 `checkCaptcha()` 接口 | | `VerifyServiceImpl.java` | 修改:实现图形验证码生成和校验逻辑 | | `VerifyController.java` | 修改:新增 `/verify/captcha` 接口 | | `AuthLoginReqVO.java` | 修改:新增 `captchaCode` 字段 | | `AuthServiceImpl.java` | 修改:`login()` 方法新增验证码校验 | | `login.js` | 修改:新增 `getCaptcha()` API | | `passwordlogin.vue` | 修改:注释滑块,新增验证码组件 | | `phonelogin.vue` | 修改:注释滑块,新增验证码组件 | --- ## Task 1: 后端 - VerifyService 接口定义 **Files:** - Modify: `txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/service/verify/VerifyService.java` **Steps:** - [ ] Step 1: 在 `VerifyService.java` 中新增两个接口方法定义 ```java package com.css.txw.sso.service.verify; import com.css.ggzc.framework.common.pojo.CommonResult; public interface VerifyService { // === 原有方法保留 === CommonResult getVerifyToken(String remoteId); Boolean checkVerifyToken(String verifyToken); // === 新增图形验证码方法 === /** * 生成图形验证码,返回验证码图片base64和uuid * @param remoteId IP+UserAgent * @return { uuid, imageBase64 } */ CommonResult> getCaptcha(String remoteId); /** * 校验图形验证码 * @param uuid 验证码唯一标识 * @param code 用户输入的验证码 * @return true=校验通过 */ Boolean checkCaptcha(String uuid, String code); } ``` --- ## Task 2: 后端 - VerifyServiceImpl 实现 **Files:** - Modify: `txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/service/verify/VerifyServiceImpl.java` **Steps:** - [ ] Step 1: 添加必要的 import ```java import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.ICaptcha; import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.core.img.Img; import cn.hutool.core.img QRCodeUtil; import org.springframework.data.redis.core.StringRedisTemplate; import javax.imageio.ImageIO; import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; ``` - [ ] Step 2: 新增 Captcha 生成器 Bean (在类上添加) ```java @Bean public ICaptcha lineCaptcha() { // 自定义验证码内容为4位数字+字母 RandomGenerator randomGenerator = new RandomGenerator("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 4); // 使用 hutool 的 CircleCaptcha,验证码图片 120x40,带干扰线 cn.hutool.captcha.CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(120, 40, 4, 20); captcha.setGenerator(randomGenerator); return captcha; } ``` - [ ] Step 3: 实现 `getCaptcha` 方法(在类中添加) ```java @Resource private ICaptcha lineCaptcha; @Override public CommonResult> getCaptcha(String remoteId) { // 生成验证码内容 String code = lineCaptcha.getCode(); String uuid = IdUtil.fastSimpleUUID(); // 生成图片 base64 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ImageIO.write(lineCaptcha.getImage(), "png", baos); } catch (IOException e) { throw new RuntimeException("生成验证码图片失败", e); } String imageBase64 = "data:image/png;base64," + Base64.getEncoder().encodeToString(baos.toByteArray()); // 存入 Redis,key = captcha:{uuid}, value = 验证码内容, TTL = 5分钟 stringRedisTemplate.opsForValue().set( formatCaptchaKey(uuid), code.toLowerCase(), // 忽略大小写 5, TimeUnit.MINUTES ); Map result = new HashMap<>(); result.put("uuid", uuid); result.put("imageBase64", imageBase64); return CommonResult.success(result); } ``` - [ ] Step 4: 实现 `checkCaptcha` 方法 ```java @Override public Boolean checkCaptcha(String uuid, String code) { if (!this.ssoProperties.isLoginCaptcha()) { return true; } if (StringUtils.isBlank(uuid) || StringUtils.isBlank(code)) { return false; } String key = formatCaptchaKey(uuid); String cachedCode = stringRedisTemplate.opsForValue().get(key); if (cachedCode == null) { return false; } // 校验成功后删除验证码(一次性) stringRedisTemplate.delete(key); return cachedCode.equalsIgnoreCase(code); } ``` - [ ] Step 5: 添加 format 方法 ```java private static String formatCaptchaKey(String uuid) { return "captcha:" + uuid; } ``` - [ ] Step 6: 提交 ```bash git add txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/service/verify/VerifyServiceImpl.java git commit -m "feat(sso): 实现图形验证码生成和校验逻辑" ``` --- ## Task 3: 后端 - VerifyController 新增接口 **Files:** - Modify: `txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/controller/verify/VerifyController.java` **Steps:** - [ ] Step 1: 新增 `/verify/captcha` 接口方法(在类中添加) ```java @Operation(summary = "获取图形验证码") @PermitAll @PostMapping("/captcha") public CommonResult> getCaptcha(HttpServletRequest request) { final String remoteId = getRemoteId(request); return verifyService.getCaptcha(remoteId); } ``` - [ ] Step 2: 添加 import ```java import java.util.Map; ``` - [ ] Step 3: 提交 ```bash git add txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/controller/verify/VerifyController.java git commit -m "feat(sso): 新增图形验证码获取接口 /verify/captcha" ``` --- ## Task 4: 后端 - AuthLoginReqVO 新增字段 **Files:** - Modify: `txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/pojo/vo/AuthLoginReqVO.java` **Steps:** - [ ] Step 1: 在 `captchaVerification` 字段后新增 `captchaCode` 字段 ```java @Schema(description = "图形验证码uuid(原有字段,复用为验证码标识)") private String captchaVerification; @Schema(description = "用户输入的图形验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "Kp7m") @JsonProperty("CaptchaCode") private String captchaCode; ``` - [ ] Step 2: 提交 ```bash git add txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/pojo/vo/AuthLoginReqVO.java git commit -m "feat(sso): 登录请求新增captchaCode字段" ``` --- ## Task 5: 后端 - AuthServiceImpl 登录校验改动 **Files:** - Modify: `txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/service/auth/impl/AuthServiceImpl.java` **Steps:** - [ ] Step 1: 修改 `login()` 方法,将原来的 `checkVerifyToken` 替换为新的 `checkCaptcha` 校验 找到原始代码(约在114-118行): ```java @Override public AuthLoginRespVO login(AuthLoginReqVO reqVO) { final Boolean checked = verifyService.checkVerifyToken(reqVO.getCaptchaVerification()); if (!checked) { throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); } // 使用账号密码,进行登录 YhxxbDTO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); return createTokenAfterLoginSuccess(user.getYhUuid()); } ``` 替换为: ```java @Override public AuthLoginRespVO login(AuthLoginReqVO reqVO) { // 校验图形验证码 final Boolean checked = verifyService.checkCaptcha( reqVO.getCaptchaVerification(), reqVO.getCaptchaCode() ); if (!checked) { throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); } // 使用账号密码,进行登录 YhxxbDTO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); return createTokenAfterLoginSuccess(user.getYhUuid()); } ``` - [ ] Step 2: 提交 ```bash git add txw-sso/txw-sso-service-biz/src/main/java/com/css/txw/sso/service/auth/impl/AuthServiceImpl.java git commit -m "feat(sso): 登录接口改为校验图形验证码" ``` --- ## Task 6: 前端 - login.js 新增 API **Files:** - Modify: `txw-mhzc-web/src/pages/index/api/login.js` **Steps:** - [ ] Step 1: 在文件末尾(约195行之后)新增 `getCaptcha` 方法 ```javascript // 获取图形验证码 export function getCaptcha() { return fetchSso({ url: `${basurl}/sso/verify/captcha`, method: 'post', }); } ``` - [ ] Step 2: 提交 ```bash git add txw-mhzc-web/src/pages/index/api/login.js git commit -m "feat(mhzc): 新增getCaptcha API" ``` --- ## Task 7: 前端 - passwordlogin.vue 重构 **Files:** - Modify: `txw-mhzc-web/src/pages/index/views/login/components/login/passwordlogin.vue` **Steps:** - [ ] Step 1: 注释掉 `import { getVerify }` 并改为 `import { getCaptcha }`(约68行) 原始: ```javascript import { getVerify } from '@/pages/index/api/login'; ``` 改为: ```javascript // import { getVerify } from '@/pages/index/api/login'; // 滑块验证已注释 import { getCaptcha } from '@/pages/index/api/login'; ``` - [ ] Step 2: 在 `data()` 的 `loginForm` 中新增 `captchaCode` 和 `captchaUuid` 字段,并注释掉滑块相关数据(约83-100行) 原始 `data()` 返回: ```javascript return { beginClientX: 0, mouseMoveState: false, maxWidth: '', confirmWords: '请按住滑块,拖动到最右边', confirmSuccess: false, width: 350, height: 42, textSize: '18px', FORM_RULES, loginForm: { loginType: 'password', username: '', password: '', captchaVerification: '', mobile: '', mobileCode: '', rememberMe: false, }, countDown: 0, intervalTimer: null, }; ``` 注释后: ```javascript // === 滑块验证相关数据(已注释,保留以便回滚)=== // beginClientX: 0, // mouseMoveState: false, // maxWidth: '', // confirmWords: '请按住滑块,拖动到最右边', // confirmSuccess: false, // width: 350, // height: 42, // textSize: '18px', // === 滑块验证相关数据 end === return { // captchaVerification 改为存储 uuid captchaUuid: '', captchaImage: '', captchaCode: '', FORM_RULES, loginForm: { loginType: 'password', username: '', password: '', captchaVerification: '', // 仍然保留,登录时传uuid mobile: '', mobileCode: '', rememberMe: false, }, countDown: 0, intervalTimer: null, }; ``` - [ ] Step 3: 注释掉滑块相关方法(`mouseDown`, `successFunction`, `mouseMoveFn`, `moseUpFn`),约216-260行 原始方法注释掉后改为: ```javascript // === 滑块验证方法(已注释,保留以便回滚) === // mousedown 事件 // mouseDown(e) { // if (!this.confirmSuccess) { // e.preventDefault && e.preventDefault(); // this.mouseMoveState = true; // this.beginClientX = e.clientX; // } // }, // 验证成功函数 // successFunction() { // getVerify().then((res) => { // this.loginForm.captchaVerification = res.data; // this.confirmSuccess = true; // this.confirmWords = '验证通过'; // }); // // ... (移除 mousemove/mouseup 监听器代码) // }, // mousemove事件 // mouseMoveFn(e) { // if (this.mouseMoveState) { // const width = e.clientX - this.beginClientX; // if (width > 0 && width <= this.maxWidth) { // document.getElementsByClassName('handler')[0].style.left = `${width}px`; // document.getElementsByClassName('drag_bg')[0].style.width = `${width}px`; // } else if (width > this.maxWidth) { // this.successFunction(); // } // } // }, // mouseup事件 // moseUpFn(e) { // this.mouseMoveState = false; // const width = e.clientX - this.beginClientX; // if (width < this.maxWidth) { // document.getElementsByClassName('handler')[0].style.left = `${0}px`; // document.getElementsByClassName('drag_bg')[0].style.width = `${0}px`; // } // }, // reSetSlider() { // this.confirmSuccess = false; // ... // }, // === 滑块验证方法 end === // 新增:刷新验证码 refreshCaptcha() { getCaptcha().then((res) => { this.captchaUuid = res.data.uuid; this.captchaImage = res.data.imageBase64; this.loginForm.captchaVerification = res.data.uuid; this.loginForm.captchaCode = ''; }); }, ``` - [ ] Step 4: 在 `mounted()` 中注释掉滑块初始化代码,新增验证码初始化(约278-284行) 原始: ```javascript mounted() { this.maxWidth = this.$refs.dragDiv.clientWidth - this.$refs.moveDiv.clientWidth; document.getElementsByTagName('html')[0].addEventListener('mousemove', this.mouseMoveFn); document.getElementsByTagName('html')[0].addEventListener('mouseup', this.moseUpFn); } ``` 改为: ```javascript mounted() { // === 滑块初始化(已注释) === // this.maxWidth = this.$refs.dragDiv.clientWidth - this.$refs.moveDiv.clientWidth; // document.getElementsByTagName('html')[0].addEventListener('mousemove', this.mouseMoveFn); // document.getElementsByTagName('html')[0].addEventListener('mouseup', this.moseUpFn); // 新增:初始化图形验证码 this.refreshCaptcha(); } ``` - [ ] Step 5: 在 `beforeDestroy()` 中注释掉清理代码(约105-107行) 原始: ```javascript beforeDestroy() { clearInterval(this.intervalTimer); } ``` 改为: ```javascript beforeDestroy() { clearInterval(this.intervalTimer); // === 滑块清理(已注释) === // document.getElementsByTagName('html')[0].removeEventListener('mousemove', this.mouseMoveFn); // document.getElementsByTagName('html')[0].removeEventListener('mouseup', this.moseUpFn); } ``` - [ ] Step 6: 修改 `onSubmit` 方法中的校验逻辑(约125-131行) 原始: ```javascript if (!this.confirmSuccess) { MessagePlugin.info({ content: '请先完成滑块验证', duration: 1000, }); return; } ``` 改为: ```javascript if (!this.captchaUuid) { MessagePlugin.info({ content: '请先获取验证码', duration: 1000, }); return; } if (!this.loginForm.captchaCode || this.loginForm.captchaCode.length !== 4) { MessagePlugin.info({ content: '请输入4位验证码', duration: 1000, }); return; } ``` - [ ] Step 7: 替换模板中的滑块区域为验证码组件(约36-48行) 原始: ```html
{{ confirmWords }}
``` 改为: ```html
验证码 刷新
``` - [ ] Step 8: 新增验证码相关样式 在 `