SpringSecurity ,oAuth2.0 从入门到源码精通 之 (四)spring security 的 认证流程、权限验证流程源码解读

(我将我的 Spring Security 专栏地址放在这里,供大家参考,后面会持续更新: SpringSecurity ,oAuth2.0 从入门到源码精通 ,希望可以帮助到正在学习 Spring Security 和 oAuth2.0 的小伙伴)。

经过前面三篇文章的学习,我想大家应该已经很熟悉 Spring Security 在 Spring Boot 中的基本使用了,相信大家肯定也不会满足于此,所以今天我们就一起来讨论一下 Spring Security 的源码,让我们不仅会使用 Spring Security 还能够明白它的原理,这样我们在以后的开发中就可以自己对 Spring Security 的功能进行完善、改进。

本文我将试着用最通俗易懂的语言和方式来介绍 Spring Security 的源码,以保证只要看了我这篇文章的小伙伴绝对能透彻理解 Spring Security 。不过,需要各位小伙伴要有一定的耐心,

【简单说明】:如果小伙伴已经对 Spring Security 的各种配置、使用方法已经挺熟练了,可以直接阅读本文;如果小伙伴是 Spring Security 的初学者,笔者建议先看一下我的前三篇文章,这三篇文章是 Spring Security 必知必会的,我也写的很通俗易懂。当你学完这三篇之后再来看本文,你就有一定的基础来学习源码了,不然你很有可能不知道我们在说什么 。
【代码说明】:本文会用到我们之前的文章中的代码,大家可以直接通过 git 克隆:代码地址

好了,闲话不多说,我们现在就开始。

1 不使用 Spring Security ,如何进行登录验证?(不是重点,可忽略)

这个问题很简单,我们在学习 javaweb 时,都会考虑在所有 servlet 前加上一个 filter 过滤器来拦住我们所有的请求,然后在过滤器中判断该请求对应的用户是否登录过

	@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpSession session = httpServletRequest.getSession();
        //如果用户登录过,那么session中会保存该用户信息
        //尝试从 session 中获取当前用户信息
        User user = session.getAttribute("login_user");//如果没找到说明这个访问者没有登录过
        if(user == null){
            //跳转到登录界面
            request.getRequestDispatcher("/toLoginPage").forward(request,response);
        }else {
            //如果找到了说明这个用户登陆过就继续下一个Filter(权限验证或者别的),没有Filter就执行 servlet 
            chain.doFilter(request,response);
        }
    }

这小段代码,我想大家肯定都很清楚,但是我们的目的不是怎么写这段代码。而是想引出 Spring Security 也是通过这种实现 filter 过滤器的方式来完成用户认证与授权。只不过 Spring Security 在完成登录认证的过程中用到了很多个过滤器,这些过滤器连在一起形成了一个过滤器链。类似下图:
在这里插入图片描述
那么,接下来我们就认识一下这些过滤器。

2 Spring Security 中所有的 过滤器:(了解)

官方文档中给出的所有 Filter,他们的执行顺序,从上到下依次执行:
ChannelProcessingFilter
ConcurrentSessionFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
ConcurrentSessionFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter

大家一下看到这么多过滤器不要被吓到,这些我们不需要全部都知道,只需要掌握其中最常见、最重要的即可(黄色标记的过滤器)。

3 一次请求会用到哪些过滤器:(了解)

前面列出的所有过滤器中,也不会所有的都执行,比如我们没有开启 csrf 防护的话,就不会执行 CsrfFilter
在 Spring Security 中,我们可以开启 debug 模式来查看每一次请求都执行了哪些过滤器。
在使用 @EnableWebSecurity 处加上 debug 参数即可:

@EnableWebSecurity(debug = true)

【注意】:在生产环境最好不要打开 debug 模式,否则会降低系统性能

我们开启 debug 模式后,在登录界面输入正确的用户名密码后,来看一下登录过程中会输出哪些日志信息:
(1)、登录的请求信息示例:

在这里插入图片描述
(2)、执行的过滤器:
在这里插入图片描述
上面这些过滤器就组成了一个过滤器链,当浏览器发出请求后,就会执行这些过滤器。当然也不一定就是这些,如果没有开启 csrf 防护就没有 CsrfFilter 过滤器,如果没有开启 “记住我” 功能就不会有 RememberMeAuthenticationFilter 过滤器。

下面我就将上面这 12 个过滤器的职责作一个简要介绍,本文只对几个过滤器进行详细介绍,另外的不是很难先不做过多介绍,如果后面有时间就单独写文章来一 一介绍这些过滤器。不过,我相信大家只要看了我这篇文章,自己就可以去看明白那些过滤器的源代码了。

4 分别介绍过滤器的职责:(了解)

我们知道 Spring Security 有两大功能:用户认证和授权

4.1 WebAsyncManagerIntegrationFilter

将 securityContext 实例放到异步线程管理器的拦截器中,这样做的目的是我们可以在后续的任意一个异步任务中都可以获取到认证用户的信息。所以 securityContext 就是用来保存用户认证信息的一个上下文。
大家应该还记得我们在第一篇文章中 获取当前登录用户的信息时,就是通过获取 securityContext 实例来完成的。

		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //获取用户的权限,我们在 WebSecurityConfiguration 类中通过 authorities() 配置的值
        Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();
        for(GrantedAuthority authority:authorities){
            System.out.println(authority.getAuthority());
        }

        //获取用户信息
        //这里的 User 类 是 org.springframework.security.core.userdetails 包中的
        User principal = (User)authentication.getPrincipal();
        String username = principal.getUsername();
        System.out.println("username:"+username);

