txw/docs/superpowers/plans/2026-04-15-login-captcha-plan.md

689 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 登录图形验证码重构实施计划
> **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<String> getVerifyToken(String remoteId);
Boolean checkVerifyToken(String verifyToken);
// === 新增图形验证码方法 ===
/**
* 生成图形验证码返回验证码图片base64和uuid
* @param remoteId IP+UserAgent
* @return { uuid, imageBase64 }
*/
CommonResult<Map<String, String>> 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<Map<String, String>> 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());
// 存入 Rediskey = captcha:{uuid}, value = 验证码内容, TTL = 5分钟
stringRedisTemplate.opsForValue().set(
formatCaptchaKey(uuid),
code.toLowerCase(), // 忽略大小写
5,
TimeUnit.MINUTES
);
Map<String, String> 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<Map<String, String>> 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
<t-form-item name="captchaVerification" user-select:none>
<div class="drag" ref="dragDiv">
<div class="drag_bg"></div>
<div class="drag_text">{{ confirmWords }}</div>
<div
ref="moveDiv"
@mousedown="mouseDown($event)"
:class="{ handler_ok_bg: confirmSuccess }"
class="handler handler_bg"
style="position: absolute; top: 0; left: 0"
></div>
</div>
</t-form-item>
```
改为:
```html
<!-- 滑块验证区域(已注释,保留以便回滚) -->
<!--
<t-form-item name="captchaVerification" user-select:none>
<div class="drag" ref="dragDiv">
...
</div>
</t-form-item>
-->
<!-- 新增:图形验证码 -->
<t-form-item name="captchaCode">
<div class="captcha-wrapper">
<img
v-if="captchaImage"
:src="captchaImage"
@click="refreshCaptcha"
class="captcha-img"
alt="验证码"
/>
<t-input
v-model="loginForm.captchaCode"
placeholder="请输入验证码"
maxlength="4"
style="width: 120px"
@enterkey="onSubmit"
/>
<span class="captcha-refresh" @click="refreshCaptcha">刷新</span>
</div>
</t-form-item>
```
- [ ] Step 8: 新增验证码相关样式
`<style>` 末尾约349行后添加
```css
/* === 滑块验证样式(已注释,保留以便回滚) ===
.drag { ... }
.handler { ... }
...
*/
/* 新增:图形验证码样式 */
.captcha-wrapper {
display: flex;
align-items: center;
gap: 12px;
width: 350px;
height: 40px;
}
.captcha-img {
width: 120px;
height: 40px;
cursor: pointer;
border-radius: 4px;
border: 1px solid #ccc;
}
.captcha-refresh {
color: #0052d9;
cursor: pointer;
font-size: 14px;
user-select: none;
}
.captcha-refresh:hover {
text-decoration: underline;
}
```
- [ ] Step 9: 提交
```bash
git add txw-mhzc-web/src/pages/index/views/login/components/login/passwordlogin.vue
git commit -m "refactor(mhzc): 登录页滑块验证改为图形验证码(原代码注释保留)"
```
---
## Task 8: 前端 - phonelogin.vue 重构
**Files:**
- Modify: `txw-mhzc-web/src/pages/index/views/login/components/login/phonelogin.vue`
**Steps:**
- 参照 Task 7 对 `phonelogin.vue` 进行相同改动(注释滑块,新增图形验证码)
**主要改动点:**
1. import `getCaptcha` 替代 `getVerify`
2. 注释 `confirmSuccess` 相关逻辑
3. `onSubmit` 中验证码非空校验
4. `handleCounter`(发送短信前)验证码检查
5. 模板替换 `.drag` 区域
6. `mounted()` 中初始化验证码
7. 样式新增 `.captcha-wrapper`
- [ ] Step 1: 执行 phonelogin.vue 的上述改动
- [ ] Step 2: 提交
```bash
git add txw-mhzc-web/src/pages/index/views/login/components/login/phonelogin.vue
git commit -m "refactor(mhzc): 短信登录页滑块验证改为图形验证码(原代码注释保留)"
```
---
## Task 9: 整体测试
**Steps:**
- [ ] Step 1: 启动 SSO 服务和前端项目
- [ ] Step 2: 访问登录页,验证验证码图片正确显示
- [ ] Step 3: 点击刷新,验证验证码更换
- [ ] Step 4: 输入正确账号密码 + 正确验证码 → 登录成功
- [ ] Step 5: 输入错误验证码 → 登录失败,提示"验证码错误"
- [ ] Step 6: 验证码5分钟过期后使用 → 提示"验证码已过期"
- [ ] Step 7: 短信登录流程测试(获取验证码 → 填入 → 登录)
- [ ] Step 8: 确认滑块代码注释后页面正常,功能不受影响
---
## Task 10: 提交全部改动
```bash
git add -A
git status
```