Spring Security实战(二)—— 实现图形验证码

目录

一. 使用过滤器实现图形验证码

1. 自定义过滤器

2. 图形验证码过滤器

(1)引入kaptcha依赖

(2)配置一个 kaptcha 实例

(3)创建一个CaptchaController,用于获取图形验证码

(4)用于校验验证码的过滤器

(5)spring security 配置过滤器链

(6)修改 login.html 

扫描二维码关注公众号,回复: 15688385 查看本文章

(7)debug调试:

二、使用自定义认证实现图形验证码

1. 认识 AuthenticationProvider

2. 自定义AuthenticationProvider

 2. 实现图形验证码的AuthenticationProvider


一. 使用过滤器实现图形验证码

        验证码是为了防止恶意用户暴力重试而设置的。

1. 自定义过滤器

        在Spring Security中,实现验证码校验的方式有很多种,最简单的方式就是自定义一个专门处理验证码逻辑的过滤器,将其添加到Spring Security过滤器链的合适位置。当匹配到登录请求时,立刻对验证码进行校验,成功则放行,失败则提前结束整个验证请求。

(1)自定义一个过滤器

在该过滤器执行的时候在控制台输出一句日志

public class CustomFilter implements Filter {
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 在这里添加自定义逻辑
        chain.doFilter(request, response);
        System.out.println("自定义的过滤器执行了!");
    }
 
    // 可以在这里实现 Filter 接口的其他方法
}

(2)把该过滤器加在UsernamePasswordAuthenticationFilter之后

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")
                .permitAll()
                .and()
                .csrf().disable();
    }

(3)测试

登录前后,可以看到控制台会多次打印该过滤器内容

这可能是因为在 CustomFilter 的 doFilter() 方法中打印了一条日志,而该方法在请求到达服务器时就会被调用,而不是只有在登录时才执行。因此,无论是在登录前还是登录后,只要有请求到达服务器,CustomFilter 的 doFilter() 方法都会被调用,并输出一条日志。

