基于SpringSecurity OAuth2实现单点登录——未认证的请求是如何重定向到登录地址的?(SpringSecurity的认证流程)

导读:本篇文章讲解 基于SpringSecurity OAuth2实现单点登录——未认证的请求是如何重定向到登录地址的?(SpringSecurity的认证流程),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》

1、前言

  在上一篇《入门示例和流程分析》的流程分析过程中,当第一次(未认证的情况下)访问应用A(http://localhost:8082/index)时,会重定向到应用A的登录http://localhost:8082/login地址(Get请求),从浏览器这个视角我们看到的是这样的情况,那么在应用A的服务端又经历了什么呢?我们通过代码进行分析。

2、SpringSecurity过滤器链

  这节分析的问题,其实就是SpringSecurity关于认证过程的逻辑。SpringSecurity实现认证逻辑,就是通过SpringSecurity 过滤器链实现的,我们先了解一下SpringSecurity过滤器链中的核心类FilterChainProxy。

2.1、FilterChainProxy

在这里插入图片描述
  在SpringSecurity中,SpringSecurity 的过滤器并不是直接内嵌到Servlet Filter中的,而是通过FilterChainProxy来统一管理的,即所有的Spring Security Filter的执行,都在FilterChainProxy中进行管理的,所以我们选择从FilterChainProxy类入手进行分析。

  为了实现上述描述的功能,SpringSecurity 过滤器由FilterChainProxy统一管理,然后在在内部定义了一个VirtualFilterChain内部类,用于表示SpringScurity内部的过滤器链,其中doFilter()方法用于执行过滤器链中的过滤器。如下所示:

//FilterChainProxy#VirtualFilterChain,省略了Debug相关信息
@Override
public void doFilter(ServletRequest request, ServletResponse response)
		throws IOException, ServletException {
	if (currentPosition == size) {
		// Deactivate path stripping as we exit the security filter chain
		this.firewalledRequest.reset();
		//执行Web中的过滤器
		originalChain.doFilter(request, response);
	}
	else {//执行SpringSecurity过滤器链中的过滤器
		currentPosition++;
		//additionalFilters中定义了SpringSecurity过滤器链中的所有过滤器
		Filter nextFilter = additionalFilters.get(currentPosition - 1);
		nextFilter.doFilter(request, response, this);
	}
}

  我们通过断点,可以查看additionalFilters变量中的过滤器集合,即SpringSecurity过滤器链中所有过滤器,下面是应用A中的SpringSecurity 过滤器,如下所示:
nextFilter.doFilter

3、FilterSecurityInterceptor过滤器

  通过Debug执行代码,我们发现,在执行完FilterSecurityInterceptor过滤器时,前端页面重定向到了应用A的登录http://localhost:8082/login地址(Get请求)。在执行过滤器FilterSecurityInterceptor过滤器时,发生了什么呢?我们通过Debug方式,进行逐步的分析。

  首先,我们进入FilterSecurityInterceptor过滤器的doFilter()方法,在doFilter()方法中又调用了invoke()方法,而在invoke()方法中,又调用了父类AbstractSecurityInterceptor的beforeInvocation()方法,如下所示:

//FilterSecurityInterceptor.java

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) {
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	}else {
		// first time this request being called, so perform security checking
		if (fi.getRequest() != null && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		//访问应用A的地址,首先会经过beforeInvocation()方法获取请求中的Token
		InterceptorStatusToken token = super.beforeInvocation(fi);
		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}
}

  在上述代码中,调用父类AbstractSecurityInterceptor的beforeInvocation()方法,来获取请求需要的Token值,因为第一次访问,还没有进行认证,所以会抛出认证异常(AccessDeniedException ),如下所示:

