述
前面的文章中主要实现了登录认证的功能,本文来在登录的时候加个图形验证码,然后做成一个可配置的图形验证码,先来看一下图形验证码该如何实现
图形验证码
- 首先我们要生成一个图片验证码
- 这个验证码需要放到session中或者缓存中,用来跟用户输入的做验证
- 然后还要把这个验证加到我们的认证流程中去
- 然后将生成图片的接口接到前端的页面上去
这就是一个简单的图形验证码的步骤,下面先来看一下如何实现
图形验证码Model
首先我们需要一个实体类来放这个图形验证码的信息, 这些就都放在 core 项目中,因为web或者app都可能用的到, 新建类 ImageCode
,代码如下: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@Data
public class ImageCode implements Serializable {
/**
* 图片对象
* transient 序列化的时候这个字段不会被序列化
*/
private transient BufferedImage image;
/**
* 验证码
*/
private String code;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 验证码是否过期
* @return
*/
public boolean expired(){
return LocalDateTime.now().isAfter(expireTime);
}
/**
* 构造
* @param imageCode 图片对象
* @param code 验证码
* @param expireIn 过期秒数
*/
public ImageCode (ImageCode imageCode, String code, int expireIn){
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
}
生成验证码
前端需要调用我们的接口,拿到验证码,所以这里需要写一个接口来获取验证码,如下: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@RestController
public class ValidateCodeController {
private static final int EXPIRE = 60;
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("code/image")
public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 根据随机数生成数字
ImageCode imageCode = createImageCode();
// 将随机数存到缓存中
String redisKey = Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId();
redisTemplate.opsForValue().set(redisKey, imageCode);
// 将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode() {
ValidateCode code = new ValidateCode();
return new ImageCode(code.getBuffImg(), code.getCode(), EXPIRE);
}
}
这里是把验证码放到了redis中, 关于redis的用法这里不多做介绍,当然也可以放到session里面去 , 生成图片验证码的话,网上有很多种方式 ,随便复制一直过来就好,最后拼成 ImageCode
对象就ok了
然后前端的页面也需要改一下,加入如下代码 如下:1
2
3<input class="text" type="text" name="imageCode" placeholder="验证码" required="">
<br>
<img src="/code/image">
一个验证码的输入框和图片标签, 图片的src就是我们上面接口的路径
最后在配置里面把这个接口加到不需要验证权限里面去
1 | .antMatchers( |
现在就可以启动项目看下效果了,如下:
验证
验证码生成完了之后,就还差验证了,我们需要一个过滤器,来放在 UsernamePasswordAuthenticationFilter
这个过滤器前面, 就是先验证验证码,然后再验证 用户名密码
这里我们要写一个过滤器, 新建一个类 ValidateCodeFilter
,代码如下: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@Data
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler;
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equals("/authentication/from", request.getRequestURI())
&&
StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
try {
// 这里做验证
validate(request);
} catch (ValidateCodeException exception) {
// 验证失败调用验证失败的处理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
// 直接返回 不走下一个过滤器了
return;
}
}
doFilter(request, response, filterChain);
}
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 缓存中拿出验证码来
String redisKey = Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId();
log.info("从缓存里取验证码,redisKey:{}", redisKey);
ImageCode code = (ImageCode) redisTemplate.opsForValue().get(redisKey);
// 参数中拿出验证码来,作比较
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (code == null) {
throw new ValidateCodeException("验证码不存在");
}
if (code.expired()) {
redisTemplate.delete(Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId());
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equalsIgnoreCase(code.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码错误");
}
// 最后通过的话也从缓存清除掉
redisTemplate.delete(Constants.IMAGE_CODE_KEY_PREFIX + request.getSession().getId());
}
}
看一下代码,继承 OncePerRequestFilter
表示这个过滤器只会被执行一次, 首先是判断,只处理 /authentication/from
这个请求, 然后必须是post请求, 然后是 下面是做验证, catch里面要捕获 ValidateCodeException
,这个异常是自定义的异常,然后如果捕获到异常了,那就掉失败处理器把这个异常传给失败处理器,然后直接return掉,就不走下面的逻辑了
然后是 ValidateCodeException
的代码,如下:1
2
3
4
5
6
7public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
这里就继承 ValidateCodeException
然后写个构造就ok了
错误信息处理
在我们自定义的失败处理器上,修改一下, 只返回错误信息,那些堆栈信息什么的,就不返回了, MyAuthenticationFailureHandler
中修改,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("认证失败...");
if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginResponseType())) {
// 只返回错误消息
Map<String, Object> map = new HashMap<>(1);
map.put("message", exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(map));
} else {
super.onAuthenticationFailure(request, response, exception);
}
}
配置过滤器使其生效
最后一步,就是把这个过滤器加到 UsernamePasswordAuthenticationFilter
的前面
修改 BrowserSecurityConfig
,部分代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14@Autowired
private RedisTemplate redisTemplate;
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
validateCodeFilter.setRedisTemplate(redisTemplate);
// formLogin 表示表单认证
http
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// ... 省略其他配置
}
先是实例化了我们刚创建的 ValidateCodeFilter
, 然后设置了失败处理器和 redisTemplete
,最后在下面加到 UsernamePasswordAuthenticationFilter
前面就ok了
测试
上面的配置完成之后,启动项目做一下测试
首先看一下输入一个错误的验证码,效果如下:
再看一下输入一个正确的,如下:
总结
本文主要就是加入了一个自定义的验证, 以及如何在 security 的过滤器链上加一个过滤器, 要继承 OncePerRequestFilter
表示这个过滤器只会被执行一次, 然后再配置类中通过 .addFilterBefore()
,方法来指定过滤器的位置
本文代码已上传github,传送门