4.2 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 过滤器也是用来处理 securityContext 的。当浏览器发起一个请求时,该过滤器先从 session 中尝试获取 securityContext 。如果没有获取到,则创建一个空的 securityContext,即其中的属性都没有赋值;然后将 securityContext 存放到当前 ThreadLocal 实例中;最后当一系列的过滤器都执行完成后,向浏览器返回数据前,再次执行 SecurityContextPersistenceFilter 过滤器时,将 securityContext 从ThreadLocal 实例中清除,并将 securityContext 保存到 session 中。之所以要从本地线程中清除,是因为这个线程还要返回线程池,供别的 http 请求使用。

4.3 HeaderWriterFilter

该过滤器一般情况下是向 response 中添加响应头,比如过期时间:Expires,缓存控制等:Cache-Control。

4.4 LogoutFilter

当用户请求的路径为 /logout 时(这是默认路径,如果我们可以通过 logoutUrl() 进行配置),就会被该过滤器拦截,执行登出操作。从 session 中清空用户认证信息、执行 LogoutSuccessHandler 的 onLogoutSuccess() 方法等。

4.5 UsernamePasswordAuthenticationFilter (本文重点)

该过滤器就是我们本文的重点。通过 AuthenticationManager 来验证用户名密码。所以一般经过该过滤器后,用户已经是认证成功了。

4.6 RequestCacheAwareFilter

从 session 中取出之前保存的请求。其实该过滤器不是重点,而是何时将请求放到 requestCache 缓存中的,大家可以查看 我的第二篇文章的第 3.2 小节

4.7 SecurityContextHolderAwareRequestFilter

将通过认证的请求与用户信息包装在一起:SecurityContextHolderAwareRequestWrapper。这样我们可以在 Controller 中直接使用:

@RequestMapping("/delete")
    public String delete(SecurityContextHolderAwareRequestWrapper request){
        //模拟删除书籍的代码
        System.out.println("删除书籍");
        System.out.println(request);
        System.out.println(request.getRemoteUser());
        return "删除成功";
    }

4.8 RememberMeAuthenticationFilter

当用户直接访问服务器某个 http 接口时,经过该过滤器时,会判断该用户曾经是否启用了记住我功能,并曾经是否登录过,如果登录过,那么用户此次就不需要登录,可以直接访问接口。

4.9 AnonymousAuthenticationFilter

当用户没有登录,直接访问资源时,经过该过滤器是会给用户设置为 anonymousUser,角色设置为 ROLE_ANONYMOUS 。该角色的用户可以访问不需要权限的接口。

4.10 SessionManagementFilter

该过滤器的任务是再次判断登录成功的用户信息是否保存在 session 中,如果没有,就保存一次。它最主要的任务是维护一个 session 管理策略实例:sessionAuthenticationStrategy。在 Spring security 中有多种策略。比如:
ConcurrentSessionControlAuthenticationStrategy:允许同一用户同时在线数。
SessionFixationProtectionStrategy:当用户在一个浏览器中登录后,换一个浏览器登录时:首先让原来的session过期,然后创建一个新的session,把原来session的属性拷贝到新的session中。

4.11 ExceptionTranslationFilter

该过滤器主要用于解决权限过滤器 FilterSecurityInterceptor 抛出的异常。

4.12 FilterSecurityInterceptor(重点)

是整个Security filter 链中的最后一个,也是很重要的一个,它的主要功能就是判断认证成功的用户是否有权限访问接口。

好了,到此为止我们将 Spring Security 中比较常见的过滤器的主要职责作了一个简单的介绍,由于本文篇幅,就不一 一去分析他们的源码,如果大家感兴趣的话,在知道每个过滤器的主要作用的情况下去查看这些过滤器的源码也是很轻松的。我后续也会再写文章来详细介绍这些过滤器。

那么,前面的饭前凉菜吃完了,接下来我们进入正席,主要介绍基于表单登录的认证流程。和基于 web 接口的权限验证流程

5 认证流程(重点)

前面讲了这么多,只是想让大家对 Spring Security 的执行流程有一个整体性的认识。

本小节就对认证流程做一个详细介绍:
【说明】本文的认证流程是基于表单认证的,我前面几篇文章的示例也都是基于表单认证。大家可以先不用管认证方式,可以先和我一起把整个流程弄明白,其他认证方式基本差不多,大家就可以自己来分析了。

众所周知,用户认证的过程,实际上就是对用户在登录界面中输入的 username 和 password 与服务器中保存的用户密码进行比对,如果一致则表示认证通过;反之,就是未认证通过。经过前面小节的简单介绍,我们已经清楚基于表单验证时,主要完成判断用户密码是否正确的过滤器是:UsernamePasswordAuthenticationFilter。从这个类的名字就可以看出,它的主要功能就是用户名密码认证过滤器。

UsernamePasswordAuthenticationFilter 继承自抽象类 AbstractAuthenticationProcessingFilter

在开始之前,我们先了解一下整个认证过程的时序图:
在这里插入图片描述

5.1 AbstractAuthenticationProcessingFilter

我们先看一下 AbstractAuthenticationProcessingFilter 类的 doFilter() 方法,当有请求发送到
UsernamePasswordAuthenticationFilter 过滤器时,会执行它的 doFilter() 方法,其实是执行其父类的 doFilter() 方法:
AbstractAuthenticationProcessingFilter 类的 doFilter() 方法:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		//(1)判断请求地址是否是 loginProcessingUrl()
		if (!requiresAuthentication(request, response)) {
			//如果不是,就执行后面的过滤器了
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
			//(2)认证用户
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
			//session 管理
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		//(3)在认证过程中,或者是 session 处理过程中出现异常,就认为认证未通过。
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}

		// 认证成功后,是否继续执行后续的过滤器。默认值为 false
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//(4)认证成功后的处理
		successfulAuthentication(request, response, chain, authResult);
	}

