概述
总所周知,验证码方式的登录模式十分的普遍,不过 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")
private C generate(ServletWebRequest res) {
// 根据传入的res来做类型判断
String type = getValidateCodeType(res).getType();
// 获取具体的Generator的名字
String generatorName = type.concat(ValidateCodeGenerator.class.getSimpleName());
ValidateCodeGenerator codeGenerator = Optional.ofNullable(validateCodeGeneratorMap.get(generatorName))
.orElseThrow(() -> new ValidateCodeException("验证码生成器:" + generatorName + "不存在"));
return (C) codeGenerator.generate(res);
}
/**
* 存储短信验证码
* 可复用---抽象类中定义
*
* @param res
* @param validateCode
*/
private void save(ServletWebRequest res, C validateCode) {
ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
validateCodeRepository.save(res, code, getValidateCodeType(res));
}
/**
* 验证码的发送
* 图形验证码和短线验证码的发送逻辑不一样,因此设计为抽象方法,由具体的子类实现各自的发送逻辑
*
* @param res
* @param validateCode
* @throws ServletRequestBindingException
* @throws IOException
*/
protected abstract void send(ServletWebRequest res, C validateCode) throws ServletRequestBindingException, IOException;
/**
* 根据请求的url获取校验码的类型
*
* @param res
* @return ValidateCodeType
*/
private ValidateCodeEnum getValidateCodeType(ServletWebRequest res) {
String uri = res.getRequest().getRequestURI();
if (StringUtils.contains(uri, SMS)) {
return ValidateCodeEnum.SMS;
}
return ValidateCodeEnum.IMAGE;
}
}
AbstractValidateCodeProcessor
抽象类实现 ValidateCodeProcessor
接口,主要功能是对验证码相关的共有逻辑进行一个抽离,达到功能的复用。
-
create
流程可以细分为以下几个步骤:生成、存储、发送。 -
validate
是做验证码的校验,无论是图形验证码or短信验证码;验证的逻辑是一致的。
类中有2个成员变量:
-
private Map<String, ValidateCodeGenerator> validateCodeGeneratorMap
验证码生成器,不同的验证码生成逻辑不同,因此生成模块抽离出去由外部实现。
需要提到的是:这里使用到Spring
的定向查找
技巧进行注入,Spring
启动时,会查找容器中所有ValidateCodeGenerator
接口的实现,并把Bean
的名字作为Key
,实体作为Value
放到Map
中。 -
private ValidateCodeRepository validateCodeRepository
验证码存储层,生成的验证码code值需要存储到某个存储介质中,用以后续校验的时候取得(我这里使用的是Redis作为存储介质)。
验证码生命周期
验证码的生命周期可简单的划分为:生成、存储、发送。
生成验证码
生成和发送模块也相对简单,就是定义了验证码的具体生成器以及发送器。
ValidateCodeGenerator.class
/**
* 验证码生成器
*
* @author 小奇
* @date 2020/09/26
*/
public interface ValidateCodeGenerator {
/**
* 生成验证码
*
* @param res http请求中的request和response
* @return ValidateCode
*/
ValidateCode generate(ServletWebRequest res);
}
ValidateCodeGenerator
接口定义了验证码生成方法generate()
,具体的生成逻辑由对应的子类SmsValidatecodeGenerator
和ImageValidateCodeGenerator
实现。
SmsValidateCodeGenerator.class
/**
* 短信验证码生成器
*
* @author 小奇
* @date 2020/09/26
**/
public class SmsValidateCodeGenerator implements ValidateCodeGenerator {
private final SecurityProperties securityProperties;
public SmsValidateCodeGenerator(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
/**
* 短信验证码生成逻辑
*
* @param res
* @return ValidateCode
*/
@Override
public ValidateCode generate(ServletWebRequest res) {
//随机生成指定长度的短信验证码
String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
}
}
短信验证码的生成逻辑,代码相对简单,当然可以定义自己的生成逻辑,反正就是随你开心就行拉!
ImageValidateCodeGenerator.class
/**
* 图形验证码生成器
*
* @author 小奇
* @date 2020/09/26
**/
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {
private final SecurityProperties securityProperties;
public ImageValidateCodeGenerator(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
/**
* 图形验证码生成逻辑
* todo 这里的生成逻辑可稍微优化一下成utils
*
* @param res
* @return ValidateCode
*/
@Override
public ValidateCode generate(ServletWebRequest res) {
// 这里是实现了验证码参数的三级可配:请求级>应用级>默认配置 从请求中获取width 如果没有则从 securityProperties的配置中获取
int width = ServletRequestUtils.getIntParameter(res.getRequest(), "width",
securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(res.getRequest(), "height",
securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageValidateCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return Color
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
ImageValidateCodeGenerator
为图形验证码生成器,相对简单;当然里面有些类util的代码可以更好的封装,这里就不额外封装了。
存储验证码
验证码生成之后后端服务需要进行存储,方便后续校验的时候取得,相对比较简单,我这里使用的是Redis来存储。
ValidateCodeRepository.class
/**
* 验证码存取器 接口
*
* @author 小奇
* @date 2020/09/26
*/
public interface ValidateCodeRepository {
/**
* 保存验证码
*
* @param res 请求HttpRequest 和HttpResponse的封装
* @param code 验证码
* @param validateCodeType 验证码类型
*/
void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum validateCodeType);
/**
* 获取验证码
*
* @param res
* @param validateCodeType
* @return Optional<ValidateCode>
*/
Optional<ValidateCode> get(ServletWebRequest res, ValidateCodeEnum validateCodeType);
/**
* 移除验证码
*
* @param request
* @param codeType
*/
void remove(ServletWebRequest request, ValidateCodeEnum codeType);
}
ValidateCodeRepository
接口主要定义了三个方法save
保存验证码, get
获取验证码, remove
移除验证码
RedisValidateCodeRepository.class
/**
* 基于redis的验证码存取器
*
* @author 小奇
*/
@Slf4j
@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* 设备id
*/
private static final String DEVICE_ID = "deviceId";
@Override
public void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum codeEnum) {
redisTemplate.opsForValue().set(buildKey(res, codeEnum), code, 30, TimeUnit.MINUTES);
}
@Override
public Optional<ValidateCode> get(ServletWebRequest request, ValidateCodeEnum codeEnum) {
Object value = redisTemplate.opsForValue().get(buildKey(request, codeEnum));
if (value == null) {
log.warn("不存在对应的验证码");
return Optional.empty();
}
return Optional.of((ValidateCode) value);
}
@Override
public void remove(ServletWebRequest request, ValidateCodeEnum codeEnum) {
redisTemplate.delete(buildKey(request, codeEnum));
}
/**
* 根据请求的设备生成验证码的key,如果同一个设备多次请求 则先前的验证码则被覆盖无效
*
* @param res
* @param codeEnum
* @return String redis存储的key
*/
private String buildKey(ServletWebRequest res, ValidateCodeEnum codeEnum) {
String deviceId = res.getHeader(DEVICE_ID);
if (StringUtils.isBlank(deviceId)) {
throw new ValidateCodeException("请在请求头中携带deviceId参数");
}
String codeKey = SecurityConstant.DEFAULT_PARAMETER_NAME_CODE.concat(codeEnum.getType().toLowerCase())
.concat(CommonConstant.COLON).concat(deviceId);
log.info("本次请求生成的codeKey:{}", codeKey);
return codeKey;
}
}
RedisValidateCodeRepository
类是接口的具体实现,使用Redis
作为存储媒介,代码相对比较简单,不做过多的赘述。
发送验证码
验证码经过生成,后端存储后,就要进入最后一步:发送。
ValidareCodeSender.class
/**
* 短信验证码发送器
*
* @author 小奇
* @date 2020/09/26
*/
public interface SmsCodeSender {
/**
* 发送短线验证码
*
* @param code
* @param mobile
*/
void send(String code, String mobile);
}
DefaultSmsCodeSender.class
/**
* 默认的短信验证码的发送器
* @author 小奇
* @date 2020/6/16
**/
@Slf4j
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String code, String mobile) {
// 这里做简单的输出即可
log.info("向手机号" + mobile + "发送短信验证码" + code);
}
}
真实的生产中发送验证码需要使用第三方的短信服务,由于这里是学习记录,就简单的log一下记录发送。
图形验证码
图形验证码继承于验证码骨架,实现图形验证码有关的自定义逻辑,诸如:生成、发送。
ImageValidateCodeProcessor.class
/**
* 模板方法最底层 --- 基于各自的特定实现各自的发送行为
*
* @author 小奇
* @date 2020/09/26
**/
@Component("imageValidateCodeProcessor")
public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor<ImageValidateCode> {
private static final String JPEG = "JPEG";
@Override
protected void send(ServletWebRequest res, ImageValidateCode validateCode) throws IOException {
if (Objects.nonNull(res.getResponse())) {
ImageIO.write(validateCode.getBufferedImage(), JPEG, res.getResponse().getOutputStream());
}
}
}
ImageValidateCodeProcessor
类主要自定义图形验证码的发送逻辑,生成的逻辑已经封装在ImageValidateCodeGenerator
类,由依赖查找的方式注入到验证码骨架中了。
短信验证码
短信验证码继承于验证码骨架,实现短信验证码有关的自定义逻辑,诸如:生成、发送。
SmsValidateCodeProcessor.class
/**
* 短信验证码的处理器
* 模板方法最底层 --- 基于各自的特定实现各自的发送行为
*
* @author 小奇
* @date 2020/6/16
**/
@Component("smsValidateCodeProcessor")
public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {
@Autowired
private SmsCodeSender smsCodeSender;
private static final String MOBILE = "mobile";
@Override
protected void send(ServletWebRequest res, ValidateCode validateCode) throws ServletRequestBindingException {
smsCodeSender.send(validateCode.getCode(), ServletRequestUtils.getRequiredStringParameter(res.getRequest(), MOBILE));
}
}
SmsValidateCodeProcessor
类同样自定义短信验证码的发送逻辑,生成的逻辑已经封装在SmsValidateCodeGenerator
类,由依赖查找的方式注入到验证码骨架中了。
其他模块
其他模块主要是一些配置类、枚举类、异常类以及是一些用以提升代码质量的封装,需要特别介绍的是ValidateCodeBeanConfig
配置类和ValidateCodeException
异常类。
ValidateCodeBeanConfig
配置了bean
的生成规则,契合springBoot的默认实现原理:用户有自定义则使用自定义,没有则使用默认实现。
ValidateCodeBeanConfig.class
/**
* @author 小奇
* @date 2020/09/26
**/
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
/**
* 注册图形验证码生成器
* 使用conditionalOnMissingBean是为了 如果业务方有自己的生成逻辑 则使用业务方的;否则使用该默认配置
* 方法名就是bean的名字
*
* @return ValidateCodeGenerator
*/
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
return new ImageValidateCodeGenerator(securityProperties);
}
/**
* 短线验证码生成器
*
* @return ValidateCodeGenerator
*/
@Bean
@ConditionalOnMissingBean(name = "smsValidateCodeGenerator")
public ValidateCodeGenerator smsValidateCodeGenerator() {
return new SmsValidateCodeGenerator(securityProperties);
}
/**
* 找到smsCodeSender接口的所有实现类
* 默认实现是用来被覆盖的
* 如果之前用户已经配置了 则不再装载Default的
*/
@Bean
@ConditionalOnMissingBean(SmsCodeSender.class)
public SmsCodeSender smsCodeSender() {
return new DefaultSmsCodeSender();
}
}
配置Bean的生成规则,例如:Generator
模块,用户可通过实现ValidateCodeGenerator
来达到自定义验证码生成,否则使用默认的生成器,也是一种编程技巧。
ValidateCodeException
异常类继承于 SpringSecurity
的异常基类AuthenticationException
,这是因为我们是基于SpringSecurity
做扩展开发自定义验证码认证模式。
/**
* AuthenticationException是整个security异常中的基类
* 验证码异常属于认证过程中的一个特例,归属于该基类之下
*
* @author 小奇
* @date 2020/09/26
**/
public class ValidateCodeException extends AuthenticationException {
/**
* 验证码异常
* @param msg
* @return t
*/
public ValidateCodeException(String msg, Throwable t) {
super(msg, t);
}
public ValidateCodeException(String msg) {
super(msg);
}
}
其他的一些可以根据类名大致猜出作用的类这里就不做过多的展示。
总结
本篇文章主要结合模板方法模式介绍了验证码的生成,并且介绍了2个比较常用的编程技巧:依赖查找
和 使用ConditionalOnMissingBean
契合SpringBoot
默认实现思想。
本文为我在之前在b站上学习某大佬课程过程中所做的学习笔记,可能会出现理解上的偏差,如有错误或不妥指出,烦请指出!
文章到这就结束拉,欢迎大家扫码关注小奇公众号~