spring security 认证源码跟踪

2021-11-28

spring security 认证源码跟踪

​ 在跟踪认证源码之前,我们先根据官网说明一下security的内部原理,主要是依据一系列的filter来实现,大家可以根据https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#servlet-hello 查看相关的文档说明,英文不好的可以配合使用google翻译。

security 原理说明

​ 在上图中,红色方框圈出来的是security 的filter,每一个http request都会经过上图的每一个指定的过滤器。请求其中:

DelegatingFilterProxy:主要负责在servlet容器的生命周期和Spring上下文进行衔接,也就是说security的所有过滤器都委托给它进行代理。

FilterChainProxy:是一个特殊的过滤器,被包装在DelegatingFilterProxy内部。它代理代理了SecurityFilterChain

SecurityFilterChain:SecurityFilterChain 确定应为此请求调用哪些 Spring 安全过滤器。

DelegatingFilterProxy

​ 这是一个过滤器,所以肯定会有doFilter方法,我们主要查看内部的2个方法,首先从doFilter方法看起:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {

   // Lazily initialize the delegate if necessary.
   Filter delegateToUse = this.delegate;
   if (delegateToUse == null) {
      synchronized (this.delegateMonitor) {
         delegateToUse = this.delegate;
         if (delegateToUse == null) {
             // 拿到Spring Web上下文
            WebApplicationContext wac = findWebApplicationContext();
            if (wac == null) {
               throw new IllegalStateException("No WebApplicationContext found: " +
                     "no ContextLoaderListener or DispatcherServlet registered?");
            }
             // 初始化委托filter
            delegateToUse = initDelegate(wac);
         }
         this.delegate = delegateToUse;
      }
   }

   // Let the delegate perform the actual doFilter operation.
   invokeDelegate(delegateToUse, request, response, filterChain);
}

// 初始化委托filter
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    	// 众多filter中,会有一个是FilterChainProxy
		String targetBeanName = getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		Filter delegate = wac.getBean(targetBeanName, Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}

FilterChainProxy

​ 它也是一个过滤器,那一定也会有doFilter方法,我们查看该方法

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    // 当前request是否已经清除了上下文,因为每一个请求都会经过这个过滤器
   boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
   if (!clearContext) {
      doFilterInternal(request, response, chain);
      return;
   }
   try {
      request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
       // 内部的filter方法,我们看到该方法
      doFilterInternal(request, response, chain);
   }
   catch (RequestRejectedException ex) {
      this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
   }
   finally {
      SecurityContextHolder.clearContext();
      request.removeAttribute(FILTER_APPLIED);
   }
}

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    // 拿到防火墙配置,对于这里不重要
		FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    // 这里可以看到,FilterChainProxy在这里拿到了这次请求request具体还要经过的一系列过滤器链,其中包括CsrfFilter、UsernamePasswordAuthenticationFilter等过滤器,包含了SecurityFilterChain 涉及的filter
		List<Filter> filters = getFilters(firewallRequest);
		if (filters == null || filters.size() == 0) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
			}
			firewallRequest.reset();
			chain.doFilter(firewallRequest, firewallResponse);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
		}
		VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
		virtualFilterChain.doFilter(firewallRequest, firewallResponse);
	}

认证源码跟踪

​ 回到认证这里,在网上随便搜一搜就能搜到spring scurity认证的几种方式,这次我们主要跟踪第三种认证方式:数据库认证,也是我们平时在用的方式。先给大家说明一下数据库认证的知识点,有个大概印象:

  1. UsernamePasswordAuthenticationFilter
  2. 实现UserDetailsService接口并注入到spring管理

这三种认证方式分为为:

1、在xml中配置账号密码

spring.security.user.name=user
spring.security.user.password=123456

2、在代码中将账号、密码加载到内存中

@Bean
public UserDetailsService userDetailsService() {
    UserDetails userDetails = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
    return new InMemoryUserDetailsManager(userDetails);
}

3、从数据库中读取账号进行认证校验

public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库尝试读取该用户
        User user = userMapper.findByUserName(username);
        // 用户不存在,抛出异常
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 将数据库形式的roles解析为UserDetails的权限集
        // AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
        //提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
        // 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        return user;
    }
}

