飞道的博客

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

408人阅读  评论(0)

前言

在前面的几篇文章中,登录时都是使用用户名 + 密码进行登录的,但是在实际项目当中,登录时,还需要输入图形验证码。那如何在 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. 复制代码

测试

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

  • 请求获取验证码接口

  • 输入错误的验证码

  • 输入正确的验证码

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

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


转载:https://blog.csdn.net/BASK2311/article/details/127863127
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场