/**
* 用于简化与验证码相关的操作,如发送验证码、校验验证码
*
* @author NightDW 2022/3/2 16:05
*/
@SuppressWarnings("All")
public abstract class AbstractVerifyCoder {
/** 默认的key的前缀,以及默认的项目名称 */
private static final String VERIFY_CODE_KEY_PREFIX = "VERIFY_CODER:VerifyCode";
private static final String FREQUENT_CHECK_ONLY_CHECK_MODE_KEY_PREFIX = "VERIFY_CODER:FrequentCheck_OnlyCheckMode";
private static final String FREQUENT_CHECK_DELETE_IF_CORRECT_KEY_PREFIX = "VERIFY_CODER:FrequentCheck_DeleteIfCorrectMode";
private static final String DEFAULT_PROJECT_ID = "verify-code-project";
// =================================================================================================================
/** 新增一个键值对(一般是在NoSql上),并设置其过期时间 */
protected abstract void set(String key, String value, int expireSeconds);
/** 获取key的剩余过期时间,返回null则说明key不存在 */
protected abstract Long getRemainExpire(String key);
/** 获取key对应的value */
protected abstract String get(String key);
/** 删除key */
protected abstract void delete(String key);
/** 发送验证码(比如通过短信方式等),返回值代表是否发送成功 */
protected abstract boolean doSendVerifyCode(String receiver, String codeType, String verifyCode);
// =================================================================================================================
/** 获取项目的id;项目id将作为key的一部分;建议重写该方法,避免多个项目的验证码功能相互干扰 */
protected String getProjectId() { return DEFAULT_PROJECT_ID; }
/** 验证码的过期时间 */
protected int getVerifyCodeExpireSeconds() { return 15 * 60; }
/** 发送验证码后,经过多久才可以再次发送;该值不能大于验证码的过期时间 */
protected int getResendGapSeconds() { return 60; }
/** 校验验证码失败后,经过多久才可以再次校验 */
protected int getRecheckGapSeconds() { return 3; }
/** 生成验证码 */
protected String generateVerifyCode() { return String.valueOf((int)(Math.random() * Math.pow(10, 6))); }
/** 获取key的前缀 */
protected String getVerifyCodeKeyPrefix() { return VERIFY_CODE_KEY_PREFIX; }
protected String getFrequentCheckOnlyCheckModeKeyPrefix() { return FREQUENT_CHECK_ONLY_CHECK_MODE_KEY_PREFIX; }
protected String getFrequentCheckDeleteIfCorrectKeyPrefix() { return FREQUENT_CHECK_DELETE_IF_CORRECT_KEY_PREFIX; }
// =================================================================================================================
/** 生成验证码,并向接收者发送验证码消息 */
public final void sendVerifyCode(String codeReceiver, String codeType) {
String verifyCodeKey = getRedisKey(getVerifyCodeKeyPrefix(), getProjectId(), codeType, codeReceiver);
int initExpire = getVerifyCodeExpireSeconds();
int resendGap = getResendGapSeconds();
Long remainExpire = getRemainExpire(verifyCodeKey);
if (remainExpire != null && (initExpire - remainExpire < resendGap)) {
throw new RuntimeException(resendGap + "秒内只能发送一次验证码!");
}
String verifyCode = generateVerifyCode();
if (doSendVerifyCode(codeReceiver, codeType, verifyCode)){
set(verifyCodeKey, verifyCode, initExpire);
} else {
throw new RuntimeException("验证码发送失败!");
}
}
public final boolean checkVerifyCode(String codeReceiver, String codeType, String code, boolean deleteIfCorrect) {
if (isBlank(code)) {
return false;
}
String verifyCodeKey = getRedisKey(getVerifyCodeKeyPrefix(), getProjectId(), codeType, codeReceiver);
String trueCode = get(verifyCodeKey);
if (isBlank(trueCode)) {
throw new RuntimeException("验证码过期,或者未发送验证码!");
}
String frequentCheckKey = getRedisKey(deleteIfCorrect ? getFrequentCheckDeleteIfCorrectKeyPrefix() : getFrequentCheckOnlyCheckModeKeyPrefix(), getProjectId(), codeType, codeReceiver);
int recheckGap = getRecheckGapSeconds();
if (!isBlank(get(frequentCheckKey))) {
throw new RuntimeException("校验操作频繁,请在" + recheckGap + "秒后重试!");
}
if (!code.equals(trueCode)) {
set(frequentCheckKey, "校验cd重置中", recheckGap);
return false;
}
if (deleteIfCorrect) {
delete(verifyCodeKey);
}
return true;
}
// =================================================================================================================
private static String getRedisKey(String prefix, String projectId, String codeType, String codeReceiver) {
return prefix + ":" + projectId + ":" + codeType + ":" + codeReceiver;
}
private static boolean isBlank(final CharSequence cs) {
if (cs == null) return true;
final int strLen = cs.length();
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
}
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 使用Redis作为NoSql的VerifyCoder
*
* @author NightDW 2022/3/3 16:06
*/
public abstract class RedisVerifyCoder extends AbstractVerifyCoder {
protected abstract RedisTemplate<String, String> getRedisTemplate();
@Override
protected final void set(String key, String value, int expireSeconds) {
getRedisTemplate().opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
}
@Override
protected final Long getRemainExpire(String key) {
return getRedisTemplate().getExpire(key);
}
@Override
protected final String get(String key) {
return getRedisTemplate().opsForValue().get(key);
}
@Override
protected final void delete(String key) {
getRedisTemplate().delete(key);
}
}