“mitao”
2026-03-27 cde5977f64ff1dfa928d1076c059e8865c02d70b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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;
        }
    }
}