security整合captcha验证码
前述
源代码已经上传到 Gitee,对应项目为
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>
二、新增登录失败处理器
/**
* 登录失败处理器
*
* @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)));
}
}
}
三、自定义验证验证码过滤器
/**
* 自定义验证代码过滤
*
* @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);
}
}
四、修改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;
}
}
五、新增验证码接口
/**
* @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());
}
}
六、登录页修改
<!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">
<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>