Анализ исходного кода Spring Security 2: процесс авторизации Spring Security

Spring Безопасность CDN редкоземельный

предисловие

Эта статья является продолжением предыдущей главыАнализ исходного кода Spring Security 1: процесс аутентификации Spring Securityдальнейший анализSpring SecurityКак достигается авторизация при входе в систему с использованием имени пользователя и пароля?

Диаграмма классов

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c4a662d2~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c4a662d2~tplv-t2oaga2asx-image.image

процесс отладки

Начните с отладкиGitHub.com/Longfeizhen…элемент, ввод браузераhttp://localhost:8080/persons, имя пользователя не является обязательным, а пароль — 123456;

Анализ исходного кода

Как показано на рисунке, он показываетfiltersДля связанного процесса вызова автор выделил несколько фильтров, которые он считает важными.

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c4868b1d~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c4868b1d~tplv-t2oaga2asx-image.image

Порядок выполнения виден из рисунка. Рассмотрим логику обработки нескольких фильтров, которые автор считает более важными.UsernamePasswordAuthenticationFilter,AnonymousAuthenticationFilter,ExceptionTranslationFilter,FilterSecurityInterceptorИ соответствующий поток обработки выглядит следующим образом;

UsernamePasswordAuthenticationFilter

Весь процесс вызова состоит в том, чтобы сначала вызвать метод родительского класса AbstractAuthenticationProcessingFilter.doFilter(), а затем выполнить метод UsernamePasswordAuthenticationFilter.attemptAuthentication() для проверки;

AbstractAuthenticationProcessingFilter

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

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		#1.判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

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

		Authentication authResult;

		try {
			#2.抽象方法由子类UsernamePasswordAuthenticationFilter实现
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			#2.认证成功后,处理一些与session相关的方法 
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			#3.认证失败后的的一些操作
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		#3. 认证成功后的相关回调方法 主要将当前的认证放到SecurityContextHolder中
		successfulAuthentication(request, response, chain, authResult);
	}

Последовательность выполнения всей программы выглядит следующим образом:

  1. Определить, может ли фильтр обработать текущий запрос, если нет, передать его следующему фильтру.
  2. вызов абстрактного методаattemptAuthenticationДля проверки этот метод используется подклассамиUsernamePasswordAuthenticationFilterвыполнить
  3. После успешной аутентификации вызовите некоторые методы, связанные с сеансом;
  4. После успешной аутентификации, соответствующий метод обратного вызова после успешной аутентификации, после успешной аутентификации, соответствующий метод обратного вызова после успешной аутентификации;
protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
1. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中;
2. 将当前认证成功的 Authentication 放置到 SecurityContextHolder 中;
3. 调用其它可扩展的 handlers 继续处理该认证成功以后的回调事件;(实现`AuthenticationSuccessHandler`接口即可)

UsernamePasswordAuthenticationFilter

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		#1.判断请求的方法必须为POST请求
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		#2.从request中获取username和password
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

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

		username = username.trim();
		#3.构建UsernamePasswordAuthenticationToken(两个参数的构造方法setAuthenticated(false))
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		#4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历所有的AuthenticationProvider认证)
		return this.getAuthenticationManager().authenticate(authRequest);
	}
  1. Метод аутентификации запроса должен бытьPOST
  2. Получить логин и пароль из запроса
  3. упаковкаAuthenticaitonкласс реализацииUsernamePasswordAuthenticationToken, (UsernamePasswordAuthenticationTokenВызов конструктора с двумя параметрами setAuthenticated(false))
  4. перечислитьAuthenticationManagerизauthenticateметод проверки см.ProviderManagerчасть;

AnonymousAuthenticationFilter

Как видно из схемы последовательности выполнения фильтра на рисунке вышеAnonymousAuthenticationFilterфильтр находится вUsernamePasswordAuthenticationFilterПосле ожидания фильтра, если ни один из предыдущих фильтров не был успешно аутентифицирован,Spring Securityэлектрический токSecurityContextHolderдобавитьAuthenticaitonАнонимный класс реализацииAnonymousAuthenticationToken;

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		#1.如果前面的过滤器都没认证通过,则SecurityContextHolder中Authentication为空
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			#2.为当前的SecurityContextHolder中添加一个匿名的AnonymousAuthenticationToken
			SecurityContextHolder.getContext().setAuthentication(
					createAuthentication((HttpServletRequest) req));

			if (logger.isDebugEnabled()) {
				logger.debug("Populated SecurityContextHolder with anonymous token: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}

		chain.doFilter(req, res);
	}

	#3.创建匿名的AnonymousAuthenticationToken
	protected Authentication createAuthentication(HttpServletRequest request) {
		AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
				principal, authorities);
		auth.setDetails(authenticationDetailsSource.buildDetails(request));

		return auth;
	}
	
		/**
	 * Creates a filter with a principal named "anonymousUser" and the single authority
	 * "ROLE_ANONYMOUS".
	 *
	 * @param key the key to identify tokens created by this filter
	 */
	 ##.创建一个用户名为anonymousUser 授权为ROLE_ANONYMOUS
	public AnonymousAuthenticationFilter(String key) {
		this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	}
  1. судитьSecurityContextHolder中Authenticationпусто;
  2. Если воздух течетSecurityContextHolderдобавить анонимAnonymousAuthenticationToken(с именем пользователя анонимный пользовательAnonymousAuthenticationToken)

