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);
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
回答: 在你的配置中考虑定义一个类型为'org.springframework.security.oauth2.provider.token.RemoteTokenServices'的bean。根据引用和引用中的信息,可能需要对依赖进行排除和修改,以避免与spring cloud gateway的webflux冲突。你可以尝试使用以下配置来解决这个问题: ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </exclusion> </exclusions> </dependency> ``` 通过这样的配置,你可以排除掉与spring cloud gateway的webflux冲突的依赖,并定义一个类型为'org.springframework.security.oauth2.provider.token.RemoteTokenServices'的bean来解决问题。123 #### 引用[.reference_title] - *1* [解决:Consider defining a bean of type ‘org.springframework.web.client.RestTemplate‘ in your](https://blog.csdn.net/weixin_51426680/article/details/112545171)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* [springCloud Gateway 报错:Consider defining a bean of type ‘org.springframework](https://blog.csdn.net/m0_67393619/article/details/124043478)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *3* [springCloud如果遇到网关问题Consider defining a bean of type 'org.springframework....](https://blog.csdn.net/wenhao24725/article/details/103980540)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路过君_P

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值