security整合captcha验证码
LiJunYi 7/27/2022 安全框架
# 前述
源代码已经上传到 Gitee (opens new window),对应项目为
codefilter
,登录方式为表单登录
本文在security基于表单提交使用 基础上进行修改
# SpringSecurity整合验证码登录实现
# 一、引入依赖
<properties>
<velocity-engine-core.version>2.3</velocity-engine-core.version>
<kaptcha.version>2.3.2</kaptcha.version>
</properties>
<dependencies>
<!--验证码-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity-engine-core.version}</version>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 二、新增登录失败处理器
/**
* 登录失败处理器
*
* @author LiJunYi
* @date 2022/07/27
*/
@Component
@Slf4j
public class CustomAuthenticationFailureImpl implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.error(e.getMessage());
String msg = "验证码";
if (e.getMessage().contains(msg))
{
int code = HttpStatus.ERROR;
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, e.getMessage())));
}else
{
int code = HttpStatus.UNAUTHORIZED;
msg = "用户名或密码错误";
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
}
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
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
# 三、自定义验证验证码过滤器
/**
* 自定义验证代码过滤
*
* @author LiJunYi
* @date: 2022/07/27
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter
{
@Resource
private CustomAuthenticationFailureImpl authenticationFailure;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
{
String loginUrl = "/userLogin";
if (loginUrl.equalsIgnoreCase(request.getRequestURI())
&& HttpMethod.POST.equalsIgnoreCase(request.getMethod())) {
try {
HttpSession session = request.getSession();
// Session中的校验码
String sessionImgCode = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
// Session 中校验码过期时间
LocalDateTime expireTime = (LocalDateTime) session.getAttribute(Constants.KAPTCHA_SESSION_DATE);
// 客户端提交的校验码
String requestImgCode = request.getParameter(com.example.codefilter.common.constant.Constants.CAPTCHA_CODE);
if (StrUtil.isEmpty(requestImgCode)) {
throw new ValidateCodeException("验证码不能为空!");
}
if (expireTime == null || LocalDateTime.now().isAfter(expireTime)) {
// 清除Session中校验码相关信息
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
session.removeAttribute(Constants.KAPTCHA_SESSION_DATE);
throw new ValidateCodeException("验证码已过期!");
}
if (!requestImgCode.equalsIgnoreCase(sessionImgCode)) {
throw new ValidateCodeException("验证码不正确!");
}
// 清除Session中校验码相关信息
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
session.removeAttribute(Constants.KAPTCHA_SESSION_DATE);
} catch (ValidateCodeException e) {
authenticationFailure.onAuthenticationFailure(request, response, e);
return;
}
}
chain.doFilter(request, response);
}
}
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
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
# 四、修改SecurityConfig,加入自定义过滤器
/**
* @version 1.0.0
* @className: SecurityConfig
* @description: SpringSecurity 5.7.x新用法配置
* @author: LiJunYi
* @create: 2022/7/26 8:43
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
/**
* 自定义用户登录处理逻辑
*/
private final MyUserDetailsServiceImpl userDetailsService;
/**
* 登录成功处理
*/
private final CustomAuthenticationSuccessImpl customAuthenticationSuccess;
/**
* 登录失败处理
*/
private final CustomAuthenticationFailureImpl customAuthenticationFailure;
/**
* 注销成功后处理
*/
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 未经授权处理程序
*/
private final AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 并发登录控制处理
*/
private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;
/**
* 验证代码过滤
*/
private final ValidateCodeFilter validateCodeFilter;
/**
* 构造函数注入
*
* @param userDetailsService 用户详细信息服务
* @param logoutSuccessHandler 注销成功处理程序
* @param unauthorizedHandler 未经授权处理程序
* @param expiredSessionStrategy 过期会话策略
* @param customAuthenticationSuccess 自定义身份验证成功
* @param customAuthenticationFailure 登录失败处理器
* @param validateCodeFilter 自定义验证码过滤器
*/
@Autowired
public SecurityConfig(MyUserDetailsServiceImpl userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler,
AuthenticationEntryPointImpl unauthorizedHandler,
CustomExpiredSessionStrategyImpl expiredSessionStrategy,
CustomAuthenticationSuccessImpl customAuthenticationSuccess, CustomAuthenticationFailureImpl customAuthenticationFailure, ValidateCodeFilter validateCodeFilter) {
this.userDetailsService = userDetailsService;
this.logoutSuccessHandler = logoutSuccessHandler;
this.unauthorizedHandler = unauthorizedHandler;
this.expiredSessionStrategy = expiredSessionStrategy;
this.customAuthenticationSuccess = customAuthenticationSuccess;
this.customAuthenticationFailure = customAuthenticationFailure;
this.validateCodeFilter = validateCodeFilter;
}
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
return configuration.getAuthenticationManager();
}
/**
* 配置加密方式
*
* @return {@link PasswordEncoder}
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
* 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
* @return {@link HttpSessionEventPublisher}
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
/**
* 过滤器链
*
* @param http http
* @return {@link SecurityFilterChain}
* @throws Exception 异常
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
// 添加验证码校验过滤器,在UsernamePasswordAuthenticationFilter过滤器前执行
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests(authorize ->
authorize.mvcMatchers("/login","/userLogin","/captchaCode","/noPermission","/static/css/**","/static/util/javascript/**").permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/userLogin")
// 登录成功后的处理器
.successHandler(customAuthenticationSuccess)
// 登录失败处理器
.failureHandler(customAuthenticationFailure)
.permitAll());
// 退出过滤器
http.logout(logout -> logout
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(logoutSuccessHandler));
// 无权限访问处理器
http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
// 并发会话控制
http.sessionManagement(session -> session
.maximumSessions(1)
.expiredSessionStrategy(expiredSessionStrategy));
http.userDetailsService(userDetailsService);
return http.build();
}
/**
* 验证码生产商
*
* @return {@link DefaultKaptcha}
*/
@Bean
public DefaultKaptcha captchaProducer() {
Properties properties = new Properties();
// 显示边框
properties.setProperty("kaptcha.border","yes");
// 边框颜色
properties.setProperty("kaptcha.border.color","105,179,90");
// 字体颜色
properties.setProperty("kaptcha.textproducer.font.color","blue");
// 字体大小
properties.setProperty("kaptcha.textproducer.font.size","35");
// 图片宽度
properties.setProperty("kaptcha.image.width","125");
// 图片高度
properties.setProperty("kaptcha.image.height","40");
// 验证码长度
properties.setProperty("kaptcha.textproducer.char.length","4");
// 文本内容 从设置字符中随机抽取
properties.setProperty("kaptcha.textproducer.char.string","0A1B2C3D4E5F6g7h8i9jk");
DefaultKaptcha kaptcha = new DefaultKaptcha();
kaptcha.setConfig(new Config(properties));
return kaptcha;
}
}
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# 五、新增验证码接口
/**
* @version 1.0.0
* @className: ValidateController
* @description: 获取验证码
* @author: LiJunYi
* @create: 2022/7/27 9:49
*/
@Controller
public class ValidateController
{
@Resource
private Producer captchaProducer;
@GetMapping("/captchaCode")
public void createImgCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成图形校验码内容
String text = captchaProducer.createText();
// 将验证码内容存入HttpSession
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);
// 将验证码有效期存入HttpSession,60秒有效
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_DATE, LocalDateTime.now().plusSeconds(60));
// 生成图形校验码图片
BufferedImage bufferedImage = captchaProducer.createImage(text);
// 将校验码图片信息输出到浏览器
ImageIO.write(bufferedImage, "jpeg", response.getOutputStream());
}
}
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
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
# 六、登录页修改
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统登录</title>
<link rel="stylesheet" type="text/css" th:href="@{/static/css/elemui-2.15.9.css}"/>
</head>
<body>
<div class="el-form-item">
<div class="el-form-item__content" style="margin-left: 80px;">
<h1>登录页面</h1>
</div>
</div>
<form id="form-login" method="post">
<p class="error"></p>
<div class="el-form-item">
<label class="el-form-item__label" style="width: 80px">用户名</label>
<div class="el-form-item__content" style="margin-left: 80px;">
<div class="el-input">
<input style="width: 300px;" type="text" autocomplete="off" class="el-input__inner" name="username"/>
</div>
</div>
</div>
<div class="el-form-item">
<label class="el-form-item__label" style="width: 80px">密码</label>
<div class="el-form-item__content" style="margin-left: 80px;">
<div class="el-input">
<input style="width: 300px;" type="password" autocomplete="off" class="el-input__inner" name="password"/>
</div>
</div>
</div>
<div class="el-form-item">
<label class="el-form-item__label" style="width: 80px">验证码</label>
<div class="el-form-item__content" style="margin-left: 80px;">
<div class="el-input">
<input style="width: 300px;" type="text" autocomplete="off" class="el-input__inner" name="captchaCode"/>
</div>
</div>
</div>
<div class="el-form-item">
<div class="el-form-item__content" style="margin-left: 80px;">
<img id="captchaCode" src="/captchaCode" style="cursor: pointer;" title="看不清?换一张"/>
</div>
</div>
<div class="el-form-item">
<div class="el-form-item__content" style="margin-left: 80px;">
<button type="button" class="el-button el-button--primary" id="btn-login">
<span>登录</span>
</button>
</div>
</div>
</form>
<script type="text/javascript" th:src="@{/static/util/javascript/jquery-3.5.1.min.js}"></script>
<script th:inline="javascript">
const ctx = [[${#httpServletRequest.getContextPath()}]];
$('#btn-login').bind('click',function () {
$.ajax({
url: '/userLogin',
type: 'post',
data: $('#form-login').serialize(),
success: function (obj) {
debugger
if (obj.code == 200) {
window.location.href = ctx + '/index';
} else {
$('.error').text(obj.msg);
}
},
dataType:'json'
});
})
// 刷新验证码
$("#captchaCode").bind("click", function () {
$(this).hide().attr('src', '/captchaCode?random=' + Math.random()).fadeIn();
});
</script>
</body>
</html>
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
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