2023-04-14 23:57:00.362  INFO 35992 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
自定义的过滤器执行了!
自定义的过滤器执行了!
2023-04-14 23:57:07.250  INFO 35992 --- [nio-8080-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-04-14 23:57:07.410  INFO 35992 --- [nio-8080-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
自定义的过滤器执行了!
自定义的过滤器执行了!

2. 图形验证码过滤器

        想要实现图形验证码校验功能,首先应当有一个获取图形验证码的API,绘制图形验证码的方法有很多,使用开源的验证码组件即可。例如kaptcha

(1)引入kaptcha依赖

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

(2)配置一个 kaptcha 实例

    @Bean
    public Producer captcha() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width","150");
        properties.setProperty("kaptcha.image.hright","150");
        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;
    }

这个代码片段是一个Spring的Java配置方法。它使用Kaptcha库创建一个实现验证码服务的Java Bean。下面是代码块的各部分解释:

  1. @Bean - 这是Spring注解,用于告诉Spring容器,要将此方法返回的对象作为bean注册到容器中。

  2. Properties - 这是一个Java类,用于管理一组键值对。这里我们使用Properties来存储Kaptcha配置属性。

  3. Config - 这是Kaptcha库中的一个Java类,它需要从上述Properties对象中加载一组Kaptcha配置属性。

  4. DefaultKaptcha - 这是Kaptcha库中的一个Java类,它实现了默认的验证码生成算法。

  5. setConfig - 这是DefaultKaptcha类中的一个setter方法,用于将从Config对象中读取的属性设置到DefaultKaptcha实例中。

  6. return defaultKaptcha - 最后,这个方法返回一个DefaultKaptcha实例,它包含了我们所需的所有配置信息,可以生成图形验证码。

(3)创建一个CaptchaController,用于获取图形验证码

@Controller
public class CaptchaController {

    @Autowired
    protected 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 bi = captchaProducer.createImage(capText);
        //获取响应输出流
        ServletOutputStream out = response.getOutputStream();
        //将图形验证码数据写道响应输出流
        ImageIO.write(bi,"jpg",out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
}
  •  @GetMapping("/captcha.jpg") - 这是Spring MVC注解,用于将控制器方法映射到GET请求"/captcha.jpg"。
  • response.setContentType("image/jpeg"); - 这一行设置响应的内容类型为JPEG图像。
  • String capText = captchaProducer.createText(); - 这一行使用captchaProducer对象创建一个包含随机验证码文本的字符串capText。
  • request.getSession().setAttribute("captcha",capText); - 这一行将capText保存到当前HTTP会话的属性“captcha”中,以便稍后验证输入。
  • BufferedImage bi = captchaProducer.createImage(capText); - 这一行使用captchaProducer对象创建一个包含capText文本的验证码图片bi。
  • ServletOutputStream out = response.getOutputStream(); - 这一行获取HTTP响应输出流对象out,以便我们可以将验证码图像数据写入响应流。
  • ImageIO.write(bi,"jpg",out); - 这一行将bi对象的图像数据写入out输出流中。
  • out.flush(); - 这一行刷新输出流。
  • out.close(); - 最后,使用out.close()关闭输出流。

        当用户访问 /captcha.jpg时,即可得到一张携带验证码的图片,验证码文本则被存放到session中,用于后续校验。现在我们可以启动项目先看一下:

(4)用于校验验证码的过滤器

        虽然Spring Security 的过滤器链对过滤器没有特殊要求,只要继承了Filter即可,但是在Spring体系中,推荐使用OncePerRequestFilter来实现。它可以确保一次请求只会通过一次该过滤器(Filter实际上并不能保证这一点)

public class CaptchaFilter extends OncePerRequestFilter {

    private static final String CAPTCHA_SESSION_KEY = "captcha";
    private static final String CAPTCHA_PARAM_NAME = "captcha";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getMethod().equalsIgnoreCase("POST")) { //只对POST请求进行验证码验证
            HttpSession session = request.getSession(false);
            if (session != null) {
                //拿到session中存放的 captcha 属性
                String captcha = (String) session.getAttribute(CAPTCHA_SESSION_KEY);
                if (captcha == null) {
                    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "验证码已过期,请重新获取。");
                    return;
                }
                //获取输入的验证码信息
                String inputCaptcha = request.getParameter(CAPTCHA_PARAM_NAME);
                if (inputCaptcha == null || !captcha.equals(inputCaptcha.trim())) {
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "验证码错误,请重新输入。");
                    return;
                }
            } else {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "无法验证验证码,因为HTTP会话不存在。");
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

 这是一个验证码过滤器类,用于在用户登录等需要输入验证码的场景下,对用户输入的验证码进行验证。具体来说:

  • 在doFilterInternal()方法中,首先通过request对象获取用户请求的方法,如果是POST请求,则对验证码进行验证。
  • 然后通过request.getSession(false)获取当前的session对象,如果session对象不为null,则继续进行验证码验证;否则返回错误信息,提示无法验证验证码,因为HTTP会话不存在。
  • 验证码的具体验证流程是:先通过CAPTCHA_SESSION_KEY获取session中存放的验证码信息,如果获取到的验证码为null,则返回错误信息,提示验证码已过期,请重新获取;否则获取用户输入的验证码信息,如果用户输入的验证码为null或者与session中的验证码信息不一致,则返回错误信息,提示验证码错误,请重新输入。
  • 最后,如果验证码验证通过,则通过filterChain.doFilter()方法继续执行后续的过滤器或请求处理。 总之,这个验证码过滤器类的作用是保证用户输入的验证码正确,从而增强了系统的安全性和防范机制。

(5)spring security 配置过滤器链

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new CaptchaFilter(),UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                //开放验证码的访问权限
                .antMatchers("/captcha.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")
                .permitAll()
                .and()
                .csrf().disable();
    }

(6)修改 login.html 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login Page</title>
    <style>
        /* 样式可以自行修改 */
        body {
            background-color: cadetblue;
        }

        .login-form {
            width: 350px;
            margin: 150px auto;
            background-color: #fff;
            padding: 20px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
        }

        h1 {
            font-size: 24px;
            text-align: center;
            margin-bottom: 30px;
        }

        input[type="text"], input[type="password"], input[type="number"] {
            width: 100%;
            padding: 10px;
            margin-bottom: 20px;
            border: 2px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
        }

        button {
            background-color: darksalmon;
            color: white;
            padding: 12px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
        }

        button:hover {
            background-color: #45a049;
        }

        .captcha-container {
            text-align: center;
            margin-bottom: 20px;
        }

        .captcha-img {
            width: 150px;
            height: 50px;
        }
    </style>
