短信验证码生成与发送
代码结构
短信验证码和图形验证码生成验证逻辑基本相同,可抽象出接口,分别不同实现,代码结构如下:
接口说明:
ValidateCodeController 是这个验证码接口体系的入口,它主要抽象出可以同时接收两种验证码的请求方式,使用请求类型type来进行区分。
ValidateCodeProcessor 是一个接口,专门用来生成验证码,并将验证码存入到session中,最后将验证码发送出去,发送的方式有两种,图片验证码是写回到response中,短信验证码调用第三方短信服务平台的API进行发送,比如阿里巴巴的短信服务。
AbstractValidateCodeProcessor 是一个抽象类,它实现了 ValidateCodeProcessor 接口,并提供了抽象方法send方法,因为图片的发送方法和短信的发送方法具体实现不同,所以得使用具体的方法进行发送。这里面的create方法完成了验证码的生成、保存与发送功能。
ValidateCodeGenerator 也是一个接口,它有两个实现类,分别是 ImageCodeGenerator 和 SmsCodeGenerator,它们具体是完成了代码的生成逻辑。
ImageCodeProcessor 和 SmsCodeProcessor 是专门用来重写send方法的一个处理器,展示了两种验证码的不同发送方式。
短信验证码实体
创建验证码父类,抽象出短信验证码和图形验证码相同的属性,短信验证码和图形验证后包含属性有code和expireTime,短信验证码只有这两个属性,而图形验证码还多一个BufferedImage实例对象属性,所以将共同属性进行抽取,抽取为ValidateCode类:1
2
3
4
5
6
7
8
9
10
11
12
public class ValidateCode {
private String code;
private LocalDateTime expireTime;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
抽取后的图片验证码实体类为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16true) (callSuper =
public class ImageCode extends ValidateCode {
private BufferedImage image;
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
super(code, expireTime);
this.image = image;
}
public ImageCode(BufferedImage image, String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
this.image = image;
}
}
短信验证码实体:1
2
3
4
5
6
7
8
9
10public class SmsCode extends ValidateCode {
public SmsCode(String code, LocalDateTime expireTime) {
super(code, expireTime);
}
public SmsCode(String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
}
}
短信验证码属性类
属性类和实体类一样,抽象出验证码属性父类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CodeProperties {
/**
* 验证码长度
*/
private int length = 6;
/**
* 验证码过期时间
*/
private int expireIn = 60;
/**
* 需要验证码的url字符串,用英文逗号隔开
*/
private String url;
}
图片验证码属性类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17true) (callSuper =
public class ImageCodeProperties extends CodeProperties {
public ImageCodeProperties() {
setLength(4);
}
/**
* 验证码宽度
*/
private int width = 67;
/**
* 验证码高度
*/
private int height = 23;
}
短信验证码属性类:1
2
3public class SmsCodeProperties extends CodeProperties {
}
为了实现配置信息可以由用户自定义配置,还需要将其加入到读取配置文件的配置类中,创建一个 ValidateCodeProperties 类,将图片验证码和短信验证码实例对象作为属性配置进去,代码如下:1
2
3
4
5
6
7
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
private SmsCodeProperties sms = new SmsCodeProperties();
}
再将ValidateCodeProperties封装到整个安全配置类SecurityProperties中,具体的代码如下:1
2
3
4
5
6
7
8
"com.lemon.security") (prefix =
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
}
配置文件中可配置验证码相应属性:
1 | com.lemon.security.code.image.length=4 |
验证码生成接口
ValidateCodeProcessor 接口主要是完成了验证码的生成、保存与发送的一整套流程,接口的主要设计如下所示:
1 | public interface ValidateCodeProcessor { |
由于图片验证码和短信验证码的生成和保存、发送等流程是固定的,只是在生成两种验证码的时候分别调用各自的生成方法,保存到session中是完全一致的,最后的发送各有不同,图片验证码是写到response中,而短信验证码是调用第三方短信发送平台的SDK来实现发送功能。所以这里写一个抽象类来实现 ValidateCodeProcessor 接口。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public abstract class AbstractValidateCodeProcessor<C> implements ValidateCodeProcessor {
private static final String SEPARATOR = "/code/";
/**
* 操作session的工具集
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 这是Spring的一个特性,就是在项目启动的时候会自动收集系统中 {@link ValidateCodeGenerator} 接口的实现类对象
*/
private Map<String, ValidateCodeGenerator> validateCodeGeneratorMap;
public void create(ServletWebRequest request) throws Exception {
C validateCode = generate(request);
save(request, validateCode);
send(request, validateCode);
}
/**
* 生成验证码
*
* @param request ServletWebRequest实例对象
* @return 验证码实例对象
*/
"unchecked") (
private C generate(ServletWebRequest request) {
String type = getProcessorType(request);
ValidateCodeGenerator validateCodeGenerator = validateCodeGeneratorMap.get(type.concat(ValidateCodeGenerator.CODE_GENERATOR));
return (C) validateCodeGenerator.generate(request);
}
/**
* 保存验证码到session中
*
* @param request ServletWebRequest实例对象
* @param validateCode 验证码
*/
private void save(ServletWebRequest request, C validateCode) {
sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX.concat(getProcessorType(request).toUpperCase()), validateCode);
}
/**
* 发送验证码
*
* @param request ServletWebRequest实例对象
* @param validateCode 验证码
* @throws Exception 异常
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
/**
* 获取请求URL中具体请求的验证码类型
*
* @param request ServletWebRequest实例对象
* @return 验证码类型
*/
private String getProcessorType(ServletWebRequest request) {
// 获取URI分割后的第二个片段
return StringUtils.substringAfter(request.getRequest().getRequestURI(), SEPARATOR);
}
}
对上面的代码进行解释:
首先将验证码生成接口 ValidateCodeGenerator 的实现类对象注入到Map集合中,这个是Spring的一个特性。
抽象类中实现了 ValidateCodeProcessor 接口的create方法,从代码中可以看出,它主要是完成了验证码的创建、保存和发送的功能。
generate方法根据传入的不同泛型而生成了特定的验证码,而泛型的传入是通过AbstractValidateCodeProcessor 的子类来实现的。
save方法是将生成的验证码实例对象存入到session中,两种验证码的存储方式一致,所以代码也是通用的。
send方法一个抽象方法,分别由 ImageCodeProcessor 和 SmsCodeProcessor 来具体实现,也是根据泛型来判断具体调用哪一个具体的实现类的send方法。
验证码生成接口实现
图片验证码
1 |
|
短信验证码
1 |
|
两个实现类完成了具体的验证码生成逻辑,根据传入的泛型然后进行强转之后便可调用各自的生成逻辑方法。
验证码发送
不同的验证码的发送逻辑是不一样的,图片验证码是写回response中,而短信验证码是将验证码发送到指定手机号的手机上。
图形验证码
图片验证码的发送逻辑类的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17"imageCodeProcessor") (
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {
private static final String FORMAT_NAME = "JPEG";
/**
* 发送图形验证码,将其写到相应中
*
* @param request ServletWebRequest实例对象
* @param imageCode 验证码
* @throws Exception 异常
*/
protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream());
}
}
短信验证码
1 | "smsCodeProcessor") ( |
注意到上面的短信发送调用了 SmsCodeSender 的实现类,因此和图片的发送有所区别。而在设计中,SmsCodeSender 有一个默认的实现,也就是自带的短信发送方式,但是在实际的开发过程中,往往需要开发者覆盖自带的发送逻辑,而是采用自定义的发送逻辑,所以需要默认的短信发送方式是可以被覆盖的。SmsCodeSender 接口代码如下:
1 | /** |
默认实现类代码:1
2
3
4
5
6
7
8
9
10
11
12/**
* 默认的短信发送逻辑
*
*/
public class DefaultSmsCodeSender implements SmsCodeSender {
public void send(String mobile, String code) {
// 这里仅仅写个打印,具体逻辑一般都是调用第三方接口发送短信
System.out.println("向手机号为:" + mobile + "的用户发送验证码:" + code);
}
}
注意到上面的代码并没有使用@Component注解来标注为一个Spring的Bean,这么做不是说它不由Spring管理,而是需要配置的可以被覆盖的形式,所以在 ValidateCodeBeanConfig 类中加上配置其为Spring Bean的代码,为了体现代码的完整性,这里贴出 ValidateCodeBeanConfig 类中的所有代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ValidateCodeBeanConfig {
private final SecurityProperties securityProperties;
public ValidateCodeBeanConfig(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
"imageCodeGenerator") (name =
public ValidateCodeGenerator imageCodeGenerator() {
ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
imageCodeGenerator.setSecurityProperties(securityProperties);
return imageCodeGenerator;
}
(SmsCodeSender.class)
public SmsCodeSender smsCodeSender() {
return new DefaultSmsCodeSender();
}
}
在最后一个Bean的配置中,使用了@ConditionalOnMissingBean注解,这里是告诉Spring,如果上下文环境中没有SmsCodeSender接口的实现类对象,那么就执行下面的方法进行默认的Bean创建。所以对于用户自定义方式,只需要写一个类实现SmsCodeSender接口,并将其标注为Spring的Bean即可,就可以覆盖自带的短信发送逻辑。如果一开始使用@Component注解来进行标注了,那就无法获得这样自定义的效果。