| | |
| | | @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; |
| | |
| | | * 生成并发送登录验证码,同时写入缓存。 |
| | | */ |
| | | 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(phone), |
| | | buildCacheKey(cachePrefix, phone), |
| | | code, |
| | | appSmsAuthProperties.getExpireSeconds(), |
| | | TimeUnit.SECONDS |
| | | ); |
| | | appSmsSender.sendLoginCode(phone, code); |
| | | return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds()); |
| | | } |
| | | |
| | | /** |
| | | * 校验登录验证码,成功后销毁缓存中的一次性验证码。 |
| | | * 统一校验指定场景的验证码,验证码仅按缓存 TTL 自然失效。 |
| | | */ |
| | | public void validateLoginCode(String phone, String code) { |
| | | if (matchesDebugCode(code)) { |
| | | // 开发环境允许固定验证码直通,便于前后端联调。 |
| | | private void validateCode(String phone, String code, String cachePrefix, String scene) { |
| | | if (matchesDebugCode(scene, code)) { |
| | | // 调试验证码按场景控制,生产环境仅放开短信登录场景。 |
| | | return; |
| | | } |
| | | String cacheKey = buildCacheKey(phone); |
| | | 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("验证码错误"); |
| | | } |
| | | // 验证码登录成功后立即删除,避免重复使用。 |
| | | stringRedisTemplate.delete(cacheKey); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 判断是否命中本地调试验证码。 |
| | | * 判断当前场景是否命中调试验证码。 |
| | | */ |
| | | private boolean matchesDebugCode(String code) { |
| | | if (!appSmsAuthProperties.isDebugEnabled()) { |
| | | private boolean matchesDebugCode(String scene, String code) { |
| | | AppSmsAuthProperties.DebugScene debugScene = resolveDebugScene(scene); |
| | | if (debugScene == null || !debugScene.isEnabled()) { |
| | | return false; |
| | | } |
| | | String debugCode = appSmsAuthProperties.getDebugCode(); |
| | | 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 phone) { |
| | | return LOGIN_CODE_CACHE_PREFIX + phone; |
| | | private String buildCacheKey(String cachePrefix, String phone) { |
| | | return cachePrefix + phone; |
| | | } |
| | | } |