​ 在这个例子中,我们会有一个自定义WebSecurityConfig类,其中定义了哪些Url路径需要拦截,以及需要哪些权限才能够访问,同时在这个配置中,注入一个一个密码编码类,默认是不采用加密方式NoOpPasswordEncoder

@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/app/api/**").permitAll()
                .antMatchers("/css/**", "/index").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login-error")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

​ 我们先不去实现UserDetailsService接口,看看spring security是怎么去实现认证的?

UsernamePasswordAuthenticationFilter

​ 首先找到UsernamePasswordAuthenticationFilter类,发现它继承了AbstractAuthenticationProcessingFilter类,那我们就先看一下AbstractAuthenticationProcessingFilter类,发现这个类中主要有四个方法,分别是:

  1. doFilter(reqeust,response,chain):每个filter都会有的方法,最重要的一个。
  2. attemptAuthentication(request,response); 是个抽象方法,交给具体的实现类去实现认证的逻辑
  3. successfulAuthentication(request,response,chain,authenticationResult); 认证成功后的处理逻辑,通过不同的策略实现
  4. unsuccessfulAuthentication(request,resonse,failed);认证失败后的处理逻辑,通过不同的策略实现
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);
      return;
   }
   try {
       // 具体的认证方法,是个抽象方法,交给具体的实现类去实现认证的逻辑
      Authentication authenticationResult = attemptAuthentication(request, response);
      if (authenticationResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         return;
      }
      this.sessionStrategy.onAuthentication(authenticationResult, request, response);
      // Authentication success
      if (this.continueChainBeforeSuccessfulAuthentication) {
         chain.doFilter(request, response);
      }
       // 认证成功后的处理逻辑
      successfulAuthentication(request, response, chain, authenticationResult);
   }
   catch (InternalAuthenticationServiceException failed) {
      this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
      unsuccessfulAuthentication(request, response, failed);
   }
   catch (AuthenticationException ex) {
      // Authentication failed
       // 认证失败后的处理逻辑
      unsuccessfulAuthentication(request, response, ex);
   }
}

​ 接着我们看回UsernamePasswordAuthenticationFilter类,发现它主要是重写了AbstractAuthenticationProcessingFilter类的attemptAuthentication(request,response)认证方法。具体如下:

@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对象
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
   // Allow subclasses to set the "details" property
    // 设置认证的对象
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);
}

接着我们运行程序,直接访问http://localhost:8080/admin/api/hello地址,重定向到登录页后,随意输入账号、密码后,在UsernamePasswordAuthenticationFilter类的attemptAuthentication方法上打断点进行跟踪,跟踪到DaoAuthenticationProvider类的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法,

​ 在AbstractUserDetailsAuthenticationProvider抽象中的需要指定一个实现UserDetailsService 接口的实现类,如果我们没有指定,就是会去加载默认的InMemoryUserDetailManager类。

​ 因为采用的是上面提过的第二种方式:在代码中将账号、密码加载到内存中,然后我们并没有在内存中预先加载我们输入的账号、密码,所以自然是认证不通过的。

UserDetailsService 接口

​ 想要通过自定义的认证方式,也就是上面提到的第三种认证方式:从数据库中读取账号进行认证校验。所以需要自己去实现UserDetailsService 接口。刚才我们在跟踪代码的过程中,发现AbstractUserDetailsAuthenticationProvider类是需要一个实现了UserDetailsService接口的对象,于是我们就自定义一个实现该接口的实现类,并注入到spring容器中。

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库尝试读取该用户
        User user = userMapper.findByUserName(username);
        // 用户不存在,抛出异常
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 将数据库形式的roles解析为UserDetails的权限集
        // AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
        //提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
        // 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        return user;
    }
}

​ 如上图,我们重写了UserDetailsService接口的loadUserByUsername(String username)方法,从而实现我们的自定义认证逻辑。然后我们再重启服务,重新访问http://localhost:8080/admin/api/hello,再次登录,并进行代码跟踪,

这个时候就发现DaoAuthenticationProvider从自己的userDetailsService拿到了我们自定义的对象,接着就会走我们的自定义认证逻辑。

​ 认证源码跟踪就到这里,接下来是授权的源码跟踪,跟踪文章较短,但大家了解一下还是有些收获的。加油!