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

在之前的博文《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运行效果图

其他参考/学习资料:

v源码地址

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

作  者:请叫我头头哥

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

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

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

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

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

相关推荐
菠萝崽.2 小时前
Elasticsearch进阶篇-DSL
大数据·分布式·elasticsearch·搜索引擎·全文检索·jenkins·springboot
猛踹瘸子那条好腿の8 小时前
Spring-boot初次使用
java·springboot
带刺的坐椅3 天前
SpringBoot3 使用 SolonMCP 开发 MCP
java·ai·springboot·solon·mcp
LUCIAZZZ4 天前
JVM之虚拟机运行
java·jvm·spring·操作系统·springboot
堕落年代4 天前
SpringSecurity当中的CSRF防范详解
前端·springboot·csrf
一零贰肆4 天前
深入理解SpringBoot中的SpringCache缓存技术
java·springboot·springcache·缓存技术
LUCIAZZZ6 天前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot
LUCIAZZZ7 天前
JVM之内存管理(二)
java·jvm·后端·spring·操作系统·springboot
天上掉下来个程小白8 天前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
程序员buddha10 天前
SpringBoot+Dubbo+Zookeeper实现分布式系统步骤
分布式·zookeeper·dubbo·springboot