13404089107
2026-04-24 8b81df3a7638e768b640b7935d9da52b77cd7649
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package com.tcm.training.admin.app.auth.service;
 
import com.tcm.training.admin.app.auth.config.AppSmsAuthProperties;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
 
/**
 * APP 登录短信验证码服务,负责验证码的生成、缓存和校验。
 */
@Service
public class AppSmsCodeService {
 
    private static final String SCENE_LOGIN = "login";
    private static final String SCENE_REGISTER = "register";
    private static final String SCENE_PASSWORD_RESET = "password-reset";
    private static final String SCENE_PHONE_CHANGE = "phone-change";
    private static final String LOGIN_CODE_CACHE_PREFIX = "app:sms:login:";
    private static final String REGISTER_CODE_CACHE_PREFIX = "app:sms:register:";
    private static final String PASSWORD_RESET_CODE_CACHE_PREFIX = "app:sms:password-reset:";
    private static final String PHONE_CHANGE_CODE_CACHE_PREFIX = "app:sms:phone-change:";
 
    private final StringRedisTemplate stringRedisTemplate;
    private final AppSmsSender appSmsSender;
    private final AppSmsAuthProperties appSmsAuthProperties;
 
    /**
     * 注入验证码缓存、发送器与调试配置。
     */
    public AppSmsCodeService(StringRedisTemplate stringRedisTemplate,
                             AppSmsSender appSmsSender,
                             AppSmsAuthProperties appSmsAuthProperties) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.appSmsSender = appSmsSender;
        this.appSmsAuthProperties = appSmsAuthProperties;
    }
 
    /**
     * 生成并发送登录验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createLoginCode(String phone) {
        return createCode(phone, LOGIN_CODE_CACHE_PREFIX, "login");
    }
 
    /**
     * 生成并发送注册验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createRegisterCode(String phone) {
        return createCode(phone, REGISTER_CODE_CACHE_PREFIX, "register");
    }
 
    /**
     * 生成并发送忘记密码验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createPasswordResetCode(String phone) {
        return createCode(phone, PASSWORD_RESET_CODE_CACHE_PREFIX, "password-reset");
    }
 
    /**
     * 生成并发送修改手机号验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createPhoneChangeCode(String phone) {
        return createCode(phone, PHONE_CHANGE_CODE_CACHE_PREFIX, "phone-change");
    }
 
    /**
     * 校验登录验证码,缓存有效期内允许重复提交。
     */
    public void validateLoginCode(String phone, String code) {
        validateCode(phone, code, LOGIN_CODE_CACHE_PREFIX, SCENE_LOGIN);
    }
 
    /**
     * 校验注册验证码,缓存有效期内允许重复提交。
     */
    public void validateRegisterCode(String phone, String code) {
        validateCode(phone, code, REGISTER_CODE_CACHE_PREFIX, SCENE_REGISTER);
    }
 
    /**
     * 校验忘记密码验证码,缓存有效期内允许重复提交。
     */
    public void validatePasswordResetCode(String phone, String code) {
        validateCode(phone, code, PASSWORD_RESET_CODE_CACHE_PREFIX, SCENE_PASSWORD_RESET);
    }
 
    /**
     * 校验修改手机号验证码,缓存有效期内允许重复提交。
     */
    public void validatePhoneChangeCode(String phone, String code) {
        validateCode(phone, code, PHONE_CHANGE_CODE_CACHE_PREFIX, SCENE_PHONE_CHANGE);
    }
 
    /**
     * 统一生成指定场景的验证码并写入缓存。
     */
    private AppSmsCodeSendResponse createCode(String phone, String cachePrefix, String scene) {
        String code = generateCode();
        if (SCENE_LOGIN.equals(scene)) {
            appSmsSender.sendLoginCode(phone, code);
        } else if (SCENE_REGISTER.equals(scene)) {
            appSmsSender.sendRegisterCode(phone, code);
        } else if (SCENE_PASSWORD_RESET.equals(scene)) {
            appSmsSender.sendPasswordResetCode(phone, code);
        } else {
            appSmsSender.sendPhoneChangeCode(phone, code);
        }
        stringRedisTemplate.opsForValue().set(
                buildCacheKey(cachePrefix, phone),
                code,
                appSmsAuthProperties.getExpireSeconds(),
                TimeUnit.SECONDS
        );
        return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds());
    }
 
    /**
     * 统一校验指定场景的验证码,验证码仅按缓存 TTL 自然失效。
     */
    private void validateCode(String phone, String code, String cachePrefix, String scene) {
        if (matchesDebugCode(scene, code)) {
            // 调试验证码按场景控制,生产环境仅放开短信登录场景。
            return;
        }
        String cacheKey = buildCacheKey(cachePrefix, phone);
        String cachedCode = stringRedisTemplate.opsForValue().get(cacheKey);
        if (cachedCode == null || cachedCode.trim().isEmpty()) {
            throw new IllegalArgumentException("验证码已失效,请重新获取");
        }
        if (!cachedCode.equals(code)) {
            throw new IllegalArgumentException("验证码错误");
        }
    }
 
    /**
     * 生成 6 位数字验证码。
     */
    private String generateCode() {
        int code = ThreadLocalRandom.current().nextInt(100000, 1000000);
        return String.valueOf(code);
    }
 
    /**
     * 判断当前场景是否命中调试验证码。
     */
    private boolean matchesDebugCode(String scene, String code) {
        AppSmsAuthProperties.DebugScene debugScene = resolveDebugScene(scene);
        if (debugScene == null || !debugScene.isEnabled()) {
            return false;
        }
        String debugCode = debugScene.getCode();
        return debugCode != null && !debugCode.trim().isEmpty() && debugCode.equals(code);
    }
 
    /**
     * 读取指定场景的调试验证码配置。
     */
    private AppSmsAuthProperties.DebugScene resolveDebugScene(String scene) {
        if (SCENE_LOGIN.equals(scene)) {
            return appSmsAuthProperties.getLogin();
        }
        if (SCENE_REGISTER.equals(scene)) {
            return appSmsAuthProperties.getRegister();
        }
        if (SCENE_PASSWORD_RESET.equals(scene)) {
            return appSmsAuthProperties.getPasswordReset();
        }
        if (SCENE_PHONE_CHANGE.equals(scene)) {
            return appSmsAuthProperties.getPhoneChange();
        }
        return null;
    }
 
    /**
     * 构造手机号维度的验证码缓存 key。
     */
    private String buildCacheKey(String cachePrefix, String phone) {
        return cachePrefix + phone;
    }
}