Spring Security-9-实现短信验证码接口

很多情况下,一个网站登录需要支持短信登录,就是输入手机号,发送验证码,然后用验证码去登录,这也是一种很常见的登录方式, 但是 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
10
public interface ValidateCodeGenerator {

/**
* 生成图形验证码
* @param request
* @return
*/
BaseCode generate(HttpServletRequest request);

}

返回类型改成父类的类型

短信发送的实现

之前的图形验证码,我们是直接返回到了页面上给用户,这里的手机验证码的话,就需要去调用第三方的验证码去实现了, 但是每个应用用的第三方都不同, 所以这里我们需要一个可覆盖的发送方法, 就跟图形验证码的生成一样, 我们提供一个默认的方式,然后应用层可以去替换

新建一个接口 SmsCodeSender ,代码如下:

1
2
3
4
5
6
7
8
9
public 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

到这里呢,发送验证码的逻辑就完了,下一节再来看下如何去验证

总结

其实本节的配置跟图形验证码的生成是一样的,这里主要是把一些通用的配置都抽出来, 重构了一下之前的配置

本文代码传送门