登录验证码

本文最后更新于:2025年6月23日 晚上

登录验证码

一、图形验证码

通过将文本值生成经过干扰处理的图片,然后发送给前端供用户识别输入,并将文本值存储在session中,供用户输入后进行验证。要是分布式的话,可以将验证码存储到redis中。

1.1、静态图形验证码

前端页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<img id="demo" onclick="refreshed()" src="/captcha/generate" alt="图片验证码" title="点击刷新">
<label for="captcha"></label>
<input id="captcha" type="text" name="captcha" placeholder="请输入验证码">
<input type="submit" value="登录/注册">
</form>
<script>
var captcha = document.getElementById('demo');
function refreshed(){
captcha.src = '/captcha/generate';
}
</script>
</body>
</html>
后端代码
Maven依赖
1
2
3
4
5
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;
// 验证码配置类
@Configuration
public class KaptchaConfig {

@Bean
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 验证码图片是否有边界
properties.put("kaptcha.border", "yes");
// 验证码图片边界颜色
properties.put("kaptcha.border.color", "105,179,90");
// 验证码图片字体颜色
properties.put("kaptcha.textproducer.font.color", "blue");
// 验证码图片宽度
properties.put("kaptcha.image.width", "125");
// 验证码图片高度
properties.put("kaptcha.image.height", "45");
// 验证码图片字体大小
properties.put("kaptcha.textproducer.font.size", "45");
// 验证码图片存入session中的文本值, session key,实测没效果
//properties.put("kaptcha.session.key", "code");
// 验证码图片字符长度
properties.put("kaptcha.textproducer.char.length", "4");
// 验证码图片字符字体
properties.put("kaptcha.textproducer.font.names", "Arial,Courier");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

// 图片验证码控制器
@RestController
@RequestMapping("/captcha")
public class CaptchaController {

@Autowired
private DefaultKaptcha defaultKaptcha;

@GetMapping("/generate")
public void generateCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置响应头
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");

// 生成验证码文本和图片
String captchaText = defaultKaptcha.createText();
BufferedImage captchaImage = defaultKaptcha.createImage(captchaText);

// 将验证码文本保存到session中
request.getSession().setAttribute("captcha", captchaText);

// 输出图片
ServletOutputStream out = response.getOutputStream();
ImageIO.write(captchaImage, "jpg", out);
out.flush();
out.close();
}
}

页面控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
* 页面控制
*/
@Controller
public class IndexController {

@GetMapping
public String index(){
return "index";
}
}
验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

// 登录校验控制器
@RestController
public class LoginController {

@PostMapping("/login")
public String login(HttpServletRequest request,
@RequestParam("captcha") String captcha) {

// 从session中获取验证码
String sessionCaptcha = (String) request.getSession().getAttribute("captcha");

// 验证验证码
if (!captcha.equals(sessionCaptcha)) {
return "Captcha verification failed!";
}

// 进行用户名和密码的验证...

return "Login successful!";
}
}

1.2、算数验证码

算是普通图片验证码的变体,通过生成算数图片,并将结果存储到session中,将前端用户传来的计算值与session中的答案进行匹配。要是分布式的话,可以将验证码存储到redis中。

关键的修改部分是生成图片的字段,需改成算数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

// 图片验证码控制器
@RestController
@RequestMapping("/captcha")
public class CaptchaController {

@Autowired
private DefaultKaptcha defaultKaptcha;

@GetMapping("/generate")
public void generateCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置响应头
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");

// 生成验证码文本和图片
Random random = new Random();
int x = random.nextInt(10);
int y = random.nextInt(10);

String expression = x+"+"+y+"=?";
int result = x+y;

BufferedImage captchaImage = defaultKaptcha.createImage(expression);

// 将验证码文本保存到session中
request.getSession().setAttribute("captcha", result);

// 输出图片
ServletOutputStream out = response.getOutputStream();
ImageIO.write(captchaImage, "jpg", out);
out.flush();
out.close();
}
}

二、行为验证码

推荐开源组件(该组件实现了大多类型的行为验证码,非常推荐):https://gitee.com/dromara/tianai-captcha

官方文档:http://doc.captcha.tianai.cloud/

2.1、滑动验证码

直接滑动到另一边的类型:这种通常会记录用户滑动轨迹然后发送给后端进行验证

2.2、滑块验证码

滑块拼图类型:这种则匹配拼图的位置来进行验证

2.3、旋转验证码

滑动将图片摆正的类型

2.4、点选验证码

1、通过按照描述的规则,顺序选择一张图片中存在的文字。

2、类似谷歌的将一张图片切割成多份,选择文本描述中符合特征的图片,然后将选择的图片传给后端进行验证,据说这种有些并没有标准答案,可以让用户选择正确的图片来白嫖代码标注的工作。

官方示例项目:

https://gitee.com/tianai/tianai-captcha-demo

他们提供收费版的轨迹验证,可根据需要自行购买使用。

三、短信/语音/邮箱验证码