ExceptionTranslationFilter

ExceptionTranslationFilterФильтр обработки исключений, который используется для обработки исключений, возникающих во время системной аутентификации и авторизации (то есть следующий фильтрFilterSecurityInterceptor), в основном обработкаAuthenticationExceptionиAccessDeniedException.

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			#.判断是不是AuthenticationException
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				#. 判断是不是AccessDeniedException
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

FilterSecurityInterceptor

Этот фильтр является последним фильтром в цепочке фильтров аутентификации и авторизации, после этого фильтра выполняется реальный запрос./personsСлужить

public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			#1. before invocation重要
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				#2. 可以理解开始请求真正的 /persons 服务
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}
			#3. after Invocation
			super.afterInvocation(token, null);
		}
	}
  1. перед вызовом важно
  2. Запросить услугу реального / человека
  3. after Invocation

Из трех частей наиболее важной является № 1, которая вызываетAccessDecisionManagerЧтобы проверить, есть ли у текущего аутентифицированного пользователя разрешение на доступ к ресурсу;

before invocation: AccessDecisionManager

protected InterceptorStatusToken beforeInvocation(Object object) {
		...

		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		...
		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
		try {
			#1.重点
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));

			throw accessDeniedException;
		}

		...
	}

authenticatedВ настоящее время сертифицированоAuthentication,ТакobjectиattributesЧто это?

Что такое атрибуты и объекты?

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

отладка

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0b928457e~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0b928457e~tplv-t2oaga2asx-image.image

мы обнаруживаемobjectдля текущего запросаurl:/persons, ТакgetAttributesМетод заключается в использовании текущего пути к ресурсу доступа к匹配Правила соответствия мы определяем сами.

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//使用表单登录,不再使用默认httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果请求的URL需要认证则跳转的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//处理表单中自定义的登录URL
                .and()
                .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
                SecurityConstants.DEFAULT_REGISTER_URL,
                "/**/*.js",
                "/**/*.css",
                "/**/*.jpg",
                "/**/*.png",
                "/**/*.woff2")
                .permitAll()//以上的请求都不需要认证
                .anyRequest()//剩下的请求
                .authenticated()//都需要认证
                .and()
                .csrf().disable()//关闭csrd拦截
        ;
    }

0-7возвращениеpermitALLкоторый не требует аутентификации,8соответствоватьanyRequestвозвращениеauthenticatedI.e. текущий запрос требует аутентификации;

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0bbef6204~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0bbef6204~tplv-t2oaga2asx-image.image

Вы можете увидеть текущийauthenticatedбыть анонимнымAnonymousAuthenticationИмя пользователяanonymousUser

Как осуществляется авторизация AccessDecisionManager?

Spring SecurityИспользовать по умолчаниюAffirmativeBasedвыполнитьAccessDecisionManagerизdecideспособ реализации авторизации

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		#1.调用AccessDecisionVoter 进行vote(投票)
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			#1.1只要有voter投票为ACCESS_GRANTED,则通过 直接返回
			case AccessDecisionVoter.ACCESS_GRANTED://1
				return;
			@#1.2只要有voter投票为ACCESS_DENIED,则记录一下
			case AccessDecisionVoter.ACCESS_DENIED://-1
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
		#2.如果有两个及以上AccessDecisionVoter(姑且称之为投票者吧)都投ACCESS_DENIED,则直接就不通过了
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
  1. Позвоните AccessDecisionVoter, чтобы проголосовать (проголосовать)
  2. Пока есть голосование за проход (ACCESS_GRANTED), он напрямую считается пройденным.
  3. Если не проголосовал, тоdeny++, окончательный приговорif(deny>0бросатьAccessDeniedException(несанкционированный)

WebExpressionVoter.vote()

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

в это местоauthenticationактуальная информация о пользователе,flТекущий путь к ресурсу иattributesРешение о текущем пути к ресурсу (т. е. требуется ли аутентификация). Остальное определить роль текущего пользователяAuthentication.authoritesРешение о разрешении доступа для доступа к текущему ресурсуfi.

Временная диаграмма

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c490ea00~tplv-t2oaga2asx-image.image
https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/1/5/160c51a0c490ea00~tplv-t2oaga2asx-image.image