longz
2026-06-02 937f99a139bedbacdf15e25cfd55aa592f56082c
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
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);
        }
    }
}