SpringBoot进阶教程(八十二)Spring Security图形验证码

cnblogs 2024-10-15 08:09:01 阅读 64

在之前的博文《SpringBoot进阶教程(八十)Spring Security》中,已经介绍了在Spring Security中如何基于formLogin认证、基于HttpBasic认证和自定义用户名和密码。这篇文章,我们将介绍自定义登录界面的登录验证方式。在上一篇博文《SpringBoot进阶教程(八十一)Spring Security自定义认证》中,已经介绍了如何实现Spring Security自定义认证。

v生成图形验证码

添加maven依赖

<dependency>

<groupId>org.springframework.social</groupId>

<artifactId>spring-social-config</artifactId>

<version>1.1.6.RELEASE</version>

</dependency>

创建验证码对象

/**

* @Author chen bo

* @Date 2023/12

* @Des

*/

@Data

public class ImageCode {

/**

* image图片

*/

private BufferedImage image;

/**

* 验证码

*/

private String code;

/**

* 过期时间

*/

private LocalDateTime expireTime;

public ImageCode(BufferedImage image, String code, int expireIn) {

this.image = image;

this.code = code;

this.expireTime = LocalDateTime.now().plusSeconds(expireIn);

}

/**

* 判断验证码是否已过期

* @return

*/

public boolean isExpire() {

return LocalDateTime.now().isAfter(expireTime);

}

}

创建ImageController

编写接口,返回图形验证码:

/**

* @Author chen bo

* @Date 2023/12

* @Des

*/

@RestController

public class ImageController {

public final static String SESSION_KEY_IMAGE_CODE = "SESSION_VERIFICATION_CODE";

private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@GetMapping("/code/image")

public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {

ImageCode imageCode = createImageCode();

sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);

ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());

}

private ImageCode createImageCode() {

// 验证码图片宽度

int width = 100;

// 验证码图片长度

int height = 36;

// 验证码位数

int length = 4;

// 验证码有效时间 60s

int expireIn = 60;

BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

Graphics graphics = image.getGraphics();

Random random = new Random();

graphics.setColor(getRandColor(200, 500));

graphics.fillRect(0, 0, width, height);

graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));

graphics.setColor(getRandColor(160, 200));

for (int i = 0; i < 155; i++) {

int x = random.nextInt(width);

int y = random.nextInt(height);

int xl = random.nextInt(12);

int yl = random.nextInt(12);

graphics.drawLine(x, y, x + xl, y + yl);

}

StringBuilder sRand = new StringBuilder();

for (int i = 0; i < length; i++) {

String rand = String.valueOf(random.nextInt(10));

sRand.append(rand);

graphics.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));

graphics.drawString(rand, 13 * i + 6, 16);

}

graphics.dispose();

return new ImageCode(image, sRand.toString(), expireIn);

}

private 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);

}

}

org.springframework.social.connect.web.HttpSessionSessionStrategy对象封装了一些处理Session的方法,包含了setAttribute、getAttribute和removeAttribute方法,具体可以查看该类的源码。使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。

v改造登录页面

添加验证码控件

在上一篇博文《SpringBoot进阶教程(八十一)Spring Security自定义认证》中的"重写form登录页",已经创建了login.html,在login.html中添加如下代码:

<span style="display: inline">

<input type="text" name="请输入验证码" placeholder="验证码" required="required"/>

<img src="/code/image"/>

</span>

img标签的src属性对应ImageController的createImageCode方法。

v认证流程添加验证码效验

定义异常类

在校验验证码的过程中,可能会抛出各种验证码类型的异常,比如“验证码错误”、“验证码已过期”等,所以我们定义一个验证码类型的异常类:

/**

* @Author chen bo

* @Date 2023/12

* @Des

*/

public class ValidateCodeException extends AuthenticationException {

private static final long serialVersionUID = 1715361291615299823L;

public ValidateCodeException(String explanation) {

super(explanation);

}

}

