1. 整体流程分析
前面我们已经得知一个单点登录的流程需要以下步骤
- 客户端1正常访问业务,由于没有登录,因此重定向到登录接口
- 浏览器请求登录。login中通过
@EnableSso
创建的spring-security过滤器链拦截登录请求,重定向到授权服务器获取授权码
- 授权服务器重定向至登录界面,登录完成后重定向到授权码接口继续授权
- 授权服务器授权完成后重定向到客户端1登录接口
- 客户端1携带授权码code再次访问/login接口,这次有了code,则不再请求授权码而是请求token
- 授权服务器校验code并返回token
- 客户端1拿到token后调用获取用户信息的端点接口请求用户信息
- 完成登录
- 最后重定向请求业务接口
其中,第1步,第5步,第6步,第7步是需要客户端参与的。
再分细一点,服务端有以下几个步骤
- 客户端执行业务接口,由于没有认证,则重定向客户端的登录接口
/login
- 浏览器请求登录接口,执行客户端的登录,由于此时没有携带code,会重定向到配置的授权服务器登录
- 浏览器重定向到授权服务端登录完成后,再次携带code请求登录客户端,此时已经有了code,因此会继续走客户端的登录
- 客户端拿到code调用服务端接口请求token
- 客户端拿到token之后再调用服务端(本质上来说是资源服务器,如果授权服务器和资源服务器在一起,则相当于请求的授权服务器)请求用户信息
- 拿到用户信息后完成登录
- 最后访问定义的主页
2. 源码解析
SpringSecurity的过滤器链相关的分析见 SpringSecurity过滤器链源码法分析章节,总的来说就是有多个过滤器链,根据请求url选择不同的链执行。
在理解了SpringSecurity的过滤器链上,我们来看客户端的过滤器链是怎么的结构
首先debug至FilterChainProxy#doFilterInternal()
,

可以看出来此处除了静态资源的过滤器链就只有一个链,拦截所有的方法,这是在配置文件中配置的,配置如下,我们并没有开启formLogin(因为走单点登录,也不应开起表单登录)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration @EnableOAuth2Sso public class ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/images/**"); }
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and().csrf().disable(); } }
|
2.1. 第一次重定向至登录接口分析
然后debug至AbstractAuthenticationProcessingFilter
,它的实现是OAuth2ClientAuthenticationProcessingFilter
,从SpringSecurity分析得知AbstractAuthenticationProcessingFilter
是一个认证的抽象类,主要是负责认证和认证完成的如session、返回结构等的处理,我们主要看OAuth2ClientAuthenticationProcessingFilter#attemptAuthentication()
这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); }
} }
|
那我们来看restTemplate.getAccessToken()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class OAuth2RestTemplate extends RestTemplate implements OAuth2RestOperations {
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException { OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) { try { accessToken = acquireAccessToken(context); } catch (UserRedirectRequiredException e) { context.setAccessToken(null); accessToken = null; String stateKey = e.getStateKey(); if (stateKey != null) { Object stateToPreserve = e.getStateToPreserve(); if (stateToPreserve == null) { stateToPreserve = "NONE"; } context.setPreservedState(stateKey, stateToPreserve); } throw e; } } return accessToken; } protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context) throws UserRedirectRequiredException { AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest(); OAuth2AccessToken accessToken = null; accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest); } }
|
再来看accessTokenProvider.obtainAccessToken()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public class AuthorizationCodeAccessTokenProvider extends OAuth2AccessTokenSupport implements AccessTokenProvider { public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException, OAuth2AccessDeniedException { AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details; if (request.getAuthorizationCode() == null) { if (request.getStateKey() == null) { throw getRedirectForAuthorization(resource, request); } }
} private UserRedirectRequiredException getRedirectForAuthorization(AuthorizationCodeResourceDetails resource, AccessTokenRequest request) { TreeMap<String, String> requestParameters = new TreeMap<String, String>(); requestParameters.put("response_type", "code"); requestParameters.put("client_id", resource.getClientId()); String redirectUri = resource.getRedirectUri(request); if (redirectUri != null) { requestParameters.put("redirect_uri", redirectUri); } if (resource.isScoped()) { StringBuilder builder = new StringBuilder(); List<String> scope = resource.getScope();
if (scope != null) { Iterator<String> scopeIt = scope.iterator(); while (scopeIt.hasNext()) { builder.append(scopeIt.next()); if (scopeIt.hasNext()) { builder.append(' '); } } } requestParameters.put("scope", builder.toString()); }
UserRedirectRequiredException redirectException = new UserRedirectRequiredException( resource.getUserAuthorizationUri(), requestParameters); String stateKey = stateKeyGenerator.generateKey(resource); redirectException.setStateKey(stateKey); request.setStateKey(stateKey); redirectException.setStateToPreserve(redirectUri); request.setPreservedState(redirectUri);
return redirectException;
} }
|

执行完上面之后就往外抛出异常,最后由OAuth2ClientContextFilter
借住异常,然后处理重定向url了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class OAuth2ClientContextFilter implements Filter, InitializingBean { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try { chain.doFilter(servletRequest, servletResponse); } catch (IOException ex) { throw ex; } catch (Exception ex) {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer .getFirstThrowableOfType( UserRedirectRequiredException.class, causeChain); if (redirect != null) { redirectUser(redirect, request, response); } else { } } } protected void redirectUser(UserRedirectRequiredException e, HttpServletRequest request, HttpServletResponse response) throws IOException { String redirectUri = e.getRedirectUri(); UriComponentsBuilder builder = UriComponentsBuilder .fromHttpUrl(redirectUri); Map<String, String> requestParams = e.getRequestParams(); for (Map.Entry<String, String> param : requestParams.entrySet()) { builder.queryParam(param.getKey(), param.getValue()); }
if (e.getStateKey() != null) { builder.queryParam("state", e.getStateKey()); }
this.redirectStrategy.sendRedirect(request, response, builder.build() .encode().toUriString()); } }
|
到这里就直接返回浏览器重定向获取授权了
2.2. 第二次重定向至登录接口分析
上面重定向设置了回调地址的,其实也是相同的/login
地址。
从服务端获取授权首先也重定向到登录,然后在重定向到获取授权地址,授权完成后再重定向回来到登录地址(就是上一步配置的回调地址)
第二次进来我们依然从OAuth2ClientAuthenticationProcessingFilter#attemptAuthentication()
这里开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); } catch (OAuth2Exception e) { } try { OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
return result; } catch (InvalidTokenException e) { }
} }
|
2.2.1. 获取AccessToken
我们首先来看获取restTemplate.getAccessToken()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class OAuth2RestTemplate extends RestTemplate implements OAuth2RestOperations { public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException { OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) { try { accessToken = acquireAccessToken(context); } catch (UserRedirectRequiredException e) { } } return accessToken; } protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context) throws UserRedirectRequiredException { AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest(); OAuth2AccessToken accessToken = null; accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest); oauth2Context.setAccessToken(accessToken); return accessToken; } }
|
再来看AuthorizationCodeAccessTokenProvider#obtainAccessToken()
真正获取到token的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| public class AuthorizationCodeAccessTokenProvider extends OAuth2AccessTokenSupport implements AccessTokenProvider { public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException, OAuth2AccessDeniedException { AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details; if (request.getAuthorizationCode() == null) { if (request.getStateKey() == null) { throw getRedirectForAuthorization(resource, request); } obtainAuthorizationCode(resource, request); } return retrieveToken(request, resource, getParametersForTokenRequest(resource, request), getHeadersForTokenRequest(request));
}
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
try { authenticationHandler.authenticateTokenRequest(resource, form, headers); tokenRequestEnhancer.enhance(request, resource, form, headers); final AccessTokenRequest copy = request;
final ResponseExtractor<OAuth2AccessToken> delegate = getResponseExtractor(); ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() { @Override public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException { if (response.getHeaders().containsKey("Set-Cookie")) { copy.setCookie(response.getHeaders().getFirst("Set-Cookie")); } return delegate.extractData(response); } }; return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(), getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
}
} }
|
2.2.2. 获取Authentication
再回到OAuth2ClientAuthenticationProcessingFilter
继续执行后续
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class OAuth2ClientAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); } catch (OAuth2Exception e) { } try { OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
return result; } catch (InvalidTokenException e) { }
} }
|
我们来看tokenServices.loadAuthentication(accessToken.getValue())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
public class UserInfoTokenServices implements ResourceServerTokenServices { @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken); return extractAuthentication(map); } private Map<String, Object> getMap(String path, String accessToken) { try { OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); }
return restTemplate.getForEntity(path, Map.class).getBody(); } catch (Exception ex) { } } private OAuth2Authentication extractAuthentication(Map<String, Object> map) { Object principal = getPrincipal(map); List<GrantedAuthority> authorities = this.authoritiesExtractor .extractAuthorities(map); OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null, null, null, null, null); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( principal, "N/A", authorities); token.setDetails(map); return new OAuth2Authentication(request, token); } }
|
这样认证就算完成了,然后就会发布AuthenticationSuccessEvent
事件继续走AbstractAuthenticationProcessingFilter
的过滤器的后处理逻辑,如设置上下文等