feat(安全): 添加验证码和登录安全增强功能

新增验证码功能用于敏感操作,包括删除账号、修改密码等
添加登录失败锁定机制和限流策略
实现防重放攻击和XSS防护增强
重构XSS拦截器使用请求包装器
This commit is contained in:
ikmkj
2026-03-03 17:49:50 +08:00
parent 5a24569ebd
commit 23929a974f
13 changed files with 763 additions and 26 deletions

View File

@@ -0,0 +1,201 @@
package com.test.bijihoudaun.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 图形验证码工具类
* 使用本地内存存储验证码
*/
public class CaptchaUtil {
// 验证码有效期(分钟)
private static final int CAPTCHA_EXPIRE_MINUTES = 5;
// 验证码长度
private static final int CAPTCHA_LENGTH = 4;
// 图片宽度
private static final int IMAGE_WIDTH = 120;
// 图片高度
private static final int IMAGE_HEIGHT = 40;
// 存储验证码key=验证码IDvalue=验证码记录
private static final Map<String, CaptchaRecord> captchaStore = new ConcurrentHashMap<>();
// 安全随机数生成器
private static final SecureRandom random = new SecureRandom();
private static class CaptchaRecord {
String code;
LocalDateTime createTime;
CaptchaRecord(String code) {
this.code = code;
this.createTime = LocalDateTime.now();
}
boolean isExpired() {
return ChronoUnit.MINUTES.between(createTime, LocalDateTime.now()) > CAPTCHA_EXPIRE_MINUTES;
}
}
/**
* 验证码结果
*/
public static class CaptchaResult {
private String captchaId;
private String base64Image;
public CaptchaResult(String captchaId, String base64Image) {
this.captchaId = captchaId;
this.base64Image = base64Image;
}
public String getCaptchaId() {
return captchaId;
}
public String getBase64Image() {
return base64Image;
}
}
/**
* 生成验证码
* @return 包含验证码ID和Base64图片的结果
*/
public static CaptchaResult generateCaptcha() {
// 清理过期验证码
cleanupExpiredCaptchas();
// 生成验证码ID
String captchaId = UuidV7.generate().toString();
// 生成验证码字符
String code = generateCode();
// 生成图片
String base64Image = generateImage(code);
// 存储验证码
captchaStore.put(captchaId, new CaptchaRecord(code));
return new CaptchaResult(captchaId, base64Image);
}
/**
* 验证验证码
* @param captchaId 验证码ID
* @param code 用户输入的验证码
* @return 验证成功返回true失败返回false
*/
public static boolean validateCaptcha(String captchaId, String code) {
if (captchaId == null || code == null) {
return false;
}
CaptchaRecord record = captchaStore.get(captchaId);
if (record == null || record.isExpired()) {
// 验证码不存在或已过期,移除
if (record != null) {
captchaStore.remove(captchaId);
}
return false;
}
// 验证码比对(不区分大小写)
boolean success = record.code.equalsIgnoreCase(code);
// 验证成功后立即删除(一次性使用)
if (success) {
captchaStore.remove(captchaId);
}
return success;
}
/**
* 生成随机验证码
*/
private static String generateCode() {
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CAPTCHA_LENGTH; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
/**
* 生成验证码图片Base64格式
*/
private static String generateImage(String code) {
BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
// 绘制干扰线
g.setColor(Color.LIGHT_GRAY);
for (int i = 0; i < 5; i++) {
int x1 = random.nextInt(IMAGE_WIDTH);
int y1 = random.nextInt(IMAGE_HEIGHT);
int x2 = random.nextInt(IMAGE_WIDTH);
int y2 = random.nextInt(IMAGE_HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
// 绘制验证码字符
g.setFont(new Font("Arial", Font.BOLD, 24));
int x = 15;
for (int i = 0; i < code.length(); i++) {
// 随机颜色
g.setColor(new Color(
random.nextInt(100),
random.nextInt(100),
random.nextInt(100)
));
// 随机旋转角度
int angle = random.nextInt(30) - 15;
g.rotate(Math.toRadians(angle), x + 10, 25);
g.drawString(String.valueOf(code.charAt(i)), x, 28);
g.rotate(-Math.toRadians(angle), x + 10, 25);
x += 25;
}
// 添加噪点
for (int i = 0; i < 30; i++) {
int x1 = random.nextInt(IMAGE_WIDTH);
int y1 = random.nextInt(IMAGE_HEIGHT);
image.setRGB(x1, y1, Color.GRAY.getRGB());
}
g.dispose();
// 转为 Base64
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes);
} catch (IOException e) {
throw new RuntimeException("生成验证码图片失败", e);
}
}
/**
* 清理过期验证码
*/
private static void cleanupExpiredCaptchas() {
captchaStore.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
}

View File

@@ -0,0 +1,131 @@
package com.test.bijihoudaun.util;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ConcurrentHashMap;
/**
* 登录锁定工具类
* 使用本地内存存储登录失败记录
*/
public class LoginLockUtil {
// 最大失败次数
private static final int MAX_FAILED_ATTEMPTS = 5;
// 锁定时间(分钟)
private static final int LOCK_TIME_MINUTES = 30;
// 失败记录过期时间(分钟)
private static final int RECORD_EXPIRE_MINUTES = 60;
// 登录失败记录key=用户名value=失败记录
private static final ConcurrentHashMap<String, LoginAttempt> attempts = new ConcurrentHashMap<>();
private static class LoginAttempt {
int failedCount;
LocalDateTime lastAttemptTime;
LocalDateTime lockUntil;
LoginAttempt() {
this.failedCount = 0;
this.lastAttemptTime = LocalDateTime.now();
this.lockUntil = null;
}
}
/**
* 记录登录失败
* @param username 用户名
*/
public static void recordFailedAttempt(String username) {
if (username == null || username.isEmpty()) return;
cleanupExpiredRecords();
LoginAttempt attempt = attempts.computeIfAbsent(username, k -> new LoginAttempt());
attempt.failedCount++;
attempt.lastAttemptTime = LocalDateTime.now();
// 达到最大失败次数,锁定账号
if (attempt.failedCount >= MAX_FAILED_ATTEMPTS) {
attempt.lockUntil = LocalDateTime.now().plusMinutes(LOCK_TIME_MINUTES);
}
}
/**
* 记录登录成功(清除失败记录)
* @param username 用户名
*/
public static void recordSuccess(String username) {
if (username == null || username.isEmpty()) return;
attempts.remove(username);
}
/**
* 检查账号是否被锁定
* @param username 用户名
* @return true-已锁定false-未锁定
*/
public static boolean isLocked(String username) {
if (username == null || username.isEmpty()) return false;
LoginAttempt attempt = attempts.get(username);
if (attempt == null) return false;
// 检查是否仍在锁定时间内
if (attempt.lockUntil != null) {
if (LocalDateTime.now().isBefore(attempt.lockUntil)) {
return true;
} else {
// 锁定时间已过,清除记录
attempts.remove(username);
return false;
}
}
return false;
}
/**
* 获取剩余锁定时间(秒)
* @param username 用户名
* @return 剩余秒数如果未锁定返回0
*/
public static long getRemainingLockTime(String username) {
if (username == null || username.isEmpty()) return 0;
LoginAttempt attempt = attempts.get(username);
if (attempt == null || attempt.lockUntil == null) return 0;
long remaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), attempt.lockUntil);
return Math.max(0, remaining);
}
/**
* 获取剩余失败次数
* @param username 用户名
* @return 剩余次数
*/
public static int getRemainingAttempts(String username) {
if (username == null || username.isEmpty()) return MAX_FAILED_ATTEMPTS;
LoginAttempt attempt = attempts.get(username);
if (attempt == null) return MAX_FAILED_ATTEMPTS;
return Math.max(0, MAX_FAILED_ATTEMPTS - attempt.failedCount);
}
/**
* 清理过期记录
*/
private static void cleanupExpiredRecords() {
LocalDateTime now = LocalDateTime.now();
attempts.entrySet().removeIf(entry -> {
LoginAttempt attempt = entry.getValue();
// 未锁定且长时间没有登录的记录
if (attempt.lockUntil == null) {
return ChronoUnit.MINUTES.between(attempt.lastAttemptTime, now) > RECORD_EXPIRE_MINUTES;
}
// 锁定已过期的记录
return now.isAfter(attempt.lockUntil);
});
}
}