liu
2026-09-22 0837c3ff473499d8a267924125c776b34cc8b517
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
108
109
110
111
112
113
package com.ruoyi.web.controller.miniapp;
 
import java.awt.image.BufferedImage;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.FastByteArrayOutputStream;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.service.ISysConfigService;
import jakarta.annotation.Resource;
 
/**
 * 学生端图形验证码:生成 + 一次性消费校验。
 * 供 MiniappAccountController(登录/注册/找回)与 MiniappController(绑定/开放登记)共用,
 * 堵死"无验证码枚举花名册"的匿名爆破面。
 */
@Service
public class MiniappCaptchaService
{
    /** 学生端独立验证码 key 前缀(不与管理端 captcha_codes: 互串) */
    public static final String CAPTCHA_KEY = "miniapp_captcha:";
 
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;
 
    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;
 
    @Autowired private RedisCache redisCache;
    @Autowired private ISysConfigService configService;
 
    /** 生成验证码,返回 { captchaEnabled, uuid, img };开关与管理端一致 */
    public AjaxResult generate()
    {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }
        String uuid = IdUtils.simpleUUID();
        String capStr;
        String code;
        BufferedImage image;
        if ("math".equals(RuoYiConfig.getCaptchaType()))
        {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        redisCache.setCacheObject(CAPTCHA_KEY + uuid, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        try (FastByteArrayOutputStream os = new FastByteArrayOutputStream())
        {
            ImageIO.write(image, "jpg", os);
            ajax.put("uuid", uuid);
            ajax.put("img", Base64.encode(os.toByteArray()));
        }
        catch (Exception e)
        {
            return AjaxResult.error("验证码生成失败");
        }
        return ajax;
    }
 
    /** 一次性校验:通过返回 null,否则返回错误消息(先删 key 防重放) */
    public String verify(String code, String uuid)
    {
        if (!configService.selectCaptchaEnabled()) return null;
        if (code == null || code.trim().isEmpty() || uuid == null || uuid.trim().isEmpty()) return "请输入图形验证码";
        String key = CAPTCHA_KEY + uuid;
        Object expected = redisCache.getCacheObject(key);
        if (expected == null) return "验证码已失效,请刷新后重试";
        redisCache.deleteObject(key);
        if (!code.trim().equalsIgnoreCase(expected.toString())) return "验证码错误";
        return null;
    }
 
    /** 失败计数:固定 10 分钟窗口内累计(首次失败设定窗口,后续只增量,不滚存续命) */
    public int incrFail(String key, int max)
    {
        long cnt = redisCache.redisTemplate.opsForValue().increment(key);
        if (cnt == 1L)
        {
            redisCache.redisTemplate.expire(key, 10, TimeUnit.MINUTES);
        }
        return (int) cnt;
    }
 
    public int failCount(String key)
    {
        Object v = redisCache.getCacheObject(key);
        return v instanceof Number ? ((Number) v).intValue() : 0;
    }
 
    public void clearFail(String key)
    {
        redisCache.deleteObject(key);
    }
}