源码系列 — Spring Security之过滤器篇

源码系列 — Spring Security之过滤器篇,第1张

Spring Security之过滤器篇
  • SecurityFilterChain
  • FilterChainProxy
  • 内置SecurityFilter
    • ChannelProcessingFilter
    • WebAsyncManagerIntegrationFilter
    • SecurityContextPersistenceFilter
    • HeaderWriterFilter
    • CorsFilter
    • CsrfFilter
    • LogoutFilter
    • UsernamePasswordAuthenticationFilter
    • ConcurrentSessionFilter
    • BearerTokenAuthenticationFilter
    • BasicAuthenticationFilter
    • RequestCacheAwareFilter
    • RememberMeAuthenticationFilter
    • AnonymousAuthenticationFilter
    • SessionManagementFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor

SecurityFilterChain

讲过滤器[Security Filter]之前,首先要提到SecurityFilterChain

public interface SecurityFilterChain {
	boolean matches(HttpServletRequest request);
	List<Filter> getFilters();
}

SecurityFilterChain包含了两个方法。

  • matches(HttpServletRequest request):主要是提供给FilterChainProxy进行请求的路由。
  • List getFilters(): 提供SecurityFilterChain中的Security Filter
FilterChainProxy

FilterChainProxy继承于GenericFilterBean,是spring容器中的一个过滤器。

它主要做了两个工作:

  • 内置了一个防火强HttpFirewall。对ServletRequest中的HttpMethodContextPathHostnameuri等做了限制。具体内容可以查看StrictHttpFirewall::getFirewalledRequest
  • 执行内部的SecurityFilterChain,直到SecurityFilterChain中的Security Filter执行完毕后,继续执行Servlet Container中的剩余的Filter。详见FilterChainProxy.VirtualFilterChain::doFilter
		/**
		 * additionalFilters 表示路由选中的SecurityFilterChain。
		 * currentPosition 表示当前正在执行的SecurityFilterChain中的SecurityFilter位置标记。
		 * this.size 表示整条SecurityFilterChain中SecurityFilter的长度
		 */
		public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
		    //如果SecurityFilterChain中的最后一个SecurityFilter执行完成,则继续执行原Servlet容器中的过滤器 
			if (this.currentPosition == this.size) {
				if (logger.isDebugEnabled()) {
					logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
				}
				// Deactivate path stripping as we exit the security filter chain
				this.firewalledRequest.reset();
				this.originalChain.doFilter(request, response);
				return;
			}
			//SecurityFilterChain标识位移动到下个
			this.currentPosition++;
			//获取SecurityFilterChain中的下一个Security Filter
			Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
						this.currentPosition, this.size));
			}
			//执行获取的这个Security Filter
			nextFilter.doFilter(request, response, this);
		}
内置SecurityFilter ChannelProcessingFilter

ChannelProcessingFilter主要负责检测当前请求的安全通道secure channel是否符合配置要求。

主要有三种配置:

  • REQUIRES_INSECURE_CHANNEL: 当前请求没有使用了安全通道,例如https请求。
  • REQUIRES_SECURE_CHANNEL: 当前请求使用了安全通道,例如http请求。
  • ANY_CHANNEL: 没有限制。

配置用法举例:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        		//访问/admin-api,需要以https开头的方式访问
                .antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
                //访问/actuator,不受安全通道限制
                .antMatchers("/actuator").access("ANY_CHANNEL");

    }
WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter主要是用于集成WebAsyncManager,支持在异步线程中管理SecurityContext

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要实现了对SecurityContext的在Session中的持久化和存取功能。

/**
* this.repo 是HttpSessionSecurityContextRepository默认实现
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		/**省略部分代码.... */
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		//HttpSessionSecurityContextRepository 从Session中获取SecurityContext
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
			//把SecurityContext注入到SecurityContextHolder,SecurityContextHolder中三种SecurityContextHolderStrategy策略,详见下文。
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			/**省略部分代码.... */
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			//删除线程中的SecurityContext
			SecurityContextHolder.clearContext();
			//持久化到SecurityContextRepository
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			/**省略部分代码.... */
		}
	}

默认提供了3种对SecurityContext线程持有策略,可以通过设置系统参数spring.security.strategy来实现切换:

  • MODE_THREADLOCAL: 只能获取当前线程的SecurityContext
  • MODE_INHERITABLETHREADLOCAL: 能够获取父线程和当前线程的SecurityContext
  • MODE_GLOBAL: 能够获全局应用的SecurityContext
HeaderWriterFilter

HeaderWriterFilter通过一系列HeaderWriter,往HttpServletResponse的Http Header中写入值。可以通过设置shouldWriteHeadersEagerly,来控制前置写入还是后置写入。