</head>
<body>
<div class="login-form">
    <h1>Login Page</h1>
    <form th:action="@{/login}" method="post">
        <label for="username">Username</label>
        <input type="text" id="username" name="username" placeholder="Enter username" required>

        <label for="password">Password</label>
        <input type="password" id="password" name="password" placeholder="Enter password" required>

        <div class="captcha-container">
            <img class="captcha-img" th:src="@{/captcha.jpg}" onclick="this.src='captcha.jpg?'+Math.random()">
        </div>

        <label for="captcha">Verification Code</label>
        <input type="number" id="captcha" name="captcha" placeholder="Enter verification code" required>

        <button type="submit">Login</button>
    </form>
</div>
</body>
</html>

(7)debug调试:

 

二、使用自定义认证实现图形验证码

        上面使用过滤器方式实现类带图形验证码的验证功能,属于Servlet层面,Spring Security还提供了一种更优雅的实现图形验证码的方式,即自定义认证。

1. 认识 AuthenticationProvider

        我们所面对的系统中的用户,在Spring Security中被称为主体(principal),主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。Spring Security 通过一层将其定义为一个Authentication。

public interface Authentication extends Principal, Serializable {

    //获取主体权限列表
    Collection<? extends GrantedAuthority> getAuthorities();

    //获取主体凭据,通常为用户密码
    Object getCredentials();
    
    //获取主体详细信息
    Object getDetails();

    //获取主体,通常为一个用户名
    Object getPrincipal();

    //主体是否验证成功
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

        UsernamePasswordAuthenticationToken也是Authentication的一个实现类。         

        大部分情况下身份验证都是基于用户名和密码进行的,所以 Spring Security提供了一个UsernamePasswordAuthenticationToken 用于代指这一类证明,例如用SSH KEY也可以登录,但它不属于用户名和密码登录这个范畴。

        在前面使用的表单登录中,每一个登录用户被包装为一个UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动。

        AuthenticationProvider 被Spring Security定义为一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。

public interface AuthenticationProvider {
//验证过程,成功的话返回一个验证完成的Authentication
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

2. 自定义AuthenticationProvider

        Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

  • HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种
  • 基于LDAP的认证技术
  • 聚焦于证明用户身份的OpenID认证技术
  • 聚焦于授权的OAuth认证技术
  • 系统内维护的用户名和密码认证技术(最广泛)

         系统内维护的用户名和密码认证技术使用最为广泛,通常会涉及数据库访问。为了更好的按需定制,Spring Security并没有直接糅合整个认证过程,而是提供了一个抽象的AuthenticationProvider,那就是 AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider是Spring Security提供的抽象类,用于支持验证用户身份,并从用户详细信息中构建身份验证对象

该类实现了AuthenticationProvider接口,可以作为身份验证的提供者。同时,它还实现了UserDetailsService接口,用于从数据源中获取用户详细信息。

在具体的实现中,AbstractUserDetailsAuthenticationProvider的主要作用是将Authentication对象中的用户名和密码提取出来,然后通过UserDetailsService获取用户详细信息,并将其与用户名和密码进行比较,以确定用户的身份是否正确。

如果身份验证成功,AbstractUserDetailsAuthenticationProvider会将用户详细信息封装到一个新的身份验证对象中,并将其返回。如果身份验证失败,则会抛出一个AuthenticationException异常。

此外,AbstractUserDetailsAuthenticationProvider还可以处理密码加密和解密的逻辑,以及支持定制化的身份验证逻辑。

实现:自定义AuthenticationProvider示例

public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 在此处实现密码验证逻辑
        if (!authentication.getCredentials().equals(userDetails.getPassword())) {
            throw new BadCredentialsException("Invalid username or password");
        }
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 在此处从自定义的用户详情服务中获取用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return userDetails;
    }

}

 2. 实现图形验证码的AuthenticationProvider

        现在重新回到自定义认证实现图形验证码登录的这个案例中,由于只是在常规的认证之上增加了图形验证码的校验,其他流程并没有变化,所以只需要继承DaoAuthenticationProvider并稍微修改即可。

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //实现图形验证码的校验逻辑

        //调用父类方法完成密码验证
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

        用户提交的验证码和session存储的验证码都需要从用户的请求中获取,但是传入的对象只有userDetails 和 authentication。是否还需要一个HttpServletRequest对象呢?并非如此,Authentication实际上还可以携带账号信息之外的数据。

