diff --git a/docs/superpowers/plans/2026-04-15-login-captcha-plan.md b/docs/superpowers/plans/2026-04-15-login-captcha-plan.md new file mode 100644 index 0000000..667f375 --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-login-captcha-plan.md @@ -0,0 +1,688 @@ +# 登录图形验证码重构实施计划 + +> **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: 新增验证码相关样式 + +在 `