搬砖小抄

spring security oauth2 资源服务器认证过程实现源码

字数统计: 1k阅读时长: 5 min
2020/07/28 Share

典型的OAuth2架构大概是这个样子

  1. 前端带着access token来访问资源服务器的REST接口
  2. 资源服务器的OAuth2AuthenticationProcessingFilter发现请求中携带了access token,触发认证过程
  3. 资源服务器的@RestController响应前端请求

代码分析

核心流程由OAuth2AuthenticationProcessingFilter这个过滤器实现,它的引入轨迹为:

@EnableResourceServer → ResourceServerConfiguration → ResourceServerSecurityConfigurer

OAuth2AuthenticationProcessingFilter 代码分析

这个过滤器负责执行认证流程:通过AuthenticationManageraccessToken进行认证,若认证成功则保存认证信息,认证失败则结束过滤链。

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
60
61
62
63
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {

final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

try {
// 从当前请求中取出 access-token,这个token是客户端填的,默认实现为BearerTokenExtractor
Authentication authentication = tokenExtractor.extract(request);

if (authentication == null) {
// 没有token,清空Security上下文
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
// 有token,触发认证流程
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
// 把原始请求信息保存起来
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// OAuth2AuthenticationManager,调用RemoteTokenServices去请求认证服务器的"/oauth/check_token" 端点
// 即:用token 换取用户信息(OAuth2Authentication),有了用户信息,则认证成功,后续鉴权都是以认证成功为前提
Authentication authResult = authenticationManager.authenticate(authentication);

if (debug) {
logger.debug("Authentication success: " + authResult);
}
// 广播认证成功事件,保存认证信息
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);

}
}
catch (OAuth2Exception failed) {
// 处理所有OAuth2Exception类型的异常
SecurityContextHolder.clearContext();

if (debug) {
logger.debug("Authentication request failed: " + failed);
}
// 广播认证失败事件
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
// 向前台写响应信息,如果需要自定义输出信息,则可以注入自己的 AuthenticationEntryPoint
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
// 终结过滤链
return;
}

chain.doFilter(request, response);
}

此时的过滤链大概是这个样子

当因为token失效而导致认证失败时,异常信息如下

OAuth2AuthenticationManager 代码分析

实现AuthenticationManager,负责对accessToken进行认证

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
/**
* Expects the incoming authentication request to have a principal value that is an access token value (e.g. from an
* authorization header). Loads an authentication from the {@link ResourceServerTokenServices} and checks that the
* resource id is contained in the {@link AuthorizationRequest} (if one is specified). Also copies authentication
* details over from the input to the output (e.g. typically so that the access token value and request details can
* be reported later).
*
* @param authentication an authentication request containing an access token value as the principal
* @return an {@link OAuth2Authentication}
*
* @see org.springframework.security.authentication.AuthenticationManager#authenticate(org.springframework.security.core.Authentication)
*/
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
// token 换用户身份信息
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
// 资源ID匹配
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 尝试检查请求方oauth clientId 的合法性,取决于是否注入了ClientDetailsService
checkClientDetails(auth);

if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;

}

RemoteTokenServices 代码分析

实现了ResourceServerTokenServices接口,负责根据accessToken获取认证信息

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
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
// 调用认证服务器的'CheckTokenEndpoint'
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

// 调用成功但认证服务器返回失败
if (map.containsKey("error")) {
if (logger.isDebugEnabled()) {
logger.debug("check_token returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}

// 见RFC7662 https://tools.ietf.org/search/rfc7662#section-2.2
if (map.containsKey("active") && !"true".equals(String.valueOf(map.get("active")))) {
logger.debug("check_token returned active attribute: " + map.get("active"));
throw new InvalidTokenException(accessToken);
}
// 使用AccessTokenConverter将认证服务器返回的键值对转换成OAuth2Authentication
return tokenConverter.extractAuthentication(map);
}

参考资料

代码来源:spring-security-oauth2-2.3.6.RELEASE

CATALOG
  1. 1. 代码分析
    1. 1.1. OAuth2AuthenticationProcessingFilter 代码分析
    2. 1.2. OAuth2AuthenticationManager 代码分析
    3. 1.3. RemoteTokenServices 代码分析
  2. 2. 参考资料