芋道源码 - 基于滑块验证码(blockPuzzle), 登录实现
本文记录了在 芋道源码框架 中,如何通过滑块验证码(blockPuzzle)实现安全的登录流程。通过调用验证码服务接口,校验验证码后再执行登录逻辑,防止暴力破解和恶意登录请求。
一、验证码配置
在 application.yml
中配置验证码参数
#################### 验证码相关配置 ####################
aj:captcha:jigsaw: classpath:images/jigsaw # 滑动验证底图路径pic-click: classpath:images/pic-click # 文字点选底图路径cache-type: redis # 缓存方式 local/rediscache-number: 1000 # local 缓存阈值timing-clear: 180 # 定时清除过期缓存时间(秒)type: blockPuzzle # 验证码类型:blockPuzzle(滑块拼图) | clickWord(文字点选)water-mark: 芋道源码 # 水印文字interference-options: 0 # 滑动干扰项req-frequency-limit-enable: false # 接口请求频率限制开关req-get-lock-limit: 5 # 验证失败次数锁定阈值req-get-lock-seconds: 10 # 锁定时间req-get-minute-limit: 30 # get 接口每分钟请求限制req-check-minute-limit: 60 # check 接口每分钟请求限制req-verify-minute-limit: 60 # verify 接口每分钟请求限制
二、验证码对象模型
CaptchaVO
package com.xingyuv.captcha.model.vo;public class CaptchaVO implements java.io.Serializable {private String captchaId;private String projectCode;private String captchaType;private String captchaOriginalPath;private String captchaFontType;private Integer captchaFontSize;private String secretKey;private String originalImageBase64;private PointVO point;private String jigsawImageBase64;private java.util.List<String> wordList;private java.util.List<java.awt.Point> pointList;private String pointJson;private String token;private Boolean result;private String captchaVerification;private String clientUid;private Long ts;private String browserInfo;
}
PointVO
package com.xingyuv.captcha.model.vo;public class PointVO {private String secretKey;public int x;public int y;
}
三、验证码接口实现
CaptchaController
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
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.*;import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;@Tag(name = "管理后台 - 验证码")
@RestController("adminCaptchaController")
@RequestMapping("/system/captcha")
public class CaptchaController {@Resourceprivate CaptchaService captchaService;@PostMapping("/get")@Operation(summary = "获得验证码")@PermitAllpublic ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {data.setBrowserInfo(getRemoteId(request));return captchaService.get(data);}@PostMapping("/check")@Operation(summary = "校验验证码")@PermitAllpublic 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");return (StrUtil.isNotBlank(ip) ? ip : request.getRemoteAddr()) + ua;}
}
四、登录参数对象
AuthLoginReqVO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginReqVO extends CaptchaVerificationReqVO {@Schema(description = "账号(与手机号二选一)", example = "yudaoyuanma")@Length(min = 4, max = 16)@Pattern(regexp = "^[A-Za-z0-9]+$")private String username;@Schema(description = "手机号", example = "13800138000")@Pattern(regexp = "^1[3-9]\\d{9}$")private String mobile;@Schema(description = "密码", example = "buzhidao")@NotEmpty@Length(min = 4, max = 16)private String password;@Schema(description = "社交平台类型", example = "10")private Integer socialType;@Schema(description = "授权码", example = "1024")private String socialCode;@Schema(description = "state", example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")private String socialState;@AssertTrue(message = "用户名或手机号至少填写一个")public boolean isUsernameOrMobilePresent() {return StrUtil.isNotEmpty(username) || StrUtil.isNotEmpty(mobile);}@AssertTrue(message = "授权码不能为空")public boolean isSocialCodeValid() {return socialType == null || StrUtil.isNotEmpty(socialCode);}@AssertTrue(message = "授权 state 不能为空")public boolean isSocialState() {return socialType == null || StrUtil.isNotEmpty(socialState);}
}
五、登录接口
Controller
@PostMapping("/mobile-login")
@PermitAll
@Operation(summary = "使用手机/密码登录")
public CommonResult<AuthLoginRespVO> mobileLogin(@RequestBody @Valid AuthLoginReqVO reqVO) {return success(authService.h5MobileLogin(reqVO));
}
Service 实现
@Override
public AuthLoginRespVO h5MobileLogin(AuthLoginReqVO reqVO) {// 1. 校验验证码validateCaptcha(reqVO);// 2. 校验手机号与密码AdminUserDO user = authenticateMobile(reqVO.getMobile(), reqVO.getPassword());// 3. 社交用户绑定(可选)if (reqVO.getSocialType() != null) {socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));}// 4. 登录成功后生成 Tokenreturn createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
}
六、接口调用流程
1️⃣ 获取验证码
URL:
POST https://xxx/admin-api/system/captcha/get
请求体:
{"captchaType": "blockPuzzle", // 滑块类型 (前端传)"clientUid": "slider-44bd2b7a-3443-4823-8a60-5cf680ca05ba", // 唯一标识(前端传)"ts": 1760775769847 // 时间戳(前端传)
}
参数名 | 来源 | 示例值 | 说明 |
---|---|---|---|
captchaType | 前端传入(固定值) | "blockPuzzle" | 验证码类型。可选值包括 blockPuzzle (滑块拼图) 和 clickWord (文字点选)。前端根据需要指定。 |
clientUid | 前端生成 | "slider-44bd2b7a-3443-4823-8a60-5cf680ca05ba" | 客户端唯一标识,用于区分不同用户或设备的验证码会话。前端一般使用 UUID 或随机字符串生成,整个验证码流程中保持不变。 |
ts | 前端生成 | 1760775769847 | 时间戳(毫秒),用于标识请求发起时间,防止接口缓存或重复请求。一般使用 Date.now() 生成。 |
响应示例:
{"repCode": "0000","repData": {"secretKey": "VczzXIMcPK3o3I0M","originalImageBase64": "Base64背景图","jigsawImageBase64": "Base64滑块图","token": "c6b9ff2bdd8d4735b8699f5fbe3201ec"},"success": true
}
2️⃣ 校验验证码
URL:
POST https://xxx/admin-api/system/captcha/check
请求体:
{"captchaType": "blockPuzzle","pointJson": "0Np587J0fyWkJB/Kjx7NNlHa8K8nplSaJon2xNDt4UM=", // 用户拖动后滑块的坐标信息(加密)"token": "baf157e8395d44758e44a27c1c4dac39"
}
参数名 | 生成方 | 说明 |
---|---|---|
captchaType | 前端固定写死 | 验证码类型 |
pointJson | ✅ 前端根据拖动坐标 + secretKey 加密生成 | 滑块位置验证核心参数 |
token | ✅ 后端 /captcha/get 返回 | 校验验证码时使用的唯一标识 |
响应示例:
{"repCode": "0000","repData": {"captchaType": "blockPuzzle","result": true},"success": true
}
3️⃣ 执行登录
URL:
POST https://xxx/admin-api/h5/exam/user/mobile-login
请求体:
{"mobile": "185xxx","password": "123456","captchaVerification": "zFuzN8EXE6mO+UNBmWdwSoEUcbCZqUrbkMhUM5Em6CwPJikXFgCTuu6wWDj35xTnOgX2igMiTqjiX9rG3ycFNOsQfxwJWu1/p1QKzcW9mNE="
}
参数名 | 来源 | 示例值 | 说明 |
---|---|---|---|
mobile | 前端输入 | "账号" | 用户的手机号,由用户在登录页面输入。 |
password | 前端输入 | "密码" | 用户密码,由用户在登录页面输入。一般会在前端或传输层(HTTPS)加密传输。 |
captchaVerification | 前端传入(由验证码组件生成) | "zFuzN8EXE6mO+UNBmWdwSoEUcbCZqUrbkMhUM5Em6CwPJikXFgCTuu6wWDj35xTnOgX2igMiTqjiX9rG3ycFNOsQfxwJWu1/p1QKzcW9mNE=" | 验证码二次校验凭证(加密字符串)。用户在滑动或点选验证码成功后,前端会从验证码组件中拿到该加密结果,并传给后台。后台通过此字段验证验证码是否通过。 |
响应示例:
{"code": 0,"data": {"userId": 173,"accessToken": "8ec0c0b171424a5ea0d92931e051b47d","refreshToken": "5a63eefc570c49fda9247a2537e99284","expiresTime": 1760777570708},"msg": "操作成功"
}
七、整体流程总结
步骤 | 接口 | 描述 |
---|---|---|
1 | /system/captcha/get | 获取滑块验证码(返回背景图与滑块图) |
2 | /system/captcha/check | 校验滑块位置正确性 |
3 | /h5/exam/user/mobile-login | 校验验证码成功后登录 |
八、优点
✅ 安全防刷:通过滑块验证避免暴力破解。
✅ 前后端分离:前端完成拖动逻辑,后端只负责校验。
✅ 兼容性强:支持多类型验证码(滑块、文字点选)。
✅ 扩展性好:可切换缓存类型(local / redis)。