文章目录

  • 1. SecurityContextPersistenceFilter
  • 2. UsernamePasswordAuthenticationFilter
  • 3. ExceptionTranslationFilter
  • 4. FilterSecurityInterceptor



SpringSecurity的核心是一条过滤器链(

spring authorization server认证流程_ide

1. SecurityContextPersistenceFilter

在用户请求成功认证后,会将认证成功后的上下文信息SecurityContext通过该类存储在HttpSession中。当用户下次进行请求时,可以直接从通过该类从HttpSession中获取已经认证后的上下文信息SecurityContext,从而避免重复认证。

先来看下该类的 doFilter() 方法

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

	// 确保每个请求都要经过过滤器链认证
	if (request.getAttribute(FILTER_APPLIED) != null) {
		// ensure that filter is only applied once per request
		chain.doFilter(request, response);
		return;
	}

	final boolean debug = logger.isDebugEnabled();

	request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

	if (forceEagerSessionCreation) {
		HttpSession session = request.getSession();

		if (debug && session.isNew()) {
			logger.debug("Eagerly created session: " + session.getId());
		}
	}

	HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
			response);
	// 获取Spring Security上下文对象 SecurityContext 
	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

	try {
		SecurityContextHolder.setContext(contextBeforeChainExecution);
		chain.doFilter(holder.getRequest(), holder.getResponse());
	}
	finally {
		SecurityContext contextAfterChainExecution = SecurityContextHolder
				.getContext();
		// Crucial removal of SecurityContextHolder contents - do this before anything
		// else.
		SecurityContextHolder.clearContext();
		// 将 SecurityContext 中的信息保存到 HttpSession 中
		repo.saveContext(contextAfterChainExecution, holder.getRequest(),
				holder.getResponse());
		request.removeAttribute(FILTER_APPLIED);

		if (debug) {
			logger.debug("SecurityContextHolder now cleared, as request processing completed");
		}
	}
}

最终调用的 HttpSessionSecurityContextRepositorysaveContext() 方法

@Override
protected void saveContext(SecurityContext context) {
	final Authentication authentication = context.getAuthentication();
	HttpSession httpSession = request.getSession(false);

	// See SEC-776
	if (authentication == null || trustResolver.isAnonymous(authentication)) {
		if (logger.isDebugEnabled()) {
			logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
		}

		if (httpSession != null && authBeforeExecution != null) {
			// SEC-1587 A non-anonymous context may still be in the session
			// SEC-1735 remove if the contextBeforeExecution was not anonymous
			httpSession.removeAttribute(springSecurityContextKey);
		}
		return;
	}

	if (httpSession == null) {
		httpSession = createNewSessionIfAllowed(context);
	}

	// If HttpSession exists, store current SecurityContext but only if it has
	// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
	if (httpSession != null) {
		// We may have a new session, so check also whether the context attribute
		// is set SEC-1561
		if (contextChanged(context)
				|| httpSession.getAttribute(springSecurityContextKey) == null) {
			// 将 SecurityContext 信息保存到 HttpSession 中
			httpSession.setAttribute(springSecurityContextKey, context);

			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContext '" + context
						+ "' stored to HttpSession: '" + httpSession);
			}
		}
	}
}

2. UsernamePasswordAuthenticationFilter

进行用户名和密码的认证。

UsernamePasswordAuthenticationFilter

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());
		}
		
		String username = obtainUsername(request);
		String password = obtainPassword(request);

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

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

		username = username.trim();

		// 封装用户名和密码
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		setDetails(request, authRequest);

		// 调用 AuthenticationManager 接口的 authenticate()方法进行认证
		// 实际上是调用它的实现类 ProviderManager类中的 authenticate()方法
		return this.getAuthenticationManager().authenticate(authRequest);
	}

主要内容

1.将用户名和密码封装成 UsernamePasswordAuthenticationToken
2.调用 ProviderManager 类中的 authenticate() 方法进行认证

ProviderManager

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();
		
		/*
		 遍历所有的provider
		 SpringSecurity提供了许多AuthenticationProvider的实现类
		 用于处理各种认证
		*/
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

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

			try {
				// 找到处理该认证的provider类并进行认证
				// 这里找到的是 AbstractUserDetailsAuthenticationProvider
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {

			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}
		...
	}

主要内容
1.遍历所有的 AuthenticationProvider 接口的实现类,找到对应的provider类处理认证信息

AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		
		// 从缓存中获取 username并封装成 UserDetails对象(实际上是它的实现类org.springframework.security.core.userdetails.User)
		UserDetails user = this.userCache.getUserFromCache(username);

		// 缓存中获取不到 UserDetails对象
		if (user == null) {
			cacheWasUsed = false;

			try {
				// 调用该方法获取 UserDetails对象,具体实现方法在
				// DaoAuthenticationProvider中
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}
		...
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

主要内容
1.先尝试从缓存中获取 UserDetails对象,UserDetails 接口有唯一实现类User
2.如果没有从缓存中获取到对象,则调用 DaoAuthenticationProvider类中的retrieveUser()获取 UserDetails对象

DaoAuthenticationProvider

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			// 调用 UserDetailsService接口中的 loadUserByUsername()方法获取 UserDetails 对象
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

主要内容
1.调用了 UserDetailsService接口中的loadUserByUsername()方法实现认证

综上所述,我们只要自定义类实现UserDetailsService接口,并重写其中的loadUserByUsername()方法,就可以实现自己的认证逻辑。

再回到AbstractUserDetailsAuthenticationProvider类的authenticate()方法

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		...
		// 创建 UsernamePasswordAuthenticationToken对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// 使用构造器构造 UsernamePasswordAuthenticationToken 对象
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}
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
	}
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
		if (authorities == null) {
			this.authorities = AuthorityUtils.NO_AUTHORITIES;
			return;
		}
		// 进行授权处理
		for (GrantedAuthority a : authorities) {
			if (a == null) {
				throw new IllegalArgumentException(
						"Authorities collection cannot contain any null elements");
			}
		}
		ArrayList<GrantedAuthority> temp = new ArrayList<>(
				authorities.size());
		temp.addAll(authorities);
		this.authorities = Collections.unmodifiableList(temp);
	}

总结:
最后获取到的 UsernamePasswordAuthenticationToken包含了用户名,密码,权限信息

注意:用户一定要被授予相关权限,否则会认证失败,后面的文章我还会提到

3. ExceptionTranslationFilter

用于在过滤器链中抛出AccessDeniedExceptionAuthenticationException异常,抛出的异常由AccessDeniedHandler接口的实现类AccessDeniedHandlerImpl处理

public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException,
			ServletException {
		if (!response.isCommitted()) {
			// 如果定义了异常页面
			if (errorPage != null) {

				request.setAttribute(WebAttributes.ACCESS_DENIED_403,
							accessDeniedException);

				// 设置响应码为403
				response.setStatus(HttpStatus.FORBIDDEN.value());

				// 跳转到错误页面
				RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
				dispatcher.forward(request, response);
			}
			else {
				response.sendError(HttpStatus.FORBIDDEN.value(),
					HttpStatus.FORBIDDEN.getReasonPhrase());
			}
		}
	}

主要内容
1.ExceptionTranslationFilter 用于在过滤器链中抛出AccessDeniedException 和 AuthenticationException异常,并交给 AccessDeniedHandler接口的实现类 AccessDeniedHandlerImpl处理
2.如果自定义了错误页面,AccessDeniedHandlerImpl会跳往该页面并设置响应码为403

4. FilterSecurityInterceptor

用于对http请求进行过滤
(引用了https://www.jb51.net/article/176217.htm)这里的解释

public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //获取当前http请求的地址,比如说“/login”
        if ((fi.getRequest() != null)
                        && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                        && observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }
            // 这里做主要URL比对,将当前URL与securityMetadataSource(我们自己配置)中的URL过滤条件进行比对
            // 首先判断当前URL是permit的还是需要验证的
            // 若需要验证,尝试加载保存在SecurityContext类中的已登录信息
            // 调用 AbstractSecurityInterceptor中的 AccessDecisionManager对象的decide方法
            // 如果对于配置中需要登录才可访问的URL,已经查找到登录信息,则执行下一个Filter
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }
            super.afterInvocation(token, null);
        }
    }

这里大家可能有疑问,用户信息为什么保存在SecurityContext类中,我们再回到UsernamePasswordAuthenticationFilter类中,它是一个Filter类,那么肯定有doFilter()方法对请求进行过滤,该方法继承自它的父类AbstractAuthenticationProcessingFilter

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

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

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

		Authentication authResult;

		try {
			// 该方法在前面具体讲了
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			// 认证失败
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			// 认证失败
			unsuccessfulAuthentication(request, response, failed);
			return;
		}

		// 认证成功,继续沿着Filter链执行
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		// 最终认证成功
		successfulAuthentication(request, response, chain, authResult);
	}

主要内容
1.UsernamePasswordAuthenticationFilter调用了父类AbstractAuthenticationProcessingFilter中的doFilter()方法进行过滤,如果认证失败,则调用unsuccessfulAuthentication()方法,如果认证成功,则调用successfulAuthentication()方法

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

		// 将认证成功后的用户信息保存在SecurityContext类中		
		SecurityContextHolder.getContext().setAuthentication(authResult);
		// 实现记住我功能
		rememberMeServices.loginSuccess(request, response, authResult);

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

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

主要内容
1.在successfulAuthentication()方法中,通过SecurityContextHolder.getContext().setAuthentication(authResult)这个方法将认证成功后的用户信息保存在了SecurityContext类中