验证码生成指南
目录
一、验证码基本概念
1.1 什么是验证码
验证码(CAPTCHA)是一种用于区分计算机和人类用户的测试,通常用于防止自动化程序(如机器人)进行批量操作。验证码可以是文本、图像、音频或视频等形式。
1.2 验证码的类型
- 文本验证码:由字母、数字或符号组成的文本
- 图形验证码:将文本渲染为图像,增加识别难度
- 音频验证码:通过语音播放验证码内容
- 视频验证码:通过视频展示验证码内容
- 短信验证码:通过短信发送的验证码
- 邮箱验证码:通过邮件发送的验证码
1.3 验证码的应用场景
- 用户注册和登录
- 表单提交
- 敏感操作确认
- 防止批量注册
- 防止爬虫抓取
- 防止暴力破解
二、验证码生成方法
2.1 纯数字验证码
/**
* 生成纯数字验证码
* @param length 验证码长度
* @return 数字验证码
*/
public static String generateNumericCode(int length) {
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
code.append(random.nextInt(10)); // 生成0-9的随机数
}
return code.toString();
}
2.2 字母数字混合验证码
/**
* 生成字母数字混合验证码
* @param length 验证码长度
* @return 混合验证码
*/
public static String generateAlphanumericCode(int length) {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = random.nextInt(characters.length());
code.append(characters.charAt(index));
}
return code.toString();
}
2.3 使用SecureRandom生成更安全的验证码
/**
* 使用SecureRandom生成更安全的验证码
* @param length 验证码长度
* @return 安全验证码
*/
public static String generateSecureCode(int length) {
SecureRandom secureRandom = new SecureRandom();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
code.append(secureRandom.nextInt(10));
}
return code.toString();
}
2.4 排除易混淆字符的验证码
/**
* 生成排除易混淆字符的验证码
* @param length 验证码长度
* @return 清晰验证码
*/
public static String generateClearCode(int length) {
// 排除易混淆的字符:0,1,O,I,l
String characters = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
SecureRandom secureRandom = new SecureRandom();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = secureRandom.nextInt(characters.length());
code.append(characters.charAt(index));
}
return code.toString();
}
三、验证码工具类实现
3.1 完整的验证码工具类
import java.security.SecureRandom;
import java.util.Random;
/**
* 验证码生成工具类
*/
public class VerificationCodeGenerator {
// 默认验证码长度
private static final int DEFAULT_LENGTH = 6;
// 数字字符集
private static final String NUMERIC_CHARS = "0123456789";
// 字母数字混合字符集(排除易混淆字符)
private static final String ALPHANUMERIC_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
// 大写字母字符集
private static final String UPPERCASE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ";
// 小写字母字符集
private static final String LOWERCASE_CHARS = "abcdefghijkmnopqrstuvwxyz";
/**
* 生成默认长度的数字验证码
* @return 数字验证码
*/
public static String generateNumericCode() {
return generateNumericCode(DEFAULT_LENGTH);
}
/**
* 生成指定长度的数字验证码
* @param length 验证码长度
* @return 数字验证码
*/
public static String generateNumericCode(int length) {
return generateCustomCode(length, NUMERIC_CHARS);
}
/**
* 生成默认长度的字母数字混合验证码
* @return 混合验证码
*/
public static String generateAlphanumericCode() {
return generateAlphanumericCode(DEFAULT_LENGTH);
}
/**
* 生成指定长度的字母数字混合验证码
* @param length 验证码长度
* @return 混合验证码
*/
public static String generateAlphanumericCode(int length) {
return generateCustomCode(length, ALPHANUMERIC_CHARS);
}
/**
* 生成默认长度的大写字母验证码
* @return 大写字母验证码
*/
public static String generateUppercaseCode() {
return generateUppercaseCode(DEFAULT_LENGTH);
}
/**
* 生成指定长度的大写字母验证码
* @param length 验证码长度
* @return 大写字母验证码
*/
public static String generateUppercaseCode(int length) {
return generateCustomCode(length, UPPERCASE_CHARS);
}
/**
* 生成默认长度的小写字母验证码
* @return 小写字母验证码
*/
public static String generateLowercaseCode() {
return generateLowercaseCode(DEFAULT_LENGTH);
}
/**
* 生成指定长度的小写字母验证码
* @param length 验证码长度
* @return 小写字母验证码
*/
public static String generateLowercaseCode(int length) {
return generateCustomCode(length, LOWERCASE_CHARS);
}
/**
* 使用自定义字符集生成验证码
* @param length 验证码长度
* @param charSet 自定义字符集
* @return 自定义验证码
*/
public static String generateCustomCode(int length, String charSet) {
if (charSet == null || charSet.isEmpty()) {
throw new IllegalArgumentException("字符集不能为空");
}
if (length <= 0) {
throw new IllegalArgumentException("验证码长度必须大于0");
}
SecureRandom secureRandom = new SecureRandom();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = secureRandom.nextInt(charSet.length());
code.append(charSet.charAt(index));
}
return code.toString();
}
/**
* 生成指定格式的验证码
* @param format 验证码格式,例如:NNN-AAA-NNN(N表示数字,A表示字母)
* @return 格式化的验证码
*/
public static String generateFormattedCode(String format) {
if (format == null || format.isEmpty()) {
throw new IllegalArgumentException("格式不能为空");
}
StringBuilder code = new StringBuilder();
for (char c : format.toCharArray()) {
if (c == 'N') {
code.append(generateNumericCode(1));
} else if (c == 'A') {
code.append(generateUppercaseCode(1));
} else if (c == 'a') {
code.append(generateLowercaseCode(1));
} else if (c == 'X') {
code.append(generateAlphanumericCode(1));
} else {
code.append(c);
}
}
return code.toString();
}
}
四、验证码服务实现
4.1 验证码服务接口
/**
* 验证码服务接口
*/
public interface VerificationCodeService {
/**
* 生成验证码
* @param type 验证码类型
* @param length 验证码长度
* @return 验证码
*/
String generateCode(CodeType type, int length);
/**
* 生成默认验证码
* @param type 验证码类型
* @return 验证码
*/
String generateCode(CodeType type);
/**
* 生成格式化验证码
* @param format 验证码格式
* @return 格式化的验证码
*/
String generateFormattedCode(String format);
}
4.2 验证码类型枚举
/**
* 验证码类型枚举
*/
public enum CodeType {
NUMERIC, // 纯数字
ALPHANUMERIC, // 字母数字混合
UPPERCASE, // 大写字母
LOWERCASE, // 小写字母
CUSTOM // 自定义
}
4.3 验证码服务实现
import org.springframework.stereotype.Service;
/**
* 验证码服务实现
*/
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
// 默认验证码长度
private static final int DEFAULT_LENGTH = 6;
// 数字字符集
private static final String NUMERIC_CHARS = "0123456789";
// 字母数字混合字符集(排除易混淆字符)
private static final String ALPHANUMERIC_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
// 大写字母字符集
private static final String UPPERCASE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ";
// 小写字母字符集
private static final String LOWERCASE_CHARS = "abcdefghijkmnopqrstuvwxyz";
@Override
public String generateCode(CodeType type, int length) {
switch (type) {
case NUMERIC:
return VerificationCodeGenerator.generateNumericCode(length);
case ALPHANUMERIC:
return VerificationCodeGenerator.generateAlphanumericCode(length);
case UPPERCASE:
return VerificationCodeGenerator.generateUppercaseCode(length);
case LOWERCASE:
return VerificationCodeGenerator.generateLowercaseCode(length);
case CUSTOM:
return VerificationCodeGenerator.generateCustomCode(length, ALPHANUMERIC_CHARS);
default:
throw new IllegalArgumentException("不支持的验证码类型: " + type);
}
}
@Override
public String generateCode(CodeType type) {
return generateCode(type, DEFAULT_LENGTH);
}
@Override
public String generateFormattedCode(String format) {
return VerificationCodeGenerator.generateFormattedCode(format);
}
}
4.4 验证码控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 验证码控制器
*/
@RestController
@RequestMapping("/api/verification-code")
public class VerificationCodeController {
@Autowired
private VerificationCodeService verificationCodeService;
/**
* 生成验证码
* @param type 验证码类型
* @param length 验证码长度
* @return 验证码
*/
@GetMapping("/generate")
public ResponseEntity<?> generateCode(
@RequestParam(defaultValue = "NUMERIC") CodeType type,
@RequestParam(defaultValue = "6") int length) {
try {
String code = verificationCodeService.generateCode(type, length);
return ResponseEntity.ok(new ApiResponse(true, null, "验证码生成成功", code));
} catch (Exception e) {
return ResponseEntity.badRequest().body(new ApiResponse(false, "GENERATION_FAILED", e.getMessage()));
}
}
/**
* 生成格式化验证码
* @param format 验证码格式
* @return 格式化的验证码
*/
@GetMapping("/generate-formatted")
public ResponseEntity<?> generateFormattedCode(@RequestParam String format) {
try {
String code = verificationCodeService.generateFormattedCode(format);
return ResponseEntity.ok(new ApiResponse(true, null, "验证码生成成功", code));
} catch (Exception e) {
return ResponseEntity.badRequest().body(new ApiResponse(false, "GENERATION_FAILED", e.getMessage()));
}
}
}
五、验证码最佳实践
5.1 验证码长度选择
- 短信验证码:通常为4-6位数字
- 图形验证码:通常为4-6位字母数字混合
- 邮箱验证码:通常为6-8位字母数字混合
5.2 字符集选择
- 短信验证码:纯数字,避免字母
- 图形验证码:字母数字混合,排除易混淆字符
- 邮箱验证码:字母数字混合,可包含特殊字符
5.3 安全性考虑
- 使用SecureRandom代替Random
- 避免使用易混淆字符
- 验证码有效期限制
- 防止暴力破解
5.4 用户体验考虑
- 验证码长度适中
- 字符清晰易识别
- 提供刷新功能
- 错误提示友好
六、常见问题与解决方案
6.1 验证码生成不够随机
问题:使用Random生成的验证码可能不够随机,容易被预测。
解决方案:
- 使用SecureRandom代替Random
- 增加验证码长度
- 使用更复杂的字符集
6.1.1 使用SecureRandom代替Random
// 不安全的随机数生成
Random random = new Random();
int code = random.nextInt(1000000); // 可能被预测
// 安全的随机数生成
SecureRandom secureRandom = new SecureRandom();
int code = secureRandom.nextInt(1000000); // 更难被预测
SecureRandom
类使用操作系统提供的熵源(如硬件事件、系统事件等)来生成随机数,比Random
类更安全。
6.1.2 增加验证码长度
// 短验证码(4位)
String shortCode = generateNumericCode(4); // 只有10000种可能
// 长验证码(6位)
String longCode = generateNumericCode(6); // 有1000000种可能
增加验证码长度可以显著提高破解难度。例如,4位数字验证码有10000种可能,而6位数字验证码有1000000种可能。
6.1.3 使用更复杂的字符集
// 简单字符集(仅数字)
String simpleChars = "0123456789"; // 10个字符
// 复杂字符集(字母数字混合,排除易混淆字符)
String complexChars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; // 54个字符
使用更复杂的字符集可以增加每个位置的可能字符数,从而提高破解难度。例如,使用54个字符的字符集,6位验证码就有54^6种可能,远大于仅使用10个数字的6位验证码。
6.1.4 结合时间戳和用户信息
public static String generateTimeBasedCode(String userId) {
// 获取当前时间戳
long timestamp = System.currentTimeMillis();
// 结合用户ID和时间戳生成种子
String seed = userId + timestamp;
// 使用种子初始化SecureRandom
SecureRandom secureRandom = new SecureRandom(seed.getBytes());
// 生成验证码
return generateNumericCode(6, secureRandom);
}
结合时间戳和用户信息生成验证码可以增加随机性,使验证码更难被预测。
6.2 验证码字符难以识别
问题:验证码中的字符可能难以识别,影响用户体验。
解决方案:
- 排除易混淆字符(如0、1、O、I、l等)
- 增加字符间距
- 使用更清晰的字体
6.2.1 排除易混淆字符
// 包含易混淆字符的字符集
String allChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// 易混淆字符:0,1,O,I,l
// 排除易混淆字符的字符集
String clearChars = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
排除易混淆字符(如0、1、O、I、l等)可以显著提高验证码的可读性。
6.2.2 增加字符间距
// 生成图形验证码时增加字符间距
public BufferedImage generateImage(String code) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 24));
// 计算字符间距
int charWidth = width / (code.length() + 1);
// 绘制字符
for (int i = 0; i < code.length(); i++) {
g.setColor(getRandomColor());
g.drawString(String.valueOf(code.charAt(i)), charWidth * (i + 1), height / 2 + 8);
}
g.dispose();
return image;
}
增加字符间距可以防止字符重叠,提高可读性。
6.2.3 使用更清晰的字体
// 使用清晰的字体
g.setFont(new Font("Arial", Font.BOLD, 24));
// 或者使用无衬线字体
g.setFont(new Font("SansSerif", Font.BOLD, 24));
使用清晰的字体(如Arial、SansSerif等)可以提高字符的可读性。
6.2.4 添加干扰元素但要适度
// 添加适度的干扰线
public BufferedImage generateImageWithNoise(String code) {
BufferedImage image = generateImage(code);
Graphics2D g = image.createGraphics();
// 添加少量干扰线
for (int i = 0; i < 3; i++) {
g.setColor(getRandomColor());
g.drawLine(
random.nextInt(image.getWidth()), random.nextInt(image.getHeight()),
random.nextInt(image.getWidth()), random.nextInt(image.getHeight())
);
}
// 添加少量干扰点
for (int i = 0; i < 50; i++) {
g.setColor(getRandomColor());
g.fillOval(random.nextInt(image.getWidth()), random.nextInt(image.getHeight()), 1, 1);
}
g.dispose();
return image;
}
添加适度的干扰元素(如干扰线、干扰点)可以增加验证码的安全性,但要注意不要过度干扰,影响可读性。
6.3 验证码被暴力破解
问题:验证码可能被暴力破解,导致安全问题。
解决方案:
- 限制验证码尝试次数
- 设置验证码有效期
- 增加验证码复杂度
- 结合其他安全措施(如IP限制、设备指纹等)
6.3.1 限制验证码尝试次数
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String ATTEMPT_KEY_PREFIX = "verification:attempt:";
private static final int MAX_ATTEMPTS = 5;
public boolean verifyCode(String phone, String code) {
// 获取尝试次数
String attemptKey = ATTEMPT_KEY_PREFIX + phone;
Integer attempts = redisTemplate.opsForValue().get(attemptKey);
if (attempts != null && attempts >= MAX_ATTEMPTS) {
// 超过最大尝试次数
return false;
}
// 验证验证码
boolean verified = doVerifyCode(phone, code);
if (!verified) {
// 验证失败,增加尝试次数
if (attempts == null) {
redisTemplate.opsForValue().set(attemptKey, 1, 1, TimeUnit.HOURS);
} else {
redisTemplate.opsForValue().increment(attemptKey);
}
} else {
// 验证成功,清除尝试次数
redisTemplate.delete(attemptKey);
}
return verified;
}
}
限制验证码尝试次数可以防止暴力破解。例如,可以设置每个手机号在1小时内最多尝试5次,超过次数后需要等待1小时或重新获取验证码。
6.3.2 设置验证码有效期
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CODE_KEY_PREFIX = "verification:code:";
private static final long CODE_EXPIRE_SECONDS = 300; // 5分钟
public void saveCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
public boolean verifyCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
String savedCode = redisTemplate.opsForValue().get(codeKey);
if (savedCode == null) {
// 验证码已过期
return false;
}
// 验证码正确
boolean verified = savedCode.equals(code);
if (verified) {
// 验证成功后立即删除验证码
redisTemplate.delete(codeKey);
}
return verified;
}
}
设置验证码有效期可以限制验证码的使用时间,减少被破解的风险。例如,可以设置验证码有效期为5分钟,超过时间后需要重新获取验证码。
6.3.3 增加验证码复杂度
// 增加验证码长度和字符集复杂度
public String generateComplexCode() {
// 使用更长的验证码(8位)
int length = 8;
// 使用更复杂的字符集(字母数字混合,包含特殊字符)
String charSet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz!@#$%^&*";
return generateCustomCode(length, charSet);
}
增加验证码复杂度(长度和字符集)可以显著提高破解难度。
6.3.4 结合其他安全措施
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String IP_KEY_PREFIX = "verification:ip:";
private static final int MAX_IP_REQUESTS = 10;
private static final long IP_EXPIRE_SECONDS = 3600; // 1小时
public boolean canRequestCode(String ip) {
String ipKey = IP_KEY_PREFIX + ip;
Integer requests = redisTemplate.opsForValue().get(ipKey);
if (requests == null || requests < MAX_IP_REQUESTS) {
// 可以请求验证码
if (requests == null) {
redisTemplate.opsForValue().set(ipKey, "1", IP_EXPIRE_SECONDS, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().increment(ipKey);
}
return true;
}
// 超过IP请求限制
return false;
}
public String getDeviceFingerprint(HttpServletRequest request) {
// 获取设备指纹(浏览器信息、操作系统等)
String userAgent = request.getHeader("User-Agent");
String ip = request.getRemoteAddr();
// 简单哈希
return DigestUtils.md5Hex(userAgent + ip);
}
}
结合其他安全措施(如IP限制、设备指纹等)可以进一步提高验证码的安全性。例如,可以限制每个IP在1小时内最多请求10次验证码,或者使用设备指纹来识别不同的设备。
6.4 验证码生成性能问题
问题:在高并发场景下,验证码生成可能成为性能瓶颈。
解决方案:
- 使用缓存存储验证码
- 批量预生成验证码
- 使用更高效的随机数生成算法
6.4.1 使用缓存存储验证码
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CODE_KEY_PREFIX = "verification:code:";
private static final long CODE_EXPIRE_SECONDS = 300; // 5分钟
public void saveCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
public String getCode(String phone) {
String codeKey = CODE_KEY_PREFIX + phone;
return redisTemplate.opsForValue().get(codeKey);
}
}
使用缓存(如Redis)存储验证码可以提高验证码的读取性能,并自动处理验证码的过期。
6.4.2 批量预生成验证码
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CODE_POOL_KEY = "verification:code:pool";
private static final int POOL_SIZE = 1000;
@PostConstruct
public void initCodePool() {
// 初始化验证码池
if (redisTemplate.opsForList().size(CODE_POOL_KEY) < POOL_SIZE) {
for (int i = 0; i < POOL_SIZE; i++) {
String code = generateNumericCode(6);
redisTemplate.opsForList().rightPush(CODE_POOL_KEY, code);
}
}
}
public String getCodeFromPool() {
// 从验证码池中获取验证码
return redisTemplate.opsForList().leftPop(CODE_POOL_KEY);
}
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void replenishCodePool() {
// 补充验证码池
long size = redisTemplate.opsForList().size(CODE_POOL_KEY);
if (size < POOL_SIZE) {
for (int i = 0; i < POOL_SIZE - size; i++) {
String code = generateNumericCode(6);
redisTemplate.opsForList().rightPush(CODE_POOL_KEY, code);
}
}
}
}
批量预生成验证码可以避免在高并发场景下频繁生成验证码,提高系统性能。
6.4.3 使用更高效的随机数生成算法
// 使用ThreadLocalRandom代替SecureRandom(在非安全关键场景)
public static String generateNumericCodeWithThreadLocalRandom(int length) {
ThreadLocalRandom random = ThreadLocalRandom.current();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
code.append(random.nextInt(10));
}
return code.toString();
}
在非安全关键场景下,可以使用ThreadLocalRandom
代替SecureRandom
,提高随机数生成性能。
6.4.4 异步生成验证码
@Service
public class VerificationCodeService {
@Autowired
private AsyncTaskExecutor executor;
public CompletableFuture<String> generateCodeAsync(String phone) {
return CompletableFuture.supplyAsync(() -> {
// 异步生成验证码
String code = generateNumericCode(6);
// 保存验证码
saveCode(phone, code);
// 异步发送验证码
sendCodeAsync(phone, code);
return code;
}, executor);
}
private void sendCodeAsync(String phone, String code) {
CompletableFuture.runAsync(() -> {
// 发送验证码
smsService.sendSms(phone, "您的验证码是: " + code);
}, executor);
}
}
使用异步方式生成和发送验证码可以提高系统响应速度,避免阻塞主线程。
6.5 验证码存储安全问题
问题:验证码存储不当可能导致安全问题。
解决方案:
- 使用加密存储验证码
- 设置验证码有效期
- 验证后立即销毁验证码
- 使用安全的存储方式(如Redis)
6.5.1 使用加密存储验证码
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CODE_KEY_PREFIX = "verification:code:";
private static final String SECRET_KEY = "your-secret-key";
public void saveCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
// 加密验证码
String encryptedCode = encryptCode(code);
// 存储加密后的验证码
redisTemplate.opsForValue().set(codeKey, encryptedCode, 300, TimeUnit.SECONDS);
}
public boolean verifyCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
String encryptedCode = redisTemplate.opsForValue().get(codeKey);
if (encryptedCode == null) {
// 验证码已过期
return false;
}
// 解密验证码
String decryptedCode = decryptCode(encryptedCode);
// 验证码正确
boolean verified = decryptedCode.equals(code);
if (verified) {
// 验证成功后立即删除验证码
redisTemplate.delete(codeKey);
}
return verified;
}
private String encryptCode(String code) {
// 使用AES加密
try {
SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(code.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
throw new RuntimeException("加密验证码失败", e);
}
}
private String decryptCode(String encryptedCode) {
// 使用AES解密
try {
SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedCode));
return new String(decryptedBytes);
} catch (Exception e) {
throw new RuntimeException("解密验证码失败", e);
}
}
}
使用加密存储验证码可以防止验证码被窃取。例如,可以使用AES加密算法对验证码进行加密,只有知道密钥的人才能解密。
6.5.2 设置验证码有效期
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String CODE_KEY_PREFIX = "verification:code:";
private static final long CODE_EXPIRE_SECONDS = 300; // 5分钟
public void saveCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
}
设置验证码有效期可以限制验证码的使用时间,减少被窃取的风险。
6.5.3 验证后立即销毁验证码
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean verifyCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
String savedCode = redisTemplate.opsForValue().get(codeKey);
if (savedCode == null) {
// 验证码已过期
return false;
}
// 验证码正确
boolean verified = savedCode.equals(code);
if (verified) {
// 验证成功后立即删除验证码
redisTemplate.delete(codeKey);
}
return verified;
}
}
验证成功后立即销毁验证码可以防止验证码被重复使用。
6.5.4 使用安全的存储方式
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 使用Redis存储验证码,并设置访问控制
public void saveCode(String phone, String code) {
String codeKey = CODE_KEY_PREFIX + phone;
// 使用Redis的SET命令,并设置过期时间
redisTemplate.opsForValue().set(codeKey, code, 300, TimeUnit.SECONDS);
// 设置访问控制,只允许特定IP访问
String ipKey = CODE_KEY_PREFIX + phone + ":ip";
redisTemplate.opsForValue().set(ipKey, getCurrentIp(), 300, TimeUnit.SECONDS);
}
public boolean verifyCode(String phone, String code, String ip) {
String codeKey = CODE_KEY_PREFIX + phone;
String ipKey = CODE_KEY_PREFIX + phone + ":ip";
// 验证IP
String savedIp = redisTemplate.opsForValue().get(ipKey);
if (savedIp == null || !savedIp.equals(ip)) {
// IP不匹配
return false;
}
// 验证验证码
String savedCode = redisTemplate.opsForValue().get(codeKey);
if (savedCode == null) {
// 验证码已过期
return false;
}
// 验证码正确
boolean verified = savedCode.equals(code);
if (verified) {
// 验证成功后立即删除验证码和IP
redisTemplate.delete(codeKey);
redisTemplate.delete(ipKey);
}
return verified;
}
private String getCurrentIp() {
// 获取当前IP
// 实际应用中应从请求中获取
return "127.0.0.1";
}
}
使用安全的存储方式(如Redis)可以防止验证码被窃取。例如,可以使用Redis的访问控制功能,只允许特定IP访问验证码。
七、验证码实现最佳实践总结
7.1 安全性最佳实践
- 使用
SecureRandom
生成随机数 - 增加验证码长度和字符集复杂度
- 限制验证码尝试次数
- 设置验证码有效期
- 验证后立即销毁验证码
- 使用加密存储验证码
- 结合其他安全措施(如IP限制、设备指纹等)
7.2 性能最佳实践
- 使用缓存存储验证码
- 批量预生成验证码
- 在非安全关键场景下使用
ThreadLocalRandom
- 异步生成和发送验证码
7.3 用户体验最佳实践
- 排除易混淆字符
- 增加字符间距
- 使用清晰的字体
- 添加适度的干扰元素
- 提供刷新功能
- 错误提示友好
7.4 可维护性最佳实践
- 使用可配置的验证码参数
- 将验证码生成逻辑封装为服务
- 使用注解或配置文件管理验证码规则
- 提供详细的日志记录和监控