这类验证码需要第三方服务配置。其中邮箱是这三个中成本最小,也最容易实现的。其他的都需要付费,而且有些个人还无法使用,必须企业才行。

3.1、邮箱验证码

大致流程:构建邮箱验证码网页模板,然后以模板的形式发送邮件验证码给用户。

核心依赖
1
2
3
4
5
<!-- 邮件验证码 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
其他依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

QQ邮箱官方集成说明: https://wx.mail.qq.com/list/readtemplate?name=app_intro.html#/agreement/authorizationCode

网易邮箱官方集成说明: https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac2a5feb28b66796d3b

示例:
application.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
server:
port: 8080

spring:
thymeleaf:
enabled: true
redis:
database: 0
host: localhost
port: 6379

# qq邮箱配置相关说明 https://wx.mail.qq.com/list/readtemplate?name=app_intro.html#/agreement/authorizationCode
# mail:
# host: smtp.qq.com
# port: 587
# protocol: smtp
# default-encoding: UTF-8
# username: <邮箱名 例如xxx@qq.com>
# password: <授权码>
# properties:
# mail:
# smtp:
# auth: true
# starttls:
# enable: true
# required: true

# 网易邮箱配置相关说明 https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac2a5feb28b66796d3b
mail:
host: smtp.163.com
port: 25
protocol: smtp
default-encoding: UTF-8
username: <邮箱名 例如xxx@163.com>
password: <授权码>
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true

