SpringSecurity使用过滤器实现图形验证码
1、图形验证码的作用
图形验证码(CAPTCHA,Completely Automated Public Turing test to tell Computers and Humans Apart)是一种用于区分用户是人类还是计算机程序的自动化测试。它通常用于防止自动化软件(如机器人或爬虫程序)进行恶意操作,如滥用在线服务、暴力破解密码或进行垃圾邮件发送等。
图形验证码的工作原理基于一个假设:计算机程序难以自动识别和处理复杂的图像或模式,而人类则相对容易。因此,图形验证码通常包含扭曲的文字、数字、图像或它们的组合,这些元素对人类来说相对容易辨认,但对计算机程序来说则非常困难。
下面将介绍 Spring Boot 整合 Spring Security 实现图形验证码功能,执行结果如下如:
(1)登录页面
(2)登录成功后,跳转至首页
2、创建项目
【示例】SpringBoot 整合 SpringSecurity 使用过滤器实现图形验证码功能。
2.1 创建 Spring Boot 项目
创建 SpringBoot 项目,项目结构如下图:
2.2 添加 Maven 依赖
在 pom.xml 配置文件中添加 Spring Security、谷歌 Kaptcha 图形验证码。
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.18</version>
</dependency>
<!-- 谷歌 Kaptcha 图形验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
3、整合 Spring Security 框架实现认证与授权
3.1 配置类(Config 层)
创建 WebSecurityConfig 类(Spring Security 配置类),并添加 @EnableWebSecurity 注解和继承 WebSecurityConfigurerAdapter 类。
package com.pjb.securitydemo.config;
import com.pjb.securitydemo.filter.VerificationCodeFilter;
import com.pjb.securitydemo.handler.LoginFailureHandler;
import com.pjb.securitydemo.handler.LoginSuccessHandler;
import com.pjb.securitydemo.handler.PermissionDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 配置类
* @author pan_junbiao
**/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private PermissionDeniedHandler permissionDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests() //返回一个URL拦截注册器
.antMatchers("/captcha.jpg").permitAll() //公开其权限
.anyRequest() //匹配所有的请求
.authenticated() //所有匹配的URL都需要被认证才能访问
.and() //结束当前标签,让上下文回到 HttpSecurity
.formLogin() //启动表单认证
.loginPage("/myLogin.html") //自定义登录页面
.loginProcessingUrl("/auth/form") //指定处理登录请求路径
.permitAll() //使登录页面不设限访问
//.defaultSuccessUrl("/index") //登录认证成功后的跳转页面
.successHandler(loginSuccessHandler) //指定登录成功时的处理
.failureHandler(loginFailureHandler) //指定登录失败时的处理
.and()
.exceptionHandling().accessDeniedHandler(permissionDeniedHandler) //403无权时的返回操作
.and().csrf().disable(); //关闭CSRF的防御功能
//图形验证码过滤器(核心代码):将自定义过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 内存中添加登录账号
*/
@Bean
public UserDetailsService userDetailsService()
{
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password("123456").roles("ADMIN").build());
manager.createUser(User.withUsername("user").password("123456").roles("USER").build());
manager.createUser(User.withUsername("panjunbiao").password("123456").roles("USER").build());
return manager;
}
/**
* 密码编译器
* 由于5.x版本之后默认启用了委派密码编译器,
* 因而按照以往的方式设置内存密码将会读取异常,
* 所以需要暂时将密码编码器设置为 NoOpPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder()
{
return NoOpPasswordEncoder.getInstance();
}
}
3.2 处理类(Handler 层)
(1)登录成功处理类
package com.pjb.securitydemo.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登录成功处理类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException
{
//重定向至首页
httpServletResponse.sendRedirect("/");
}
}
(2)登录失败处理类
package com.pjb.securitydemo.handler;
import com.pjb.securitydemo.exception.VerificationCodeException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler
{
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException
{
//获取登录失败原因
String errorMessage = "";
if(authenticationException instanceof BadCredentialsException){
errorMessage = "用户名或密码不正确";
}else if(authenticationException instanceof DisabledException){
errorMessage = "账号被禁用";
}else if(authenticationException instanceof UsernameNotFoundException){
errorMessage = "用户名不存在";
}else if(authenticationException instanceof CredentialsExpiredException){
errorMessage = "密码已过期";
}else if(authenticationException instanceof LockedException) {
errorMessage = "账号被锁定";
}else if(authenticationException instanceof VerificationCodeException){
errorMessage = "无效的图形验证码";
}else{
errorMessage = "未知异常";
}
//设置响应编码
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(errorMessage);
}
}
(3)403无权限处理类
package com.pjb.securitydemo.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 403无权限处理类
*/
@Component
public class PermissionDeniedHandler implements AccessDeniedHandler
{
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException
{
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("403无权限");
}
}
4、整合 Kaptcha 框架实现图形验证码
4.1 配置类(Config 层)
创建 KaptchaConfig 类(Kaptcha 图形验证码配置类),设置图形验证码相关属性。
package com.pjb.securitydemo.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 谷歌Kaptcha图形验证码配置类
*/
@Configuration
public class KaptchaConfig
{
@Bean
public Producer captcha()
{
//配置图形验证码的基本参数
Properties properties = new Properties();
//图片宽度
properties.setProperty("kaptcha.image.width","150");
//图片长度
properties.setProperty("kaptcha.image.height","50");
//字符集(从哪些字符中产生)
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
//字符长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字体颜色
properties.put("kaptcha.textproducer.font.color", "red");
// 文字间隔,这里设置为10px
properties.put("kaptcha.textproducer.char.space", "10");
// 背景颜色渐变开始
properties.put("kaptcha.background.clear.from", "yellow");
// 背景颜色渐变结束
properties.put("kaptcha.background.clear.to", "green");
//初始化配置
Config config = new Config(properties);
//使用默认的图形验证码实现,当然也可以自定义实现
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
图形验证码配置属性表
属性名 | 属性作用 | 默认值 |
---|---|---|
kaptcha.border | 图片边框,合法值:yes , no | yes |
kaptcha.border.color | 边框颜色,合法值: r,g,b (and optional alpha) 或者 white,black,blue. | black |
kaptcha.image.width | 图片宽 | 200 |
kaptcha.image.height | 图片高 | 50 |
kaptcha.producer.impl | 图片实现类 | com.google.code.kaptcha.impl.DefaultKaptcha |
kaptcha.textproducer.impl | 文本实现类 | com.google.code.kaptcha.text.impl.DefaultTextCreator |
kaptcha.textproducer.char.string | 文本集合,验证码值从此集合中获取 | abcde2345678gfynmnpwx |
kaptcha.textproducer.char.length | 验证码长度 | 5 |
kaptcha.textproducer.font.names | 字体 | Arial, Courier |
kaptcha.textproducer.font.size | 字体大小 | 40px. |
kaptcha.textproducer.font.color | 字体颜色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.textproducer.char.space | 文字间隔 | 2 |
kaptcha.noise.impl | 干扰实现类 | com.google.code.kaptcha.impl.DefaultNoise |
kaptcha.noise.color | 干扰 颜色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.obscurificator.impl | 图片样式:<br />水纹 com.google.code.kaptcha.impl.WaterRipple <br /> 鱼眼 com.google.code.kaptcha.impl.FishEyeGimpy <br /> 阴影 com.google.code.kaptcha.impl.ShadowGimpy | com.google.code.kaptcha.impl.WaterRipple |
kaptcha.background.impl | 背景实现类 | com.google.code.kaptcha.impl.DefaultBackground |
kaptcha.background.clear.from | 背景颜色渐变,开始颜色 | light grey |
kaptcha.background.clear.to | 背景颜色渐变, 结束颜色 | white |
kaptcha.word.impl | 文字渲染器 | com.google.code.kaptcha.text.impl.DefaultWordRenderer |
kaptcha.session.key | session key | KAPTCHA_SESSION_KEY |
kaptcha.session.date | session date |
4.2 控制器层(Controller 层)
创建 CaptchaController 类(验证码控制器),实现生成验证码图片方法。
package com.pjb.securitydemo.controller;
import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
* 验证码控制器
*/
@Controller
public class CaptchaController
{
@Autowired
private Producer captchaProducer;
@GetMapping("/captcha.jpg")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException
{
//设置内容类型
response.setContentType("image/jpeg");
//创建验证码文本
String capText = captchaProducer.createText();
//将验证码文本保存到Session中
request.getSession().setAttribute("captcha", capText);
//创建验证码图片
BufferedImage bufferedImage = captchaProducer.createImage(capText);
//获取响应输出流
ServletOutputStream out = response.getOutputStream();
//将图片验证码数据写入响应输出流
ImageIO.write(bufferedImage,"jpg",out);
//推送并关闭响应输出流
try
{
out.flush();
}
finally
{
out.close();
}
}
}
4.3 自定义异常类(Exception 层)
自定义异常类 VerificationCodeException(验证码校验失败的异常类),继承 AuthenticationException 类。
package com.pjb.securitydemo.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 验证码校验失败的异常类
*/
public class VerificationCodeException extends AuthenticationException
{
public VerificationCodeException()
{
super("图形验证码校验失败");
}
}
4.4 自定义过滤器(Filter 层)
自定义过滤器类 VerificationCodeFilter (验证码校验过滤器),继承 OncePerRequestFilter 类。
有了图形验证码的 API 之后,就可以自定义验证码校验过滤器了。虽然 Spring Security 的过滤器链对过滤器没有特殊要求,只要继承 Filter 接口即可,但是在 Spring 体系中,推荐使用 OncePerRequestFilter 类来实现,它可以确保一次请求只会通过一次该过滤器(Filter 实际上并不能保证这一点)。
package com.pjb.securitydemo.filter;
import com.pjb.securitydemo.exception.VerificationCodeException;
import com.pjb.securitydemo.handler.LoginFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 验证码校验过滤器
*/
public class VerificationCodeFilter extends OncePerRequestFilter
{
private AuthenticationFailureHandler authenticationFailureHandler = new LoginFailureHandler();
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
{
//非登录请求不校验验证码
String requestURI = httpServletRequest.getRequestURI();
if(!"/auth/form".equals(requestURI))
{
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
else
{
try
{
//验证码校验
verificationCode(httpServletRequest);
//验证成功
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
catch (VerificationCodeException ex)
{
//验证失败
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, ex);
}
}
}
/**
* 验证码校验
*/
public void verificationCode(HttpServletRequest httpServletRequest) throws VerificationCodeException
{
String requestCode = httpServletRequest.getParameter("captcha");
HttpSession session = httpServletRequest.getSession();
String savedCode = (String)session.getAttribute("captcha");
if(!StringUtils.isEmpty(savedCode))
{
//随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
session.removeAttribute("captcha");
}
//验证不通过,抛出异常
if(StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(savedCode) || !requestCode.equals(savedCode))
{
throw new VerificationCodeException();
}
}
}
至此整合 Kaptcha 框架实现图形验证码已完成,最后注意,一定要把自定义过滤器类 VerificationCodeFilter 添加到 Spring Security 的过滤器链中。
打开 WebSecurityConfig 类(Spring Security 配置类),将自定义过滤器类 VerificationCodeFilter 添加到过滤器链中,如下:
5、前端页面
5.1 控制器层(Controller 层)
创建 IndexController 类(首页控制器),实现获取当前登录用户名并跳转至首页。
package com.pjb.securitydemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
/**
* 首页控制器
* @author pan_junbiao
**/
@Controller
public class IndexController
{
/**
* 首页
*/
@RequestMapping("/")
public String index(HttpServletRequest request)
{
//获取当前登录人
String userName = "未登录";
Principal principal = request.getUserPrincipal();
if(principal!=null)
{
userName = principal.getName();
}
//返回页面
request.setAttribute("userName",userName);
return "/index.html";
}
}
5.2 编写登录页面
在 resources\static 静态资源目录下,创建 myLogin.html 页面。
注意:myLogin.html 页面必须放在 resources\static 静态资源目录下,否则页面无法加载。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<form name="myForm" action="/auth/form" method="post">
<table align="center">
<caption>用户登录</caption>
<tr>
<td>登录账户:</td>
<td>
<input type="text" name="username" placeholder="请输入登录账户" value="panjunbiao" />
</td>
</tr>
<tr>
<td>登录密码:</td>
<td>
<input type="password" name="password" placeholder="请输入登录密码" value="123456" />
</td>
</tr>
<tr>
<td>验证码:</td>
<td>
<!-- 新增图形验证码的输入框 -->
<input type="text" name="captcha" placeholder="请输入验证码" />
<!-- 图片指向图形验证码API -->
<img src="/captcha.jpg" alt="captch" height="50px" width="150px" style="margin-left:20px;" >
</td>
</tr>
<!-- 以下是提交、取消按钮 -->
<tr>
<td colspan="2" style="text-align: center; padding: 5px;">
<input type="submit" value="提交" />
<input type="reset" value="重置" />
</td>
</tr>
</table>
</form>
</body>
</html>
5.3 编写首页
在 resources\templates 资源目录下,创建 index.html 页面。
注意:首页 index.html 页面中使用 Thymeleaf 模板 。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<meta name="author" content="pan_junbiao的博客">
</head>
<body>
<h1 style="color: red">Hello,Spring Security</h1>
<p>博客信息:您好,欢迎访问 pan_junbiao的博客</p>
<p>博客地址:https://blog.csdn.net/pan_junbiao</p>
<p th:text="'当前登录人:' + ${userName}"></p>
<a href="/logout" onclick="return confirm('确认注销吗?');">登出</a>
</body>
</html>
6、运行项目
6.1 登录页面
6.2 图形验证码校验失败
6.3 登录成功后,跳转至首页