述
很多情况下,一个网站登录需要支持短信登录,就是输入手机号,发送验证码,然后用验证码去登录,这也是一种很常见的登录方式, 但是 Spring Security 并没有给我们提供一个短信验证码的登录的方式,这个就需要我们自己去实现了.
本文先来配置一个发送短信的接口,以及做一些验证码这块的代码的重构
验证码实体类重构
我们现在有一个图片验证码的类,现在还需要一个短信验证码的类,他们的差别就是一个有图片,一个没有图片
所以这个可以写一个验证码的父类,然后以后再有什么验证码都可以继承这个类,反正共有的属性就是验证码和过期时间
这里新建了一个 BaseCode
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@Data
@AllArgsConstructor
public class BaseCode implements Serializable {
/**
* 验证码
*/
private String code;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 验证码是否过期
* @return
*/
public boolean expired(){
return LocalDateTime.now().isAfter(expireTime);
}
}
然后之前的 ImageCode
继承这个类, 修改后代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Data
public class ImageCode extends BaseCode {
/**
* 图片对象
*/
private transient BufferedImage image;
/**
* 构造
* @param image 图片对象
* @param code 验证码
* @param expireIn 过期秒数
*/
public ImageCode (BufferedImage image, String code, int expireIn){
super(code, LocalDateTime.now().plusSeconds(expireIn));
this.image = image;
}
}
还需要一个验证码的类,然后这里新建一个 SmsCode
,这个只需要用到父类的属性就ok了,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13@Data
public class SmsCode extends BaseCode {
/**
* 构造
* @param code 验证码
* @param expireIn 过期秒数
*/
public SmsCode(String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
}
}
然后之前的验证码生成器的接口也需要改一下:1
2
3
4
5
6
7
8
9
10public interface ValidateCodeGenerator {
/**
* 生成图形验证码
* @param request
* @return
*/
BaseCode generate(HttpServletRequest request);
}
返回类型改成父类的类型
短信发送的实现
之前的图形验证码,我们是直接返回到了页面上给用户,这里的手机验证码的话,就需要去调用第三方的验证码去实现了, 但是每个应用用的第三方都不同, 所以这里我们需要一个可覆盖的发送方法, 就跟图形验证码的生成一样, 我们提供一个默认的方式,然后应用层可以去替换
新建一个接口 SmsCodeSender
,代码如下:1
2
3
4
5
6
7
8
9public interface SmsCodeSender {
/**
* 发送短信的接口
* @param phoneNo
* @param code
*/
void send(String phoneNo, String code);
}
然后我们需要一个默认的实现. 新建 DefaultSmsCodeSender
实现上面的接口,代码如下:1
2
3
4
5
6
7
8
9@Slf4j
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String phoneNo, String code) {
log.info("调用第三方短信接口,目标手机号:[{}], 验证码:[{}]", phoneNo, code);
}
}
这里就不去做真正的实现了,只在控制台打印一下就ok
然后因为这个可以在应用层替换,所以还需要做一下配置,在配置类 ValidateCodeBeanConfig
中加入以下配置1
2
3
4
5@Bean
@ConditionalOnMissingBean(SmsCodeSender.class)
public SmsCodeSender smsCodeSender(){
return new DefaultSmsCodeSender();
}
这个和之前的图形验证码类似,都加了一个 @ConditionalOnMissingBean
注解, 不同的是这里是指定的类, 图形验证码指定的是名字
这里 @ConditionalOnMissingBean(SmsCodeSender.class)
作用就是,先去容器里面找,有没有 SmsCodeSender
的实现, 没有的话, 再用我们下面配置的 DefaultSmsCodeSender
短信验证码配置类
短信验证码的可配置项,应该是过期时间和验证码长度,还有拦截请求路径这三个, 所以这里也需要一个配置类, 因为图形验证码也有这三个属性, 所以这两个的配置类又可以继承同一个
首先,搞一个父类,里面放通用的,就是过期时间和长度和要拦截的路径, 名称是 BaseCodeProperties
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Data
public class BaseCodeProperties {
/**
* 验证码字符个数
*/
private int codeCount = 4;
/**
* 过期秒数
*/
private int expireIn = 60;
/**
* 要拦截的请求 逗号隔开
*/
private String urls;
}
然后图形验证码要继承这个类, ImageCodeProperties
修改如下:1
2
3
4
5
6
7
8
9
10
11
12
13@Data
public class ImageCodeProperties extends BaseCodeProperties{
/**
* 图片的宽度
*/
private int width = 160;
/**
* 图片的高度
*/
private int height = 40;
}
然后是短信验证码的配置,新建 SmsCodeProperties
代码如下:1
2
3
4
5
6@Data
public class SmsCodeProperties extends BaseCodeProperties{
public SmsCodeProperties(){
setCodeCount(6);
}
}
构造设置了短信验证码长度默认是6
最后 ValidateCodeProperties
中, 加入一个短信验证码的配置1
2
3
4
5
6
7
8
9
10
11
12
13@Data
public class ValidateCodeProperties {
/**
* 图片验证码配置
*/
private ImageCodeProperties imageCode = new ImageCodeProperties();
/**
* 短信验证码配置
*/
private SmsCodeProperties smsCode = new SmsCodeProperties();
}
短信验证码生成器
之前有了图片验证码生成器, 这里还需要一个短信验证码的生成器, 新建类 SmsCodeGenerator
,然后实现 ValidateCodeGenerator
接口1
2
3
4
5
6
7
8
9
10
11
12
13@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
@Override
public BaseCode generate(HttpServletRequest request) {
String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSmsCode().getCodeCount());
return new SmsCode(code, securityProperties.getCode().getSmsCode().getExpireIn());
}
}
这里就是生成了一个随机数
新增短信验证码的接口
这些配置都完成之后,我们还需要一个短信验证码的接口,供前端使用, 由于验证码生成器的接口有修改(返回改成了验证码父类),所以这个controller修改后的代码如下: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@RestController
@Slf4j
public class ValidateCodeController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
@Autowired
private SmsCodeSender smsCodeSender;
@GetMapping("code/image")
public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 根据随机数生成数字
ImageCode imageCode = (ImageCode)imageCodeGenerator.generate(request);
// 将随机数存到缓存中
String redisKey = Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId();
log.info("将验证码放到缓存中,redisKey:{}", request.getSession().getId());
redisTemplate.opsForValue().set(redisKey, imageCode);
// 将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
@GetMapping("code/sms")
public void getSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
// 从请求中获取手机号
String phoneNo = ServletRequestUtils.getRequiredStringParameter(request, "phoneNo");
// 根据随机数生成数字
SmsCode smsCode = (SmsCode) smsCodeGenerator.generate(request);
// 将随机数存到缓存中
String redisKey = Constants.LOGIN_SMS_CODE_KEY_PREFIX + phoneNo;
log.info("将验证码放到缓存中,redisKey:{}", request.getSession().getId());
redisTemplate.opsForValue().set(redisKey, smsCode);
// 发送短信验证码
smsCodeSender.send(phoneNo, smsCode.getCode());
}
}
最后一步,修改登录页面
需要再加一个表单1
2
3
4
5
6
7<form action="/authentication/mobile" method="post">
<input class="text" type="text" value="13200000000" name="phoneNo" placeholder="手机号" required="">
<input class="text" type="text" name="smsCode" placeholder="验证码" required="">
<br>
<a href="/code/sms?phoneNo=13200000000">发送验证码</a>
<input type="submit" value="登录">
</form>
手机号这里先给个默认值,然后请求发送到 /authentication/mobile
到这里呢,发送验证码的逻辑就完了,下一节再来看下如何去验证
总结
其实本节的配置跟图形验证码的生成是一样的,这里主要是把一些通用的配置都抽出来, 重构了一下之前的配置