Spring Security OAUTH2 获取用户信息

Spring Security OAUTH2 获取用户信息

1.user-info-uri 与 token-info-uri

作用:二者皆是为了check token,并且顺带返回了用户信息。配置信息位置在资源服务器上。

解释:下面代码列举的都是token-info-uri,user-info-uri不解释。user-info-uri原理是在授权服务器认证后将认证信息Principal通过形参绑定的方法通过URL的方式获取用户信息。当然它也有配套的UserInfoTokenService等等,我没有研究,不过流程大概跟token-info-uri差不多。

  1. server:

  2. port: 9007

  3. security:

  4. oauth2:

  5. client:

  6. clientId: resource1

  7. clientSecret: secret

  8. userAuthorizationUri: http://localhost:9005/oauth/authorize

  9. grant-type: password

  10. scope: read

  11. access-token-uri: http://localhost:9005/oauth/token

  12.  
  13. resource:

  14. token-info-uri: http://localhost:9005/oauth/check_token

  15. user-info-uri: http://localhost:9005/user

  16. authorization:

  17. check-token-access: http://localhost:9005/oauth/check_token

  18.  
  19. # resource:

  20. # jwt:

  21. # key-uri: http://localhost:9005/oauth/token_key

  22. basic:

  23. enabled: false

下面内容默认已经自定义了UserDetail的实现,我们从开始向授权服务器验证token开始分析源码

从RemoteTokenService发起验证请求,可以看到是通过restTemplate发起请求的,并且返回map类型的响应结果


 
  1. public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

  2. MultiValueMap<String, String> formData = new LinkedMultiValueMap();

  3. formData.add(this.tokenName, accessToken);

  4. HttpHeaders headers = new HttpHeaders();

  5. headers.set("Authorization", this.getAuthorizationHeader(this.clientId, this.clientSecret));

  6. Map<String, Object> map = this.postForMap(this.checkTokenEndpointUrl, formData, headers);

  7. if (map.containsKey("error")) {

  8. this.logger.debug("check_token returned error: " + map.get("error"));

  9. throw new InvalidTokenException(accessToken);

  10. } else {

  11. Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");

  12. return this.tokenConverter.extractAuthentication(map);

  13. }

  14. }


 
  1. public class RemoteTokenServices implements ResourceServerTokenServices {

  2.  
  3. ......

  4.  
  5. private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {

  6. if (headers.getContentType() == null) {

  7. headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

  8. }

  9.  
  10. Map map = (Map)this.restTemplate.exchange(path, HttpMethod.POST, new HttpEntity(formData, headers), Map.class, new Object[0]).getBody();

  11. return map;

  12. }

  13. }

接着我们会到达check_token端点,可以看到通过loadAuthentication将token转化成OAuth2Authentication。然后再通过convertAccessToken转换我们需要返回的信息


 
  1. @RequestMapping({"/oauth/check_token"})

  2. @ResponseBody

  3. public Map<String, ?> checkToken(@RequestParam("token") String value) {

  4. OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);

  5. if (token == null) {

  6. throw new InvalidTokenException("Token was not recognised");

  7. } else if (token.isExpired()) {

  8. throw new InvalidTokenException("Token has expired");

  9. } else {

  10. OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());

  11. Map<String, ?> response = this.accessTokenConverter.convertAccessToken(token, authentication);

  12. return response;

  13. }

  14. }

