JAVA——springSecurity——底层原理分析:处理认证请求和非认证请求的流程,主要过滤器链的作用

Posted 叶不修233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA——springSecurity——底层原理分析:处理认证请求和非认证请求的流程,主要过滤器链的作用相关的知识,希望对你有一定的参考价值。

JAVA——springSecurity底层原理分析:处理认证请求和非认证请求的流程,12条过滤器链的作用

一、认证请求(login)

1.spring管理过滤器的生命周期

在加载时启动一个叫DelegatingFilterProxy的过滤器代理对象,让spring管理这些过滤器的生命周期

DelegatingFilterProxy类——doFilter()

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException 
    //启动一个叫DelegatingFilterProxy的过滤器代理对象
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) 
        synchronized(this.delegateMonitor) 
            delegateToUse = this.delegate;
            if (delegateToUse == null) 
                WebApplicationContext wac = this.findWebApplicationContext();
                if (wac == null) 
                    throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                

                delegateToUse = this.initDelegate(wac);
            

            this.delegate = delegateToUse;
        
    

    this.invokeDelegate(delegateToUse, request, response, filterChain);

2.初始化一个FilterChainProxy过滤器链代理对象

初始化一个过滤链FilterChainProxy过滤器链的代理对象,在这个过滤器链代理对象中有一个过滤器链集合,每一个过滤器链都有一组过滤器来处理不同的请求

FilterChainProxy类——doFilter()

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException 
    if (this.currentPosition == this.size) 
        if (FilterChainProxy.logger.isDebugEnabled()) 
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
        

        this.firewalledRequest.reset();
        this.originalChain.doFilter(request, response);
     else 
        ++this.currentPosition;
        Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
        if (FilterChainProxy.logger.isDebugEnabled()) 
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
        
		//迭代过滤器
        nextFilter.doFilter(request, response, this);
    

3.处理认证请求

(1)判断是否是认证请求

AbstractAuthenticationProcessingFilter类——doFilter()

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;
    //(1)判断是否是认证请求,如果不是,直接放行
    if (!this.requiresAuthentication(request, response)) 
        chain.doFilter(request, response);
     else 
        //此时/login是认证的请求,执行else部分代码
        if (this.logger.isDebugEnabled()) 
            this.logger.debug("Request is to process authentication");
        
		...
    

(2)调用试图认证的方法

AbstractAuthenticationProcessingFilter类——doFilter()

如果是认证请求,调用试图认证的方法: attemptAuthentication()

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;
    //(1)判断是否是认证请求,如果不是,直接放行
    if (!this.requiresAuthentication(request, response)) 
        chain.doFilter(request, response);
     else 
        //此时/login是认证的请求,执行else部分代码
        if (this.logger.isDebugEnabled()) 
            this.logger.debug("Request is to process authentication");
        
		//声明认证的token对象  authResult
        Authentication authResult;
        try 
            //(2)如果是认证请求,调用试图认证的方法: attemptAuthentication()
            authResult = this.attemptAuthentication(request, response);
            if (authResult == null) 
                return;
            

            ...
    

(3)判断请求类型是否为post

UsernamePasswordAuthenticationFilter类——attemptAuthentication()

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());
     else 
        //是post请求,则获取表求中的用户名与密码
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) 
            username = "";
        

        if (password == null) 
            password = "";
        

        username = username.trim();
        //将用户名与密码封装至 UsernamePasswordAuthenticationToken对象中
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        //获取认证管理器ProviderManager对象,并调用认证的方法
        return this.getAuthenticationManager().authenticate(authRequest);
    

(4)做认证处理

通过ProviderManager认证管理器对象,调用authenticate()方法来做认证处理,在该方法中又去调用了一个认证器DaoAuthenticationProvider的authenticate()方法【注:该类没有此方法,需要从父类AbstractUserDetailsAuthenticationProvider继承中的方法authenticate()调用】

ProviderManager类——authenticate()

public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();
    Iterator var8 = this.getProviders().iterator();

    while(var8.hasNext()) 
        AuthenticationProvider provider = (AuthenticationProvider)var8.next();
        if (provider.supports(toTest)) 
            if (debug) 
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            

            try 
                //进入这个方法
                result = provider.authenticate(authentication);
                if (result != null) 
                    this.copyDetails(authentication, result);
                    break;
                
            ...
    

AbstractUserDetailsAuthenticationProviderf类——authenticate()

调用retrieveUser()方法,查询用户. 封装成UserDetails接口对象返回

public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> 
        return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
    );
    String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) 
        cacheWasUsed = false;

        try 
            //查询用户. 以UserDetails接口对象返回
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        ...

