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

18 KiB
Raw Blame History

登录图形验证码重构实施计划

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 中新增两个接口方法定义
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
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 (在类上添加)
@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 方法(在类中添加)
@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 方法
@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 方法
private static String formatCaptchaKey(String uuid) {
    return "captcha:" + uuid;
}
  • Step 6: 提交
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 接口方法(在类中添加)
@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
import java.util.Map;
  • Step 3: 提交
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 字段
@Schema(description = "图形验证码uuid原有字段复用为验证码标识")
private String captchaVerification;

@Schema(description = "用户输入的图形验证码", requiredMode = Schema.RequiredMode.REQUIRED,
        example = "Kp7m")
@JsonProperty("CaptchaCode")
private String captchaCode;
  • Step 2: 提交
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行

@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());
}

替换为:

@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: 提交
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 方法
// 获取图形验证码
export function getCaptcha() {
  return fetchSso({
    url: `${basurl}/sso/verify/captcha`,
    method: 'post',
  });
}
  • Step 2: 提交
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行

原始:

import { getVerify } from '@/pages/index/api/login';

改为:

// import { getVerify } from '@/pages/index/api/login'; // 滑块验证已注释
import { getCaptcha } from '@/pages/index/api/login';
  • Step 2: 在 data()loginForm 中新增 captchaCodecaptchaUuid 字段并注释掉滑块相关数据约83-100行

原始 data() 返回:

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,
  };

注释后:

// === 滑块验证相关数据(已注释,保留以便回滚)===
// 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行

原始方法注释掉后改为:

// === 滑块验证方法(已注释,保留以便回滚) ===
// 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行

原始:

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);
}

改为:

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行

原始:

beforeDestroy() {
  clearInterval(this.intervalTimer);
}

改为:

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行

原始:

if (!this.confirmSuccess) {
  MessagePlugin.info({
    content: '请先完成滑块验证',
    duration: 1000,
  });
  return;
}

改为:

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行

原始:

<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>

改为:

<!-- 滑块验证区域(已注释,保留以便回滚) -->
<!--
<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行后添加

/* === 滑块验证样式(已注释,保留以便回滚) ===
.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: 提交
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: 提交

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: 提交全部改动

git add -A
git status