这段代码主要就上述 4 个部分,其中第 (3)、(4)步大家应该很熟悉,我们在第二篇文章中已经具体讲过如何配置,当认证成功或失败时就会执行我们配置的 AuthenticationFailureHandler 和 AuthenticationSuccessHandler。这里就不做过多的介绍了,这部分的源码也很简单。
sessionStrategy.onAuthentication(authResult, request, response) ,session 管理,我后续出单独文章来介绍。

第(1)步是用来判断用户当前请求的地址是不是处理登录的地址(loginProcessingUrl),即我们在配置文件中配置的:

http
     .formLogin()
         .loginPage("/toLoginPage")
         .loginProcessingUrl("/test/login")

该值默认为 /login,我们现在把它改成 /test/login。注意,改这里的同时,记得改一下 myLoginPage.html 中的 action 的值。在第二篇文章中,我们已经讨论过。

我们点进 requiresAuthentication() 方法,然后打上断点
在这里插入图片描述
我们可以看到,UsernamePasswordAuthenticationFilter 过滤器只拦截请求 URI 为 /test/login,请求方式为 POST 的请求,并进行认证处理。

判断的依据是 AbstractAuthenticationProcessingFilter 的 requiresAuthenticationRequestMatcher 属性,该属性的默认值为:

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

我们配置的 loginProcessingUrl 值最终就是交给了 requiresAuthenticationRequestMatcher 属性。

接下来,我们将重点介绍第(2)步认证过程:调用 attemptAuthentication(request, response) 方法。

5.2 attemptAuthentication() 方法

UsernamePasswordAuthenticationFilter 类的 attemptAuthentication() 方法完成表单验证

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		
		//判断是否为 POST 请求
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//获取用户名和密码
		//request.getParameter(usernameParameter);
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
		
		//(1)创建一个 UsernamePasswordAuthenticationToken 实例,最终给 AuthenticationProvider 使用
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		//向 authRequest 中设置一些客户端的 ip 信息、session 信息等
		setDetails(request, authRequest);
		//(2)调用 AuthenticationManager 来完成认证功能。
		return this.getAuthenticationManager().authenticate(authRequest);
	}

看了这段代码,大家可能觉得有点气人,我看了这么久,你就给我看个这,就这,就这。

从第(2)步,可以看出这段代码根本没有实际进行认证处理。后面详细介绍 AuthenticationManager。

第(1)步中创建了一个 UsernamePasswordAuthenticationToken 实例该实例其实就是将用户输入的用户名和密码进行了封装,并且额外添加了用户的 ip 信息等

UsernamePasswordAuthenticationToken 最终实现了一个 Authentication 接口,该接口我们在前面的文章中也已经介绍过,主要用来存放用户认证信息。

在这里插入图片描述

我们点进第(1)步创建实例 authRequest 的构造方法中:

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		//现在还没有开始认证,所以认证标记 authenticated 的值为 false。
		setAuthenticated(false);
	}

我们再往下看,它还有另外一个构造方法:

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);//用户的权限
		this.principal = principal;
		this.credentials = credentials;
		//设置为认证成功
		super.setAuthenticated(true); // must use super, as we override
	}

第二个构造器,设置了用户的权限,以及 authenticated 设置为了 true,所以该构造方法会在用户通过认证时被调用。

5.3 AuthenticationManager:认证管理器

从 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法中我们发现:

return this.getAuthenticationManager().authenticate(authRequest);

过滤器将认证任务交给了 AuthenticationManager 的 authenticate(Authentication authentication) 方法来完成。

我们查看 AuthenticationManager 的源码发现,AuthenticationManager 是一个接口

public interface AuthenticationManager {
	/**
	 * Attempts to authenticate the passed Authentication object, returning a
	 * fully populated Authentication object (including granted authorities)
	 * if successful.
	 */
	 //从上面的官方文档,我们可以看出:根据传入的 Authentication 对象进行验证,
	 //然后再返回一个填充了用户权限信息的 Authentication 对象
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

所以认证任务还是交给了 AuthenticationManager 的实现类,在 Spring Security 中我们最常用的是 ProviderManagerProviderManager 实现了 AuthenticationManager 接口

5.4 ProviderManager 和 AuthenticationProvider

我们看这个名字,也能猜个大概,ProviderManager 不是真正完成认证任务的,真正完成任务的是 AuthenticationProvider :认证提供者。ProviderManager :认证提供者管理器。

AuthenticationProvider 是一个接口:

public interface AuthenticationProvider {
	//完成认证任务的方法
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	// 该 provider 支持的认证方式
	boolean supports(Class<?> authentication);
}

这里我们把在第二篇文章中介绍的自定义 AuthenticationProvider 拿来再分析一下,让大家对 AuthenticationProvider 有一个更深刻的认识。

MyAuthenticationProvider.java:

@Component//注册到容器中
public class MyAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //authentication 其实也是一个 UsernamePasswordAuthenticationToken 对象
        System.out.println(authentication.getClass());
        //获取用户在登录界面输入的信息
        String userName = (String) authentication.getPrincipal(); //拿到username
        String password = (String) authentication.getCredentials(); //拿到password

        //在 WebSecurityConfiguration 中配置的用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
        if(userDetails == null){
            throw new UsernameNotFoundException("not found this username");
        }

        if ( passwordEncoder.matches(password,userDetails.getPassword()) ) {//验证密码
            //验证成功将结果返回给 PrivoderManager
            return new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
        }else{
            throw new BadCredentialsException("the password and the username are not matches");
        }
        //如果返回了null,AuthenticationManager会交给下一个支持 authentication类型的AuthenticationProvider处理。
        //return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //该 provider 支持的认证方式
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

MyAuthenticationProvider 实现了 AuthenticationProvider 接口,它就具有了认证功能。在 authenticate() 方法中的代码,我们可以看出,传进来的 authentication 对象其实是一个 UsernamePasswordAuthenticationToken 类的实例,这是因为我们使用了表单登录的原因,并且 MyAuthenticationProvider 的 supports() 方法也表明该 provider 只支持 UsernamePasswordAuthenticationToken。
因为我们在认证的时候要从 UsernamePasswordAuthenticationToken 中获取 uaername 和 password那假如我们以后要实现一个可以用手机号、验证码登录的功能,我们就可以自定义一个 PhoneProvider 和 PhoneAuthenticationToken 来完成这个功能,用 PhoneProvider 来完成手机号验证,PhoneAuthenticationToken 来保存用户输入的手机号和验证码所以 provider 和 authenticationToken 都是成对出现的。

provider 通过 UserDetailsService 来获取服务器中保存的用户信息 UserDetails 来与用户输入的 username 和 password 进行比对来判断是否匹配。

在 Spring Security 中默认的 provider 是 DaoAuthenticationProvider

到这我想大家应该对 AuthenticationProvider 有了了解了。

那么接下来,我们分析一下 ProviderManager 。

ProviderManager 中维护了一个 providers 和 一个 parent。

	private List<AuthenticationProvider> providers = Collections.emptyList();
	private AuthenticationManager parent;

我们先介绍 providers ,该集合由一系列 AuthenticationProvider 的实现类构成,并且在执行认证任务时,这些 provider 是在 ProviderManager 的 authenticate(Authentication authentication) 方法中按顺序执行的。每个 provider 在执行前会判断传入的 authentication 是否符合当前 provider ,如果符合才执行该 provider 的 authenticate() 方法,如果不符合就判断下一个 provider ,如果 providers 中的 provider 都不符合就由 parent 来执行,这个 parent 也是一个 ProviderManager 实例

如果 parent 中的 providers 也不符合,就叫给 parent 的 parent ,最终是 DaoAuthenticationProvider 来执行。

那么,接下来我们就来看一下 ProviderManager 的 authenticate(Authentication authentication) 方法:

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTestAuthentication = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
		//遍历执行 providers 中的每一个 provider
		for (AuthenticationProvider provider : getProviders()) {
			//根据 provider 的 supports() 方法来判断当前 provider 是否支持传入的 authentication 
			if (!provider.supports(toTestAuthentication )) {
				//当前 provider 不支持,就换下一个 provider
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				//执行 provider 的认证方法
				result = provider.authenticate(authentication);
				//如果 result 不为 null ,就说明当前 provider 完成了用户认证任务,并且用户通过了认证
				if (result != null) {
					//通过认证后,填充 authentication 一些信息
					copyDetails(authentication, result);
					break;
				}
			}
			//认证过程中发生的异常抛给 UsernamePasswordAuthenticationFilter
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}
		
		//providers 中的 provider 都没有执行或者都返回了 null
		//认为当前认证还未完成,将交给 parent 来完成
		//parent 一般为 DaoAuthenticationProvider
		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				//认证通过后,将密码等都清除
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

经过前面的分析,我想大家应该已经对整个认证流程有了一个基本认识。

最后我们再回到 AbstractAuthenticationProcessingFilter 的 doFilter() 方法中打下断点,进行 debug ,查看认证成功后 Authentication 的变化:

在这里插入图片描述

5.5 认证流程小结

下面我们就把前面介绍认证过程做一个总结,大家可以先看一下下面的时序图,将认证流程再熟悉一下。
在这里插入图片描述

(1)当用户在登录界面输入用户名和密码之后,登录请求将依次进入我们前面介绍的大概 12 个过滤器,每个过滤器执行不同的任务。

(2)过滤器间通过调用过滤器链的 doFilter() 方法将请求交给后续过滤器处理。

(3)当请求到 UsernamePasswordAuthenticationFilter 过滤器时,该过滤器比对请求的地址是否与 loginProcessingUrl() 设置的地址相同,如果相同则拦截该请求。然后从请求中获取用户的 username 和 password ,并将它们封装到 UsernamePasswordAuthenticationToken 中(是 Authentication 的实现类,在后续的验证过程中,都是使用该接口的实现类来传递用户信息)。

(4)UsernamePasswordAuthenticationFilter 调用 AuthenticationManager 的 authenticate() 方法,将认证任务交给 AuthenticationManager 来完成。

(5)AuthenticationManager 只是一个认证管理器,只提供接口,具体的认证功能需要通过其实现类来完成。Spring Security 中默认的实现是 ProviderManager。所以又将任务交给了 ProviderManager 的 authenticate() 来完成。

(6、7、8)实际上 ProviderManager 也不是真正完成认证任务的类,它实际上管理了很多 AuthenticationProvider。当需要认证时,ProviderManager 就遍历所有的 AuthenticationProvider,然后根据 AuthenticationProvider 的 supports() 方法来判断当前 Authentication 由哪些 AuthenticationProvider 来处理。如果这些 AuthenticationProvider 都不能处理则交给默认的 DaoAuthenticationProvider 来处理,如果 DaoAuthenticationProvider 也不能处理则抛出异常。
AuthenticationProvider 在完成认证任务时,通过 UserDetailsService.loadUserByUsername() 方法来获取用户之前注册的密码等,被封装到 UserDetails 中,然后使用 PasswordEncoder 来比对用户输入的密码是否与 UserDetails 中保存的一致。

(9)如果用户输入的密码正确,则认证成功。认证成功后,在 AuthenticationProvider 中会将用户的权限信息设置到 Authentication 中。

(10)AuthenticationProvider 最终将添加了用户认证成功信息和用户权限的 Authentication 返回给 UsernamePasswordAuthenticationFilter 过滤器。该过滤器调用 SecurityContextHolder.getContext().setAuthentication() 将 Authentication 保存在安全上下文中。

(11)最后调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess() 方法来处理登录成功后的业务逻辑。

到目前为止,关于 Spring Security 的认证流程已经全部介绍完了。接下来,我们将继续分析授权流程。

6 权限验证流程(重点)

6.1 FilterSecurityInterceptor

接下来,我们就详细介绍一下 Spring Security 中的权限验证流程。

我们在不使用 Spring Security 安全框架时,如果想对我们的接口进行鉴权的话,我们可能会定义一个下面这样的 Filter:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        HttpServletRequest req = (HttpServletRequest) request;
        String servletPath = req.getServletPath();
        HttpSession session = req.getSession();
        User user = (User)session.getAttribute("LOGIN_USER");
        if("USER".equles(user.Authority())){
        	if("/book/get".equles(servletPath )){
        	}
        	chain.doFilter(request,response);
        }else if("ADMIN".equles(user.Authority())){
        	//具有 ADMIN 权限的用户可以访问哪些接口
        	//...
        }else{
        	//...	
        }
    }

