™技术博客

框架 | 11.扩展资源服务器解决oauth2性能瓶颈

2021年8月11日

  • 用户携带token 请求资源服务器
  • 资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验
  • 资源服务器拿到token,默认只会含有用户名信息
  • 通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息

默认check-token 解析逻辑


  • RemoteTokenServices入口
1
2
3
4
5
6
7
8
9
10
11
12
@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));
// 调用认证服务器的check-token 接口检查token
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

return tokenConverter.extractAuthentication(map);
}
  • 解析认证服务器返回的信息 DefaultAccessTokenConverter
1
2
3
4
5
6
7
8
9
10
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
Map<String, String> parameters = new HashMap<String, String>();
Set<String> scope = extractScope(map);
// 主要是 用户的信息的抽取
Authentication user = userTokenConverter.extractAuthentication(map);
// 一些oauth2 信息的填充
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
null);
return new OAuth2Authentication(request, user);
}
  • 组装当前用户信息 DefaultUserAuthenticationConverter
1
2
3
4
5
6
7
8
9
10
11
12
13
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}

问题分析


  • 认证服务器check-token 返回的全部信息
  • 资源服务器在根据返回信息组装用户信息的时候,只是用了username
  • 如果设置了 userDetailsService 的实现则去调用 loadUserByUsername 再去查询一次用户信息

造成问题现象


  • 如果设置了userDetailsService 即可在spring security 上下文获取用户的全部信息,不设置则只能得到用户名。
  • 增加了一次查询逻辑,对性能产生不必要的影响

解决问题


  • 扩展UserAuthenticationConverter 的解析过程,把认证服务器返回的信息全部组装到spring security的上下文对象中
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class AiopsUserAuthenticationConverter implements UserAuthenticationConverter {

private static final String N_A = "N/A";

/**
* Extract information about the user to be used in an access token (i.e. for resource
* servers).
* @param authentication an authentication representing a user
* @return a map of key values representing the unique information about the user
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<>();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}

/**
* Inverse of {@link #convertUserAuthentication(Authentication)}. Extracts an
* Authentication from a map.
* @param responseMap a map of user information
* @return an Authentication representing the user or null if there is none
*/
@Override
public Authentication extractAuthentication(Map<String, ?> responseMap) {
if (responseMap.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(responseMap);
Map<String, ?> map = MapUtil.get(responseMap, SecurityConstants.DETAILS_USER, Map.class);
validateTenantId(map);
String username = MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME);
Integer id = MapUtil.getInt(map, SecurityConstants.DETAILS_USER_ID);
Integer deptId = MapUtil.getInt(map, SecurityConstants.DETAILS_DEPT_ID);
Integer tenantId = MapUtil.getInt(map, SecurityConstants.DETAILS_TENANT_ID);
String phone = MapUtil.getStr(map, SecurityConstants.DETAILS_PHONE);
String avatar = MapUtil.getStr(map, SecurityConstants.DETAILS_AVATAR);
AiopsUser user = new AiopsUser(id, deptId, phone, avatar, tenantId, username, N_A, true, true, true, true,
authorities);
return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
}
return null;
}

private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(
StringUtils.collectionToCommaDelimitedString((Collection<?>) authorities));
}
return AuthorityUtils.NO_AUTHORITIES;
}

private void validateTenantId(Map<String, ?> map) {
String headerValue = getCurrentTenantId();
Integer userValue = MapUtil.getInt(map, SecurityConstants.DETAILS_TENANT_ID);
if (StrUtil.isNotBlank(headerValue) && !userValue.toString().equals(headerValue)) {
log.warn("请求头中的租户ID({})和用户的租户ID({})不一致", headerValue, userValue);
// TODO: 不要提示租户ID不对,可能被穷举
throw new AiopsAuth2Exception(AiopsSecurityMessageSourceUtil.getAccessor().getMessage(
"AbstractUserDetailsAuthenticationProvider.badTenantId", new Object[] { headerValue },
"Bad tenant ID"));
}
}

private Optional<HttpServletRequest> getCurrentHttpRequest() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes()).filter(
requestAttributes -> ServletRequestAttributes.class.isAssignableFrom(requestAttributes.getClass()))
.map(requestAttributes -> ((ServletRequestAttributes) requestAttributes))
.map(ServletRequestAttributes::getRequest);
}

private String getCurrentTenantId() {
return getCurrentHttpRequest()
.map(httpServletRequest -> httpServletRequest.getHeader(CommonConstants.TENANT_ID)).orElse(null);
}

}

  • 给remoteTokenServices 注入这个实现
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
public class AiopsResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {

@Autowired
protected AuthenticationEntryPoint resourceAuthExceptionEntryPoint;

@Autowired
protected RemoteTokenServices remoteTokenServices;

@Autowired
private PermitAllUrlResolver permitAllUrlResolver;

@Autowired
private TokenExtractor tokenExtractor;

@Autowired
private RestTemplate lbRestTemplate;

/**
* 默认的配置,对外暴露
* @param httpSecurity
*/
@Override
@SneakyThrows
public void configure(HttpSecurity httpSecurity) {
// 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题
httpSecurity.headers().frameOptions().disable();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
// 配置对外暴露接口
permitAllUrlResolver.registry(registry);
registry.anyRequest().authenticated().and().csrf().disable();
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
UserAuthenticationConverter userTokenConverter = new AiopsUserAuthenticationConverter();
accessTokenConverter.setUserTokenConverter(userTokenConverter);

remoteTokenServices.setRestTemplate(lbRestTemplate);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(tokenExtractor)
.tokenServices(remoteTokenServices);
}

}

  • 完成扩展,再来看文章开头的流程图就变成了如下

扫描二维码,分享此文章