欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

记录使用Spring OAuth2RestTemplate 遇到的坑

程序员文章站 2022-03-03 19:37:43
...
   最近工作的一个项目中需要调用第三方提供的API获取数据,该API接口采用的授权方式是OAuth2,授权类型采用client_crdentials。鉴于Spring框架完善的生态系统我们直接采用了spring-security-oauth2 框架中提供的OAuth2客户端能力。现将使用框架过程中遇到的几个问题记录,供以后参考,以希望其他开发的同学能够避免踩坑。

(1)关于security.oauth2.client.*几个相关配置项容易混淆
  •     .security.oauth2.client.authentication-scheme
  •        这个配置项主要指定访问API时传输bearer token的方式,有四个可选值
           header,query,form,none,
           默认值为:header,表示在HTTP头中传输
  •     .security.oauth2.client.client-authentication-scheme
  •        这个配置项主要指定进行客户端认证时传输client_id,client-secret的方式,同样有四
           个可选值header,query,form,none,
           默认值为:header,表示在HTTP头中传输
  •     .security.oauth2.client.grant-type
  •         这个配置项主要指定进行客户端获取access_token的授予类型,有四个可选值
            authorization_code,client_credentials,password,implicit
            Spring OAuth2框架默认会采用authorization_code (授权码授予)access_token方式
            这里一定要记得根据API的access_token授予类型配置,例如我们访问的API实现的是
            client_crdentials 这种授予类型因此要配置成:
            security.oauth2.client.grant-type=client_crdentials

(2)  关于配置 OAuth2RestTemplate BEAN实例化时要传入正确的OAuth2资源保护类型
	public OAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
		super();
		if (resource == null) { //不能传空的
			throw new IllegalArgumentException("An OAuth2 resource must be supplied.");
		}

		this.resource = resource;
		this.context = context;
		[color=red]setErrorHandler(new OAuth2ErrorHandler(resource))[/color];//设置出错处理器
	}

       Spring OAuth2框架默认会使用AuthorizationCodeResourceDetails来映射配置项,例如果想要使用client_crdentials,就必须采用ClientCredentialsResourceDetails,这个类必须要自己实例化,否则框架默认会传AuthorizationCodeResourceDetails给你,这个地方要特别注意。
         
   @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
            OAuth2ProtectedResourceDetails details) {
        ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();//根据资源的访问授预类型来选取
         ....
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, oauth2ClientContext);
        ....
    }

      探究一下原码OAuth2RestTemplate容易发现,它内部委托AccessTokenProvider 获取access_token,内部管理着一AccessTokenProviderChain,如下所示:
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider> asList(
			new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
			new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));

在acquireAccessToken方法中委托
protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context){
   ....
   accessToken = accessTokenProvider.obtainAccessToken(resource, accessTokenRequest);
   ....
}

AccessTokenProviderChain 使用 List<AccessTokenProvider> chain 集合中的每一种AccessTokenProvider尝试根据OAuth2ProtectedResourceDetails的类型调用如下具体AccessTokenProvider的实现类完成access_token的获取,同样有4个实现类:1.ClientCredentialsAccessTokenProvider
2.AuthorizationCodeAccessTokenProvider
3.ImplicitAccessTokenProvider
4.ResourceOwnerPasswordAccessTokenProvider

每个实现都有一个supportsResource 方法检查配置的OAuth2资源保护类型自己是否能处理,以ClientCredentialsAccessTokenProvider为例
	public boolean supportsResource(OAuth2ProtectedResourceDetails resource) {
		return resource instanceof ClientCredentialsResourceDetails
				&& "client_credentials".equals(resource.getGrantType());
	}

(3)OAuth2RestTemplate 默认对错误的处理可能导致API返回的业务错误信息我们无法获取
     在前面的代码片段里我们知道OAuth2RestTemplate  把出错处理交给了类OAuth2ErrorHandler处理,这个类有一个重要的方法来判断响应状态码是否是4XX,5XX:
public boolean hasError(ClientHttpResponse response) throws IOException {
		return HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())
				|| this.errorHandler.hasError(response);
	}

然后OAuth2ErrorHandler又交给了默认的 DefaultResponseErrorHandler 来处理,该个类会将5xx的错误包装成HttpServerErrorException 走了WEB的出错理机制。因此通过ResponseEntity无法获业务错误信息。
      此时就需要定制OAuth2ErrorHandler,主要是重写hasError方法,如下所示:
     public class NoOpResponseErrorHandler extends OAuth2ErrorHandler {

    public NoOpResponseErrorHandler(OAuth2ProtectedResourceDetails resource) {
        super(resource);
    }
    
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        return response.getStatusCode().equals(HttpStatus.UNAUTHORIZED) ||
                response.getStatusCode().equals(HttpStatus.FORBIDDEN);
    }

}

然后通过调用OAuth2RestTemplate 的 setErrorHandler()方法注入
(4)关于Error creating bean with name 'scopedTarget.oauth2ClientContext' despite defining RequestContextListener 异常
    通过在配置类中添加
@Bean
@Order(0)
public RequestContextListener requestContextListener() {
    return new RequestContextListener();
}

或在
web.xml文件中添加片段
<listener>
 <listener-class>
        org.springframework.web.context.request.RequestContextListener
 </listener-class>
</listener>

总之想要快速处理问题就需要我们对其框架原理有基本的理解和研究,这样才能少走弯路
最后推荐几个链接,大家可以去了解一下
(1)Spring Boot and OAuth2
https://spring.io/guides/tutorials/spring-boot-oauth2/
(2)Protecting REST API with OAuth2
http://stackmirror.caup.cn/page/s18eicq1kmrc