这段代码大家应该很熟悉,就是在访问真正的接口前,先用该过滤器来判断访问该接口的当前用户是否具有访问该接口的权限。那么在 Spring Security 中也是如此,在前面介绍 Spring Security 中的过滤器时我们已经介绍了 FilterSecurityInterceptor这个过滤器就是用来验证用户权限的

FilterSecurityInterceptor 过滤器是整个过滤器链中的最后一个,在它后面一般就是处理请求的的控制器了。

我们来看一下 FilterSecurityInterceptor 的源码:

这里我们只贴出主要的内容:
FilterSecurityInterceptor.java:

public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
			//将 request 与 response 对象进行封装一下
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

//主要任务在该方法中完成
public void invoke(FilterInvocation fi) throws IOException, ServletException {
		//如果用户已经鉴权过一次就不需要再鉴权了,第二次可以直接访问接口
		//需要 observeOncePerRequest 为 true
		//默认情况下 observeOncePerRequest 为 false ,即每次访问都要进行权限验证
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			//(1)对用户的权限进行验证
			InterceptorStatusToken token = super.beforeInvocation(fi);
			try {
				//(2)调用请求的接口
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				//(3)请求接口完成后的处理
				super.finallyInvocation(token);
			}
			(4)请求接口完成后的处理
			super.afterInvocation(token, null);
		}
	}

简单分析一下:
由于 FilterSecurityInterceptor 是过滤器链中的最后一个过滤器,所以第(2)步的代码其实就是在执行控制器中处理请求的方法了。所以需要在第(1)步对用户的权限进行验证,只有用户通过权限验证后才能进行访问接口。其中的第(3)、(4)步内容差不多,如果在第(1)、(2)步中对 SecurityContext 做了修改,需要在第(3)(4)步中修改回原来的样子(即用户通过认证时的样子)。

这 4 步中,除了 doFilter() 方法,其他 3 步都是调用了父类的方法。所以下面我们来讨论一下 FilterSecurityInterceptor 的父类。
在这里插入图片描述
从上面的类图可以看出 FilterSecurityInterceptor 是继承自 AbstractSecurityInterceptor 的。

6.2 AbstractSecurityInterceptor

6.2.1 beforeInvocation() 方法

我们先看一下该方法的代码,由于太多,我只贴主要部分:

