1. 整体流程分析

前面我们已经得知一个单点登录的流程需要以下步骤

  1. 客户端1正常访问业务,由于没有登录,因此重定向到登录接口
  2. 浏览器请求登录。login中通过@EnableSso创建的spring-security过滤器链拦截登录请求,重定向到授权服务器获取授权码
  3. 授权服务器重定向至登录界面,登录完成后重定向到授权码接口继续授权
  4. 授权服务器授权完成后重定向到客户端1登录接口
  5. 客户端1携带授权码code再次访问/login接口,这次有了code,则不再请求授权码而是请求token
  6. 授权服务器校验code并返回token
  7. 客户端1拿到token后调用获取用户信息的端点接口请求用户信息
  8. 完成登录
  9. 最后重定向请求业务接口

其中,第1步,第5步,第6步,第7步是需要客户端参与的。
再分细一点,服务端有以下几个步骤

  • 客户端执行业务接口,由于没有认证,则重定向客户端的登录接口/login
  • 浏览器请求登录接口,执行客户端的登录,由于此时没有携带code,会重定向到配置的授权服务器登录
  • 浏览器重定向到授权服务端登录完成后,再次携带code请求登录客户端,此时已经有了code,因此会继续走客户端的登录
  • 客户端拿到code调用服务端接口请求token
  • 客户端拿到token之后再调用服务端(本质上来说是资源服务器,如果授权服务器和资源服务器在一起,则相当于请求的授权服务器)请求用户信息
  • 拿到用户信息后完成登录
  • 最后访问定义的主页

2. 源码解析

SpringSecurity的过滤器链相关的分析见 SpringSecurity过滤器链源码法分析章节,总的来说就是有多个过滤器链,根据请求url选择不同的链执行。
在理解了SpringSecurity的过滤器链上,我们来看客户端的过滤器链是怎么的结构
首先debug至FilterChainProxy#doFilterInternal()

b3f5eb8258b84f7ecd4ab46f38439315
可以看出来此处除了静态资源的过滤器链就只有一个链,拦截所有的方法,这是在配置文件中配置的,配置如下,我们并没有开启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 {

//尝试获取Accesstoken,context是一个session级别的context,然后会通过Spring的代理从session中拿到state和code,代码很绕,我找了好久才找到咋回事
//这个我们先记下来,后续专门研究这个是怎么把code和state放到session中并
//这里肯定也是null
OAuth2AccessToken accessToken = context.getAccessToken();

if (accessToken == null || accessToken.isExpired()) {
try {
//这里获取token,没有获取到,会报错并重定向
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
context.setAccessToken(null); // No point hanging onto it now
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;
}
//获取token
protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context)
throws UserRedirectRequiredException {
//这里拿请求token,拿到的是内容为空的对象
AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
//......
//获取 token,拿不到数据,会重定向异常的错
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"); // oauth2 spec, section 3
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);
//生成state,并加入reqeust和redirectException重定向异常中
String stateKey = stateKeyGenerator.generateKey(resource);
redirectException.setStateKey(stateKey);
request.setStateKey(stateKey);
redirectException.setStateToPreserve(redirectUri);
request.setPreservedState(redirectUri);

return redirectException;

}
}

5a66d0915b9323f250f329d7cbf59b7f

执行完上面之后就往外抛出异常,最后由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());
}
//最后builder构成 http://localhost:8080/oauth/authorize?client_id=sso-client2&redirect_uri=http://localhost:8082/login&response_type=code&state=NNeM1A这样的链接

//这里就是设置Redirect到response中
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 {
//这里尝试获取token,第二次因为有code和state了,因此是可以获取到的,我们在下面方法看内部逻辑,先不忙进去
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
//......
}
try {
//这里尝试通过token获取认证结果,这个方法就能获取到用户信息,
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 {
//这里从context中拿,是拿不到的
OAuth2AccessToken accessToken = context.getAccessToken();

if (accessToken == null || accessToken.isExpired()) {
try {
//然后走这里,继续拿,这里是能拿到的
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
//拿到了,后面就不走了
}
}
return accessToken;
}
//获取Accesstoken
protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context)
throws UserRedirectRequiredException {
//这里拿到tokenRequest,主要是封装了state和code,是通过jdk代理实现的,但是在target = targetSource.getTarget() 方法 就拿到了state和code,
//我也很疑惑,跟踪找了半天没搞明白,最后大概确定是通过RequestContextHoder拿到reqeust,然后在session中取的,至于怎么放在session中的,暂时不清楚
AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
//......异常不走
//......校验state

//获取accessToken,这里面通过restTemplate调用服务端就能获取到了
OAuth2AccessToken accessToken = null;
accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
//......没有异常,忽略
//在上下文中设置accessToken,然后返回
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;
//这里因为有code了,就不会走重定向了,这也是和第一次分叉的开始
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
//这里外呼拿token
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));

}

//通过restTemplate拿token
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {

try {
//这里组装请求头Authorization,就忽略了,里面内容也好理解
authenticationHandler.authenticateTokenRequest(resource, form, headers);
tokenRequestEnhancer.enhance(request, resource, form, headers);
final AccessTokenRequest copy = request;

//生成 RestTemplate 并执请求服务端拿到返回数据,这样就请求回来了token了
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 {
//这里尝试获取token,第二次因为有code和state了,因此是可以获取到的,我们在下面方法看内部逻辑,先不忙进去
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
//......
}
try {
//现在就要分析这里了
//这里尝试通过token获取认证结果,这个方法就能获取到用户信息
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
/**
* {@link ResourceServerTokenServices} that uses a user info REST service.
*
* @author Dave Syer
* @since 1.3.0
*/
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);
}
//......

//这里通过restTemplate获取用户信息
return restTemplate.getForEntity(path, Map.class).getBody();
}
catch (Exception ex) {
//......不抛出异常,抛出的话那大概率是token过期了
}
}
//组装Authentication
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的过滤器的后处理逻辑,如设置上下文等