验证码技术与功能解读
2025/12/3大约 6 分钟
验证码技术与功能解读
1. 技术概述
验证码技术是保障系统安全的重要组成部分,通过人机交互验证防止恶意攻击和自动化操作。淘票票项目实现了多种类型的验证码,适用于不同的业务场景。
2. 应用场景
- 用户登录:防止暴力破解密码
- 注册验证:防止批量注册垃圾账号
- 支付确认:增强支付安全性
- 敏感操作:修改密码、绑定手机等操作的二次验证
- 接口防刷:保护核心接口免受恶意调用
- 活动参与:防止刷票、刷奖等行为
3. 验证码类型及实现
3.1 图形验证码
3.1.1 实现原理
基于Java的图形处理能力,生成包含随机字符、干扰线和噪点的图片:
@Component
public class CaptchaService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成图形验证码
* @param sessionId 会话ID
* @return 验证码图片信息
*/
public CaptchaResult generateImageCaptcha(String sessionId) {
// 生成随机字符
String captchaCode = generateRandomCode(4);
// 生成图片
BufferedImage image = createCaptchaImage(captchaCode);
// 转换为Base64
String imageBase64 = imageToBase64(image);
// 存储到Redis,设置过期时间
String redisKey = "captcha:image:" + sessionId;
redisTemplate.opsForValue().set(redisKey, captchaCode, 5, TimeUnit.MINUTES);
return new CaptchaResult(imageBase64);
}
/**
* 验证图形验证码
* @param sessionId 会话ID
* @param captchaCode 用户输入的验证码
* @return 是否验证成功
*/
public boolean verifyImageCaptcha(String sessionId, String captchaCode) {
String redisKey = "captcha:image:" + sessionId;
String storedCode = redisTemplate.opsForValue().get(redisKey);
if (storedCode != null && storedCode.equalsIgnoreCase(captchaCode)) {
// 验证成功后删除验证码,防止重复使用
redisTemplate.delete(redisKey);
return true;
}
return false;
}
// 生成随机验证码
private String generateRandomCode(int length) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder code = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
// 创建验证码图片
private BufferedImage createCaptchaImage(String captchaCode) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 添加噪点
Random random = new Random();
for (int i = 0; i < 200; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
g.drawOval(x, y, 1, 1);
}
// 添加干扰线
for (int i = 0; i < 5; i++) {
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 绘制验证码
g.setFont(new Font("Arial", Font.BOLD, 24));
for (int i = 0; i < captchaCode.length(); i++) {
g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
g.drawString(String.valueOf(captchaCode.charAt(i)), 30 + i * 20, 25);
}
g.dispose();
return image;
}
// 图片转Base64
private String imageToBase64(BufferedImage image) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new RuntimeException("图片转换失败", e);
}
}
}3.2 短信验证码
3.2.1 实现原理
通过短信服务发送随机验证码到用户手机:
@Service
public class SmsService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SmsClient smsClient; // 第三方短信服务客户端
/**
* 发送短信验证码
* @param phone 手机号
* @return 是否发送成功
*/
public boolean sendSmsCaptcha(String phone) {
// 检查发送频率
String frequencyKey = "sms:frequency:" + phone;
String count = redisTemplate.opsForValue().get(frequencyKey);
if (count != null && Integer.parseInt(count) >= 5) {
throw new BusinessException("验证码发送过于频繁,请稍后再试");
}
// 生成6位随机验证码
String captchaCode = generateRandomNumberCode(6);
// 调用短信服务发送
boolean success = smsClient.sendSms(phone, "您的验证码是:" + captchaCode + ",有效期5分钟,请勿泄露给他人");
if (success) {
// 存储验证码到Redis
String captchaKey = "sms:captcha:" + phone;
redisTemplate.opsForValue().set(captchaKey, captchaCode, 5, TimeUnit.MINUTES);
// 更新发送次数
redisTemplate.opsForValue().increment(frequencyKey);
redisTemplate.expire(frequencyKey, 24, TimeUnit.HOURS);
}
return success;
}
/**
* 验证短信验证码
* @param phone 手机号
* @param captchaCode 用户输入的验证码
* @return 是否验证成功
*/
public boolean verifySmsCaptcha(String phone, String captchaCode) {
String captchaKey = "sms:captcha:" + phone;
String storedCode = redisTemplate.opsForValue().get(captchaKey);
if (storedCode != null && storedCode.equals(captchaCode)) {
// 验证成功后删除验证码
redisTemplate.delete(captchaKey);
return true;
}
return false;
}
// 生成随机数字验证码
private String generateRandomNumberCode(int length) {
StringBuilder code = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
}3.3 滑动验证码
3.3.1 实现原理
滑动验证码通过生成带有缺口的图片,用户拖动滑块到正确位置完成验证:
@Service
public class SlidingCaptchaService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 生成滑动验证码
* @param sessionId 会话ID
* @return 滑动验证码信息
*/
public SlidingCaptchaResult generateSlidingCaptcha(String sessionId) {
// 生成背景图和滑块
BufferedImage backgroundImage = generateBackgroundImage();
BufferedImage sliderImage = generateSliderImage(backgroundImage);
// 随机生成缺口位置
int targetPosition = generateRandomPosition();
// 转换为Base64
String backgroundBase64 = imageToBase64(backgroundImage);
String sliderBase64 = imageToBase64(sliderImage);
// 存储正确位置到Redis
String redisKey = "captcha:sliding:" + sessionId;
redisTemplate.opsForValue().set(redisKey, targetPosition, 10, TimeUnit.MINUTES);
return new SlidingCaptchaResult(backgroundBase64, sliderBase64);
}
/**
* 验证滑动验证码
* @param sessionId 会话ID
* @param userPosition 用户滑动位置
* @return 是否验证成功
*/
public boolean verifySlidingCaptcha(String sessionId, int userPosition) {
String redisKey = "captcha:sliding:" + sessionId;
Integer targetPosition = (Integer) redisTemplate.opsForValue().get(redisKey);
if (targetPosition != null) {
// 允许5像素的误差范围
boolean success = Math.abs(userPosition - targetPosition) <= 5;
if (success) {
// 验证成功后删除
redisTemplate.delete(redisKey);
}
return success;
}
return false;
}
// 生成背景图片
private BufferedImage generateBackgroundImage() {
// 实际实现中可以从图片库随机选择或动态生成背景图
// 这里简化处理
BufferedImage image = new BufferedImage(300, 150, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
g.setColor(Color.LIGHT_GRAY);
g.fillRect(0, 0, 300, 150);
g.dispose();
return image;
}
// 生成滑块图片
private BufferedImage generateSliderImage(BufferedImage backgroundImage) {
// 从背景图中切出滑块并添加阴影效果
// 简化实现
return new BufferedImage(50, 50, BufferedImage.TYPE_INT_RGB);
}
// 生成随机缺口位置
private int generateRandomPosition() {
return 50 + new Random().nextInt(200); // 50-250之间的随机位置
}
// 图片转Base64(复用之前的方法)
private String imageToBase64(BufferedImage image) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new RuntimeException("图片转换失败", e);
}
}
}4. 验证码前端调用流程
4.1 图形验证码调用流程
- 前端请求生成验证码:
GET /api/captcha/image/{sessionId} - 后端生成验证码并返回Base64图片
- 前端展示验证码图片
- 用户输入验证码并提交表单
- 后端验证验证码是否正确
4.2 短信验证码调用流程
- 用户输入手机号,点击"发送验证码"
- 前端请求发送短信:
POST /api/captcha/sms,参数:{"phone": "13800138000"} - 后端验证频率并发送短信
- 前端开始倒计时,防止重复点击
- 用户输入收到的验证码并提交
- 后端验证短信验证码是否正确
4.3 滑动验证码调用流程
- 前端请求生成滑动验证码:
GET /api/captcha/sliding/{sessionId} - 后端返回背景图和滑块图
- 前端展示滑动验证码组件
- 用户拖动滑块到正确位置
- 前端发送验证请求:
POST /api/captcha/sliding/verify,参数:{"sessionId": "...", "position": 120} - 后端验证滑动位置是否正确
5. 最佳实践
- 选择合适的验证码类型:根据业务场景选择合适的验证码类型
- 设置合理的过期时间:避免验证码长时间有效导致安全风险
- 限制发送频率:防止验证码被恶意大量发送
- 添加日志记录:记录验证码生成、验证过程,便于问题排查
- 考虑用户体验:在保证安全性的同时,尽量提供良好的用户体验
- 多因素验证:重要操作可组合使用多种验证码方式
- 定期更新算法:防止验证码被破解
- 异常处理:完善的异常处理机制,确保验证码服务的稳定性
- 监控告警:监控验证码服务的异常情况,及时发现问题