protected InterceptorStatusToken beforeInvocation(Object object) {
		//(1)获取访问该接口必须具有的权限
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);
		
		//(2)安全上下文中如果没有用户的认证信息则抛出异常
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}

		//(3)获取当前用户的认证信息
		Authentication authenticated = authenticateIfRequired();

		//	(4)对用户权限进行验证,已判断是否能访问当前接口
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			//如果用户没有访问该接口的权限,则抛出异常
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}

		// (5)对用户的权限信息添加一些信息,默认为空实现,即什么都不做
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
				attributes);

		if (runAs == null) {
			if (debug) {
				logger.debug("RunAsManager did not change Authentication object");
			}

			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {
			if (debug) {
				logger.debug("Switching to RunAs Authentication: " + runAs);
			}
			//(5.1)如果 Authentication 被 runAsManager 修改过,将修改后的 Authentication 供后续控制器使用
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
	}

在该方法中其实也不是真正进行权限验证的地方,而是将权限验证的任务委托给了 AccessDecisionManager不过我们需要将该段代码的流程理清楚。后面我们就详细介绍 AccessDecisionManager 。

下面我们将 beforeInvocation() 中的流程做一个简单总结:

(1)获取访问该接口必须具有的权限。这里获取到的就是我们在 WebSecurityConfiguration 配置文件中配置的信息:

			http
				.authorizeRequests()
                    .antMatchers("/book/get/**").hasAnyAuthority("USER","ADMIN")
                    .antMatchers("/book/delete").hasAuthority("ADMIN")
                    .antMatchers("/book/detail").permitAll()
                    .anyRequest().authenticated()

如果你使用的是基于方法的权限配置,如:@PreAuthorize(“hasAnyAuthority(‘USER’,‘ADMIN’)”) 等注解方式。获取的权限信息就是这些注解里的值。
只不过是通过不同的 SecurityMetadataSource 而已,我们在后面再介绍 Spring Security 是如何获取我们配置的访问接口所需要的的权限信息的。

(2)从安全上下文获取用户的认证信息,如果没有则抛出异常。到此为止用户应该是经过了 UsernamePasswordAuthenticationFilter 过滤器完成了登录认证、或者是通过了 RememberMeAuthenticationFilter 过滤器通过 “记住我” 功能进行了自动登录,再不济也是通过了 AnonymousAuthenticationFilter 过滤器的匿名认证。前两个需要用户通过认证就可以在 SecurityContext 中设置一个 Authentication 信息;AnonymousAuthenticationFilter 过滤器也是默认打开的,主要针对一些不需要权限就可以访问的接口, Spring Security 默认给这些权限设置 anonymous 访问权限。所以假如我们将 AnonymousAuthenticationFilter 关闭了,那用户在没登录的情况下,在 SecurityContext 中连匿名权限都没有,所以也就不能进行后面的权限验证了。所以此处就是将 SecurityContext 中没有 Authentication 信息的请求给拒绝掉。

(3)从 SecurityContext 中获取当前用户的认证信息。

private Authentication authenticateIfRequired() {
		Authentication authentication = SecurityContextHolder.getContext()
				.getAuthentication();
		//是否是否已验证,匿名验证也算被验证了
		//alwaysReauthenticate是否每次都要再次认证,默认为 false,即不需要每次访问接口都要再认证 username 和 password
		if (authentication.isAuthenticated() && !alwaysReauthenticate) {
			if (logger.isDebugEnabled()) {
				logger.debug("Previously Authenticated: " + authentication);
			}

			return authentication;
		}
		
		//如果没有通过认证,又必须每次都重新认证的话,就调用 authenticationManager 进行认证。
		//一般不会执行这里,如果要这样的话,我们访问每个接口时都要将用户的 username 和 paswword 带上。
		authentication = authenticationManager.authenticate(authentication);

		// We don't authenticated.setAuthentication(true), because each provider should do
		// that
		if (logger.isDebugEnabled()) {
			logger.debug("Successfully Authenticated: " + authentication);
		}

		SecurityContextHolder.getContext().setAuthentication(authentication);

		return authentication;
	}

(4)调用 accessDecisionManager 对用户权限进行验证,以判断用户是否能访问当前接口。对于 accessDecisionManager 是如何验证的我们后面再详细介绍。

(5)对用户的权限信息添加一些信息,默认为空实现,即什么都不做。
(5.1)如果 Authentication 被 runAsManager 修改过,将修改后的 Authentication 供后续控制器使用。这样做可以避免后续控制器在使用 Authentication 时,对其进行修改,导致正常的认证 / 授权流程不可以使用。

这里我们将 beforeInvocation() 方法的流程进行介绍,其中的 第(4)步是我们的重点,我们后面再详细介绍。

6.2.1 finallyInvocation() 方法

下面介绍一下 finallyInvocation() 方法该方法是在请求被控制器处理完成后执行、或控制器抛出异常时执行

主要任务就是将 beforeInvocation() 方法中第(5.1)步设置的 Authentication 替换为最开始的 Authentication,即在 beforeInvocation() 方法中第(3)步获取的用户认证信息

protected void finallyInvocation(InterceptorStatusToken token) {
		if (token != null && token.isContextHolderRefreshRequired()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Reverting to original Authentication: "
						+ token.getSecurityContext().getAuthentication());
			}

			SecurityContextHolder.setContext(token.getSecurityContext());
		}
	}

6.2.2 afterInvocation() 方法

该方法是在 finallyInvocation() 方法之后执行的,在该方法中也会调用 finallyInvocation() 方法。然后再调用 AfterInvocationManager 的 decide() 方法,你可以理解为给 FilterSecurityInterceptor 过滤器加上了一个后置通知处理。

6.3 权限验证流程小结

经过前面的分析,我们对 Spring Security 中的权限验证流程就有了一个清晰的认识,大家可以看一下下面这个简单的时序图:
在这里插入图片描述

7 再看 AbstractSecurityInterceptor

该类是一个抽象类,主要定义了一些验证权限过程中用到的方法:beforeInvocation() 等。
在这里插入图片描述
从上述类图中,我们可以看出 AbstractSecurityInterceptor 有两个子类:FilterSecurityInterceptor和 MethodSecurityInterceptor

(1)FilterSecurityInterceptor:该类主要用于拦截基于 http 请求的接口访问。

如果我们在配置权限时使用的是下面这种方式:

		http.authorizeRequests()
                    .antMatchers("/book/get/**").hasAnyAuthority("USER","ADMIN")
                    .antMatchers("/book/delete").hasAuthority("ADMIN")
                    .antMatchers("/book/detail").permitAll()
                    .anyRequest().authenticated()

