一、图形验证码的意义
图形验证码是一种广泛应用于互联网领域的安全验证机制,它通过向用户展示包含字符、数字、图形等信息的图片,要求用户正确识别并输入其中的内容,以此来区分用户是人类还是机器程序。图形验证码具有多方面重要意义:
- 防止恶意注册和暴力破解:在网站或应用的注册、登录环节,图形验证码能有效抵御恶意程序的自动化操作。例如,一些黑客试图使用脚本程序批量注册账号,以进行垃圾邮件发送、刷取积分等非法活动。图形验证码的存在使得这些自动化程序难以准确识别和输入验证内容,从而保护网站的用户数据安全,维护正常的注册和登录秩序。同理,在登录时,若没有图形验证码,黑客可能通过暴力破解手段,利用程序不断尝试不同的密码组合来获取用户账号权限。图形验证码增加了破解的难度,保护了用户的账号安全。
- 抵御网络爬虫和数据抓取:许多网站为了保护自身数据的安全性和知识产权,不希望其内容被随意抓取。网络爬虫程序可以自动访问网站并提取大量数据。图形验证码的设置使得爬虫程序无法顺利通过验证,从而限制了它们对网站数据的非法获取。例如,新闻网站、电商平台等都依赖图形验证码来防止竞争对手或恶意用户通过爬虫获取其重要信息,如新闻内容、商品价格、用户评价等。
- 防止刷票和作弊行为:在各种投票活动、抢购活动中,图形验证码能确保活动的公平性。以在线投票为例,如果没有图形验证码,有人可能会编写程序进行刷票,使投票结果失去真实性和公正性。而加入图形验证码后,每个投票行为都需要用户手动识别并输入验证码,增加了刷票的难度,保证了投票结果能真实反映公众的意愿。在抢购限量商品或优惠券时,图形验证码也能防止机器人程序瞬间抢光商品,让普通用户有公平的机会参与抢购。
- 保护网站服务器资源:恶意程序的大量自动化请求可能会给网站服务器带来巨大的负担,甚至导致服务器瘫痪。图形验证码可以过滤掉这些非法的请求,减轻服务器的压力,确保网站能够正常运行,为合法用户提供稳定的服务。例如,一些小型网站如果遭受大量恶意程序的攻击,可能会因为服务器资源耗尽而无法访问,图形验证码在一定程度上可以避免这种情况的发生。
- 提供用户身份验证的额外保障:除了用户名和密码等常规的身份验证方式外,图形验证码作为一种额外的验证手段,增加了用户身份验证的安全性。即使黑客获取了用户的密码,由于无法通过图形验证码的验证,也无法成功登录账号,从而进一步保护了用户的个人信息和资产安全。
今天,我们来分析一下 vue3-element-admin 前后端代码,以便抽离出一套通用的图形验证码实现。vue3-element-admin 的前后端环境搭建,可以看我的博文。
让我们致敬开源的力量!
二、实现思路

三、代码分析
- 前端 src/views/login/index.vue
javascript
onMounted(() => {
getCaptcha();
});
- 请求后端 com.youlai.boot.shared.auth.controller.AuthController#getCaptcha 接口
java
/**
* 获取验证码
*
* @return 验证码
*/
@Override
public CaptchaInfo getCaptcha() {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
// 调用 hutool 验证码工具,生成验证码
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
// 验证码
String captchaCode = captcha.getCode();
// Base64 验证码图形字符串
String imageBase64Data = captcha.getImageBase64Data();
// 生成UUID的RedisKey,并返回给前端
String captchaKey = IdUtil.fastSimpleUUID();
// 验证码文本缓存至Redis,用于登录校验
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey),
captchaCode,
captchaProperties.getExpireSeconds(),
TimeUnit.SECONDS
);
return CaptchaInfo.builder()
.captchaKey(captchaKey)
.captchaBase64(imageBase64Data)
.build();
}
--------------------------------------------------------------------------------------
@Builder
public class CaptchaInfo {
@Schema(description = "验证码缓存 Key")
private String captchaKey;
@Schema(description = "验证码图片Base64字符串")
private String captchaBase64;
}
--------------------------------------------------------------------------------------
- 前端登录的时候 src/api/auth/index.ts
javascript
const AUTH_BASE_URL = "/api/v1/auth";
const AuthAPI = {
/** 登录接口*/
login(data: LoginFormData) {
const formData = new FormData();
// 用户名
formData.append("username", data.username);
// 密码
formData.append("password", data.password);
// 验证码 RedisKey
formData.append("captchaKey", data.captchaKey);
// 用户输入的验证码
formData.append("captchaCode", data.captchaCode);
return request<any, LoginResult>({
url: `${AUTH_BASE_URL}/login`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
},
- 后端过滤器 com.youlai.boot.core.security.filter.CaptchaValidationFilter#doFilterInternal
java
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 检验登录接口的验证码
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
// 请求中的验证码
String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
// TODO 兼容没有验证码的版本(线上请移除这个判断)
if (StrUtil.isBlank(captchaCode)) {
chain.doFilter(request, response);
return;
}
// 缓存中的验证码
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
);
if (cacheVerifyCode == null) {
// 过期报错提示
ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
} else {
// 验证码比对
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
// 进入登录校验接口
chain.doFilter(request, response);
} else {
// 验证码不同报错提示
ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
}
}
} else {
// 非登录接口放行
chain.doFilter(request, response);
}
}
- 登录校验接口 com.youlai.boot.shared.auth.controller.AuthController#login
java
@Operation(summary = "账号密码登录")
@PostMapping("/login")
@Log(value = "登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> login(
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
@Parameter(description = "密码", example = "123456") @RequestParam String password
) {
AuthenticationToken authenticationToken = authService.login(username, password);
return Result.success(authenticationToken);
}