package com.tcm.training.admin.auth.service;
|
|
import com.tcm.training.admin.auth.vo.CaptchaResponse;
|
import org.springframework.stereotype.Service;
|
|
import java.nio.charset.StandardCharsets;
|
import java.util.Base64;
|
import java.util.Map;
|
import java.util.UUID;
|
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ThreadLocalRandom;
|
|
@Service
|
public class CaptchaService {
|
|
private static final long EXPIRE_MILLIS = 5 * 60 * 1000L;
|
private static final char[] CAPTCHA_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ".toCharArray();
|
|
private final Map<String, CaptchaEntry> captchaStore = new ConcurrentHashMap<String, CaptchaEntry>();
|
|
public CaptchaResponse createCaptcha() {
|
clearExpired();
|
|
String code = randomCode(4);
|
String captchaId = UUID.randomUUID().toString().replace("-", "");
|
captchaStore.put(captchaId, new CaptchaEntry(code, System.currentTimeMillis() + EXPIRE_MILLIS));
|
|
String svg = buildSvg(code);
|
String imageData = "data:image/svg+xml;base64,"
|
+ Base64.getEncoder().encodeToString(svg.getBytes(StandardCharsets.UTF_8));
|
return new CaptchaResponse(captchaId, imageData);
|
}
|
|
public void validate(String captchaId, String captchaCode) {
|
clearExpired();
|
|
CaptchaEntry entry = captchaStore.remove(captchaId);
|
if (entry == null) {
|
throw new IllegalArgumentException("验证码已失效,请刷新后重试");
|
}
|
if (entry.expireAt < System.currentTimeMillis()) {
|
throw new IllegalArgumentException("验证码已过期,请刷新后重试");
|
}
|
if (!entry.code.equalsIgnoreCase(captchaCode)) {
|
throw new IllegalArgumentException("验证码错误");
|
}
|
}
|
|
private void clearExpired() {
|
long now = System.currentTimeMillis();
|
captchaStore.entrySet().removeIf(entry -> entry.getValue().expireAt < now);
|
}
|
|
private String randomCode(int length) {
|
StringBuilder builder = new StringBuilder(length);
|
ThreadLocalRandom random = ThreadLocalRandom.current();
|
for (int i = 0; i < length; i++) {
|
builder.append(CAPTCHA_CHARS[random.nextInt(CAPTCHA_CHARS.length)]);
|
}
|
return builder.toString();
|
}
|
|
private String buildSvg(String code) {
|
ThreadLocalRandom random = ThreadLocalRandom.current();
|
StringBuilder builder = new StringBuilder();
|
builder.append("<svg xmlns='http://www.w3.org/2000/svg' width='132' height='44' viewBox='0 0 132 44'>");
|
builder.append("<defs><linearGradient id='g' x1='0%' y1='0%' x2='100%' y2='100%'>");
|
builder.append("<stop offset='0%' stop-color='#f7f9fc'/>");
|
builder.append("<stop offset='100%' stop-color='#e6edf7'/></linearGradient></defs>");
|
builder.append("<rect width='132' height='44' rx='10' fill='url(#g)'/>");
|
|
for (int i = 0; i < 6; i++) {
|
builder.append("<line x1='").append(random.nextInt(132)).append("' y1='").append(random.nextInt(44))
|
.append("' x2='").append(random.nextInt(132)).append("' y2='").append(random.nextInt(44))
|
.append("' stroke='rgba(100,116,139,0.35)' stroke-width='1'/>");
|
}
|
|
for (int i = 0; i < 16; i++) {
|
builder.append("<circle cx='").append(random.nextInt(132)).append("' cy='").append(random.nextInt(44))
|
.append("' r='").append(1 + random.nextInt(2)).append("' fill='rgba(59,130,246,0.18)'/>");
|
}
|
|
for (int i = 0; i < code.length(); i++) {
|
int x = 18 + i * 24;
|
int y = 29 + random.nextInt(6);
|
int rotate = random.nextInt(-18, 19);
|
String color = random.nextBoolean() ? "#1f2937" : "#2563eb";
|
builder.append("<text x='").append(x).append("' y='").append(y)
|
.append("' font-size='24' font-family='Arial' font-weight='700' fill='").append(color)
|
.append("' transform='rotate(").append(rotate).append(" ").append(x).append(" ").append(y)
|
.append(")'>").append(code.charAt(i)).append("</text>");
|
}
|
|
builder.append("</svg>");
|
return builder.toString();
|
}
|
|
private static class CaptchaEntry {
|
private final String code;
|
private final long expireAt;
|
|
private CaptchaEntry(String code, long expireAt) {
|
this.code = code;
|
this.expireAt = expireAt;
|
}
|
}
|
}
|