springsecurity图形验证码
# 添加验证码过滤器
@Slf4j
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Autowired
private AuthenticationFailureHandler failureHandler;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 只在登录时对验证码进行拦截,验证
*/
private static String url = "/login";
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String uri = request.getRequestURI();
if(antPathMatcher.match(url,uri) && "POST".equalsIgnoreCase(request.getMethod())){
try {
check(new ServletWebRequest(request));
}catch (org.springframework.security.core.AuthenticationException e) {
log.error("验证码验证失败:{}",e.getMessage());
failureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
// 调后边的过滤器
filterChain.doFilter(request, response);
}
@SneakyThrows
private void check(ServletWebRequest request){
String code = request.getParameter("code"); // 输入的code
// 校验验证码
String uuid = StrUtil.blankToDefault(request.getParameter("uuid"),
WebUtil.getCookieVal(SecurityConstants.CAPTCHA_COOKIENAME_UUID));
if (StrUtil.isBlank(uuid)) {
throw new ValidateCodeException("没有uuid");
}
Object redisCode = redisTemplate.opsForValue().get(uuid);
if(ObjectUtil.isEmpty(redisCode)){
throw new ValidateCodeException("验证码已过期");
}
if(StrUtil.isBlank(code)){
throw new ValidateCodeException("请输入验证码");
}
if(!code.equalsIgnoreCase(redisCode.toString())){
throw new ValidateCodeException("验证码不准确");
}
}
}
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
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
# 放置过滤器于UsernamePasswordAuthenticationFilter之前
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有的请求都需要认证
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated();
// 表单登陆配置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(successHandler)
.failureHandler(failureHandler);
// 安全策略配置
http.csrf().disable();
http.httpBasic().disable()
.sessionManagement().disable()
.cors();
http.logout()
.permitAll();
}
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
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
发送请求,查看 FilterChainProxy
里面是否有过滤器,及其顺序是否合适。
# 登陆、验证码请求
/**
* 前后端分享的项目,不会进来这个url
*/
@GetMapping("/login")
public String loginUI(HttpServletRequest request,HttpServletResponse response){
String cookieVal = WebUtil.getCookieVal(SecurityConstants.CAPTCHA_COOKIENAME_UUID);
if(StrUtil.isBlank(cookieVal)){
// 验证码的uuid身份,可以保存久些,为一年
WebUtil.setCookie(response, SecurityConstants.CAPTCHA_COOKIENAME_UUID,UUID.randomUUID().toString(),360*24*3600);
}
return view("login/login");
}
/**
* 前后端app无cookie的项目,会带上uuid身份, 前后端分离的,将uuid设置到cookie值中
* @param response
* @throws Exception
*/
@GetMapping("/images/captcha")
@SneakyThrows
public void captcha(
@RequestParam(required = false) String uuid,
@RequestParam(defaultValue = "80") Integer width,
@RequestParam(defaultValue = "24") Integer height,
@RequestParam(defaultValue = "4") Integer len,
@RequestParam(defaultValue = "1") Integer type,
HttpServletRequest request, HttpServletResponse response) throws Exception{
Captcha captcha = CaptchaHelper.getEasyCaptcha(width, height, len, type);
captcha.setCharType(Captcha.TYPE_ONLY_NUMBER);
String verCode = captcha.text(); // 生成的验证码
log.debug("图片验证码:"+verCode);
if(StrUtil.isBlank(uuid)){
uuid = WebUtil.getCookieVal(SecurityConstants.CAPTCHA_COOKIENAME_UUID);
}
redisTemplate.opsForValue().set(uuid,verCode,SecurityConstants.CAPTCHA_EXPIRED_TIME, TimeUnit.SECONDS);
CaptchaUtil.out(captcha, request, response); // 默认会保存到session当中 SESSION_KEY = "captcha";
}
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
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
# 验证码工具类
开源项目EasyCaptcha、 hutool中都有图片验证码生成的逻辑,这里我把全整合了。
/**
* 验证码生成工具
*
* @Author: shollin
* @Date: 2021年5月14日 上午9:23:15
* @version:
*/
@Slf4j
@UtilityClass
public class CaptchaHelper {
// 文档:https://gitee.com/whvse/EasyCaptcha
public Captcha getEasyCaptcha(Integer width,Integer height, Integer len, Integer type) {
if (width == null) {
width = 80;
}
if (height == null) {
height = 24;
}
boolean lenIsNull = false;
if (len == null) {
len = 4;
lenIsNull = true;
}
Captcha captcha = null;
switch (type) {
case 1:
// png类型
captcha = new SpecCaptcha(width,height,len);
break;
case 2:
// gif类型
captcha = new GifCaptcha(width,height,len);
break;
case 3:
// 中文类型
captcha = new ChineseCaptcha(width,height,len);
break;
case 4:
// 中文gif类型
captcha = new ChineseGifCaptcha(width,height,len);
break;
case 5:
// 算术类型
if(lenIsNull) {
len = 2;
}
captcha = new ArithmeticCaptcha(width,height);
captcha.setLen(len); // 几位数运算,默认是两位
// captcha.getArithmeticString() 获取运算的公式:3+2=?
break;
default:
// png类型
captcha = new SpecCaptcha(width,height,len);
}
return captcha;
}
// 参考文档:https://www.hutool.cn/docs/#/captcha/%E6%A6%82%E8%BF%B0
public AbstractCaptcha getCaptcha(Integer width,Integer height, Integer codeCount, Integer type,Integer interfereCount) {
//定义图形验证码的长和宽
if (width == null) {
width = 80;
}
if (height == null) {
height = 24;
}
AbstractCaptcha captcha = null;
switch (type) {
case 1:
// Line类型, 默认干扰线150
if (interfereCount == null) {
interfereCount = 10;
}
captcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(width, height,codeCount,interfereCount);
break;
case 2:
// Circle类型 默认干扰线15
if (interfereCount == null) {
interfereCount = 15;
}
captcha = cn.hutool.captcha.CaptchaUtil.createCircleCaptcha(width, height,codeCount,interfereCount);
break;
case 3:
// gif类型 默认干扰线10
captcha = cn.hutool.captcha.CaptchaUtil.createGifCaptcha(width, height, codeCount);
break;
case 4:
// Shear扭曲类型
if (interfereCount == null) {
interfereCount = 1;
}
captcha = cn.hutool.captcha.CaptchaUtil.createShearCaptcha(width, height,codeCount,interfereCount);
break;
default:
// png类型
captcha = cn.hutool.captcha.CaptchaUtil.createGifCaptcha(width, height, codeCount);
}
return captcha;
}
}
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
100
101
102
103
104
105
106
107
108
109
110
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
100
101
102
103
104
105
106
107
108
109
110
# 高阶代码
# 1、验证码的宽、高、符号数量、失效时间可配
这里通过请求的参数width、height、len、type,来自动生成对应的代码
请求级request 覆盖应用级demo的配置、应用级配置覆盖架构core默认的配置
构架级: ValidateCodeProperties
应用级demo: 通过application.properties进行修改
请求级: 通过请求参数来修改。
# 2、哪些请求需要验证码过滤可配
上面的代码只对 POST的/login请求,进行了拦截校验, 更多的请求,需要写在数据库或者配置文件里面。
# 3、验证码的生成逻辑可配
参考文章:
SpringSecurity5 (3) ——使用验证码登录 - 山那边风景 - 博客园 (cnblogs.com) (opens new window)