• Spring Security 在登录时如何添加图形验证码


    前言

    在前面的几篇文章中,登录时都是使用用户名 + 密码进行登录的,但是在实际项目当中,登录时,还需要输入图形验证码。那如何在 Spring Security 现有的认证体系中,加入自己的认证逻辑呢?这就是本文的内容,本文会介绍两种实现方案,一是基于过滤器实现;二是基于认证器实现。

    验证码生成

    既然需要输入图形验证码,那先来生成验证码吧。

    加入验证码依赖

    1. <!--验证码生成器-->
    2. <dependency>
    3. <groupId>com.github.penggle</groupId>
    4. <artifactId>kaptcha</artifactId>
    5. <version>2.3.2</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>org.springframework.boot</groupId>
    9. <artifactId>spring-boot-starter-web</artifactId>
    10. </dependency>
    11. 复制代码

    Kaptcha 依赖是谷歌的验证码工具。

    验证码配置

    1. @Configuration
    2. public class KaptchaConfig {
    3. @Bean
    4. public DefaultKaptcha captchaProducer() {
    5. Properties properties = new Properties();
    6. // 是否显示边框
    7. properties.setProperty("kaptcha.border","yes");
    8. // 边框颜色
    9. properties.setProperty("kaptcha.border.color","105,179,90");
    10. // 字体颜色
    11. properties.setProperty("kaptcha.textproducer.font.color","blue");
    12. // 字体大小
    13. properties.setProperty("kaptcha.textproducer.font.size","35");
    14. // 图片宽度
    15. properties.setProperty("kaptcha.image.width","300");
    16. // 图片高度
    17. properties.setProperty("kaptcha.image.height","100");
    18. // 文字个数
    19. properties.setProperty("kaptcha.textproducer.char.length","4");
    20. //文字大小
    21. properties.setProperty("kaptcha.textproducer.font.size","100");
    22. //文字随机字体
    23. properties.setProperty("kaptcha.textproducer.font.names", "宋体");
    24. //文字距离
    25. properties.setProperty("kaptcha.textproducer.char.space","16");
    26. //干扰线颜色
    27. properties.setProperty("kaptcha.noise.color","blue");
    28. // 文本内容 从设置字符中随机抽取
    29. properties.setProperty("kaptcha.textproducer.char.string","0123456789");
    30. DefaultKaptcha kaptcha = new DefaultKaptcha();
    31. kaptcha.setConfig(new Config(properties));
    32. return kaptcha;
    33. }
    34. }
    35. 复制代码

    验证码接口

    1. /**
    2. * 生成验证码
    3. */
    4. @GetMapping("/verify-code")
    5. public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException {
    6. resp.setContentType("image/jpeg");
    7. // 生成图形校验码内容
    8. String text = producer.createText();
    9. // 将验证码内容存入HttpSession
    10. session.setAttribute("verify_code", text);
    11. // 生成图形校验码图片
    12. BufferedImage image = producer.createImage(text);
    13. // 使用try-with-resources 方式,可以自动关闭流
    14. try(ServletOutputStream out = resp.getOutputStream()) {
    15. // 将校验码图片信息输出到浏览器
    16. ImageIO.write(image, "jpeg", out);
    17. }
    18. }
    19. 复制代码

    代码注释写的很清楚,就不过多的介绍。属于固定的配置,既然配置完了,那就看看生成的效果吧!

    接下来就看看如何集成到 Spring Security 中的认证逻辑吧!

    加入依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-security</artifactId>
    4. </dependency>
    5. 复制代码

    基于过滤器

    编写自定义认证逻辑

    这里继承的过滤器为 UsernamePasswordAuthenticationFilter,并重写attemptAuthentication方法。用户登录的用户名/密码是在 UsernamePasswordAuthenticationFilter 类中处理,那我们就继承这个类,增加对验证码的处理。当然也可以实现其他类型的过滤器,比如:GenericFilterBeanOncePerRequestFilter,不过处理起来会比继承UsernamePasswordAuthenticationFilter麻烦一点。

    1. import org.springframework.security.authentication.AuthenticationServiceException;
    2. import org.springframework.security.core.Authentication;
    3. import org.springframework.security.core.AuthenticationException;
    4. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    5. import org.springframework.util.StringUtils;
    6. import javax.servlet.http.HttpServletRequest;
    7. import javax.servlet.http.HttpServletResponse;
    8. import javax.servlet.http.HttpSession;
    9. public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter {
    10. @Override
    11. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    12. // 需要是 POST 请求
    13. if (!request.getMethod().equals("POST")) {
    14. throw new AuthenticationServiceException(
    15. "Authentication method not supported: " + request.getMethod());
    16. }
    17. // 获得请求验证码值
    18. String code = request.getParameter("code");
    19. HttpSession session = request.getSession();
    20. // 获得 session 中的 验证码值
    21. String sessionVerifyCode = (String) session.getAttribute("verify_code");
    22. if (StringUtils.isEmpty(code)){
    23. throw new AuthenticationServiceException("验证码不能为空!");
    24. }
    25. if(StringUtils.isEmpty(sessionVerifyCode)){
    26. throw new AuthenticationServiceException("请重新申请验证码!");
    27. }
    28. if (!sessionVerifyCode.equalsIgnoreCase(code)) {
    29. throw new AuthenticationServiceException("验证码错误!");
    30. }
    31. // 验证码验证成功,清除 session 中的验证码
    32. session.removeAttribute("verify_code");
    33. // 验证码验证成功,走原本父类认证逻辑
    34. return super.attemptAuthentication(request, response);
    35. }
    36. }
    37. 复制代码

    代码逻辑很简单,验证验证码是否正确,正确则走父类原本逻辑,去验证用户名密码是否正确。 过滤器定义完成后,接下来就是用我们自定义的过滤器代替默认的 UsernamePasswordAuthenticationFilter

    • SecurityConfig
    1. import cn.cxyxj.study04.Authentication.config.MyAuthenticationFailureHandler;
    2. import cn.cxyxj.study04.Authentication.config.MyAuthenticationSuccessHandler;
    3. import org.springframework.context.annotation.Bean;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.security.authentication.AuthenticationManager;
    6. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    7. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    8. import org.springframework.security.core.userdetails.User;
    9. import org.springframework.security.core.userdetails.UserDetailsService;
    10. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    11. import org.springframework.security.crypto.password.PasswordEncoder;
    12. import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    13. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    14. @Configuration
    15. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    16. @Bean
    17. PasswordEncoder passwordEncoder() {
    18. return NoOpPasswordEncoder.getInstance();
    19. }
    20. @Bean
    21. @Override
    22. protected UserDetailsService userDetailsService() {
    23. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    24. manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
    25. manager.createUser(User.withUsername("security").password("security").roles("user").build());
    26. return manager;
    27. }
    28. @Override
    29. @Bean
    30. public AuthenticationManager authenticationManagerBean()
    31. throws Exception {
    32. return super.authenticationManagerBean();
    33. }
    34. @Override
    35. protected void configure(HttpSecurity http) throws Exception {
    36. // 用自定义的 VerifyCodeFilter 实例代替 UsernamePasswordAuthenticationFilter
    37. http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
    38. http.authorizeRequests() //开启配置
    39. // 验证码、登录接口放行
    40. .antMatchers("/verify-code","/auth/login").permitAll()
    41. .anyRequest() //其他请求
    42. .authenticated().and()//验证 表示其他请求需要登录才能访问
    43. .csrf().disable(); // 禁用 csrf 保护
    44. }
    45. @Bean
    46. VerifyCodeFilter loginFilter() throws Exception {
    47. VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter();
    48. verifyCodeFilter.setFilterProcessesUrl("/auth/login");
    49. verifyCodeFilter.setUsernameParameter("account");
    50. verifyCodeFilter.setPasswordParameter("pwd");
    51. verifyCodeFilter.setAuthenticationManager(authenticationManagerBean());
    52. verifyCodeFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
    53. verifyCodeFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    54. return verifyCodeFilter;
    55. }
    56. }
    57. 复制代码

    当我们替换了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,那些失效的属性,都可以在配置 VerifyCodeFilter 实例的时候配置;还需要记得配置AuthenticationManager,否则启动时会报错。

    • MyAuthenticationFailureHandler
    1. import org.springframework.security.authentication.BadCredentialsException;
    2. import org.springframework.security.authentication.LockedException;
    3. import org.springframework.security.core.AuthenticationException;
    4. import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    5. import javax.servlet.ServletException;
    6. import javax.servlet.http.HttpServletRequest;
    7. import javax.servlet.http.HttpServletResponse;
    8. import java.io.IOException;
    9. import java.io.PrintWriter;
    10. /**
    11. * 登录失败回调
    12. */
    13. public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    14. @Override
    15. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    16. response.setContentType("application/json;charset=utf-8");
    17. PrintWriter out = response.getWriter();
    18. String msg = "";
    19. if (e instanceof LockedException) {
    20. msg = "账户被锁定,请联系管理员!";
    21. }
    22. else if (e instanceof BadCredentialsException) {
    23. msg = "用户名或者密码输入错误,请重新输入!";
    24. }
    25. out.write(e.getMessage());
    26. out.flush();
    27. out.close();
    28. }
    29. }
    30. 复制代码
    • MyAuthenticationSuccessHandler
    1. import com.fasterxml.jackson.databind.ObjectMapper;
    2. import org.springframework.security.core.Authentication;
    3. import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    4. import javax.servlet.ServletException;
    5. import javax.servlet.http.HttpServletRequest;
    6. import javax.servlet.http.HttpServletResponse;
    7. import java.io.IOException;
    8. import java.io.PrintWriter;
    9. /**
    10. * 登录成功回调
    11. */
    12. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    13. @Override
    14. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    15. Object principal = authentication.getPrincipal();
    16. response.setContentType("application/json;charset=utf-8");
    17. PrintWriter out = response.getWriter();
    18. out.write(new ObjectMapper().writeValueAsString(principal));
    19. out.flush();
    20. out.close();
    21. }
    22. }
    23. 复制代码

    测试

    • 不传入验证码发起请求。

    • 请求获取验证码接口

    • 输入错误的验证码

    • 输入正确的验证码

    • 输入已经使用过的验证码

      各位读者是不是会觉得既然继承了 Filter,那是不是每个接口都会进入到我们的自定义方法中呀!如果是继承了 GenericFilterBean、OncePerRequestFilter 那是肯定会的,需要手动处理。 但我们继承的是 UsernamePasswordAuthenticationFilter,security 已经帮忙处理了。处理逻辑在其父类 AbstractAuthenticationProcessingFilter#doFilter 中。

    基于认证器

    编写自定义认证逻辑

    验证码逻辑编写完成,那接下来就自定义一个 VerifyCodeAuthenticationProvider 继承自 DaoAuthenticationProvider,并重写 authenticate 方法。

    1. import org.springframework.security.authentication.AuthenticationServiceException;
    2. import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    3. import org.springframework.security.core.Authentication;
    4. import org.springframework.security.core.AuthenticationException;
    5. import org.springframework.util.StringUtils;
    6. import org.springframework.web.context.request.RequestContextHolder;
    7. import org.springframework.web.context.request.ServletRequestAttributes;
    8. import javax.servlet.http.HttpServletRequest;
    9. import javax.servlet.http.HttpSession;
    10. /**
    11. * 验证码验证器
    12. */
    13. public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider {
    14. @Override
    15. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    16. HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    17. // 获得请求验证码值
    18. String code = req.getParameter("code");
    19. // 获得 session 中的 验证码值
    20. HttpSession session = req.getSession();
    21. String sessionVerifyCode = (String) session.getAttribute("verify_code");
    22. if (StringUtils.isEmpty(code)){
    23. throw new AuthenticationServiceException("验证码不能为空!");
    24. }
    25. if(StringUtils.isEmpty(sessionVerifyCode)){
    26. throw new AuthenticationServiceException("请重新申请验证码!");
    27. }
    28. if (!code.toLowerCase().equals(sessionVerifyCode.toLowerCase())) {
    29. throw new AuthenticationServiceException("验证码错误!");
    30. }
    31. // 验证码验证成功,清除 session 中的验证码
    32. session.removeAttribute("verify_code");
    33. // 验证码验证成功,走原本父类认证逻辑
    34. return super.authenticate(authentication);
    35. }
    36. }
    37. 复制代码

    自定义的认证逻辑完成了,剩下的问题就是如何让 security 走我们的认证逻辑了。

    在 security 中,所有的 AuthenticationProvider 都是放在 ProviderManager 中统一管理的,所以接下来我们就要自己提供 ProviderManager,然后注入自定义的 VerifyCodeAuthenticationProvider。

    • SecurityConfig
    1. import cn.cxyxj.study02.config.MyAuthenticationFailureHandler;
    2. import cn.cxyxj.study02.config.MyAuthenticationSuccessHandler;
    3. import org.springframework.context.annotation.Bean;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.security.authentication.AuthenticationManager;
    6. import org.springframework.security.authentication.ProviderManager;
    7. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    8. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    9. import org.springframework.security.core.userdetails.User;
    10. import org.springframework.security.core.userdetails.UserDetailsService;
    11. import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    12. import org.springframework.security.crypto.password.PasswordEncoder;
    13. import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    14. @Configuration
    15. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    16. @Bean
    17. PasswordEncoder passwordEncoder() {
    18. return NoOpPasswordEncoder.getInstance();
    19. }
    20. @Bean
    21. @Override
    22. protected UserDetailsService userDetailsService() {
    23. InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    24. manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
    25. manager.createUser(User.withUsername("security").password("security").roles("user").build());
    26. return manager;
    27. }
    28. @Bean
    29. VerifyCodeAuthenticationProvider verifyCodeAuthenticationProvider() {
    30. VerifyCodeAuthenticationProvider provider = new VerifyCodeAuthenticationProvider();
    31. provider.setPasswordEncoder(passwordEncoder());
    32. provider.setUserDetailsService(userDetailsService());
    33. return provider;
    34. }
    35. @Override
    36. @Bean
    37. public AuthenticationManager authenticationManagerBean() throws Exception {
    38. ProviderManager manager = new ProviderManager(verifyCodeAuthenticationProvider());
    39. return manager;
    40. }
    41. @Override
    42. protected void configure(HttpSecurity http) throws Exception {
    43. http.authorizeRequests() //开启配置
    44. // 验证码接口放行
    45. .antMatchers("/verify-code").permitAll()
    46. .anyRequest() //其他请求
    47. .authenticated()//验证 表示其他请求需要登录才能访问
    48. .and()
    49. .formLogin()
    50. .loginPage("/login.html") //登录页面
    51. .loginProcessingUrl("/auth/login") //登录接口,此地址可以不真实存在
    52. .usernameParameter("account") //用户名字段
    53. .passwordParameter("pwd") //密码字段
    54. .successHandler(new MyAuthenticationSuccessHandler())
    55. .failureHandler(new MyAuthenticationFailureHandler())
    56. .permitAll() // 上述 login.html 页面、/auth/login接口放行
    57. .and()
    58. .csrf().disable(); // 禁用 csrf 保护
    59. ;
    60. }
    61. }
    62. 复制代码

    测试

    • 不传入验证码发起请求。

    • 请求获取验证码接口

    • 输入错误的验证码

    • 输入正确的验证码

    • 输入已经使用过的验证码

    • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。

  • 相关阅读:
    MyBatis-Plus的乐观锁插件(Springboot版)
    k8s 入门到实战--部署应用到 k8s
    网络安全入门必知的靶场!
    期货开户经理是期市中的良师益友
    选择实验室超声波清洗机具有哪些作用?
    网页元素定位秘籍:从HTML探秘到Python自动化实战20240626
    Mybatis-plus 学习分享
    知识引擎藏经阁天花板——高性能Java架构核心原理手册
    PyTorch构建分类网络(DNN,Mnist数据集)
    Spring.事务实现方式和源码分析
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127863127