# scaffold 项目之登陆验证码

# 简介

项目基于 AJ-Captcha 实现行为验证码,包含滑动拼图、文字点选两种方式,UI 支持弹出和嵌入两种方式。

疑问:为什么采用行为验证码?

相比传统的「传统字符型验证码」的 “展示验证码 - 填写字符 - 比对答案” 的流程来说,「行为验证码」的 “展示验证码 - 操作 - 比对答案” 的流程,用户只需要使用鼠标产生指定的行为轨迹,不需要键盘手动输入,用户体验更好,更加难以被机器识别,更加安全可靠。

# 开始使用

# 实现原理

步骤:

  1. 用户访问应用页面,请求显示行为验证码

  2. 用户按照提示要求完成验证码拼图 / 点击

  3. 用户提交表单,前端将第二步的输出一同提交到后台

  4. 验证数据随表单提交到后台后,后台需要调用 captchaService.verification 做二次校验

  5. 第 4 步返回校验通过 / 失败到产品应用后端,再返回到前端

# 如何关闭验证码

管理后台的登录界面,默认开启验证码。如果需要关闭验证码,操作如下:

  1. 后端的 application-local.yaml 配置文件中,将 scaffold.captcha.enable 设置为 false

  2. 前端 scaffold-ui-admin-vue3 项目,将环境对应的 .env 配置文件中,将 VITE_APP_CAPTCHA_ENABLE 设置为 false

ps:如果你不知道环境对应的 .env 配置文件是哪个,就全部改成 false 吧!

# 场景接入

# 后端接入

scaffold-module-system-biz 模块,默认在 pom.xml 已经引入 spring-boot-starter-captcha-plus 依赖,代码如下:

<dependency>
    <groupId>com.tz.boot</groupId>
    <artifactId>scaffold-spring-boot-starter-captcha</artifactId>
</dependency>

验证码的配置,在 application.yaml 配置文件中,配置项如下:

aj:
  captcha:
    jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
    pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径
    cache-type: redis # 缓存 local/redis...
    cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存
    timing-clear: 180 # local 定时清除过期缓存 (单位秒), 设置为 0 代表不执行
    type: blockPuzzle # 验证码类型 default 两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选
    water-mark: 芋道源码 # 右下角水印文字 (我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode
    interference-options: 0 # 滑动干扰项 (0/1/2)
    req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false
    req-get-lock-limit: 5 # 验证失败 5 次,get 接口锁定
    req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔
    req-get-minute-limit: 30 # get 接口一分钟内请求数限制
    req-check-minute-limit: 60 # check 接口一分钟内请求数限制
    req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制

如果你想修改验证码的 图片,修改 resources/images 目录即可。

验证码的使用例子: CaptchaController.java

package com.tz.scaffold.module.system.controller.admin.captcha;
import cn.hutool.core.util.StrUtil;
import com.tz.scaffold.framework.common.util.servlet.ServletUtils;
import com.tz.scaffold.framework.operatelog.core.annotations.OperateLog;
import com.xingyuv.captcha.model.common.ResponseModel;
import com.xingyuv.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
/**
 * <p> Project: scaffold - CaptchaController </p>
 *
 * 验证码
 * @author Tz
 * @date 2024/01/09 23:45
 * @version 1.0.0
 * @since 1.0.0
 */
@Tag(name = "管理后台 - 验证码")
@RestController("adminCaptchaController")
@RequestMapping("/system/captcha")
public class CaptchaController {
    @Resource
    private CaptchaService captchaService;
    @PostMapping({"/get"})
    @Operation(summary = "获得验证码")
    @PermitAll
    @OperateLog(enable = false)
    public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
        assert request.getRemoteHost() != null;
        data.setBrowserInfo(getRemoteId(request));
        return captchaService.get(data);
    }
    @PostMapping("/check")
    @Operation(summary = "校验验证码")
    @PermitAll
    @OperateLog(enable = false)
    public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
        data.setBrowserInfo(getRemoteId(request));
        return captchaService.check(data);
    }
    public static String getRemoteId(HttpServletRequest request) {
        String ip = ServletUtils.getClientIP(request);
        String ua = request.getHeader("user-agent");
        if (StrUtil.isNotBlank(ip)) {
            return ip + ua;
        }
        return request.getRemoteAddr() + ua;
    }
}