51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Feign 客户端设置 Oauth2 Token

1、概览 {#1概览}

OpenFeign 是一个可以在 Spring Boot 中使用的声明式 REST 客户端。

假如想通过 OpenFeign 来调用使用 OAuth2 的 REST API,那么就需要给 OpenFeign 设置 Access Token。

本文将会带你了解如何为 OpenFeign 客户端添加 OAuth2 支持。

2、服务之间的认证 {#2服务之间的认证}

服务之间的认证 API 安全中的一个热门话题。我们可以使用 mTLSJWT 为 REST API 提供认证机制。不过,OAuth2 协议是保护 API 的事实解决方案。假设我们想使用另一个服务(客户端)调用一个受保护的服务(服务器)。在这种情况下,使用 "客户端凭证(client credential)" 授权方式。这种授权方式通常用于在两个没有终端用户的 API 或系统之间进行身份认证。

下图显示了这种授权模式中的的主要角色关系:

客户端凭证授权模式

在客户端凭证模式中,客户端通过 Token Endpoint 从授权服务器(Authorization Server)获取 Access Token。然后,客户端使用 Access Token 访问源服务器(Resource Server)上受资保护的资源。资源服务器会验证 Access Token,如果有效,则为请求提供服务。

2.1、授权服务器 {#21授权服务器}

创建一个授权服务器来发放 Access Token。为了方便,我们在 Spring Boot 中使用嵌入式 Keycloak。假设我们使用 GitHub 上的 授权服务器 项目。

首先,在嵌入式 Keycloak 服务器的 realm 管理中定义 Payment-app 客户端:

Payment-app 客户端

将 "Access Type" 设置为 "credential",并启用 "Service Accounts Enabled" 选项。然后,以 feign-realm.json 的形式导出 realm 详细信息,并在 application-feign.yml 中设置 realm 文件:

keycloak:
  server:
    contextPath: /auth
    adminUser:
      username: bael-admin
      password: pass
    realmImportFile: feign-realm.json

现在,授权服务器已经准备就绪。可以使用 -spring.profiles.active=feign 选项运行应用。本文重点在于介绍 OpenFeign OAuth2 支持,因此这里不需要深入了解。

2.2、资源服务器 {#22资源服务器}

现在我们已经配置了授权服务器,下面来设置资源服务器。

使用 GitHub 上的 资源服务器 项目。首先,添加 Payment 类资源:

public class Payment {

    private String id;
    private double amount;

   // get / set 方法省略
}

然后,声明一个 PaymentController API。

@RestController
public class PaymentController {

    @GetMapping("/payments")
    public List<Payment> getPayments() {
        List<Payment> payments = new ArrayList<>();
        for(int i = 1; i < 6; i++){
            Payment payment = new Payment();
            payment.setId(String.valueOf(i));
            payment.setAmount(2);
            payments.add(payment);
        }
        return payments;
    }

}

getPayments() API 会返回 payments 列表。此外,还在 application-feign.yml 文件中配置了资源服务器:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/master

现在,getPayments() API 使用 OAuth2 授权服务器保证安全,我们必须提供有效的 Access Token 才能调用该 API:

curl --location --request POST 'http://localhost:8083/auth/realms/master/protocol/openid-connect/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=payment-app' \
  --data-urlencode 'client_secret=863e9de4-33d4-4471-b35e-f8d2434385bb' \
  --data-urlencode 'grant_type=client_credentials'

获取 Access Token 后,将其设置在请求的 Authorization Header 中:

curl --location --request GET 'http://localhost:8081/resource-server-jwt/payments' \
  --header 'Authorization: Bearer Access_Token' 

现在,我们要使用 OpenFeign 而不是 cURL 或 Postman 调用受保护 API。

3、OpenFeign 客户端 {#3openfeign-客户端}

3.1、依赖 {#31依赖}

pom.xml 中添加 spring-cloud-starter-openfeign 依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.0</version>
</dependency>

此外,还需要在 pom.xml 中添加 spring-cloud-dependencies

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>2021.0.0</version>
    <type>pom</type>
</dependency>

3.2、配置 {#32配置}

首先,需要在 main 类中添加 @EnableFeignClients 注解:

@SpringBootApplication
@EnableFeignClients
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

然后,定义 PaymentClient 接口,用于调用 getPayments() API。此外,还需要在 PaymentClient 接口中添加 @FeignClient 注解:

@FeignClient(
  name = "payment-client", 
  url = "http://localhost:8081/resource-server-jwt", 
  configuration = OAuthFeignConfig.class)
public interface PaymentClient {

    @RequestMapping(value = "/payments", method = RequestMethod.GET)
    List<Payment> getPayments();
}

根据资源服务器的地址设置 url。在本例中,@FeignClient 的主要参数是支持 OpenFeign OAuth2 的 configuration 属性。

然后,定义一个 PaymentController 类,并将 PaymentClient 注入其中:

@RestController
public class PaymentController {

    private final PaymentClient paymentClient;

    public PaymentController(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }

    @GetMapping("/payments")
    public List<Payment> getPayments() {
        List<Payment> payments = paymentClient.getPayments();
        return payments;
    }
}

4、OAuth2 的支持 {#4oauth2-的支持}

4.1、依赖 {#41依赖}

pom.xml 文件中添加 spring-security-oauth2-clientspring-boot-starter-security 依赖,为 Spring Cloud OpenFeign 添加 OAuth2 支持:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>5.6.0</version>
</dependency>

4.2、配置 {#42配置}

现在,创建一个配置。我们的想法是获取 Access Token 并将其添加到 OpenFeign 请求中。Feign 提供了一项实用功能 - 拦截器(Interceptor),可以为每个 HTTP 请求/响应执行此任务。

使用 RequestInterceptor,通过 Authorization Bearer Header 将 OAuth2 Access Token 注入 OpenFeign 客户端的请求中。

定义 OAuthFeignConfig 配置类并定义 requestInterceptor() Bean:

@Configuration
public class OAuthFeignConfig {

    public static final String CLIENT_REGISTRATION_ID = "keycloak";

    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    public OAuthFeignConfig(OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
      ClientRegistrationRepository clientRegistrationRepository) {
        this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Bean
    public RequestInterceptor requestInterceptor() {
        ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(CLIENT_REGISTRATION_ID);
        OAuthClientCredentialsFeignManager clientCredentialsFeignManager =
          new OAuthClientCredentialsFeignManager(authorizedClientManager(), clientRegistration);
        return requestTemplate -> {
            requestTemplate.header("Authorization", "Bearer " + clientCredentialsFeignManager.getAccessToken());
        };
    }
}

requestInterceptor() Bean 中,使用 ClientRegistrationOAuthClientCredentialsFeignManager 类来注册 oauth2 客户端,并从授权服务器获取 Access Token。为此,需要在 application.properties 文件中定义 oauth2 客户端属性:

spring.security.oauth2.client.registration.keycloak.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.keycloak.client-id=payment-app
spring.security.oauth2.client.registration.keycloak.client-secret=863e9de4-33d4-4471-b35e-f8d2434385bb
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8083/auth/realms/master/protocol/openid-connect/token

创建 OAuthClientCredentialsFeignManager 类并定义 getAccessToken() 方法:

public String getAccessToken() {
    try {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
          .withClientRegistrationId(clientRegistration.getRegistrationId())
          .principal(principal)
          .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }
        return client.getAccessToken().getTokenValue();
    } catch (Exception exp) {
        logger.error("client credentials error " + exp.getMessage());
    }
    return null;
}

使用 OAuth2AuthorizeRequestOAuth2AuthorizedClient 类从授权服务器获取 Access Token。现在,对于每个请求,OpenFeign 拦截器都会管理 oauth2 客户端,并将访 Access Token 添加到请求中。

5、测试 {#5测试}

创建 PaymentClientUnitTest 类,测试 OpenFeign 客户端:

@RunWith(SpringRunner.class)
@SpringBootTest
public class PaymentClientUnitTest {

    @Autowired
    private PaymentClient paymentClient;

    @Test
    public void whenGetPayment_thenListPayments() {
        List<Payment> payments = paymentClient.getPayments();
        assertFalse(payments.isEmpty());
    }
}

如上,调用了 getPayments() API。PaymentClient 会连接到 OAuth2 客户端,并使用拦截器获取 Access Token。

6、总结 {#6总结}

本文介绍了如何在 OpenFeign 中配置 RequestInterceptor 为请求添加 Access Token 以访问 Oauth2 服务。


参考:https://www.baeldung.com/spring-cloud-feign-oauth-token

赞(0)
未经允许不得转载:工具盒子 » Feign 客户端设置 Oauth2 Token