CorsFilter

CorsFilter通过配置CorsConfiguration,设置CORS Http Header。

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
			FilterChain filterChain) throws ServletException, IOException {
		//从CorsConfigurationSource中获取CorsConfiguration
		CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
		//CorsProcessor通过CorsConfiguration对Response进行设置。
		boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
		//如果处理不成功或者当前请求是一个CORS pre-flight,则阻止当前请求继续执行。
		if (!isValid || CorsUtils.isPreFlightRequest(request)) {
			return;
		}
		filterChain.doFilter(request, response);
	}
CsrfFilter

CsrfFilter用于防止CSRF攻击。

CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事

Spring Security 防止CSRF攻击的方法:

  • 在浏览器向服务器发起请求时,服务器生成一个 CSRF Token。CSRF Token 其实就是服务器生成的字符串,然后将该字符串植入到返回的页面中。
  • 在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法获取到 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。
@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);
		//从Token库中获取CsrfToken(支持CsrfToke持久化在cookie、session或自定义库的中)
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);
		if (missingToken) {
			//第一次请求需要生成一个CsrfToken,并持久化到Token库。
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Did not protect against CSRF since request did not match "
						+ this.requireCsrfProtectionMatcher);
			}
			filterChain.doFilter(request, response);
			return;
		}
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			this.logger.debug(
					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
			AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
		filterChain.doFilter(request, response);
	}

配置用法举例:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        		//访问/admin-api,需要以https开头的方式访问
                .antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
                //访问/actuator,不受安全通道限制
                .antMatchers("/actuator").access("ANY_CHANNEL");

    }
LogoutFilter

CsrfFilter处理登出请求。

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		//匹配是否登出请求
		if (requiresLogout(request, response)) {
			//获取登录认证
			Authentication auth = SecurityContextHolder.getContext().getAuthentication();
			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Logging out [%s]", auth));
			}
			//LogoutHandler处理登出 *** 作
			this.handler.logout(request, response, auth);
			//LogoutSuccessHandler处理成功登出后的 *** 作
			this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
			return;
		}
		chain.doFilter(request, response);
	}

配置用法举例:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       			http.authorizeRequests(authorizeRequests ->
	   				authorizeRequests.antMatchers("/**").hasRole("USER"))
	   			.formLogin(withDefaults())
	   			// sample logout customization
	   			.logout(logout ->
	   				//删除Cookie 
	   				logout.deleteCookies("remove")
	   					//使session失效
	   					.invalidateHttpSession(false)
	   					//登出地址
	   					.logoutUrl("/custom-logout")
	   					//登出后跳转地址
	   					.logoutSuccessUrl("/logout-success")
	   			);

    }
UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 通过用户名密码进行认证。

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		//开启postOnly开关,默认只支持POST方式请求
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		
		//获取HttpServletRequest中的username和password,只支持表单模式提交
		String username = obtainUsername(request);
		String password = obtainPassword(request);

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

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

		username = username.trim();

		//通过用户名和密码,组装成UsernamePasswordAuthenticationToken
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		// 支持子类向UsernamePasswordAuthenticationToken注入更多属性
		setDetails(request, authRequest);
		
		//进行身份认证,获取认证结果
		return this.getAuthenticationManager().authenticate(authRequest);
	}
ConcurrentSessionFilter

ConcurrentSessionFilter主要负责对SessionInformation的维护工作。

  • 正常情况下,对Request的所对应的SessionInformation进行续期。
  • 如果检测到SessionInformation已过期,则根据过期策略进行过期处理。

ConcurrentSessionFilter通常配合着SessionManagementFilter一起使用,可以实现同一用户在不同客户端的并发登录限制。

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

		HttpSession session = request.getSession(false);

		if (session != null) {
			//从session库中获取SessionInformation
			SessionInformation info = sessionRegistry.getSessionInformation(session
					.getId());

			if (info != null) {
				//SessionInformation存在但已过期的情况下
				if (info.isExpired()) {
					// Expired - abort processing
					if (logger.isDebugEnabled()) {
						logger.debug("Requested session ID "
								+ request.getRequestedSessionId() + " has expired.");
					}
					//进行登出处理
					//主要由SecurityContextLogoutHandler负责处理清空Session、Authentication、SecurityContext
					doLogout(request, response);
					//通过ExpiredStrategy实现后续处理。比如,重定向跳转or返回提示信息
					this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
					return;
				}
				else {
					//正常情况下,更新SessionInformation中的最后请求时间
					sessionRegistry.refreshLastRequest(info.getSessionId());
				}
			}
		}

		chain.doFilter(request, response);
	}
BearerTokenAuthenticationFilter

