12、SpringSecurity实战:自定义过滤器实现登录页面添加验证码的认证

文章目录

        1. 传统Web项目开发添加认证验证码
        1. 项目依赖
        1. 登录页面 login.html
        1. 欢迎页面 index.html
        1. 配置访问页面的控制器
        1. 验证码配置类
        1. 生成验证码
        1. 配置 SpringSecurity
        1. 自定义过滤器KaptchaFilter 实现验证码的验证
        1. 配置 SpringSecurity
        1. 源码分析
      1. 前后端分离开发添加验证码的认证
        1. 项目依赖
        1. 验证码配置类
        1. 生成验证码
        1. 配置 SpringSecurity
        1. 自定义过滤器 LoginKaptchaFilter 实现验证码的验证
        1. 配置 SpringSecurity

1. 传统Web项目开发添加认证验证码

*

01. 项目依赖
<!--引入Springsecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!--验证码-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

02. 登录页面 login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">
    用户名: <input name="uname" type="text"> <br>
    密码: <input name="passwd" type="text"> <br>
    验证码: <input name="kaptcha" type="text"> <img alt="" th:src="@{/vc.jpg}"> <br>
    <input type="submit" value="登录">
</form>

</body>
</html>

03. 欢迎页面 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<h1>欢迎进入我的主页</h1>
</body>
</html>

04. 配置访问页面的控制器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
   
     

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
   
     
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/login.html").setViewName("login");
    }
}

05. 验证码配置类
@Configuration
public class KaptchaConfig {
   
     

    @Bean
    public Producer kaptcha() {
   
     
        Properties properties = new Properties();
        //1.验证码宽度
        properties.setProperty("kaptcha.image.width", "150");
        //2.验证码高度
        properties.setProperty("kaptcha.image.height", "50");
        //3.验证码字符串
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        //4.验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

06. 生成验证码
@Controller
public class VerifyCodeController {
   
     
    private final Producer producer;

    @Autowired
    public VerifyCodeController(Producer producer) {
   
     
        this.producer = producer;
    }

    @RequestMapping("/vc.jpg")
    public void verifyCode(HttpServletResponse response, HttpSession session) throws IOException {
   
     
        //1.生成验证码
        String verifyCode = producer.createText();
        //2.保存到中 session
        session.setAttribute("kaptcha", verifyCode);
        //3.生成图片
        BufferedImage bi = producer.createImage(verifyCode);
        //4.响应图片
        response.setContentType("image/png");
        ServletOutputStream os = response.getOutputStream();
        ImageIO.write(bi, "jpg", os);
    }
}

07. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
            	// 访问登录页面放行
                .mvcMatchers("/login.html").permitAll()
            	// 访问验证码放行
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .defaultSuccessUrl("/index.html",true)
                .failureUrl("/login.html")
                .and()
                .csrf().disable();
    }
}

访问项目,跳转到登录页面:
*

08. 自定义过滤器KaptchaFilter 实现验证码的验证

在UsernamePasswordAuthenticationFilter过滤器并没有实现验证码的认证:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   
     
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
   
     
        if (this.postOnly && !request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
}

因此如果想要在登录请求时验证验证码,需要自定义过滤器KaptchaFilter实现验证码验证逻辑实现验证码的验证,放在UsernamePasswordAuthenticationFilter的位置:

public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
   
     

    private static final String FORM_KAPTCHA_KEY = "kaptcha";
    private String kaptchaParameter = FORM_KAPTCHA_KEY;

    public void setKaptchaParameter(String kaptchaParameter){
   
     
        this.kaptchaParameter = kaptchaParameter;
    }

    public String getKaptchaParameter(){
   
     
        return kaptchaParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   
     
        if (!request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 1. 从请求中获取验证码
        String verifyCode = request.getParameter(getKaptchaParameter());
        // 2. 从session中获取验证码
        String sessionVerifyCode = (String)request.getSession().getAttribute("kaptcha");
        // 3. 比较
        if(!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode)
                                            && verifyCode.equalsIgnoreCase(sessionVerifyCode)){
   
     
            return super.attemptAuthentication(request,response);
        }
        throw new KaptchaNotMatchException("验证码认证异常");
    }
}

//自定义验证码认证异常
public class KaptchaNotMatchException extends AuthenticationException {
   
     
    public KaptchaNotMatchException(String msg, Throwable cause) {
   
     
        super(msg, cause);
    }

    public KaptchaNotMatchException(String msg) {
   
     
        super(msg);
    }
}

09. 配置 SpringSecurity

修改SpringSecurity配置,将KaptchaFilter放在UsernamePasswordAuthenticationFilter位置:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    // 暴露自定义的authenticationManager
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
   
     
        return super.authenticationManager();
    }

    @Bean
    public KaptchaFilter kaptchaFilter() throws Exception {
   
     
        KaptchaFilter kaptchaFilter = new KaptchaFilter();
        kaptchaFilter.setFilterProcessesUrl("/doLogin");
        kaptchaFilter.setUsernameParameter("uname");
        kaptchaFilter.setKaptchaParameter("passwd");
        kaptchaFilter.setKaptchaParameter("kaptcha");
        // 认证管理器
        kaptchaFilter.setAuthenticationManager(authenticationManagerBean());
        // 指定认证成功后的处理
        kaptchaFilter.setAuthenticationSuccessHandler(((request, response, authentication) -> {
   
     
            response.sendRedirect("/index.html");
        }));
        // 指定认证失败后的处理
        kaptchaFilter.setAuthenticationFailureHandler(((request, response, exception) -> {
   
     
            response.sendRedirect("/login.html");
        }));
        return kaptchaFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .mvcMatchers("/login.html").permitAll()
                .mvcMatchers("/vc.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .defaultSuccessUrl("/index.html",true)
                .failureUrl("/login.html")
                .and()
                .csrf().disable();
        http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

10. 源码分析

①直接访问:localhost:8080/doLogin

*

②请求首先进入认证处理入口 AbstractAuthenticationProcessingFilter 过滤器判断是否需要认证并尝试认证:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
   
     
        // 判断是否需要是配置的请求:kaptchaFilter.setFilterProcessesUrl("/doLogin");
        if (!requiresAuthentication(request, response)) {
   
     
            chain.doFilter(request, response);
            return;
        }
        try {
   
     
            // 调用子类的方法尝试认证
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
   
     
                return;
            }
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
   
     
                chain.doFilter(request, response);
            }
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
   
     
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
   
     
            // Authentication failed
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}

②然后进入自定义过滤器 KaptchaFilter 进行验证码的认证:

public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
   
     

    private static final String FORM_KAPTCHA_KEY = "kaptcha";
    private String kaptchaParameter = FORM_KAPTCHA_KEY;

    public void setKaptchaParameter(String kaptchaParameter){
   
     
        this.kaptchaParameter = kaptchaParameter;
    }

    public String getKaptchaParameter(){
   
     
        return kaptchaParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   
     
        if (!request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 1. 从请求中获取验证码
        String verifyCode = request.getParameter(getKaptchaParameter());
        // 2. 从session中获取验证码
        String sessionVerifyCode = (String)request.getSession().getAttribute("kaptcha");
        // 3. 比较
        if(!ObjectUtils.isEmpty(verifyCode) && !ObjectUtils.isEmpty(sessionVerifyCode)
                                            && verifyCode.equalsIgnoreCase(sessionVerifyCode)){
   
     
            return super.attemptAuthentication(request,response);
        }
        throw new KaptchaNotMatchException("验证码认证异常");
    }
}

③验证码验证通过后,进入 UsernamePasswordAuthenticationFilter 过滤器进行用户名和密码的认证:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   
     
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
   
     
        if (this.postOnly && !request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

2. 前后端分离开发添加验证码的认证

*

01. 项目依赖
<!--引入Springsecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!--验证码-->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

02. 验证码配置类
@Configuration
public class KaptchaConfig {
   
     

    @Bean
    public Producer kaptcha() {
   
     
        Properties properties = new Properties();
        //1.验证码宽度
        properties.setProperty("kaptcha.image.width", "150");
        //2.验证码高度
        properties.setProperty("kaptcha.image.height", "50");
        //3.验证码字符串
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        //4.验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

03. 生成验证码
@RestController
public class VerifyCodeController {
   
     

    private final Producer producer;

    @Autowired
    public VerifyCodeController(Producer producer) {
   
     
        this.producer = producer;
    }

    @GetMapping("/vc.jpg")
    public String getVerifyCode(HttpSession session) throws IOException {
   
     
        //1.生成验证码
        String text = producer.createText();
        //2.放入 session redis 实现
        session.setAttribute("kaptcha", text);
        //3.生成图片
        BufferedImage bi = producer.createImage(text);
        FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
        ImageIO.write(bi, "jpg", fos);
        //4.返回 base64
        return Base64.encodeBase64String(fos.toByteArray());
    }
}

04. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
            	// 认证失败异常处理
                .exceptionHandling()
                .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                .and()
                .csrf().disable();
    }
}

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
   
     
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
   
     
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println("请认证之后再去处理");
    }
}

05. 自定义过滤器 LoginKaptchaFilter 实现验证码的验证

在UsernamePasswordAuthenticationFilter过滤器并没有实现验证码的认证:

public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
   
     

    public static final String FORM_KAPTCHA_KEY = "kaptcha";

    private String kaptchaParameter = FORM_KAPTCHA_KEY;

    public String getKaptchaParameter() {
   
     
        return kaptchaParameter;
    }

    public void setKaptchaParameter(String kaptchaParameter) {
   
     
        this.kaptchaParameter = kaptchaParameter;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   
     
        if (!request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
   
     
            try {
   
     
                // 1.获取请求jsonString数据,并转为Map对象
                Map<String,String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                // 获取请求中验证码
                String kaptcha = userInfo.get(getKaptchaParameter());
                // 获取请求中用户名
                String username = userInfo.get(getUsernameParameter());
                // 获取请求中密码
                String password = userInfo.get(getPasswordParameter());

                // 2.获取session中验证码
                String sessionKaptcha = (String)request.getSession().getAttribute("kaptcha");
                if(!ObjectUtils.isEmpty(sessionKaptcha) && !ObjectUtils.isEmpty(kaptcha)
                        && kaptcha.equalsIgnoreCase(sessionKaptcha)){
   
     
                    // 3. 获取用户名和密码认证
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);
                    setDetails(request,authRequest);
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
                throw new KaptchaNotMatchException("验证码不正确");
            } catch (IOException e) {
   
     
                throw new RuntimeException(e);
            }
        }
        // 如果请求不是json数据,调用父类的方法尝试认证
        return super.attemptAuthentication(request,response);
    }
}

06. 配置 SpringSecurity
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
       auth.userDetailsService(userDetailsService());
    }

    // 暴露自定义的authenticationManager
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
   
     
        return super.authenticationManager();
    }

  public LoginKaptchaFilter loginKaptchaFilter() throws Exception {
   
     
        LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();
        loginKaptchaFilter.setFilterProcessesUrl("/doLogin");
        loginKaptchaFilter.setKaptchaParameter("kaptcha");
        loginKaptchaFilter.setUsernameParameter("uname");
        loginKaptchaFilter.setPasswordParameter("passwd");
        loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean());
        loginKaptchaFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        loginKaptchaFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return loginKaptchaFilter;
  }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                .and()
                .csrf().disable();
        http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

测试:

*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: