• SpringSecurity系列 - 10 传统Web项目表单认证: UsernamePasswordAuthenticationFilter 过滤器


    第一步

    ① 自定义登录页面

    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>用户登录title>
    head>
    <body>
    
    <h1>用户登录h1>
    <h2>
        <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}">div>
    h2>
    <form method="post" th:action="@{/doLogin}">
        用户名: <input name="uname" type="text"> <br>
        密码: <input name="passwd" type="text"> <br>
        <input type="submit" value="登录">
    form>
    
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    ② 登录页面的控制器

    @Controller
    public class LoginController {
        @RequestMapping("/login.html")
        public String login() {
            return "login";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    SpringSecurity配置类

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 开启请求的权限管理
            http.authorizeRequests()
                    // 放行访问登录页面的/login.html请求
                    .mvcMatchers("/login.html").permitAll()
                    // 放行/index请求
                    .mvcMatchers("/index").permitAll()
                    // 其他所有的请求都需要去认证
                    .anyRequest().authenticated()
                    .and()
                    // 认证方式为表单认证
                    .formLogin()
                        // 指定默认的登录页面
                        .loginPage("/login.html")
                        // 指定登录请求路径
                        .loginProcessingUrl("/doLogin")
                        // 指定表单用户名的 name 属性为 uname
                        .usernameParameter("uname")
                        // 指定表单密码的 name 属性为 passwd
                        .passwordParameter("passwd")
                        // 指定登录成功后的自定义处理逻辑
                        .defaultSuccessUrl("/index")
                    .and()
                    // 禁止csrf跨站请求保护
                    .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    ④ 控制器

    @Slf4j
    @RestController
    public class HelloController {
        @RequestMapping("/hello")
        public String hello() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            log.info("身份信息:{}",authentication.getPrincipal());
            log.info("权限信息:{}",authentication.getAuthorities());
            return "hello security";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    启动项目,访问 localhost:8080/hello,跳转到登录页面,打上断点,点击登录:

    在这里插入图片描述

    第二步

    当在登录表单中输入用户名和密码点击登录后,登录请求会进行AbstractAuthenticationProcessingFilte过滤器的doFilter方法,AbstractAuthenticationProcessingFilte 作为身份认证请求入口,是一个抽象类。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都继承了 AbstractAuthenticationProcessingFilter ,并重写了方法 attemptAuthentication 进行身份认证。

    在这里插入图片描述

    AbstractAuthenticationProcessingFilte源码:

    首先判断登录页面中配置的th:action="@{/doLogin}"请求路径是否是我们在WebSecurityConfigurer表单登录中配置的loginProcessingUrl("/doLogin")相同。如果相同则尝试调用子类的attemptAuthentication方法尝试认证。

    public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
          implements ApplicationEventPublisherAware, MessageSourceAware {
        
        private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 1、判断登录页面中配置的action请求路径是否是我们在WebSecurityConfigurer表单登录中配置的loginProcessingUrl相同
    		if (!requiresAuthentication(request, response)) {
    			chain.doFilter(request, response);
    			return;
    		}
    		try {
                // 2、调用子类的实现尝试认证
    			Authentication authenticationResult = attemptAuthentication(request, response);
    			if (authenticationResult == null) {
    				return;
    			}
    			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
    			// 3、Authentication success
    			if (this.continueChainBeforeSuccessfulAuthentication) {
    				chain.doFilter(request, response);
    			}
    			successfulAuthentication(request, response, chain, authenticationResult);
    		}
    		catch (InternalAuthenticationServiceException failed) {
    			unsuccessfulAuthentication(request, response, failed);
    		}
    		catch (AuthenticationException ex) {
    			// 4、Authentication failed
    			unsuccessfulAuthentication(request, response, ex);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    第三步

    调用子类UsernamePasswordAuthenticationFilter#attemptAuthentication方法,判断方法是否是post方法,根据请求参数uname,passwd获取登录用户名username和密码password,然后将需要做认证的username和password封装成Authentication对象UsernamePasswordAuthenticationToken,交给AuthenticationManager接口的子类ProviderManager去认证。

    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
            // 判断请求方式是否为post请求
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            // 从请求中获取登录用户名username
            String username = obtainUsername(request);
            username = (username != null) ? username : "";
            username = username.trim();
            // 从请求中获取登录密码password
            String password = obtainPassword(request);
            password = (password != null) ? password : "";
            // 将username和password封装成Authentication对象UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            // 将Authentication对象交给AuthenticationManager接口子类的authenticate方法尝试认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        
        @Nullable
    	protected String obtainPassword(HttpServletRequest request) {
    		return request.getParameter(this.passwordParameter);
    	}
    
    	@Nullable
    	protected String obtainUsername(HttpServletRequest request) {
    		return request.getParameter(this.usernameParameter);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    第四步

    Authentication对象ProviderManager#authenticate方法尝试认证。

    在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供

    多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider列表,在ProviderManager中遍历列表中的每⼀个AuthenticationProvider去执⾏身份认证,最终得到认证结果。

    ProviderManager源码核心流程:

    public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
        // AuthenticationProvider列表
        private List<AuthenticationProvider> providers = Collections.emptyList();
    	private AuthenticationManager parent;
        
        @Override
    	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    		Class<? extends Authentication> toTest = authentication.getClass();
    		Authentication result = null;
            Authentication parentResult = null;
    		int size = this.providers.size();
    		for (AuthenticationProvider provider : getProviders()) {
    			if (!provider.supports(toTest)) {
    				continue;
    			}
    			try {
    				result = provider.authenticate(authentication);
    			}
                // ...
    		}
    		if (result == null && this.parent != null) {
    			// Allow the parent to try.
    			try {
                    // 调用父类的ProviderManager进行认证
    				parentResult = this.parent.authenticate(authentication);
    				result = parentResult;
    			}
                // ...
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    默认情况下,ProviderManager的AuthenticationProvider列表中包含一个实现类:AnoymousAuthenticationProvider,因此for循环内遍历得到AnoymousAuthenticationProvider,执行AnonymousAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果不支持,代码如下:

    public class AnonymousAuthenticationProvider implements AuthenticationProvider,
          MessageSourceAware {
    
       public boolean supports(Class<?> authentication) {
          return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    跳出for循环,继续调用父类的ProviderManager进行认证,回调ProviderManager#authenticate方法,此时父类ProviderManager的AuthenticationProvider列表中有一个默认类DaoAuthenticationProvider。该类继承自AbstractUserDetailsAuthenticationProvider类,会调用AbstractUserDetailsAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果支持。

    public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {
        
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class
                    .isAssignableFrom(authentication));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因此 result = provider.authenticate(authentication) 最终会调用AbstractUserDetailsAuthenticationProvider#authenticate方法对UsernamePasswordAuthenticationToken对象完成认证,在该方法中根据username获取数据源中存储的用户user,然后判断user是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。

  • 相关阅读:
    四、vue-cli 介绍与使用
    rust宏
    数据排序 归并排序,计数排序以及快速排序的三路优化
    Spring-注解开发、核心容器、注解开发依赖注入
    JMeter 二次开发之环境准备
    力扣labuladong——一刷day10
    【Unity3D】摇杆
    基于JAVA的鲜花店商城平台【数据库设计、源码、开题报告】
    [Python]从入门到入门
    物联网入门
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126863604