述
上文中实现了验证码的发送,先后的发送的代码是这样的
这两个发送的方法其实都是一样的. 只是最后一个是返回图片给页面,一个是给用户发送验证码, 那这两个方法其实可以抽象出来,封装成一个, 下面看一下这些逻辑该如何去重构
验证码发送器
图片验证码或者短信验证码的逻辑如下:
- 生成一个验证码
- 放到缓存里面去
- 返回给用户
他们其实可以封装成以下三个方法
- 生成验证码对象的方法,这里返回的可能是短信验证码对象,也可能是图片验证码对象
- 放到缓存里面, 不同的验证码的key是不一样的
- 发送给用户, 图片时直接返回给页面, 然后短信是调用第三方的接口
这里就可以抽象出来,首先,写一个接口, ValidateCodeProcessor
:1
2
3
4
5
6
7
8
9
10
11
12public interface ValidateCodeProcessor {
/**
* 生成验证码,放到缓存,以及发送
* @param request
* @param response
* @throws ServletRequestBindingException
* @throws IOException
*/
void createCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException, IOException;
}
通过这个 createCode
方法里面把上面的三步都实现了, 但是第三步 返回给用户,这个步骤是不一样的,所以这个第三步我们可以写一个抽象的方法,交给各个子类去实现
抽象实现
首先得有一个抽象类实现上面的接口, AbstractValidateCodeProcessor
: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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99@Slf4j
public abstract class AbstractValidateCodeProcessor<C extend BaseCode> implements ValidateCodeProcessor {
@Autowired
private RedisTemplate redisTemplate;
/**
* 收集系统中的所有 {@link ValidateCodeGenerator} 的实现
*/
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGenerators;
@Override
public void createCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException, IOException {
// 生成
C validateCode = generate(request);
// 放缓存
saveCache(request, validateCode);
// 发送给用户
send(request, response, validateCode);
}
/**
* 生成验证码
* @param request
* @return
*/
public C generate(HttpServletRequest request) {
// 获取验证码类型
String type = getValidateCodeType(request).name().toLowerCase();
// 获取验证码生成器的名称
String generatorName = type + "CodeGenerator";
// 从 validateCodeGenerators 这个Map中取出
ValidateCodeGenerator generator = validateCodeGenerators.get(generatorName);
if (generator == null) {
throw new ValidateCodeException("验证码生成器 " + generatorName + " 不存在");
}
return (C)generator.generate(request);
}
/**
* 获取存入缓存的key
* @param request
* @return
*/
private String getRedisKey(HttpServletRequest request) throws ServletRequestBindingException {
StringBuilder redisKey = new StringBuilder();
// 获取验证码类型
ValidateCodeType validateCodeType = getValidateCodeType(request);
redisKey.append(Constants.VALIDATE_CODE_KEY_PREFIX)
.append(validateCodeType.name().toLowerCase())
.append(":");
// 判断是哪种类型的验证码
if (validateCodeType.equals(ValidateCodeType.IMAGE)) {
// 图片的
redisKey.append(request.getSession().getId());
} else if(validateCodeType.equals(ValidateCodeType.SMS)){
// 短信的
redisKey.append(ServletRequestUtils.getRequiredStringParameter(request, "phoneNo"));
}
return redisKey.toString();
}
/**
* 保存到缓存里面
* @param request
* @param validateCode
*/
private void saveCache(HttpServletRequest request, C validateCode) throws ServletRequestBindingException {
// 将随机数存到缓存中
String redisKey = getRedisKey(request);
log.info("将验证码放到缓存中,redisKey:{}", redisKey);
redisTemplate.opsForValue().set(redisKey, validateCode);
}
/**
* 根据请求的url获取校验码的类型
* @param request
* @return ValidateCodeType
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
String type = StringUtils.substringAfter(request.getRequestURI(), "/code/");
return ValidateCodeType.valueOf(type.toUpperCase());
}
/**
* 具体的发送验证码的方法, 交给子类去实现
* @param request
* @param response
* @param validateCode
* @throws IOException
* @throws ServletRequestBindingException
*/
protected abstract void send(HttpServletRequest request, HttpServletResponse response, C validateCode) throws IOException, ServletRequestBindingException;
}
这个代码比较多,首先来看一下,我们上面注入了一个 private Map<String, ValidateCodeGenerator> validateCodeGenerators;
这样的一个Map, 这个的作用就是, Spring 会查询容器里面的,所有的 ValidateCodeGenerator
的实现, 然后注入到这个Map里面, key 的值首先会取 @Compent("")
注解里面指定的名字,如果没有的话,就是默认的
在我们现在的项目中, ValidateCodeGenerator
的实现有两个, 一个是图片验证生成器,一个是短信验证码生成器
所以容器启动后,这个Map里面会有这两个实现
然后再看一下方法, 首先是 createCode()
方法,这里面就是我们上面的三个步骤
然后再具体看下里面的这三个方法
验证码生成方法
generate()
方法
这里首先用到了一个 getValidateCodeType()
方法, 我们后面controller中验证码的接口会合并成一个,通过前端传类型来判断是什么请求, 所以这里就是通过请求来判断是哪种类型的验证码,返回是个枚举类型 ValidateCodeType
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14public enum ValidateCodeType implements Serializable {
/**
* 短信验证码
*/
SMS,
/**
* 图片验证码
*/
IMAGE,
;
}
前端请求是/code/{type}
,然后传 sms ,或者 image 两种
然后我们上面的Map会把所有的验证码生成器加载到, 图片的生成器key是 imageCodeGenerator
短信的是 smsCodeGenerator
所以这里就可以通过前端传过来的 type 去决定用哪种类型的生成器了
取到生成器后调用生成方法,返回
放入缓存方法
saveCache()
,这个没什么好多说的, 用到了一个 getRedisKey()
方法, 这里根据不同的验证码类型写不同的key
最后放到缓存里面就可以了
发送方法
send()
这个方法只写了一个抽象的方法,具体去由子类的发送器去实现, 所以我们就还需要两个发送的实现类
发送功能的实现
首先是图片的发送类, 新建 ImageCodeProcessor
然后继承 AbstractValidateCodeProcessor
重写发送的方法,代码如下:1
2
3
4
5
6
7
8
9@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {
@Override
protected void send(HttpServletRequest request, HttpServletResponse response, ImageCode imageCode) throws IOException {
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
}
再建一个短信的发送器,代码如下:1
2
3
4
5
6
7
8
9
10
11
12@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<SmsCode> {
@Autowired
private SmsCodeSender smsCodeSender;
@Override
protected void send(HttpServletRequest request, HttpServletResponse response, SmsCode validateCode) throws IOException, ServletRequestBindingException {
String phoneNo = ServletRequestUtils.getRequiredStringParameter(request, "phoneNo");
smsCodeSender.send(phoneNo, validateCode.getCode());
}
}
接口修改
现在发送验证码的逻辑封装完了,controller中的接口就可以合并成一个了,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@RestController
@Slf4j
public class ValidateCodeController {
/**
* 收集系统中的所有 {@link ValidateCodeProcessor} 的实现
*/
@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;
@GetMapping("code/{type}")
public void getCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws IOException, ServletRequestBindingException {
ValidateCodeProcessor processor = validateCodeProcessors.get(type + "CodeProcessor");
processor.createCode(request, response);
}
}
首先是注入了一个Map,收集所有的 ValidateCodeProcessor
就是验证码发送器,然后调用发送的方法
配置类修改
最后 BrowserSecurityConfig
中之要把接口 code/{type}
加入到不需要权限里面去, 之前配置了 /code/image
改成 /code/*
就好了
验证逻辑
上面的修改完成之后, 之前的 ValidateCodeFilter
可能会报错, 所以先注释掉 validate()
方法,先保证验证码可以成功发送
测试
启动项目,访问登录页, 然后发一个验证码,控制台看一下输出,如下:
发送逻辑就都ok了, 关于验证的逻辑之后再做
总结
重点就是用 @Autowired
注入一个Map的用法, 最后看一下发送验证码功能重构后的类图: