跳到主要内容

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

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);
    }
}

测试: