1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》
前面我们已经完整的把应用A从访问、授权登录、获取code、换取accessToken和实现应用A的正常访问的整个过程分析了一遍,那么如果已经成功访问了应用A后,再直接跳转访问应用B会发生什么呢?让我们一起从代码中找到答案吧!
当已经成功访问应用A后,再访问应用B(http://localhost:8083/index)时,会重定向到http://localhost:8083/login地址,如下所示:
当访问前面重定向到的http://localhost:8083/login地址时,又会被重定向到授权服务器的授权地址,如下所示:
当访问授权服务器授权接口后,又重定向到了应用B的登录界面,不过这个时候,带了code参数。
当重定向到带code参数的应用B的登录地址后,会再重定向到应用B的访问页面。这个过程,用户不需要再输入用户名和密码进行登录了。
至此,我们从浏览器视角,分析了请求应用B时,请求链接的重定向过程。下面我们开始从代码层面分析,为什么会这样重定向。
这一步的实现,和《未认证的请求是如何重定向到登录地址的》中是完全一样的。首先,我们进入FilterSecurityInterceptor过滤器的doFilter()方法,在doFilter()方法中又调用了invoke()方法,而在invoke()方法中,又调用了父类AbstractSecurityInterceptor的beforeInvocation()方法,来获取请求需要的Token值,因为第一次访问,还没有进行认证,所以会抛出认证异常(AccessDeniedException ),抛出了AccessDeniedException 异常,这个异常就会被ExceptionTranslationFilter过滤器捕获,然后,经过异常处理,最终会跳转到应用B的登录界面。 前面的博文中已经详细分析了,这里不再重复。
这一步的实现,和《应用A是如何重定向到授权服务器的授权地址呢?》中的逻辑是一样的。首先,在OAuth2ClientAuthenticationProcessingFilter中,会进行单点登录的认证,即向授权服务器发送登录验证请求,因为没有携带accessToken或code,这个时候就会抛出异常,然后被前面的OAuth2ClientContextFilter过滤器拦截到,然后在OAuth2ClientContextFilter异常处理逻辑中,实现认证授权地址的重定向。
在这一步中,和《授权服务器是如何实现授权的呢?》类似,不过因为这次我们没有再跳转到统一登录界面,而是直接通过重定向完成了授权,为什么不需要重新登录了呢?我们下面跟着代码分析一下。
在《授权服务器是如何实现授权的呢?》中,之所以会跳转到登录界面是因为应用A进行授权请求(/oauth/authorize)时,其中的principal参数为空,即没有认证信息,这个时候就会抛出InsufficientAuthenticationException异常,然后被SpringSecurity过滤器链中的异常过滤器拦截,并重定向到授权服务器的登录地址。而这里之所以没有重定向到登录界面,其实就是因为这次的认证授权请求,携带了principal信息,我们下面分析应用B发送认证授权请求,是如何携带了用户信息的。
首先,当重定向到授权服务器授权地址的时候,浏览器会带有对应的缓存信息,如下所示:
那么,在授权服务器,是不是根据这些缓存信息,我们可以查询对应的用户信息呢,答案是肯定的,该过程就是在授权服务器的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)方法中,我们就可以直接从上下文中获取,而不会在跳转到登录界面让用户进行登录。
这篇博文中,我们分析了登录访问应用A后再访问应用B请求经过的授权过程。至此,我们基于SpringSecurity OAuth2实现单点登录的分析就全部完成了,当然在其中还涉及到了一些问题没有深入分析(比如该博文中获取的key=SPRING_SECURITY_CONTEXT的上下文是何时存储到Session的等),后续在使用SpringSecurity的过程中,我们再继续学习和分享。