spring security oauth2.x迁移到spring security5.x 令牌失效 资源服务器invalid_token响应状态码为500而非401
环境
资源服务器迁移到spring security5.5.2
授权服务器仍使用spring security oauth2.x搭建
现象
使用无效的令牌访问资源服务器API时,希望返回401 未授权的响应
但实际返回的时500服务器错误
原因
授权服务器校验无效令牌时返回响应状态码为400
spring security5.x资源服务器OpaqueToken认证逻辑中,将状态码非200的令牌自省响应都以服务器异常抛出,而没有正确处理包装为认证异常
解决
效果
自定义令牌内省器
import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
import com.nimbusds.oauth2.sdk.TokenIntrospectionResponse;
import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.Audience;
import com.nimbusds.oauth2.sdk.util.JSONUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.*;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.*;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.*;
public class DefaultOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final Log logger = LogFactory.getLog(getClass());
private final String authorityPrefix = "SCOPE_";
private Converter<String, RequestEntity<?>> requestEntityConverter;
private RestOperations restOperations;
public DefaultOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
protected boolean hasError(HttpStatus statusCode) {
// 不要将4xx错误以异常抛出
if (statusCode.is4xxClientError()) {
return false;
}
return super.hasError(statusCode);
}
});
this.restOperations = restTemplate;
}
public DefaultOpaqueTokenIntrospector(String introspectionUri, RestOperations restOperations) {
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
Assert.notNull(restOperations, "restOperations cannot be null");
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
this.restOperations = restOperations;
}
private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
return (token) -> {
HttpHeaders headers = requestHeaders();
MultiValueMap<String, String> body = requestBody(token);
return new RequestEntity<>(body, headers, HttpMethod.POST, introspectionUri);
};
}
private HttpHeaders requestHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
return headers;
}
private MultiValueMap<String, String> requestBody(String token) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("token", token);
return body;
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
RequestEntity<?> requestEntity = this.requestEntityConverter.convert(token);
if (requestEntity == null) {
throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity");
}
ResponseEntity<String> responseEntity = makeRequest(requestEntity);
HTTPResponse httpResponse = adaptToNimbusResponse(responseEntity);
TokenIntrospectionResponse introspectionResponse = parseNimbusResponse(httpResponse);
TokenIntrospectionSuccessResponse introspectionSuccessResponse = castToNimbusSuccess(introspectionResponse);
if (!introspectionSuccessResponse.isActive()) {
this.logger.trace("Did not validate token since it is inactive");
throw new BadOpaqueTokenException("Provided token isn't active");
}
return convertClaimsSet(introspectionSuccessResponse);
}
public void setRequestEntityConverter(Converter<String, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
private ResponseEntity<String> makeRequest(RequestEntity<?> requestEntity) {
try {
return this.restOperations.exchange(requestEntity, String.class);
} catch (Exception ex) {
throw new OAuth2IntrospectionException(ex.getMessage(), ex);
}
}
private HTTPResponse adaptToNimbusResponse(ResponseEntity<String> responseEntity) {
HTTPResponse response = new HTTPResponse(responseEntity.getStatusCodeValue());
response.setHeader(HttpHeaders.CONTENT_TYPE, responseEntity.getHeaders().getContentType().toString());
response.setContent(responseEntity.getBody());
return response;
}
private TokenIntrospectionResponse parseNimbusResponse(HTTPResponse response) {
try {
return TokenIntrospectionResponse.parse(response);
} catch (Exception ex) {
throw new OAuth2IntrospectionException(ex.getMessage(), ex);
}
}
private TokenIntrospectionSuccessResponse castToNimbusSuccess(TokenIntrospectionResponse introspectionResponse) {
if (!introspectionResponse.indicatesSuccess()) {
// 如果是失败响应,则将错误信息封装抛出
throw new BadOpaqueTokenException(((TokenIntrospectionErrorResponse) introspectionResponse).getErrorObject().toString());
}
return (TokenIntrospectionSuccessResponse) introspectionResponse;
}
private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessResponse response) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
Map<String, Object> claims = response.toJSONObject();
if (response.getAudience() != null) {
List<String> audiences = new ArrayList<>();
for (Audience audience : response.getAudience()) {
audiences.add(audience.getValue());
}
claims.put(OAuth2IntrospectionClaimNames.AUDIENCE, Collections.unmodifiableList(audiences));
}
if (response.getClientID() != null) {
claims.put(OAuth2IntrospectionClaimNames.CLIENT_ID, response.getClientID().getValue());
}
if (response.getExpirationTime() != null) {
Instant exp = response.getExpirationTime().toInstant();
claims.put(OAuth2IntrospectionClaimNames.EXPIRES_AT, exp);
}
if (response.getIssueTime() != null) {
Instant iat = response.getIssueTime().toInstant();
claims.put(OAuth2IntrospectionClaimNames.ISSUED_AT, iat);
}
if (response.getIssuer() != null) {
claims.put(OAuth2IntrospectionClaimNames.ISSUER, issuer(response.getIssuer().getValue()));
}
if (response.getNotBeforeTime() != null) {
claims.put(OAuth2IntrospectionClaimNames.NOT_BEFORE, response.getNotBeforeTime().toInstant());
}
if (response.getScope() != null) {
List<String> scopes = Collections.unmodifiableList(response.getScope().toStringList());
claims.put(OAuth2IntrospectionClaimNames.SCOPE, scopes);
for (String scope : scopes) {
authorities.add(new SimpleGrantedAuthority(this.authorityPrefix + scope));
}
}
// 封装用户权限
try {
List<String> userAuthorities = JSONUtils.to(claims.get("authorities"), List.class);
for (String userAuthority : userAuthorities) {
authorities.add(new SimpleGrantedAuthority(userAuthority));
}
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
}
private URL issuer(String uri) {
try {
return new URL(uri);
} catch (Exception ex) {
throw new OAuth2IntrospectionException(
"Invalid " + OAuth2IntrospectionClaimNames.ISSUER + " value: " + uri);
}
}
}
源码分析
授权服务器
- 令牌校验端点
org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) { // 令牌无效
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) { // 令牌过期
throw new InvalidTokenException("Token has expired");
}
...
}
// 处理InvalidTokenException异常时以状态码400返回
@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
@SuppressWarnings("serial")
InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
@Override
public int getHttpErrorCode() {
return 400;
}
};
return exceptionTranslator.translate(e400);
}
资源服务器
- 令牌认证拦截器
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 解析请求中的token
String token;
try {
token = this.bearerTokenResolver.resolve(request);
}
catch (OAuth2AuthenticationException invalid) {
this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid);
this.authenticationEntryPoint.commence(request, response, invalid);
return;
}
if (token == null) {
this.logger.trace("Did not process request since did not find bearer token");
filterChain.doFilter(request, response);
return;
}
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
try {
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
// 执行认证
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authenticationResult));
}
filterChain.doFilter(request, response);
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
}
}
- opaque token认证提供者
org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof BearerTokenAuthenticationToken)) {
return null;
}
BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
// 根据token内省获取principal
OAuth2AuthenticatedPrincipal principal = getOAuth2AuthenticatedPrincipal(bearer);
AbstractAuthenticationToken result = convert(principal, bearer.getToken());
result.setDetails(bearer.getDetails());
this.logger.debug("Authenticated token");
return result;
}
private OAuth2AuthenticatedPrincipal getOAuth2AuthenticatedPrincipal(BearerTokenAuthenticationToken bearer) {
try {
return this.introspector.introspect(bearer.getToken());
}
catch (BadOpaqueTokenException failed) { // 以无效令牌异常抛出
this.logger.debug("Failed to authenticate since token was invalid");
throw new InvalidBearerTokenException(failed.getMessage());
}
catch (OAuth2IntrospectionException failed) { // 内省失败,以认证服务异常抛出
throw new AuthenticationServiceException(failed.getMessage());
}
}
- opaque token 内省器
在调用内省请求和转换内省响应的逻辑中将非200的响应都以内省异常形式抛出,无法将授权错误的请求解析为TokenIntrospectionErrorResponse
org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector
public NimbusOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
Assert.notNull(introspectionUri, "introspectionUri cannot be null");
Assert.notNull(clientId, "clientId cannot be null");
Assert.notNull(clientSecret, "clientSecret cannot be null");
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
// 初始化restOperations
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
this.restOperations = restTemplate;
}
public OAuth2AuthenticatedPrincipal introspect(String token) {
RequestEntity<?> requestEntity = this.requestEntityConverter.convert(token);
if (requestEntity == null) {
throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity");
}
// 执行token校验请求 4XX响应以异常抛出
ResponseEntity<String> responseEntity = makeRequest(requestEntity);
// 响应转换 非200响应以OAuth2IntrospectionException抛出
HTTPResponse httpResponse = adaptToNimbusResponse(responseEntity);
// 解析内省响应
TokenIntrospectionResponse introspectionResponse = parseNimbusResponse(httpResponse);
// 非成功内省响应以OAuth2IntrospectionException抛出
TokenIntrospectionSuccessResponse introspectionSuccessResponse = castToNimbusSuccess(introspectionResponse);
if (!introspectionSuccessResponse.isActive()) {
this.logger.trace("Did not validate token since it is inactive");
throw new BadOpaqueTokenException("Provided token isn't active");
}
return convertClaimsSet(introspectionSuccessResponse);
}
// 执行内省请求
private ResponseEntity<String> makeRequest(RequestEntity<?> requestEntity) {
try {
// 此处restOperations的errorHander并未定制使用默认DefaultResponseErrorHandler,会导致状态码为4xx,5xx的响应都以异常抛出
return this.restOperations.exchange(requestEntity, String.class);
}
catch (Exception ex) {
// 此处将所有异常包装为内省异常(包括认证服务器返回400 invalid token)
throw new OAuth2IntrospectionException(ex.getMessage(), ex);
}
}
// 适配内省请求响应
private HTTPResponse adaptToNimbusResponse(ResponseEntity<String> responseEntity) {
HTTPResponse response = new HTTPResponse(responseEntity.getStatusCodeValue());
response.setHeader(HttpHeaders.CONTENT_TYPE, responseEntity.getHeaders().getContentType().toString());
response.setContent(responseEntity.getBody());
// 响应不是200就直接抛出内省异常
if (response.getStatusCode() != HTTPResponse.SC_OK) {
throw new OAuth2IntrospectionException("Introspection endpoint responded with " + response.getStatusCode());
}
return response;
}
// 解析内省请求响应
private TokenIntrospectionResponse parseNimbusResponse(HTTPResponse response) {
try {
return TokenIntrospectionResponse.parse(response);
}
catch (Exception ex) {
throw new OAuth2IntrospectionException(ex.getMessage(), ex);
}
}
// 转换为内省成功响应
private TokenIntrospectionSuccessResponse castToNimbusSuccess(TokenIntrospectionResponse introspectionResponse) {
if (!introspectionResponse.indicatesSuccess()) {
throw new OAuth2IntrospectionException("Token introspection failed");
}
return (TokenIntrospectionSuccessResponse) introspectionResponse;
}
- 默认响应错误处理器
org.springframework.web.client.DefaultResponseErrorHandler
public boolean hasError(ClientHttpResponse response) throws IOException {
int rawStatusCode = response.getRawStatusCode();
HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
}
protected boolean hasError(HttpStatus statusCode) {
return statusCode.isError(); // 此处4xx,5xx系列状态码都返回true
}
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
switch (statusCode.series()) {
case CLIENT_ERROR: // 4xx
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
case SERVER_ERROR: // 5xx
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
}
}
- 令牌内省响应
com.nimbusds.oauth2.sdk.TokenIntrospectionResponse
public static TokenIntrospectionResponse parse(final HTTPResponse httpResponse)
throws ParseException {
if (httpResponse.getStatusCode() == HTTPResponse.SC_OK) {
return TokenIntrospectionSuccessResponse.parse(httpResponse);
} else {
return TokenIntrospectionErrorResponse.parse(httpResponse);
}
}