ResourceServerTokenServices
oauth2资源服务器鉴权的一个核心服务接口类,只有2个接口方法
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
* 检验token的有效性,只返回有限的信息
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
* 获取token的完整信息的
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
默认的实现类有4个
DefaultTokenServices
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean
既实现了ResourceServerTokenServices,又实现了AuthorizationServerTokenServices,并且内部实现是对tokenStore的操作,由此可知,该类适用于认证服务器的token管理
从源码可以看到主要的功能是:
- 生成token
- 刷新token
- 获取token
RemoteTokenServices
检查token的服务,对应配置security.oauth2.resource.tokenInfoUrl=http://localhost:4000/oauth/check_token
public class RemoteTokenServices implements ResourceServerTokenServices {
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
@Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
// 省略了getter setter
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap formData = new LinkedMultiValueMap();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map 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);
}
// gh-838
if (!Boolean.TRUE.equals(map.get("active"))) {
logger.debug("check_token returned active attribute: " + map.get("active"));
throw new InvalidTokenException(accessToken);
}
return tokenConverter.extractAuthentication(map);
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
if(clientId == null || clientSecret == null) {
logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
}
String creds = String.format("%s:%s", clientId, clientSecret);
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}
private Map postForMap(String path, MultiValueMap formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
@SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity>(formData, headers), Map.class).getBody();
@SuppressWarnings("unchecked")
Map result = map;
return result;
}
}
SpringSocialTokenServices
封装了对第三方统一授权的调用方法,比如集成QQ,微信,微博
public class SpringSocialTokenServices implements ResourceServerTokenServices {
private final OAuth2ConnectionFactory> connectionFactory;
private final String clientId;
public SpringSocialTokenServices(OAuth2ConnectionFactory> connectionFactory,
String clientId) {
this.connectionFactory = connectionFactory;
this.clientId = clientId;
}
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
AccessGrant accessGrant = new AccessGrant(accessToken);
Connection> connection = this.connectionFactory.createConnection(accessGrant);
UserProfile user = connection.fetchUserProfile();
return extractAuthentication(user);
}
private OAuth2Authentication extractAuthentication(UserProfile user) {
String principal = user.getUsername();
List authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
null, null, null, null);
return new OAuth2Authentication(request,
new UsernamePasswordAuthenticationToken(principal, "N/A", authorities));
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
UserInfoTokenServices
获取鉴权用户信息的服务,对应的配置文件中security.oauth2.resource.user-info-uri=http://localhost:4000/auth/current
优化配置方式
在开发过程中,由于资源服务器和认证服务器在一个微服务体系内,所以可以考虑将配置文件的具体的ip:port的方式改为通过restTemplate的通过服务名的方式访问
比如认证服务的服务名是auth-serivces,那么http://localhost:4000/oauth/check_token 就希望配置成http://auth-services/oauth/check_token, 如果只是这样修改,运行过程中会报错,无法识别的host,由此需要对RemoteTokenServices重写
@Slf4j
public class CustomCheckTokenServices implements ResourceServerTokenServices {
private RestTemplate restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
public CustomCheckTokenServices(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.accessTokenConverter = accessTokenConverter;
}
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
}
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap formData = new LinkedMultiValueMap<>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(clientId, clientSecret));
Map map = postForMap(checkTokenEndpointUrl, formData, headers);
log.info("check token result {}", map);
if (map.containsKey("error")) {
log.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return accessTokenConverter.extractAuthentication(map);
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token.");
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
String creds = String.format("%s:%s", clientId, clientSecret);
return "Basic " + new String(Base64.encodeBase64(creds.getBytes(StandardCharsets.UTF_8)));
}
private Map postForMap(String path, MultiValueMap formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
@SuppressWarnings("rawtypes")
Map map = restTemplate.postForObject(path, new HttpEntity<>(formData, headers), Map.class);
@SuppressWarnings("unckecked")
Map result = map;
return result;
}
}
然后在 EnableResoureceServer中进行配置(要创建restTemplate的bean 并且必须要加上@LoadBalanced注解)
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerProperties resourceServerProperties;
@Autowired
private PermitAllProperties permitAllProperties;
@Autowired
private RestTemplate restTemplate;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
// 只会处理/**匹配的地址,假设该值是/aa/**, 则/bb/**的是不会被处理的
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests()
// 设置不需要鉴权的url匹配规则
.antMatchers(permitAllProperties.getPermitAllPatterns()).permitAll()
// 其他的地址全部需要鉴权
.anyRequest().authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
CustomCheckTokenServices customCheckTokenServices = new CustomCheckTokenServices(restTemplate);
customCheckTokenServices.setCheckTokenEndpointUrl(resourceServerProperties.getTokenInfoUri());
customCheckTokenServices.setClientId(resourceServerProperties.getClientId());
customCheckTokenServices.setClientSecret(resourceServerProperties.getClientSecret());
resources.tokenServices(customCheckTokenServices);
}
}
同样的方式可以重写 UserInfoTokenServices的实现
如果实在网关内重写该方法,可以直接在该方法内将一些下游服务需要的鉴权信息增强到request header中
public void enhancer(Map principal) {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, String.valueOf(principal.get("id")));
ctx.addZuulRequestHeader(SecurityConstants.USERNAME_IN_HEADER, String.valueOf(principal.get("username")));
}