验证码校验也是通过Spring Security中的过滤器链来进行校验的。
文章目录
编写图形验证码服务
1、创建 ImageCode 实体类
该实体类用于存储验证码的相关数据。
@Data
public class ImageCode {
/**
* 图形验证码
*/
private BufferedImage imageCode;
/**
* 验证码
*/
private String code;
/**
* 过期时间
*/
private LocalDateTime expireTime;
public ImageCode(BufferedImage imageCode, String code, int expireTime) {
this.imageCode = imageCode;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
}
public ImageCode(BufferedImage imageCode, String code, LocalDateTime expireTime) {
this.imageCode = imageCode;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpire() {
return LocalDateTime.now().compareTo(this.expireTime) > 0;
}
}
2、创建验证码服务
@RestController
@RequestMapping
public class ValidateCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/** ImageCode在session中的key */
public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成imageCode
ImageCode imageCode = createImageCode(request);
// 将imageCode 保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream());
}
public ImageCode createImageCode(HttpServletRequest request) {
// 在内存中创建图像
int width = 65, height = 20;
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphics = bufferedImage.getGraphics();
// 设定背景色
graphics.setColor(getRandColor(230, 255));
graphics.fillRect(0, 0, 100, 25);
// 设定字体
graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
// 产生0条干扰线,
graphics.drawLine(0, 0, 0, 0);
// 随机产生四位验证码
String sRand = "";
Random random = new Random();
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 将认证码显示到图象中
graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
graphics.drawString(rand, 15 * i + 6, 16);
}
graphics.dispose();
return new ImageCode(bufferedImage, sRand, 60);
}
/**
* 给定范围获得随机颜色
*
* @param fc
* @param bc
* @return
*/
Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
3、在 Security 的配置文件中,将获取验证码的请求,进行认证过滤。
.antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAl()
4、登录界面的html代码
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" value="user"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" value="123456"></td>
</tr>
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登陆</button></td>
</tr>
</table>
</form>
验证码校验
通过Filter实现,验证码的校验逻辑。自定义的验证码过滤器,前置到UsernamePasswordAuthenticationFilter之前。
1、自定义验证码校验过滤器
public class ValidateCodeFilter extends OncePerRequestFilter {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if("/authentication/form".equals(httpServletRequest.getRequestURI())
&& "POST".equalsIgnoreCase(httpServletRequest.getMethod())) {
try {
validate(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
// 调用失败处理器
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
ImageCode imageCodeSession = (ImageCode)sessionStrategy.getAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
String code = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),"imageCode");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (imageCodeSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (imageCodeSession.isExpire()) {
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
throw new ValidateCodeException("验证码已过期");
}
if (!StrUtil.equals(imageCodeSession.getCode(), code)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
2、自定义的异常类
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
3、配置验证码校验过滤器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
/**
* 为减少代码重复开发,多个应用使用同一个认证中心,每个应用需要自己指定登录页面。
* 这里需要将 loginpage 指向一个controlelr地址。
* 如果是html页面,就跳转到指定的登录页。
* 如果不是html页面,就提示401 没有认证信息。
* 如果有应用有指定的就使用自己的。如果没指定就使用本认证模块默认的登录页。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 引入验证码过滤器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);
// 配置过滤器的位置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.successForwardUrl("/index")
// .defaultSuccessUrl("/index")
.and()
.authorizeRequests()
.antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
验证码代码重构
重构代码将一些参数,改为可配置的,降低代码耦合度; 在这个Spring Security的案例中,是把当前应用作为可以重用的应用架构,进而可以引入到其他应用中去,减少用户认证的重复开发。每个应用可能可能自己的验证码生成逻辑不同,可以通过代码重构,让第三方应用实现自己的生成验证码的逻辑。
验证码基本参数配置
应用级别配置:配置在引用的第三方项目中
默认级别配置:给配置值指定设置默认值
1、验证码参数配置如下:
security:
browser:
logintype: html
validate_code:
image:
width: 80
height: 40
length: 5
expire_time: 100
2、对应的配置实体类
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties validateCode = new ValidateCodeProperties();
// 省略getter和setter方法
}
@Data
public class ValidateCodeProperties {
ImageCodeProperties image = new ImageCodeProperties();
}
@Data
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4; // 验证码长度
private int expireTime = 60; // 过期时间
}
3、在ValidateCodeController中,将对应的验证码参数改为从配置类中读取。
public ImageCode createImageCode(HttpServletRequest request) {
ImageCodeProperties image = securityProperties.getValidateCode().getImage();
// 在内存中创建图像
// int width = 65, height = 20;
int width = ServletRequestUtils.getIntParameter(request, "width", image.getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", image.getHeight());
int length = ServletRequestUtils.getIntParameter(request, "length", image.getLength());
int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", image.getExpireTime());
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphics = bufferedImage.getGraphics();
// 设定背景色
graphics.setColor(getRandColor(230, 255));
graphics.fillRect(0, 0, 100, 25);
// 设定字体
graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
// 产生0条干扰线,
graphics.drawLine(0, 0, 0, 0);
// 随机产生四位验证码
String sRand = "";
Random random = new Random();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 将认证码显示到图象中
graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
graphics.drawString(rand, 15 * i + 6, 16);
}
graphics.dispose();
return new ImageCode(bufferedImage, sRand, expireTime);
}
验证码拦截的接口可配置
如果 第三方应用认证的接口和默认的路径不一样呢,这时候就需要改为可配置的了。
在上面的配置类中添加url属性。
在验证码校验过滤器的 ValidateCodeFilter
类中,实现 接口 InitializingBean
的方法afterPropertiesSet()
实现了 InitializingBean
接口的类,实例化bean时,会自动执行,afterPropertiesSet()
方法。
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
private Set<String> urls; // 存储需要拦截的url
private SecurityProperties securityProperties;
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String url = securityProperties.getValidateCode().getImage().getUrl();
String[] configUrl = url.split(",");
urls = Stream.of(configUrl).collect(Collectors.toSet());
// 添加默认的地址
urls.add("/authentication/form");
}
/**
* 重构
* @param httpServletRequest
* @param httpServletResponse
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (url.equals(httpServletRequest.getRequestURI())) {
action = true;
break;
}
}
if (action) {
try {
validate(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
// 调用失败处理器
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
ValidateCodeFilter
类没有通过Spring Bean的方式注入,还是需要在SecurityConfig
中进行手动调用 afterPropertiesSet()
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// 配置过滤器的位置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.successForwardUrl("/index")
// .defaultSuccessUrl("/index")
.and()
.authorizeRequests()
.antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
验证码实现逻辑可配
思路: 逻辑可配,就是抽象成接口,实现由客户端实现。
1、 定义生成验证码逻辑的接口
/**
* @Author L.jg
* @Title 抽象接口,让客户端可配置接口
* @Date 2021/5/24 11:42
*/
public interface ValidateCodeGenerate {
ImageCode generate(HttpServletRequest request);
}
2、实现接口,自定义验证码的实现逻辑
public class ImageCodeGenerate implements ValidateCodeGenerate {
private ImageCodeProperties imageCodeProperties;
public ImageCodeGenerate(ImageCodeProperties imageCodeProperties) {
this.imageCodeProperties = imageCodeProperties;
}
@Override
public ImageCode generate(HttpServletRequest request) {
int width = ServletRequestUtils.getIntParameter(request, "width", imageCodeProperties.getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", imageCodeProperties.getHeight());
int length = ServletRequestUtils.getIntParameter(request, "length", imageCodeProperties.getLength());
int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", imageCodeProperties.getExpireTime());
return createImageCode(width, height, length, expireTime);
}
public ImageCode createImageCode(int width, int height, int length, int expireTime) {
// 在内存中创建图像
// int width = 65, height = 20;
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphics = bufferedImage.getGraphics();
// 设定背景色
graphics.setColor(getRandColor(230, 255));
graphics.fillRect(0, 0, 100, 25);
// 设定字体
graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
// 产生0条干扰线,
graphics.drawLine(0, 0, 0, 0);
// 随机产生四位验证码
String sRand = "";
Random random = new Random();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 将认证码显示到图象中
graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
graphics.drawString(rand, 15 * i + 6, 16);
}
graphics.dispose();
return new ImageCode(bufferedImage, sRand, expireTime);
}
/**
* 给定范围获得随机颜色
*
* @param fc
* @param bc
* @return
*/
Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
3、 在Spring boot中注入Bean
@Configuration
public class VlidateCodeConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
// 如果Spring 容易中存在 imageCodeGenerate 的bean就不会再初始化该bean了
@ConditionalOnMissingBean(name = "imageCodeGenerate")
public ValidateCodeGenerate imageCodeGenerate() {
ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getValidateCode().getImage());
return imageCodeGenerate;
}
}
4、 在验证码controller中,注入验证码实现类
@RestController
@RequestMapping
public class ValidateCodeController {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ValidateCodeGenerate validateCodeGenerate;
/** ImageCode在session中的key */
public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 生成imageCode
// ImageCode imageCode = createImageCode(request);
ImageCode imageCode = validateCodeGenerate.generate(request);
// 将imageCode 保存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream());
}
}