基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?

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

1、前言

  前面我们已经完整的把应用A从访问、授权登录、获取code、换取accessToken和实现应用A的正常访问的整个过程分析了一遍,那么如果已经成功访问了应用A后,再直接跳转访问应用B会发生什么呢?让我们一起从代码中找到答案吧!

2、浏览器视角

2.1、访问应用B

  当已经成功访问应用A后,再访问应用B(http://localhost:8083/index)时,会重定向到http://localhost:8083/login地址,如下所示:
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第1张图片

2.2、重定向到授权服务器

  当访问前面重定向到的http://localhost:8083/login地址时,又会被重定向到授权服务器的授权地址,如下所示:
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第2张图片

2.3、重新 重定向到应用B的登录

  当访问授权服务器授权接口后,又重定向到了应用B的登录界面,不过这个时候,带了code参数。
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第3张图片

2.4、重新 重定向到应用B的访问页面

  当重定向到带code参数的应用B的登录地址后,会再重定向到应用B的访问页面。这个过程,用户不需要再输入用户名和密码进行登录了。
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第4张图片

  至此,我们从浏览器视角,分析了请求应用B时,请求链接的重定向过程。下面我们开始从代码层面分析,为什么会这样重定向。

3、重定向到应用B的登录地址

  这一步的实现,和《未认证的请求是如何重定向到登录地址的》中是完全一样的。首先,我们进入FilterSecurityInterceptor过滤器的doFilter()方法,在doFilter()方法中又调用了invoke()方法,而在invoke()方法中,又调用了父类AbstractSecurityInterceptor的beforeInvocation()方法,来获取请求需要的Token值,因为第一次访问,还没有进行认证,所以会抛出认证异常(AccessDeniedException ),抛出了AccessDeniedException 异常,这个异常就会被ExceptionTranslationFilter过滤器捕获,然后,经过异常处理,最终会跳转到应用B的登录界面。 前面的博文中已经详细分析了,这里不再重复。

4、重定向到授权服务器的授权地址

  这一步的实现,和《应用A是如何重定向到授权服务器的授权地址呢?》中的逻辑是一样的。首先,在OAuth2ClientAuthenticationProcessingFilter中,会进行单点登录的认证,即向授权服务器发送登录验证请求,因为没有携带accessToken或code,这个时候就会抛出异常,然后被前面的OAuth2ClientContextFilter过滤器拦截到,然后在OAuth2ClientContextFilter异常处理逻辑中,实现认证授权地址的重定向。

5、授权服务器如何进行授权

  在这一步中,和《授权服务器是如何实现授权的呢?》类似,不过因为这次我们没有再跳转到统一登录界面,而是直接通过重定向完成了授权,为什么不需要重新登录了呢?我们下面跟着代码分析一下。

  在《授权服务器是如何实现授权的呢?》中,之所以会跳转到登录界面是因为应用A进行授权请求(/oauth/authorize)时,其中的principal参数为空,即没有认证信息,这个时候就会抛出InsufficientAuthenticationException异常,然后被SpringSecurity过滤器链中的异常过滤器拦截,并重定向到授权服务器的登录地址。而这里之所以没有重定向到登录界面,其实就是因为这次的认证授权请求,携带了principal信息,我们下面分析应用B发送认证授权请求,是如何携带了用户信息的。

  首先,当重定向到授权服务器授权地址的时候,浏览器会带有对应的缓存信息,如下所示:
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第5张图片
  那么,在授权服务器,是不是根据这些缓存信息,我们可以查询对应的用户信息呢,答案是肯定的,该过程就是在授权服务器的SecurityContextPersistenceFilter过滤器中完成的。我们下面开始分析SecurityContextPersistenceFilter是如何获取用户信息的。

  在请求到达授权请求(/oauth/authorize)方法之前,会先经过SpringSecurity的过滤器,其中SecurityContextPersistenceFilter过滤器就是用来初始化上下文信息的。

  我们先来分析其doFilter()方法,代码如下:

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

	// 省略 ……

	HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
			response);
	// 根据请求,获取上下文信息,这里是逻辑的核心。
	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

	try {
		SecurityContextHolder.setContext(contextBeforeChainExecution);

		chain.doFilter(holder.getRequest(), holder.getResponse());

	}
	finally {
		// 省略 ……
	}
}

  正如代码中注释,repo.loadContext()方法是获取请求上下文的核心,我们继续分析该方法,该方法在HttpSessionSecurityContextRepository类中定义,具体实现如下:

public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
	HttpServletRequest request = requestResponseHolder.getRequest();
	HttpServletResponse response = requestResponseHolder.getResponse();
	HttpSession httpSession = request.getSession(false);

	SecurityContext context = readSecurityContextFromSession(httpSession);

	if (context == null) {
		//省略 debug ……
		context = generateNewContext();
	}
	SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
			response, request, httpSession != null, context);
	requestResponseHolder.setResponse(wrappedResponse);

	requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
			request, wrappedResponse));

	return context;
}

  其中,又通过readSecurityContextFromSession()方法读取SecurityContext 信息,实现如下:

private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
	final boolean debug = logger.isDebugEnabled();
	if (httpSession == null) {
		if (debug) {
			logger.debug("No HttpSession currently exists");
		}
		return null;
	}
	Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
	if (contextFromSession == null) {
		//省略 debug ……
		return null;
	}
	// We now have the security context object from the session.
	if (!(contextFromSession instanceof SecurityContext)) {
		//省略 debug ……
		return null;
	}
	//省略 debug ……
	
	// Everything OK. The only non-null return from this method.
	return (SecurityContext) contextFromSession;
}

  在readSecurityContextFromSession()方法中,首先尝试从httpSession中获取对应上下文,对应的key=SPRING_SECURITY_CONTEXT,然后判断该上下文是否是SecurityContext类型,不是的话也直接返回null,最后如果都符合条件,就强转成SecurityContext类型并返回。返回值,包括了principal参数值,所以在授权请求(/oauth/authorize)方法中,我们就可以直接从上下文中获取,而不会在跳转到登录界面让用户进行登录。
基于SpringSecurity OAuth2实现单点登录——登录访问应用A后再访问应用B会发生什么呢?_第6张图片

6、写在最后

  这篇博文中,我们分析了登录访问应用A后再访问应用B请求经过的授权过程。至此,我们基于SpringSecurity OAuth2实现单点登录的分析就全部完成了,当然在其中还涉及到了一些问题没有深入分析(比如该博文中获取的key=SPRING_SECURITY_CONTEXT的上下文是何时存储到Session的等),后续在使用SpringSecurity的过程中,我们再继续学习和分享。

你可能感兴趣的:(Spring,Cloud,SpringSecurity,OAuth,单点登录)