注意:这里继承的是AuthenticationException而不是Exception。

创建验证码的校验过滤器

Spring Security实际上是由许多过滤器组成的过滤器链,处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后才去校验用户名和密码。由于Spring Security并没有直接提供验证码校验相关的过滤器接口,所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter:

/**

* @Author chen bo

* @Date 2023/12

* @Des

*/

@Component

public class ValidateCodeFilter extends OncePerRequestFilter {

@Autowired

private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

@Override

protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

if ("/login".equalsIgnoreCase(httpServletRequest.getRequestURI())

&& "post".equalsIgnoreCase(httpServletRequest.getMethod())) {

try {

validateCode(new ServletWebRequest(httpServletRequest));

} catch (ValidateCodeException e) {

myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);

return;

}

}

filterChain.doFilter(httpServletRequest, httpServletResponse);

}

private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {

ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);

String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");

if (StringUtils.isEmpty(codeInRequest)) {

throw new ValidateCodeException("验证码不能为空!");

}

if (codeInSession == null) {

throw new ValidateCodeException("验证码不存在!");

}

if (codeInSession.isExpire()) {

sessionStrategy.removeAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);

throw new ValidateCodeException("验证码已过期!");

}

if (!codeInRequest.equalsIgnoreCase(codeInSession.getCode())) {

throw new ValidateCodeException("验证码不正确!");

}

sessionStrategy.removeAttribute(servletWebRequest, ImageController.SESSION_KEY_IMAGE_CODE);

}

}

ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会执行一次。ValidateCodeFilter继承了org.springframework.web.filter.OncePerRequestFilter,该过滤器只会执行一次。

doFilterInternal方法中我们判断了请求URL是否为/login,该路径对应登录form表单的action路径,请求的方法是否为POST,是的话进行验证码校验逻辑,否则直接执行filterChain.doFilter让代码往下走。当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器AuthenticationFailureHandler进行处理。

我们分别从Session中获取了ImageCode对象和请求参数imageCode(对应登录页面的验证码input框name属性),然后进行了各种判断并抛出相应的异常。当验证码过期或者验证码校验通过时,我们便可以删除Session中的ImageCode属性了。

v更新配置类

验证码校验过滤器定义好了,怎么才能将其添加到UsernamePasswordAuthenticationFilter前面呢?很简单,只需要在BrowserSecurityConfig的configure方法中添加些许配置即可,顺便配置验证码请求不配拦截: "/code/image"。

/**

* @Author chen bo

* @Date 2023/12

* @Des

*/

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http.addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class) //添加验证码效验过滤器

.formLogin() // 表单登录

.loginPage("/login.html") // 登录跳转url

// .loginPage("/authentication/require")

.loginProcessingUrl("/login") // 处理表单登录url

// .successHandler(authenticationSuccessHandler)

.failureHandler(new MyAuthenticationFailureHandler())

.and()

.authorizeRequests() // 授权配置

.antMatchers("/login.html", "/css/**", "/authentication/require", "/code/image").permitAll() // 无需认证

.anyRequest() // 所有请求

.authenticated() // 都需要认证

.and().csrf().disable();

}

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

}

上面代码中,我们注入了ValidateCodeFilter,然后通过addFilterBefore方法将ValidateCodeFilter验证码校验过滤器添加到了UsernamePasswordAuthenticationFilter前面。

v运行效果图

请叫我头头哥

其他参考/学习资料:

    <li>https://www.cnblogs.com/kikochz/p/12895842.html
  • https://www.cnblogs.com/fanqisoft/p/10630556.html
  • https://www.jianshu.com/p/5a83e364869c

v源码地址

https://github.com/toutouge/javademosecond/tree/master/security-demo

作 者:请叫我头头哥

出 处:http://www.cnblogs.com/toutou/

关于作者:专注于基础平台的项目开发。如有问题或建议,请多多赐教!

版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我

声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。