在这里插入图片描述
大家可以打上断点验证一下。其中 FilterSecurityInterceptor 使用的 SecurityMetadataSource 为 ExpressionBasedFilterInvocationSecurityMetadataSource
我们接着往下单步执行,一致到 beforeInvocation() 中获取接口的访问权限:
在这里插入图片描述
从这里我们可以看出 ExpressionBasedFilterInvocationSecurityMetadataSource 对应的 ConfigAttribute 的实现类是 WebExpressionConfigAttribute 。WebExpressionConfigAttribute 中存储的就是我们在配置文件中配置的访问受保护的接口的某一条权限表达式。例如上面的:hasAnyAuthotiry(‘USER’,‘ADMIN’)
然后,我们接着往下进行单步执行到 :

this.accessDecisionManager.decide(authenticated, object, attributes);

在这里插入图片描述
我们发现 FilterSecurityInterceptor 使用的 AccessDecisionManager 的实现类为 AffirmativeBased
然后我们进入 decide() 方法,再进入 getDecisionVoters(),我们查看使用的 AccessDecisionVoter 是什么:
在这里插入图片描述
AffirmativeBased 使用的投票器是 WebExpressionVoter。
经过前面的调试分析,我们可以画个图描述一下权限验证流程中各个使用的各个组件:
在这里插入图片描述
**(2)MethodSecurityInterceptor **
该类主要是基于方法的权限验证。

我们在配置权限时是在控制器的方法上或者是 Service 的方法等类的方法上添加注解来实现权限配置:

@PreAuthorize("hasAnyAuthority('USER','ADMIN')")
    @RequestMapping("/get")
    public Book get(Authentication authentication){
        Book book = new Book();
        book.setBookId("1");
        book.setBookName("《Thinking in Java》");
        book.setAuthor("Bruce Eckel");

        return book;
    }

该方式需要在配置文件类上添加注解:

@EnableGlobalMethodSecurity(prePostEnabled = true)

我们也可以像调试 FilterSecurityInterceptor 流程一样来调试 MethodSecurityInterceptor 流程中所使用的组件。
大家可以自己调试一下,这里我们直接提出来了:
在这里插入图片描述
在权限验证的流程中的所有组件我们都可以自定义。从而达到自定义权限验证的目的。

8 AccessDecisionManager 和 AccessDecisionVoter(重点)

AccessDecisionManager 可以理解为决策器它由一系列 AccessDecisionVoter 来投票,然后将这些投票器的投票结果进行汇总,然后给 FilterSecurityInterceptor 结果,即权限验证的结果。

8.1 AccessDecisionVoter

我们先来介绍一下投票器:AccessDecisionVoter
在这里插入图片描述
AccessDecisionVoter 是一个接口
AccessDecisionVoter.java:

public interface AccessDecisionVoter<S> {
	int ACCESS_GRANTED = 1;//赞成
	int ACCESS_ABSTAIN = 0;//弃权
	int ACCESS_DENIED = -1;//反对
	
	//返回投票是否支持特定配置属性
	//比如 WebExpressionVoter 只支持 WebExpressionConfigAttribute  
	boolean supports(ConfigAttribute attribute);
	//支持的受保护对象类型,FilterInvocation
	boolean supports(Class<?> clazz);

	/**
	对用户的权限进行判断
	authentication 用户的认证信息,包含用户的权限信息
	object 受保护的接口或方法等
	attributes 访问受保护资源需具备的权限
	*/
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}

AccessDecisionVoter 主要有三个实现类:RoleVoter、WebExpressionVoter 和 AuthenticatedVoter这些投票器的不同点在于根据不同的 ConfigAttribute 来验证用户的权限

比如 RoleVoter 对应的 ConfigAttribute 是已 “ROLE_” 开头的,如果不是则弃权:
投票方法:

public int vote(Authentication authentication, Object object,
			Collection<ConfigAttribute> attributes) {
		if (authentication == null) {
			return ACCESS_DENIED;
		}
		int result = ACCESS_ABSTAIN;
		Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);

		for (ConfigAttribute attribute : attributes) {
			if (this.supports(attribute)) {
				result = ACCESS_DENIED;

				// Attempt to find a matching granted authority
				for (GrantedAuthority authority : authorities) {
					if (attribute.getAttribute().equals(authority.getAuthority())) {
						return ACCESS_GRANTED;
					}
				}
			}
		}
		return result;
	}

又比如 WebExpressionVoter 对应的 ConfigAttribute 是 WebExpressionConfigAttribute。

public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;

		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);

		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

WebExpressionVoter 的 vote() 方法看起来比较复杂,我们就只说一下它的大概流程,由于我们在配置的时候使用的是

.antMatchers("/book/get/**").hasAnyAuthority("USER","ADMIN")

Spring Security 会将 hasAnyAuthority(“USER”,“ADMIN”) 转化为一个 spel 表达式字符串 " hasAnyAuthority(‘USER’,‘ADMIN’)"。所以在与用户的权限进行比较时,就需要先解析该表达式,然后将该表达式的语义解析出来。比如 hasAnyAuthority(‘USER’,‘ADMIN’) 的语义是只要用户的权限列表中有 USER 和 ADMIN 其中一个就可以。最后根据解析后的语义来验证用户的权限。

所以,投票器才是最终验证用户权限的地方。可能有同学会问了,为什么叫投票器呢?不是直接判断就可以了吗?

我们举一个例子,在生活中,我们有的时候为了做一个决定,需要大家一起来投票。比如,我们需要通过投票来决定小王同学要不要加入我们的篮球队,那么就会在现有的队员之间发起一个投票,每一个现有的队员就是我们的投票器,每一个队员根据自己的意愿或实际情况投出自己的票:赞成、弃权或者是反对。当所有人投完票后,就需要一个人来统计票数并作出决策,假如是队长。投票的时候肯定有人投赞成票、有人投反对票或者有人弃权。那么队长怎么来判断小王要不要加入我们的篮球队呢?我们就需要有一个决策策略了,比如一般的多少服从少数原则:即多数赞成票的话就赞成小王加入。那么我们也可以采取别的策略,比如队长比较照顾每一个人的想法:只要有一个人投了反对票,那么小王就不能加入篮球队。