public interface UserDetails extends Serializable 
    //权限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();  //密码

    String getUsername();  //用户名

    boolean isAccountNonExpired();  //true 表示没有过期 

    boolean isAccountNonLocked();  //是否没有被锁定

    boolean isCredentialsNonExpired(); //密码是否没有过期 

    boolean isEnabled();  //是否可用

(5)通过用户名查询用户对象

springSecurity框架自带的loadUserByUsername(username)方法

DaoAuthenticationProvider类——retrieveUser方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException 
    this.prepareTimingAttackProtection();

    try 
        //调用loadUserByUsername(username)方法
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) 
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
         else 
            return loadedUser;
        
     
    ...

InMemoryUserDetailsManager类——loadUserByUsername(username)方法

即从内存中读取username

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
    UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
    if (user == null) 
        throw new UsernameNotFoundException(username);
     else 
        return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
    

这里的username我们可以在自定义的配置类WebSecurityConfig中配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        //在内存中配置用户名和密码
        auth.inMemoryAuthentication().withUser("tom")
                .password(passwordEncoder.encode("123")).roles();
        auth.inMemoryAuthentication().withUser("admin")
                .password(passwordEncoder.encode("admin")).roles();
    

②我们自定义的loadUserByUsername(username)方法

要想从自定义的数据库中读取账号和密码,同样可以在我们自定义的配置类WebSecurityConfig中配置,详见三、细节补充——(2)重写loadUserByUsername()方法

(6)检查用户是否通过认证

AbstractUserDetailsAuthenticationProvider类——authenticate()方法,调用preAuthenticationChecks.check()

   try 
       		//检验返回的用户是否可以使用
            this.preAuthenticationChecks.check(user);
            //将用户与提供的表单用户token进行比对
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
         catch (AuthenticationException var7) 
           ...
   
         ....
               

DaoAuthenticationProvider类——additionalAuthenticationChecks()方法

验证(内存或数据库)用户名密码是否匹配成功

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException 
        if (authentication.getCredentials() == null) 
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
         else 
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) 
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            
        
    

(7)通过认证,生成token

如果用户名与密码是正确的,则执行父类AbstractUserDetailsAuthenticationProvider中authenticate()中剩下的代码,最后返回return this.createSuccessAuthentication(principalToReturn, authentication, user)

DaoAuthenticationProvider类——createSuccessAuthentication()方法

 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) 
        //根据给定的principal和密码信息,及权限列表重新生成一个token
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    

而这个方法是重新为我们生成一个token,此token包装了用户的权限列表集。然后返回此token,接着一步步的向上返回这个 result结果对象

最终,返回到了源头AbstractAuthenticationProcessingFilter类的doFilter()方法,而此方法的末尾:

this.successfulAuthentication(request, response, chain, authResult);

此方法的关键:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException 
     	//将重新生成的token装载到SecurityContext中有很大作用---将来做鉴权时使用
        SecurityContextHolder.getContext().setAuthentication(authResult);
      

    	//此行代码就是认证成功后的处理器
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    

this.successHandler.onAuthenticationSuccess(request, response, authResult);执行的是SavedRequestAwareAuthenticationSuccessHandler类中的onAuthenticationSucess()方法:

public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException 
        SavedRequest savedRequest = this.requestCache.getRequest(request, response);
        if (savedRequest == null) 
            super.onAuthenticationSuccess(request, response, authentication);
         else 
            String targetUrlParameter = this.getTargetUrlParameter();
            if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) 
                this.clearAuthenticationAttributes(request);
                String targetUrl = savedRequest.getRedirectUrl();
                this.logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
                this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
             else 
                this.requestCache.removeRequest(request, response);
                super.onAuthenticationSuccess(request, response, authentication);
            
        
    

由于认证成功,【只走到了部份的过滤器】,而我们没有去重写成功后的handler,所以根据security的配置,走默认的url -->/home再次请求,要走一个过滤器链11个

二、非认证请求(home)

1.spring管理过滤器的生命周期

在加载时启动一个叫DelegatingFilterProxy的过滤器代理对象,让spring管理这些过滤器的生命周期

DelegatingFilterProxy类——doFilter()

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException 
    //启动一个叫DelegatingFilterProxy的过滤器代理对象
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) 
        synchronized(this.delegateMonitor) 
            delegateToUse = this.delegate;以上是关于JAVA——springSecurity——底层原理分析:处理认证请求和非认证请求的流程,主要过滤器链的作用的主要内容,如果未能解决你的问题,请参考以下文章

Java学习爆破专栏“杀青”丨千锋一一哥Spring Security系列教程总结来咯!

SpringSecurity的配置

SpringBoot整合SpringSecurity

SpringBoot之SpringSecurity(安全)

Java连载86-List集合详解

SpringSecurity:认证和自定义登陆界面