//AbstractSecurityInterceptor.java
protected InterceptorStatusToken beforeInvocation(Object object) {
	
	// …… 省略

	Authentication authenticated = authenticateIfRequired();

	// Attempt authorization
	try {
		//用于判断当前请求是否有权限进行访问,如果没有权限就会抛出AccessDeniedException 异常。
		this.accessDecisionManager.decide(authenticated, object, attributes);
	}
	catch (AccessDeniedException accessDeniedException) {
		publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
				accessDeniedException));

		throw accessDeniedException;
	}
	
	// …… 省略

}

  在执行上面代码时,抛出了AccessDeniedException 异常,这个异常就会被ExceptionTranslationFilter过滤器捕获,如下所示:

//ExceptionTranslationFilter.java
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
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		RuntimeException ase = (AuthenticationException) throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);
		if (ase == null) {
			ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
					AccessDeniedException.class, causeChain);
		}
		if (ase != null) {
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, ase);
		}else {
			if (ex instanceof ServletException) {
				throw (ServletException) ex;
			}
			else if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}
			throw new RuntimeException(ex);
		}
	}
}

  当出现AccessDeniedException 异常时,会被ExceptionTranslationFilter过滤器的doFilter()方法中第二个catch 代码块进行拦截,然后交由handleSpringSecurityException()方法进行异常的处理,具体如下:

//ExceptionTranslationFilter.java
private void handleSpringSecurityException(HttpServletRequest request,
	HttpServletResponse response, FilterChain chain, RuntimeException exception)
		throws IOException, ServletException {
	if (exception instanceof AuthenticationException) {
		//省略 debug……
		sendStartAuthentication(request, response, chain,
				(AuthenticationException) exception);
	}else if (exception instanceof AccessDeniedException) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
			//省略 debug……
			sendStartAuthentication(
					request,
					response,
					chain,
					new InsufficientAuthenticationException(
						messages.getMessage(
							"ExceptionTranslationFilter.insufficientAuthentication",
							"Full authentication is required to access this resource")));
		}else {
			//省略 debug……
			accessDeniedHandler.handle(request, response,
					(AccessDeniedException) exception);
		}
	}
}

  在handleSpringSecurityException()方法中,根据AuthenticationException或AccessDeniedException异常类型,进行下一步执行,因为我们上一步抛出的是AccessDeniedException异常,所以会执行其中sendStartAuthentication()的方法(其实两类异常都是执行这个方法,只不过参数不一样而已)。sendStartAuthentication()方法的实现如下:

//ExceptionTranslationFilter
protected void sendStartAuthentication(HttpServletRequest request,
	HttpServletResponse response, FilterChain chain,
		AuthenticationException reason) throws ServletException, IOException {
	SecurityContextHolder.getContext().setAuthentication(null);
	requestCache.saveRequest(request, response);
	logger.debug("Calling Authentication entry point.");
	authenticationEntryPoint.commence(request, response, reason);
}

  在sendStartAuthentication()方法中, 又调用了authenticationEntryPoint的commence()方法,这里的authenticationEntryPoint默认的是LoginUrlAuthenticationEntryPoint实例,最终的页面跳转也是在commence()方法中,其中又调用redirectStrategy的sendRedirect()方法来完成最终的重定向。其中LoginUrlAuthenticationEntryPoint的commence()方法定义如下:

//LoginUrlAuthenticationEntryPoint.java
public void commence(HttpServletRequest request, HttpServletResponse response,
	AuthenticationException authException) throws IOException, ServletException {
	
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			if (logger.isDebugEnabled()) {
				logger.debug("Server side forward to: " + loginForm);
			}
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}else {
		//构建重定向地址
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	//这里redirectUrl对应的就是http://localhost:8082/login地址
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

  至此,通过执行redirectStrategy.sendRedirect()方法,就实现了重定向到应用A的登录地址了。

4、写在最后

  这一节我们主要分析了未认证的请求是如何重定向到登录地址(当前应用)的,下一节我们开始分析授权服务器是如何进行授权的,敬请期待!!!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68739.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!