所以在 Spring Security 中的各种 AccessDecisionVoter 投票器,它们只根据自己的情况投票,而不负责计票和作出决定。在 Spring Security 中进行计票和作出决策的是 AccessDecisionManager

8.2 AccessDecisionManager

AccessDecisionManager 就是决策器,接下来我们先看一下 Spring Security 为我们提供了哪些具体的决策器。
在这里插入图片描述
AccessDecisionManager 是一个接口,AbstractAccessDecisionManager 是一个抽象类,其他三个就是具体的决策器了。

这三个决策器我们只介绍一下 AffirmativeBased 的源代码,另外两个大家可以自己看一下。

AffirmativeBased 是 Spring Security 默认使用的决策器。

我们贴出它的决策方法:

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		//循环遍历所有投票器
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			//投票器进行投票
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}
			//决策器进行计票
			
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED://只要有一个投了赞成票,就认为权限验证通过
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}
		//如果没有赞成票,有反对票时:权限验证不通过
		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}
		
		//如果都是弃权票也认为是验证不通过
		checkAllowIfAllAbstainDecisions();
	}

//isAllowIfAllAbstainDecisions 默认为 false
protected final void checkAllowIfAllAbstainDecisions() {
		if (!this.isAllowIfAllAbstainDecisions()) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}
	}

我们将 AffirmativeBased 总结一下就是:

  • 只要有一个赞成票就认为权限验证通过
  • 如果没有一个赞成票,只要有一个反对票就认为验证不通过
  • 如果所有 AccessDecisionVoter 都弃权,默认为不通过,取决于 allowIfAllAbstainDecisions 的值

ConsensusBased 的决策逻辑

  • 如果赞成票比反对票多就认为通过
  • 如果反对票比赞成票多就认为权限验证未通过
  • 如果全部弃权默认是不通过
  • 如果赞成票和反对票相同,默认为通过。取决于 allowIfEqualGrantedDeniedDecisions 的值

这两种决策器都是直接将 configAttributes 集合交给每一个投票器进行投票,也就是说决策器是不知道投票器如何投票的(是只要有一个 configAttribute 符合就投赞成票还是所有 configAttribute 都符合才投赞成票等),而第三种决策器 UnanimousBased 每次只给每个投票器一个 configAttribute 来投票。

我们继续举前面小王进篮球队的例子:小王有很多特征属性:configAttribute ,比如身材高大、球技一流、人缘好等。假如队长现在使用前两种决策器方案,他就给队员说:现在小王有这些 configAttributes ,请各位队员根据这些 configAttributes 进行投票。对于队长来说他是不知道各位队员根据小王的哪一个
configAttribute 来做出赞成或反对的。比如小张觉得小王球技一流直接就投赞成票,其他两个 configAttribute 不用看,就告诉队长自己投赞成票。小刘认为只有身材、球技、人缘都好自己才投赞成票。而小李却不认这些 configAttribute ,他只投长的帅的,所以他反对小王加入。最后队长再对这些票进行统计根据不同的策略(ConsensusBased ,AffirmativeBased )做出决策。所以投票器关注的是各个 configAttribute ,决策器关注的是各个投票器给出的结果。

而 UnanimousBased 决策器与前两种有一些不同。队长说我们先从小王的身材开始投票,队员依次进行投票。队员每投一次票,队长就立马做出决策。如果第一个队员在小王的身材上就投了反对票,那队长就直接做出反对决策。如果对小王的身材没有反对票,就继续针对小王的球技进行投票,一直到所有的 configAttribute 都进行了投票,如果有赞成票,队长就做出赞成决策。如果全是弃权,默认为反对。

所以我们总结一下 UnanimousBased 的决策逻辑

  • 如果受保护对象配置的某一个 ConfigAttribute 被任意的 AccessDecisionVoter 反对,则权限验证不通过
  • 对所有的 ConfigAttribute 都做出投票后,如果没有反对票,但是有赞成票,则表示通过
  • 如果全部弃权,默认为不通过

最后,我们用一个 Spring Security 官网中的一个图片来描述一下决策器和投票之间的关系,来加深大家的印象。
在这里插入图片描述
【小结】在权限验证的流程中,FilterSecurityInterceptor 通过 SecurityMetadataSource 来获取权限配置信息(ConfigAttribute)集合,然后投票器根据 ConfigAttribute 集合和用户权限做出投票,最后由决策器来对自己的管理的投票器的结果做出决策。
这其中的每一个组件我们都可以自定义,比如我们可以自定义 SecurityMetadataSource 从数据库中获取接口的权限信息,已达到动态的改变接口的权限功能,那既然我们自定义了 SecurityMetadataSource ,就要自定义一个我们的 SecurityMetadataSource 能够识别的 ConfigAttribute ,我们自定义的 ConfigAttribute ,Spring Security 内置的投票器都不支持,所以我们还要自定义自己的投票器。
关于动态的修改接口的权限信息,我们在后面的文章中专门讨论。

最后,我们讲一下研究认证 / 授权源码的目的:我们只需要掌握认证 / 授权的流程,对流程中的各个组件的作用掌握,最终能够自定义流程中的各个组件达到我们想要的功能。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值