SpringSecurity 添加验证码的两种方式

 2023-02-13
原文作者:周杰伦本人 原文地址:https://juejin.cn/post/7016269020861038623

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

SpringSecurity 添加验证码的两种方式

今天讲一下SpringSecurity 添加验证码的两种方式,一种是通过自定义认证来实现的,一种 是使用自定义过滤器实现的

验证码的生成

首先引入生成验证码工具的jar包

    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>

Kaptcha是谷歌的验证码工具,它可以实现高度的配置

第二步添加Kaptcha的配置

    @Configuration
    public class KaptchaConfig {
        @Bean
        Producer kaptcha() {
            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");
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    }

从配置类中我们可以看到可以配置生成验证码的图片宽度和高度,字符串包括的内容和长度

第三步就是生成验证码文本,放入HttpSession中,然后根据验证码文本生成图片 通过IO流写出到前端。 代码如下:

    @RestController
    public class LoginController {
        @Autowired
        Producer producer;
        @GetMapping("/vc.jpg")
        public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException {
            resp.setContentType("image/jpeg");
            String text = producer.createText();
            session.setAttribute("kaptcha", text);
            BufferedImage image = producer.createImage(text);
            try(ServletOutputStream out = resp.getOutputStream()) {
                ImageIO.write(image, "jpg", out);
            }
        }
    }

form表单

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
        <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
        <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    </head>
    <style>
        #login .container #login-row #login-column #login-box {
            border: 1px solid #9C9C9C;
            background-color: #EAEAEA;
        }
    </style>
    <body>
    <div id="login">
        <div class="container">
            <div id="login-row" class="row justify-content-center align-items-center">
                <div id="login-column" class="col-md-6">
                    <div id="login-box" class="col-md-12">
                        <form id="login-form" class="form" action="/doLogin" method="post">
                            <h3 class="text-center text-info">登录</h3>
                            <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                            <div class="form-group">
                                <label for="username" class="text-info">用户名:</label><br>
                                <input type="text" name="uname" id="username" class="form-control">
                            </div>
                            <div class="form-group">
                                <label for="password" class="text-info">密码:</label><br>
                                <input type="text" name="passwd" id="password" class="form-control">
                            </div>
                            <div class="form-group">
                                <label for="kaptcha" class="text-info">验证码:</label><br>
                                <input type="text" name="kaptcha" id="kaptcha" class="form-control">
                                <img src="/vc.jpg" alt="">
                            </div>
                            <div class="form-group">
                                <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
    </body>

form表单中验证码图片地址为我们在Controller中定义的验证码接口地址,直接对后台发起请求。

自定义认证

身份认证是AuthenticationProvider的authenticate方法完成,因此验证码可以在此之前完成:

    public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider {
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String kaptcha = req.getParameter("kaptcha");
            String sessionKaptcha = (String) req.getSession().getAttribute("kaptcha");
            if (kaptcha != null && sessionKaptcha != null && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
                return super.authenticate(authentication);
            }
            throw new AuthenticationServiceException("验证码输入错误");
        }
    }

配置AuthenticationManager:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        AuthenticationProvider kaptchaAuthenticationProvider() {
            InMemoryUserDetailsManager users = new InMemoryUserDetailsManager(User.builder()
                    .username("xiepanapn").password("{noop}123").roles("admin").build());
            KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider();
            provider.setUserDetailsService(users);
            return provider;
        }
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            ProviderManager manager = new ProviderManager(kaptchaAuthenticationProvider());
            return manager;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/vc.jpg").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/mylogin.html")
                    .loginProcessingUrl("/doLogin")
                    .defaultSuccessUrl("/index.html")
                    .failureForwardUrl("/mylogin.html")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }
  1. 配置UserDetailsService提供的数据源
  2. 提供AuthenticationProvider实例并配置UserDetailsService
  3. 重写authenticationManagerBean方法提供一个自己的ProviderManager并自定义AuthenticationManager实例。

自定义过滤器

第二种方式就是使用自定义过滤器

LoginFilter继承UsernamePasswordAuthenticationFilter 重写attemptAuthentication方法:

    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
            String kaptcha = request.getParameter("kaptcha");
            String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");
            if (!StringUtils.isEmpty(kaptcha) && !StringUtils.isEmpty(sessionKaptcha) && kaptcha.equalsIgnoreCase(sessionKaptcha)) {
                return super.attemptAuthentication(request, response);
            }
            throw new AuthenticationServiceException("验证码输入错误");
        }
    }

在SecurityConfig中配置LoginFilter

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth)
                throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("javaboy")
                    .password("{noop}123")
                    .roles("admin");
        }
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean()
                throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Bean
        LoginFilter loginFilter() throws Exception {
            LoginFilter loginFilter = new LoginFilter();
            loginFilter.setFilterProcessesUrl("/doLogin");
            loginFilter.setAuthenticationManager(authenticationManagerBean());
            loginFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/hello"));
            loginFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/mylogin.html"));
            return loginFilter;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/vc.jpg").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/mylogin.html")
                    .permitAll()
                    .and()
                    .csrf().disable();
            http.addFilterAt(loginFilter(),
                    UsernamePasswordAuthenticationFilter.class);
        }
    }

显然第二种比较简单

总结

这篇文章我们介绍了基于Spring Security的验证码功能的实现,我们使用谷歌的验证码工具Kaptcha来实现验证码的高度配置化,然后一种方案是从AuthenticationProvider的authenticate()方法进行入手,自定义一个KaptchaAuthenticationProvider来继承DaoAuthenticationProvider,同时需要配置AuthenticationManager,第二种方案是从UsernamePasswordAuthenticationFilter入手,继承UsernamePasswordAuthenticationFilter过滤器,重写他的attemptAuthentication()获取认证的方法,同时在全局配置类中添加这个自定义的过滤器,相比自定义AuthenticationProvider,显然自定义过滤器比较容易实现和理解。

好了这就是Spring Security验证码功能的实现,对应验证码功能,生产中还是用的不少的,基本每个系统必备技能,理解验证码的实现,以后遇到这种开发的情景能够不慌不忙的应对。