图1-1 验证码生成 概图
总所周知,验证码方式的登录模式十分的普遍,不过 Spring Security
并没有提供比较好的原生解决方案,但是我们可以 do it by ourselves!
,本文的篇幅相对比较长,因此分上下篇分别来介绍。上篇主要介绍:验证码的生成,下篇对自定义验证码登录的流程进行讲解。
我们比较常见的验证码主要有两种:图形验证码以及短信验证码,相对来说不是特别的复杂。可能会有人有疑惑:为什么简单的验证码生成需要花费一整篇幅来介绍呢?原因当然是:身为菜鸟的我也有一个架构师的梦!验证码的生成会结合模板方法模式一起讲解。
模板方法模式属于一种行为型的设计模式,主要是用来解决复用和扩展两个问题。
模板方法模式在一个方法中定义一个算法骨架
,并将某些步骤推迟到某些子类中实现。该模式可以让子类在不改变算法整体结构的情况,重新定义算法中的某些步骤细节。
这里提到了一个算法骨架的概念,算法
并非是指数据结构中的“算法”,可以理解为广义上的业务逻辑;骨架
架子其实就是模板;总的来说:算法骨架
可以理解为包含广义业务逻辑的模板方法。
绝大部分的设计模式的原理都十分的简单,难得是将原理落实到实践中,解决实际问题。
我们知道模板方法模式主要是用来解决 复用
和 扩展
这两个问题,结合到实际情况中来分析;验证码生成有哪些地方需要 复用
和 扩展
呢?
让我们来梳理一下验证码登录模式的流程,无论是短信验证码还是图形验证码,大致上都有如下步骤:生成验证码、存储、发送、校验;既然流程上相同,那么就能做到复用
。而扩展
并非是指代码的扩展性,而是指框架上的扩展性,模板方法模式可以让使用者在不修改骨架源码的情况下,定制化扩展功能。
废话不多说,接下来就来瞅瞅模板方法模式在验证码生成模块的落地情况吧!还是老规矩,先上图:
图1-2 验证码关系概览图
验证码的生成主要分3个模块:骨架模块、验证码生命周期模块、具体验证码模块(短信验证码和图形验证码)
骨架模块主要包含 ValidateCodeProcessor
接口以及AbstractValidateCodeProcessor
抽象类;封装了验证码相关的可复用的业务逻辑。
验证码生命周期模块是指:验证码的生成、存储、发送。
具体验证码模块涉及短信验证码和图形验证码,基于骨架重新定义自己的相关实现。
无论是图形验证码还是短信验证码,验证码的相关业务逻辑(算法骨架
)都是大同小异的;主要是验证码的 创建流程
和 验证流程
。因此使用模板方法模式,对可复用的业务逻辑进行抽离,封装成一个骨架。
ValidateCodeProcessor.class
/*** 校验码处理器 封装不同验证码的处理逻辑*/
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
/*** 抽象方法模式——算法骨架* 对验证码的一些公有的业务逻辑进行抽离,做到复用**/
@Slf4j
public abstract class AbstractValidateCodeProcessor
}
AbstractValidateCodeProcessor
抽象类实现 ValidateCodeProcessor
接口,主要功能是对验证码相关的共有逻辑进行一个抽离,达到功能的复用。
create
流程可以细分为以下几个步骤:生成、存储、发送。
validate
是做验证码的校验,无论是图形验证码or短信验证码;验证的逻辑是一致的。
类中有2个成员变量:
private Map
验证码生成器,不同的验证码生成逻辑不同,因此生成模块抽离出去由外部实现。需要提到的是:这里使用到Spring
的 定向查找
技巧进行注入,Spring
启动时,会查找容器中所有 ValidateCodeGenerator
接口的实现,并把Bean
的名字作为 Key
,实体作为Value
放到 Map
中。
private ValidateCodeRepository validateCodeRepository
验证码存储层,生成的验证码code值需要存储到某个存储介质中,用以后续校验的时候取得(我这里使用的是Redis作为存储介质)。
验证码的生命周期可简单的划分为:生成、存储、发送。
生成验证码
生成和发送模块也相对简单,就是定义了验证码的具体生成器以及发送器。
ValidateCodeGenerator.class
/*** 验证码生成器*/
public interface ValidateCodeGenerator {/*** 生成验证码** @param res http请求中的request和response* @return ValidateCode*/ValidateCode generate(ServletWebRequest res);}
ValidateCodeGenerator
接口定义了验证码生成方法generate()
,具体的生成逻辑由对应的子类SmsValidatecodeGenerator
和ImageValidateCodeGenerator
实现。
SmsValidateCodeGenerator.class
/*** 短信验证码生成器**/
public class SmsValidateCodeGenerator implements ValidateCodeGenerator {private final SecurityProperties securityProperties;public SmsValidateCodeGenerator(SecurityProperties securityProperties) {this.securityProperties = securityProperties;}/*** 短信验证码生成逻辑** @param res* @return ValidateCode*/@Overridepublic ValidateCode generate(ServletWebRequest res) {//随机生成指定长度的短信验证码String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());}
}
短信验证码的生成逻辑,代码相对简单,当然可以定义自己的生成逻辑,反正就是随你开心就行拉!
ImageValidateCodeGenerator.class
/*** 图形验证码生成器**/
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {private final SecurityProperties securityProperties;public ImageValidateCodeGenerator(SecurityProperties securityProperties) {this.securityProperties &#61; securityProperties;}/*** 图形验证码生成逻辑* todo 这里的生成逻辑可稍微优化一下成utils** &#64;param res* &#64;return ValidateCode*/&#64;Overridepublic ValidateCode generate(ServletWebRequest res) {// 这里是实现了验证码参数的三级可配&#xff1a;请求级>应用级>默认配置 从请求中获取width 如果没有则从 securityProperties的配置中获取int width &#61; ServletRequestUtils.getIntParameter(res.getRequest(), "width",securityProperties.getCode().getImage().getWidth());int height &#61; ServletRequestUtils.getIntParameter(res.getRequest(), "height",securityProperties.getCode().getImage().getHeight());BufferedImage image &#61; new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics g &#61; image.getGraphics();Random random &#61; 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 &#61; 0; i < 155; i&#43;&#43;) {int x &#61; random.nextInt(width);int y &#61; random.nextInt(height);int xl &#61; random.nextInt(12);int yl &#61; random.nextInt(12);g.drawLine(x, y, x &#43; xl, y &#43; yl);}String sRand &#61; "";for (int i &#61; 0; i < securityProperties.getCode().getImage().getLength(); i&#43;&#43;) {String rand &#61; String.valueOf(random.nextInt(10));sRand &#43;&#61; rand;g.setColor(new Color(20 &#43; random.nextInt(110), 20 &#43; random.nextInt(110), 20 &#43; random.nextInt(110)));g.drawString(rand, 13 * i &#43; 6, 16);}g.dispose();return new ImageValidateCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());}/*** 生成随机背景条纹** &#64;param fc* &#64;param bc* &#64;return Color*/private Color getRandColor(int fc, int bc) {Random random &#61; new Random();if (fc > 255) {fc &#61; 255;}if (bc > 255) {bc &#61; 255;}int r &#61; fc &#43; random.nextInt(bc - fc);int g &#61; fc &#43; random.nextInt(bc - fc);int b &#61; fc &#43; random.nextInt(bc - fc);return new Color(r, g, b);}}
ImageValidateCodeGenerator
为图形验证码生成器&#xff0c;相对简单&#xff1b;当然里面有些类util的代码可以更好的封装&#xff0c;这里就不额外封装了。
存储验证码
验证码生成之后后端服务需要进行存储&#xff0c;方便后续校验的时候取得&#xff0c;相对比较简单&#xff0c;我这里使用的是Redis来存储。
ValidateCodeRepository.class
/*** 验证码存取器 接口*/
public interface ValidateCodeRepository {/*** 保存验证码** &#64;param res 请求HttpRequest 和HttpResponse的封装* &#64;param code 验证码* &#64;param validateCodeType 验证码类型*/void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum validateCodeType);/*** 获取验证码** &#64;param res* &#64;param validateCodeType* &#64;return Optional
ValidateCodeRepository
接口主要定义了三个方法save
保存验证码&#xff0c; get
获取验证码&#xff0c; remove
移除验证码
RedisValidateCodeRepository.class
/*** 基于redis的验证码存取器*/
&#64;Slf4j
&#64;Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {&#64;Autowiredprivate RedisTemplate redisTemplate;/*** 设备id*/private static final String DEVICE_ID &#61; "deviceId";&#64;Overridepublic void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum codeEnum) {redisTemplate.opsForValue().set(buildKey(res, codeEnum), code, 30, TimeUnit.MINUTES);}&#64;Overridepublic Optional
RedisValidateCodeRepository
类是接口的具体实现&#xff0c;使用Redis
作为存储媒介&#xff0c;代码相对比较简单&#xff0c;不做过多的赘述。
发送验证码
验证码经过生成&#xff0c;后端存储后&#xff0c;就要进入最后一步&#xff1a;发送。
ValidareCodeSender.class
/*** 短信验证码发送器*/
public interface SmsCodeSender {/*** 发送短线验证码** &#64;param code* &#64;param mobile*/void send(String code, String mobile);
}
DefaultSmsCodeSender.class
/*** 默认的短信验证码的发送器**/
&#64;Slf4j
public class DefaultSmsCodeSender implements SmsCodeSender {&#64;Overridepublic void send(String code, String mobile) {// 这里做简单的输出即可log.info("向手机号" &#43; mobile &#43; "发送短信验证码" &#43; code);}
}
真实的生产中发送验证码需要使用第三方的短信服务&#xff0c;由于这里是学习记录&#xff0c;就简单的log一下记录发送。
图形验证码继承于验证码骨架&#xff0c;实现图形验证码有关的自定义逻辑&#xff0c;诸如&#xff1a;生成、发送。
ImageValidateCodeProcessor.class
/*** 模板方法最底层 --- 基于各自的特定实现各自的发送行为**/
&#64;Component("imageValidateCodeProcessor")
public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor
}
ImageValidateCodeProcessor
类主要自定义图形验证码的发送逻辑&#xff0c;生成的逻辑已经封装在ImageValidateCodeGenerator
类&#xff0c;由依赖查找的方式注入到验证码骨架中了。
短信验证码继承于验证码骨架&#xff0c;实现短信验证码有关的自定义逻辑&#xff0c;诸如&#xff1a;生成、发送。
SmsValidateCodeProcessor.class
/*** 短信验证码的处理器* 模板方法最底层 --- 基于各自的特定实现各自的发送行为**/
&#64;Component("smsValidateCodeProcessor")
public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor
}
SmsValidateCodeProcessor
类同样自定义短信验证码的发送逻辑&#xff0c;生成的逻辑已经封装在SmsValidateCodeGenerator
类&#xff0c;由依赖查找的方式注入到验证码骨架中了。
其他模块主要是一些配置类、枚举类、异常类以及是一些用以提升代码质量的封装&#xff0c;需要特别介绍的是ValidateCodeBeanConfig
配置类和ValidateCodeException
异常类。
ValidateCodeBeanConfig
配置了bean
的生成规则&#xff0c;契合SpringBoot
的默认实现原理&#xff1a;用户有自定义则使用自定义&#xff0c;没有则使用默认实现。
ValidateCodeBeanConfig.class
&#64;Configuration
public class ValidateCodeBeanConfig {&#64;Autowiredprivate SecurityProperties securityProperties;/*** 注册图形验证码生成器* 使用conditionalOnMissingBean是为了 如果业务方有自己的生成逻辑 则使用业务方的&#xff1b;否则使用该默认配置* 方法名就是bean的名字** &#64;return ValidateCodeGenerator*/&#64;Bean&#64;ConditionalOnMissingBean(name &#61; "imageValidateCodeGenerator")public ValidateCodeGenerator imageValidateCodeGenerator() {return new ImageValidateCodeGenerator(securityProperties);}/*** 短线验证码生成器** &#64;return ValidateCodeGenerator*/&#64;Bean&#64;ConditionalOnMissingBean(name &#61; "smsValidateCodeGenerator")public ValidateCodeGenerator smsValidateCodeGenerator() {return new SmsValidateCodeGenerator(securityProperties);}/*** 找到smsCodeSender接口的所有实现类* 默认实现是用来被覆盖的* 如果之前用户已经配置了 则不再装载Default的*/&#64;Bean&#64;ConditionalOnMissingBean(SmsCodeSender.class)public SmsCodeSender smsCodeSender() {return new DefaultSmsCodeSender();}
}
配置Bean的生成规则&#xff0c;例如&#xff1a;Generator
模块&#xff0c;用户可通过实现ValidateCodeGenerator
来达到自定义验证码生成&#xff0c;否则使用默认的生成器&#xff0c;也是一种编程技巧。
ValidateCodeException
异常类继承于 SpringSecurity
的异常基类AuthenticationException
&#xff0c;这是因为我们是基于SpringSecurity
做扩展开发自定义验证码认证模式。
/*** AuthenticationException是整个security异常中的基类* 验证码异常属于认证过程中的一个特例&#xff0c;归属于该基类之下**/
public class ValidateCodeException extends AuthenticationException {/*** 验证码异常* &#64;param msg* &#64;return t*/public ValidateCodeException(String msg, Throwable t) {super(msg, t);}public ValidateCodeException(String msg) {super(msg);}
}
其他的一些可以根据类名大致猜出作用的类这里就不做过多的展示。
本篇文章主要结合模板方法模式介绍了验证码的生成&#xff0c;并且介绍了2个比较常用的编程技巧&#xff1a;依赖查找
和 使用ConditionalOnMissingBean
契合SpringBoot
默认实现思想。
本文如有错误或不妥指出&#xff0c;烦请指出&#xff01;
一套非常好的springboot学习教程分享给大家&#xff08;需要的链接自行观看&#xff09;&#x1f447;&#xff1a;
https://www.bilibili.com/video/BV1PZ4y1j7QK
SpringBoot最新教程-SpringBoot框架实战