本文最后更新于: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"); 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);
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) {
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);
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>
<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
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
| <!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;
@Service @Slf4j public class EmailCaptchaService { private final JavaMailSender mailSender; private final RedisTemplate<String, String> redisTemplate; private final TemplateEngine templateEngine;
@Value("${app.email.from}") private String fromEmail;
@Value("${app.email.personal}") private String emailPersonal;
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; }
public boolean sendEmailCaptcha(String email) { try { 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); String emailContent = templateEngine.process("email-template", context);
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; } }
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; }
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;
@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;
@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; }
@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("验证码错误或已过期")); }
boolean flag = userService.findOrCreateByEmail(email);
if (flag) { 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
| <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)
无感验证:后台分析用户行为(鼠标移动点击模式),无需交互。
动态防护:根据风险评分调整验证强度。
五、类型比较
类型 |
安全性 |
用户体验 |
成本 |
适用场景 |
图形验证码 |
低 |
较差 |
低 |
基础防护低风险场景 |
短信验证码 |
高 |
中等 |
中高 |
支付账号安全 |
行为式验证码 |
中高 |
好 |
中 |
通用登录防刷 |
智能验证码 |
高 |
极佳 |
中高 |
高流量网站隐私敏感 |
语音验证码 |
中 |
中等 |
中高 |
无障碍支持短信备份 |
问答式验证码 |
低 |
中等 |
低 |
简单场景辅助验证 |
生物特征验证 |
极高 |
极佳 |
高 |
金融高安全需求 |