概述
总所周知,验证码方式的登录模式十分的普遍,不过 Spring Security
并没有提供比较好的原生解决方案,但是我们可以 do it by ourselves!
,本文的篇幅相对比较长,因此分上下篇分别来介绍。上篇主要介绍:验证码的生成,下篇对自定义验证码登录的流程进行讲解。
我们比较常见的验证码主要有两种:图形验证码以及短信验证码,相对来说不是特别的复杂。可能会有人有疑惑:为什么简单的验证码生成需要花费一整篇幅来介绍呢?原因当然是:身为菜鸟的我也有一个架构师的梦!验证码的生成会结合模板方法模式一起讲解。
初探模板方法模式
模板方法模式属于一种行为型的设计模式,主要是用来解决复用和扩展两个问题。
模板方法模式在一个方法中定义一个算法骨架
,并将某些步骤推迟到某些子类中实现。该模式可以让子类在不改变算法整体结构的情况,重新定义算法中的某些步骤细节。
这里提到了一个算法骨架的概念,算法
并非是指数据结构中的“算法”,可以理解为广义上的业务逻辑; 骨架
架子其实就是模板;总的来说:算法骨架
可以理解为包含广义业务逻辑的模板方法。
实践出真知
绝大部分的设计模式的原理都十分的简单,难得是将原理落实到实践中,解决实际问题。
我们知道模板方法模式主要是用来解决 复用
和 扩展
这两个问题,结合到实际情况中来分析;验证码生成有哪些地方需要 复用
和 扩展
呢?
让我们来梳理一下验证码登录模式的流程,无论是短信验证码还是图形验证码,大致上都有如下步骤: 生成验证码、存储、发送、校验;既然流程上相同,那么就能做到复用
。而 扩展
并非是指代码的扩展性,而是指框架上的扩展性,模板方法模式可以让使用者在不修改骨架源码的情况下,定制化扩展功能。
废话不多说,接下来就来瞅瞅模板方法模式在验证码生成模块的落地情况吧!还是老规矩,先上图:
验证码的生成主要分3个模块:骨架模块、验证码生命周期模块、具体验证码模块(短信验证码和图形验证码)
-
骨架模块主要包含
ValidateCodeProcessor
接口以及AbstractValidateCodeProcessor
抽象类;封装了验证码相关的可复用的业务逻辑。 -
验证码生命周期模块是指:验证码的生成、存储、发送。
-
具体验证码模块涉及短信验证码和图形验证码,基于骨架重新定义自己的相关实现。
验证码骨架
无论是图形验证码还是短信验证码,验证码的相关业务逻辑(算法骨架
)都是大同小异的;主要是验证码的 创建流程
和 验证流程
。因此使用模板方法模式,对可复用的业务逻辑进行抽离,封装成一个骨架。
ValidateCodeProcessor.class
/**
* 校验码处理器 封装不同验证码的处理逻辑
*
* @author 小奇
* @date 2020/09/26
*/
public interface ValidateCodeProcessor {
/**
* 创建验证码
* 1.生成验证码 2.存储 3.发送
*
* @param res http请求的request和response封装
* @throws Exception
*/
void create(ServletWebRequest res) throws Exception;
/**
* 校验验证码
*
* @param res
*/
void validate(ServletWebRequest res);
}
ValidateCodeProcessor
接口定义了2个方法:create()
方法,用于验证码的生成, validate()
方法用于验证码的校验。
AbstractValidateCodeProcessor.class
/**
* 抽象方法模式——算法骨架
* 对验证码的一些公有的业务逻辑进行抽离,做到复用
*
* @author 小奇
* @date 2020/09/26
**/
@Slf4j
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
/**
* 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
*/
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGeneratorMap;
/**
* 验证码的存储介质
*/
@Autowired
private ValidateCodeRepository validateCodeRepository;
private static final String SMS = "sms", IMAGE = "image";
@Override
public void create(ServletWebRequest res) throws Exception {
// 生成
C validateCode = generate(res);
// 存储
save(res, validateCode);
// 发送 (抽象方法 由具体的子类实现各自的发送逻辑)
send(res, validateCode);
}
@Override
public void validate(ServletWebRequest res) {
// 根据请求获取验证码的类型,并且从repository存储层中寻找匹配的验证码
ValidateCodeEnum codeEnum = getValidateCodeType(res);
Optional<ValidateCode> codeOpt = validateCodeRepository.get(res, codeEnum);
ValidateCode valCodeInStorage = codeOpt.orElseThrow(() -> new ValidateCodeException("验证码不存在"));
// 从请求中获取验证码
String codeInRequest;
try {
codeInRequest = ServletRequestUtils.getStringParameter(res.getRequest(),
codeEnum.getType());
} catch (ServletRequestBindingException e) {
throw new ValidateCodeException("获取请求验证码的值失败");
}
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException(codeEnum + "请求验证码的值不能为空");
}
// 对短信验证码做一个是否过期的判断
if (ValidateCodeEnum.SMS.equals(codeEnum) && valCodeInStorage.checkExpired()) {
validateCodeRepository.remove(res, codeEnum);
throw new ValidateCodeException(codeEnum + "验证码已过期");
}
// 验证码校验
if (!StringUtils.equals(valCodeInStorage.getCode(), codeInRequest)) {
throw new ValidateCodeException(codeEnum + "验证码不匹配");
}
log.info("验证码校验成功");
validateCodeRepository.remove(res, codeEnum);
}
/**
* 生成验证码
*
* @param res
* @return C 验证码泛型
*/
@SuppressWarnings("unchecked"