通过将token作为key从redis取出value,然后反序列化出来认证对象


 
  1. public OAuth2AccessToken readAccessToken(String tokenValue) {

  2. byte[] key = this.serializeKey("access:" + tokenValue);

  3. byte[] bytes = null;

  4. RedisConnection conn = this.getConnection();

  5.  
  6. byte[] bytes;

  7. try {

  8. bytes = conn.get(key);

  9. } finally {

  10. conn.close();

  11. }

  12.  
  13. OAuth2AccessToken var5 = this.deserializeAccessToken(bytes);

  14. return var5;

  15. }

 通过转换器挑选需要返回的数据信息


 
  1. public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {

  2. Map<String, Object> response = new HashMap();

  3. OAuth2Request clientToken = authentication.getOAuth2Request();

  4. if (!authentication.isClientOnly()) {

  5. response.putAll(this.userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));

  6. } else if (clientToken.getAuthorities() != null && !clientToken.getAuthorities().isEmpty()) {

  7. response.put("authorities", AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));

  8. }

  9.  
  10. if (token.getScope() != null) {

  11. response.put("scope", token.getScope());

  12. }

  13.  
  14. if (token.getAdditionalInformation().containsKey("jti")) {

  15. response.put("jti", token.getAdditionalInformation().get("jti"));

  16. }

  17.  
  18. if (token.getExpiration() != null) {

  19. response.put("exp", token.getExpiration().getTime() / 1000L);

  20. }

  21.  
  22. if (this.includeGrantType && authentication.getOAuth2Request().getGrantType() != null) {

  23. response.put("grant_type", authentication.getOAuth2Request().getGrantType());

  24. }

  25.  
  26. response.putAll(token.getAdditionalInformation());

  27. response.put("client_id", clientToken.getClientId());

  28. if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {

  29. response.put("aud", clientToken.getResourceIds());

  30. }

  31.  
  32. return response;

  33. }

从默认的实现类可以看出,只会将认证的用户名name返回到资源服务器,我们如果想要所有信息,最好是重构此方法。当然这个前提是我们在使用token-info-uri 


 
  1. public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {

  2. private Collection<? extends GrantedAuthority> defaultAuthorities;

  3. private UserDetailsService userDetailsService;

  4.  
  5. public DefaultUserAuthenticationConverter() {

  6. }

  7.  
  8. public void setUserDetailsService(UserDetailsService userDetailsService) {

  9. this.userDetailsService = userDetailsService;

  10. }

  11.  
  12. public void setDefaultAuthorities(String[] defaultAuthorities) {

  13. this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(defaultAuthorities));

  14. }

  15.  
  16. public Map<String, ?> convertUserAuthentication(Authentication authentication) {

  17. Map<String, Object> response = new LinkedHashMap();

  18. response.put("user_name", authentication.getName());

  19. if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {

  20. response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));

  21. }

  22.  
  23. return response;

  24. }

开始重写 DefaultUserAuthenticationConverter的convertUserAuthentication方法,将所有授权信息都返回到资源服务器


 
  1. @Service

  2. public class MyUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

  3.  
  4. @Override

  5. public Map<String, ?> convertUserAuthentication(Authentication authentication) {

  6. Map<String, Object> response = new LinkedHashMap();

  7. response.put("user_name", authentication);

  8. return response;

  9. }

  10. }

在授权服务器的配置类上,我们把重写的认证转换器设置到配置类上,由于看到源代码只有accessTokenConverter方法,可以得知需要替换整个DefaultAccessTokenConverter,而在DefaultAccessTokenConverter里面我们可以把我们刚刚重构的DefaultUserAuthenticationConverter设置进去。


 
  1. @Override

  2. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

  3. Collection<TokenEnhancer> tokenEnhancers = applicationContext.getBeansOfType(TokenEnhancer.class).values();

  4. TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();

  5. tokenEnhancerChain.setTokenEnhancers(new ArrayList<>(tokenEnhancers));

  6. DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

  7. defaultTokenServices.setReuseRefreshToken(isReuseRefreshToken);

  8. defaultTokenServices.setSupportRefreshToken(isSupportRefreshToken);

  9. defaultTokenServices.setTokenStore(tokenStore);

  10. defaultTokenServices.setAccessTokenValiditySeconds(accessTokenValiditySeconds);

  11. defaultTokenServices.setRefreshTokenValiditySeconds(refreshTokenValiditySeconds);

  12. defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);

  13. //若通过 JDBC 存储令牌

  14. if (Objects.nonNull(jdbcClientDetailsService)){

  15. defaultTokenServices.setClientDetailsService(jdbcClientDetailsService);

  16. }

  17. DefaultAccessTokenConverter defaultAccessTokenConverter=new DefaultAccessTokenConverter();

  18. defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());

  19. endpoints

  20. .authenticationManager(authenticationManager)

  21. .userDetailsService(userDetailsService)

  22. .accessTokenConverter(defaultAccessTokenConverter)

  23. .tokenServices(defaultTokenServices);

  24. }

当授权服务器响应完毕,我们重新回到资源发武器发起check token请求的函数那里,可以看到当拿到map后,开始用资源服务器也就是本引用中的tokenConverte进行数据的处理


 
  1. public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

  2. MultiValueMap<String, String> formData = new LinkedMultiValueMap();

  3. formData.add(this.tokenName, accessToken);

  4. HttpHeaders headers = new HttpHeaders();

  5. headers.set("Authorization", this.getAuthorizationHeader(this.clientId, this.clientSecret));

  6. Map<String, Object> map = this.postForMap(this.checkTokenEndpointUrl, formData, headers);

  7. if (map.containsKey("error")) {

  8. this.logger.debug("check_token returned error: " + map.get("error"));

  9. throw new InvalidTokenException(accessToken);

  10. } else {

  11. Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");

  12. return this.tokenConverter.extractAuthentication(map);

  13. }

  14. }

 我们查看DefaultAcessTokenConverter类中的extractAuthentication方法,发现了核心方法在DefaultUserAccessTokenConcerter中的extractAuthentication方法。


 
  1. public OAuth2Authentication extractAuthentication(Map<String, ?> map) {

  2. Map<String, String> parameters = new HashMap();

  3. Set<String> scope = this.extractScope(map);

  4. Authentication user = this.userTokenConverter.extractAuthentication(map);

  5. String clientId = (String)map.get("client_id");

  6. parameters.put("client_id", clientId);

  7. if (this.includeGrantType && map.containsKey("grant_type")) {

  8. parameters.put("grant_type", (String)map.get("grant_type"));

  9. }

  10.  
  11. Set<String> resourceIds = new LinkedHashSet((Collection)(map.containsKey("aud") ? this.getAudience(map) : Collections.emptySet()));

  12. Collection<? extends GrantedAuthority> authorities = null;

  13. if (user == null && map.containsKey("authorities")) {

  14. String[] roles = (String[])((Collection)map.get("authorities")).toArray(new String[0]);

  15. authorities = AuthorityUtils.createAuthorityList(roles);

  16. }

  17.  
  18. OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, (String)null, (Set)null, (Map)null);

  19. return new OAuth2Authentication(request, user);

  20. }

从这个方法可以看出,将user_name对象取出value作为principal。从这里我们也不难想到,要想拿所有的用户信息有两种解决方法。第一种就是从资源服务器那里重写Conveter方法,将所有数据返回。并直接用user_name当成key名,资源服务器无需修改。第二种方法就是不修改授权服务器,而是在资源服务器这里配置好UserDetailsService类,通过user_name从数据库加载信息,如果不配置,那么将不执行这段代码。但是我认为最好是不要采用第二种方法,让授权服务器管理认证和授权即可,不要把资源服务器也牵扯到这上面。让资源服务器保持单一性。 


 
  1. public Authentication extractAuthentication(Map<String, ?> map) {

  2. if (map.containsKey("user_name")) {

  3. Object principal = map.get("user_name");

  4. Collection<? extends GrantedAuthority> authorities = this.getAuthorities(map);

  5. if (this.userDetailsService != null) {

  6. UserDetails user = this.userDetailsService.loadUserByUsername((String)map.get("user_name"));

  7. authorities = user.getAuthorities();

  8. principal = user;

  9. }

  10.  
  11. return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);

  12. } else {

  13. return null;

  14. }

  15. }

最后查看资源服务器上获取到的认证信息。当然需要返回什么信息自己可以在授权服务端自行定义,本处就不一一列举了。

直接在形参上 绑定Principal是因为当返回map的时候我们也可以看到会调用TokenConveter的方法进行参数的转化,所以最后返回的信息是一个Authentication,而它的顶级接口就是Principal.所以我们可以通过自动绑定的方式拿到用户信息。

展开阅读全文

没有更多推荐了,返回首页