app:
email:
from: <发送邮件的邮箱名>
personal: <验证码标题,通常是系统名称>
EmailLoginRequest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class EmailLoginRequest {
private String email;

private String captcha;
}
email-template.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!-- src/main/resources/templates/email/captcha-template.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录验证码</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.header {
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.content {
padding: 20px 0;
}
.code {
font-size: 24px;
font-weight: bold;
text-align: center;
padding: 10px;
margin: 20px 0;
background-color: #f5f5f5;
border-radius: 5px;
letter-spacing: 5px;
}
.footer {
font-size: 12px;
color: #777;
text-align: center;
padding-top: 10px;
border-top: 1px solid #eee;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>登录验证码测试</h2>
</div>
<div class="content">
<p>您好,</p>
<p>您正在进行登录操作,请使用以下验证码完成验证:</p>
<div class="code" th:text="${captcha}">123456</div>
<p>验证码有效期为 <span th:text="${expirationMinutes}">10</span> 分钟,请及时使用。</p>
<p>如果这不是您的操作,请忽略此邮件。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复。</p>
</div>
</div>
</body>
</html>
EmailCaptchaService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.message.email.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* 邮箱验证码服务
* @author peter
*/
@Service
@Slf4j
public class EmailCaptchaService {

// 邮件发送器
private final JavaMailSender mailSender;
// Redis模板
private final RedisTemplate<String, String> redisTemplate;
// thymeleaf模板引擎
private final TemplateEngine templateEngine;

@Value("${app.email.from}")
private String fromEmail;

@Value("${app.email.personal}")
private String emailPersonal;

// 10分钟过期
private static final long EMAIL_CAPTCHA_EXPIRATION = 600;
// 验证码缓存前缀
private static final String EMAIL_CAPTCHA_PREFIX = "EMAIL_CAPTCHA:";
private static final Random RANDOM = new Random();

public EmailCaptchaService(JavaMailSender mailSender, RedisTemplate<String, String> redisTemplate, TemplateEngine templateEngine) {
this.mailSender = mailSender;
this.redisTemplate = redisTemplate;
this.templateEngine = templateEngine;
}

/**
* 发送邮箱验证码
* @param email 电子邮箱
* @return 是否发送成功
*/
public boolean sendEmailCaptcha(String email) {
try {
// 生成6位随机验证码
String captcha = generateCaptcha(6);

// 存储验证码
redisTemplate.opsForValue().set(
EMAIL_CAPTCHA_PREFIX + email,
captcha,
EMAIL_CAPTCHA_EXPIRATION,
TimeUnit.SECONDS);

// 准备邮件内容
Context context = new Context();
context.setVariable("captcha", captcha);
context.setVariable("expirationMinutes", EMAIL_CAPTCHA_EXPIRATION / 60);
// 渲染邮件内容, 使用thymeleaf模板,先将变量渲染到html中,然后将html模板渲染成字符串
String emailContent = templateEngine.process("email-template", context);

// 创建MIME邮件
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

// 设置发件人、收件人、主题和内容
helper.setFrom(new InternetAddress(fromEmail, emailPersonal));
helper.setTo(email);
helper.setSubject("登录验证码");
helper.setText(emailContent, true);

// 发送邮件
mailSender.send(message);
log.info("邮箱验证码已发送至: {}", email);
return true;
} catch (Exception e) {
log.error("发送邮箱验证码失败", e);
return false;
}
}

/**
* 验证邮箱验证码
* @param email 电子邮箱
* @param captcha 用户输入的验证码
* @return 是否验证成功
*/
public boolean validateEmailCaptcha(String email, String captcha) {
// 获取存储的验证码
String key = EMAIL_CAPTCHA_PREFIX + email;
String storedCaptcha = redisTemplate.opsForValue().get(key);

if (storedCaptcha != null && storedCaptcha.equals(captcha)) {
// 验证成功后删除验证码,防止重复使用
redisTemplate.delete(key);
return true;
}

return false;
}

/**
* 生成指定长度的随机数字验证码
* @param length 验证码长度
*/
private String generateCaptcha(int length) {
StringBuilder captcha = new StringBuilder();

for (int i = 0; i < length; i++) {
captcha.append(RANDOM.nextInt(10));
}

return captcha.toString();
}
}
EmailCaptchaController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.message.email.controller;

import com.message.email.domain.EmailLoginRequest;
import com.message.email.service.EmailCaptchaService;
import com.message.kaptcha.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 java.util.HashMap;
import java.util.Map;

/**
* 邮箱验证码
* @author peter
*/
@RestController
@RequestMapping("/captcha")
public class EmailCaptchaController {

@Autowired
private EmailCaptchaService emailCaptchaService;

@PostMapping("/email/send")
public ResponseEntity<R<Map<String, Object>>> sendEmailCaptcha(@RequestBody EmailLoginRequest request) {
String email = request.getEmail();

// 验证邮箱格式
if (!isValidEmail(email)) {
return ResponseEntity.badRequest().body(R.error("邮箱格式不正确"));
}

// 发送邮箱验证码
boolean sent = emailCaptchaService.sendEmailCaptcha(email);

if (sent) {
Map<String, Object> response = new HashMap<>();
response.put("token", "email");
response.put("message", "ok");
return ResponseEntity.ok(R.ok(response));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(R.error("验证码发送失败"));
}
}

// 验证邮箱格式
private boolean isValidEmail(String email) {
String regex = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$";
return email != null && email.matches(regex);
}
}
EmailLoginController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.message.email.controller;

import com.message.email.domain.EmailLoginRequest;
import com.message.email.service.EmailCaptchaService;
import com.message.kaptcha.domain.R;
import com.message.kaptcha.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 java.util.HashMap;
import java.util.Map;

/**
* 邮箱登录控制器
* @author peter
*/
@RestController
@RequestMapping("/auth/email")
public class EmailLoginController {


private final EmailCaptchaService emailCaptchaService;

private final UserService userService;

public EmailLoginController(EmailCaptchaService emailCaptchaService, UserService userService) {
this.emailCaptchaService = emailCaptchaService;
this.userService = userService;
}


// @Autowired
// private JwtTokenProvider jwtTokenProvider;

@PostMapping("/email-login")
public ResponseEntity<R<Map<String, Object>>> emailLogin(@RequestBody EmailLoginRequest request) {
String email = request.getEmail();
String captcha = request.getCaptcha();

// 验证邮箱验证码
if (!emailCaptchaService.validateEmailCaptcha(email, captcha)) {
return ResponseEntity.badRequest().body(R.error("验证码错误或已过期"));
}

// 查找或创建用户,这里为了方便测试,直接返回true。正常登录应该根据邮箱查找用户,如果用户不存在,则创建用户。
boolean flag = userService.findOrCreateByEmail(email);

if (flag) {
// 生成JWT令牌
//String token = jwtTokenProvider.generateToken(user.getUsername());
Map<String, Object> data = new HashMap<>();
data.put("token", "123");
data.put("username", email);
return ResponseEntity.ok(R.ok(data));
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(R.error("登录失败,请稍后再试"));
}
}
}

3.2、短信验证码

阿里云短信服务:https://help.aliyun.com/zh/sms/

需要企业资质,然后构建模板,再通过平台对应的SDK调用接口使用。

1
2
3
4
5
6
<!-- 阿里云的SDK,其中就有短信验证码的功能 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>

3.3、语音验证码

需要企业资质,然后构建模板,再通过平台对应的SDK调用接口使用。这时会向用户拨打电话,然后播报固定的语音。例如:阿里云的资源到期释放语音提醒。

阿里云语音服务:https://help.aliyun.com/zh/vms/

四、智能验证码

(如Google reCAPTCHA v3)

无感验证:后台分析用户行为(鼠标移动点击模式),无需交互。

动态防护:根据风险评分调整验证强度。

五、类型比较

类型 安全性 用户体验 成本 适用场景
图形验证码 较差 基础防护低风险场景
短信验证码 中等 中高 支付账号安全
行为式验证码 中高 通用登录防刷
智能验证码 极佳 中高 高流量网站隐私敏感
语音验证码 中等 中高 无障碍支持短信备份
问答式验证码 中等 简单场景辅助验证
生物特征验证 极高 极佳 金融高安全需求

登录验证码
https://superlovelace.top/2025/06/20/登录验证码/
作者
棱境
发布于
2025年6月20日
更新于
2025年6月23日
许可协议