public interface Authentication extends Principal, Serializable {

    //允许携带任意对象 Object类型
    Object getDetails();

}

        前面提到过,一次完整的认证可以包含多个AuthenticationProvider,这些AuthenticationProvider都由ProviderManager管理,ProviderManager是由UsernamePasswordAuthenticationFilter调用的。

AuthenticationProviderProviderManager介绍:

        AuthenticationProvider和ProviderManager是Spring Security框架中用来实现认证的两个关键组件。

        AuthenticationProvider是一个接口,它定义了认证的核心逻辑,即对用户提供的认证信息进行验证。其中包含一个方法authenticate(),用于执行认证操作。在Spring Security中,一般情况下需要自定义实现AuthenticationProvider接口,并将其添加到ProviderManager中,以完成对认证请求的处理。

        ProviderManager是一个认证管理器,它负责协调多个AuthenticationProvider实现,实现对认证请求的分派、调度和结果处理。当认证请求到达ProviderManager时,它会遍历其内部持有的AuthenticationProvider实现列表,逐一尝试调用各个AuthenticationProvider的authenticate()方法,直到其中一个AuthenticationProvider能够成功认证该请求,或者所有AuthenticationProvider都无法认证成功。

        总体来说,AuthenticationProvider和ProviderManager是Spring Security框架中非常重要的两个组件,它们共同协作,实现了对用户身份的验证和认证。通过实现AuthenticationProvider接口,可以对认证逻辑进行个性化定制,而通过使用ProviderManager,可以方便地协调多个AuthenticationProvider实现,以实现更加复杂的认证方案。

AuthenticationProviderProviderManagerUsernamePasswordAuthenticationFilter的关系:

        AuthenticationProvider和ProviderManager都是与身份验证相关的组件,而UsernamePasswordAuthenticationFilter是处理身份验证请求的过滤器。

        当用户提交身份验证请求时,UsernamePasswordAuthenticationFilter拦截请求并从请求中获取用户名和密码等身份验证信息,然后创建一个UsernamePasswordAuthenticationToken对象来表示这些信息,并将其传递给ProviderManager的authenticate()方法进行身份验证。

         ProviderManager通过遍历已配置的AuthenticationProvider列表来查找可以处理该UsernamePasswordAuthenticationToken的AuthenticationProvider。如果找到了匹配的AuthenticationProvider,则该提供程序会对身份验证信息进行验证,并返回一个经过身份验证的Authentication对象,ProviderManager将该Authentication对象传递给UsernamePasswordAuthenticationFilter,以表明身份验证已成功。

        如果ProviderManager无法找到匹配的AuthenticationProvider,或者已找到但是无法验证身份验证信息,则ProviderManager将抛出一个AuthenticationException异常,UsernamePasswordAuthenticationFilter将捕获该异常并处理身份验证失败的情况。

如图:

 

         Authentication中有了HttpServletRequest之后,一切变得非常顺畅,基于图形验证码的场景,我们可以继承WebAuthenticationDetails,并扩展需要的信息。

public class MyWebAuthenticationDetails extends WebAuthenticationDetails {

    private boolean imageCodeIsRight;

    public boolean getImageCodeIsRight() {
        return this.imageCodeIsRight;
    }
    //补充用户提交的验证码和session保存的验证码
    public MyWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        String imageCode = request.getParameter("captcha");
        HttpSession session = request.getSession();
        String savedImageCode = (String)session.getAttribute("captcha");
        if (!StringUtils.isEmpty(savedImageCode)) {
            session.removeAttribute("captcha");
            //当验证码正确时设置状态
            if (!StringUtils.isEmpty(imageCode) && imageCode.equals(savedImageCode)) {
                this.imageCodeIsRight = true;
            }
        }
    }
}

将它提供给一个自定义的AuthenticationDetailsSource

public class MyWebAuthenticationDetailsSource implements 
        AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

 接下来实现自定义的AuthenticationProvider

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    @SneakyThrows
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //实现图形验证码的校验逻辑
        //获取详细信息
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
        //一旦发现验证码不正确,就立刻抛出异常信息
        if (!details.getImageCodeIsRight()) {
            throw new VerificationCodeException();
        }
        //调用父类方法完成密码验证
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_49561506/article/details/130163457
今日推荐