package com.sinata.appointment.service;
|
|
import com.sinata.appointment.constants.BizErrorCode;
|
import com.sinata.appointment.dto.CaptchaResponse;
|
import com.sinata.appointment.exception.BizException;
|
import java.awt.BasicStroke;
|
import java.awt.Color;
|
import java.awt.Font;
|
import java.awt.Graphics2D;
|
import java.awt.RenderingHints;
|
import java.awt.image.BufferedImage;
|
import java.io.ByteArrayOutputStream;
|
import java.security.SecureRandom;
|
import java.time.Duration;
|
import java.util.Base64;
|
import java.util.UUID;
|
import javax.imageio.ImageIO;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.stereotype.Service;
|
|
/** 管理端登录图形验证码:Redis 存储,一次性校验。 */
|
@Service
|
@RequiredArgsConstructor
|
public class CaptchaService {
|
|
private static final String KEY_PREFIX = "auth:captcha:";
|
private static final Duration TTL = Duration.ofMinutes(2);
|
/** 易混淆字符已排除:0/O、1/l/I */
|
private static final String CHAR_POOL = "23456789ABCDEFGHJKMNPQRSTUVWXYZ";
|
private static final int CODE_LEN = 4;
|
private static final int W = 112;
|
private static final int H = 40;
|
|
private final StringRedisTemplate redisTemplate;
|
private final SecureRandom random = new SecureRandom();
|
|
public CaptchaResponse generate() {
|
String code = randomCode();
|
String captchaId = UUID.randomUUID().toString();
|
redisTemplate.opsForValue().set(KEY_PREFIX + captchaId, code.toLowerCase(), TTL);
|
String imageBase64 = renderPngBase64(code);
|
return CaptchaResponse.builder().captchaId(captchaId).imageBase64(imageBase64).build();
|
}
|
|
public void validateAndConsume(String captchaId, String captchaCode) {
|
if (captchaId == null || captchaId.isBlank()
|
|| captchaCode == null || captchaCode.isBlank()) {
|
throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "请填写验证码");
|
}
|
String key = KEY_PREFIX + captchaId;
|
String expected = redisTemplate.opsForValue().get(key);
|
if (expected == null) {
|
throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "验证码已失效,请刷新");
|
}
|
redisTemplate.delete(key);
|
if (!expected.equalsIgnoreCase(captchaCode.trim())) {
|
throw new BizException(BizErrorCode.AUTH_CAPTCHA_INVALID, "验证码错误");
|
}
|
}
|
|
private String randomCode() {
|
StringBuilder sb = new StringBuilder(CODE_LEN);
|
for (int i = 0; i < CODE_LEN; i++) {
|
sb.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length())));
|
}
|
return sb.toString();
|
}
|
|
private String renderPngBase64(String code) {
|
BufferedImage image = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
|
Graphics2D g = image.createGraphics();
|
g.setColor(new Color(245, 246, 250));
|
g.fillRect(0, 0, W, H);
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
for (int i = 0; i < 6; i++) {
|
g.setColor(new Color(random.nextInt(180), random.nextInt(180), random.nextInt(180), 80));
|
g.setStroke(new BasicStroke(1f));
|
g.drawLine(random.nextInt(W), random.nextInt(H), random.nextInt(W), random.nextInt(H));
|
}
|
g.setFont(new Font("SansSerif", Font.BOLD, 28));
|
for (int i = 0; i < code.length(); i++) {
|
g.setColor(new Color(random.nextInt(80), random.nextInt(80), random.nextInt(80)));
|
int x = 18 + i * 22;
|
int y = 28 + random.nextInt(5) - 2;
|
g.drawString(String.valueOf(code.charAt(i)), x, y);
|
}
|
g.dispose();
|
try {
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ImageIO.write(image, "png", out);
|
return Base64.getEncoder().encodeToString(out.toByteArray());
|
} catch (Exception e) {
|
throw new IllegalStateException("生成验证码图片失败", e);
|
}
|
}
|
}
|