BearerTokenAuthenticationFilter通过Bearer Token进行认证。

	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		final boolean debug = this.logger.isDebugEnabled();

		String token;

		try {
			//从HttpServletRequest中解析出Token
			token = this.bearerTokenResolver.resolve(request);
		} catch ( OAuth2AuthenticationException invalid ) {
			//组装一个bearer token error response返回。详见BearerTokenAuthenticationEntryPoint::commence
			this.authenticationEntryPoint.commence(request, response, invalid);
			return;
		}

		//没有解析到Token,直接进入下个过滤器
		if (token == null) {
			filterChain.doFilter(request, response);
			return;
		}

		//组装好BearerTokenAuthenticationToken,并注入一些额外属性
		BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
		authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

		try {
			//从Request解析生成AuthenticationManager,并进行认证
			AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
			Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);

			//生成新的SecurityContext,并注入Authentication。并把SecurityContext注入到SecurityContextHolder。
			SecurityContext context = SecurityContextHolder.createEmptyContext();
			context.setAuthentication(authenticationResult);
			SecurityContextHolder.setContext(context);

			filterChain.doFilter(request, response);
		} catch (AuthenticationException failed) {
			//清空SecurityContext
			SecurityContextHolder.clearContext();

			if (debug) {
				this.logger.debug("Authentication request for failed!", failed);
			}
			//对认证失败清楚进行处理
			this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
		}
	}
BasicAuthenticationFilter

BasicAuthenticationFilter类似于用户名密码模式的认证,区别在于用户名密码信息不是用表单提交,而是通过Base64编码存在Http Header中。例如: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

RequestCacheAwareFilter

RequestCacheAwareFilter用于获取记录在session中的包装过的HttpServletReqeust,传给下一个过滤器。如果没有,则继续把当前HttpServletRequest传下去。可以在登录后跳转到原页面的场景中用到。

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
				(HttpServletRequest) request, (HttpServletResponse) response);

		chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
				response);
	}
RememberMeAuthenticationFilter

RememberMeAuthenticationFilter主要实现了在之前的登录中选中’记住我’的选项后,本次就无需再做登录 *** 作,可直接进入系统。

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

		//未登录状态
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			//获取RememberMeAuthenticationToken
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);

			if (rememberMeAuth != null) {
				try {
					//对RememberMeAuthenticationToken进行认证
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					// 记录认证后的Authentication
					SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
					onSuccessfulAuthentication(request, response, rememberMeAuth);

					/**省略部分代码.... */

				}
				catch (AuthenticationException authenticationException) {
					/**省略部分代码.... */

					//登录失败处理,主要做的是清空Cookie
					rememberMeServices.loginFail(request, response);
					onUnsuccessfulAuthentication(request, response,
							authenticationException);
				}
			}

			chain.doFilter(request, response);
		}
		else {
			/**省略部分代码.... */

			chain.doFilter(request, response);
		}
	}

再看下RememberMeAuthenticationFilter::doFilter中的核心逻辑方法RememberMeServices::autoLogin

@Override
	public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
		//提取Cookie中的信息
		String rememberMeCookie = extractRememberMeCookie(request);

		/**省略部分代码.... */

		UserDetails user = null;

		try {
			//解析信息,获取一个UserDetails
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			user = processAutoLoginCookie(cookieTokens, request, response);
			userDetailsChecker.check(user);

			logger.debug("Remember-me cookie accepted");

			//通过UserDetails生成一个RememberMeAuthenticationToken
			return createSuccessfulAuthentication(request, user);
		}
		catch (CookieTheftException cte) {
			/**省略部分代码.... */
		}
		
		cancelCookie(request, response);
		return null;
	}
AnonymousAuthenticationFilter

AnonymousAuthenticationFilter对未认证的请求,会封装成一个匿名AnonymousAuthenticationToken

SessionManagementFilter

SessionManagementFilter主要是对Session相关的管理,例如,防止会话固定攻击Session Fixation Attack,支持多端登录踢下线等功能。

ExceptionTranslationFilter

ExceptionTranslationFilter 用于处理 AccessDeniedExceptionAuthenticationException的异常。

private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			/**省略部分代码.... */

			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				/**省略部分代码.... */

				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				/**省略部分代码.... */

				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}
FilterSecurityInterceptor

FilterSecurityInterceptor主要负责授权Authorization任务。

public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			/**省略部分代码.... */
			//主要是通过AccessDecisionManager::decide 来进行授权工作。
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				//把Token中的SecurityContext去刷新SecurityContextHolder中的SecurityContext
				super.finallyInvocation(token);
			}
			//用AfterInvocationManager::decide方法进行后置授权工作。
			super.afterInvocation(token, null);
		}
	}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/917071.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存