1. 概要

OpenFeign は、 SpringBootアプリケーションで使用できる宣言型RESTクライアントです。 OAuth2を使用して保護されたRESTAPIがあり、OpenFeignを使用してそれを呼び出したいと仮定します。 この状況では、OpenFeignでアクセストークンを提供する必要があります。

このチュートリアルでは、OpenFeignクライアントにOAuth2サポートを追加する方法について説明します。

2. サービス間認証

サービス間認証は、APIセキュリティで人気のあるトピックです。 mTLSまたはJWTを使用して、RESTAPIの認証メカニズムを提供できます。 ただし、 OAuth2プロトコルは、APIを保護するための事実上のソリューションです。 別のサービス(クライアントロール)を使用してセキュアサービス(サーバーロール)を呼び出したいとします。 このシナリオでは、クライアントクレデンシャル付与タイプを使用します。 通常、2つのAPIまたはエンドユーザーのないシステム間の認証にはクライアントクレデンシャルを使用します。 次の図は、この付与タイプの主なアクターを示しています。

クライアント資格情報では、クライアントサービスはトークンエンドポイントを使用して承認サーバーからアクセストークンを取得します。 次に、アクセストークンを使用して、リソースサーバーによって保護されているリソースにアクセスします。 リソースサーバーはアクセストークンを検証し、有効な場合はリクエストを処理します。

2.1. 承認サーバー

アクセストークンを発行するための認証サーバーを設定しましょう。 今のところ簡単にするために、Spring Bootアプリケーションに埋め込まれたKeycloakを使用します。 GitHubで利用可能な認証サーバープロジェクトを使用するとします。 まず、組み込みのKeycloakサーバーのレルムmasterpayment-appクライアントを定義します。

アクセスタイプクレデンシャルに設定し、サービスアカウントの有効化オプションを有効にします。 次に、レルムの詳細を feign-realm.json としてエクスポートし、レルムファイルをapplication-feign.ymlに設定します。

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

これで、認証サーバーの準備が整いました。 最後に、 –spring.profiles.active =feignオプションを使用してアプリケーションを実行できます。 このチュートリアルではOpenFeignOAuth2のサポートに焦点を当てているため、これ以上深く掘り下げる必要はありません。

2.2. リソースサーバー

承認サーバーを構成したので、リソースサーバーを設定しましょう。 そのために、リソースサーバープロジェクトを使用します GitHubで入手可能 。 まず、Paymentクラスをリソースとして追加します。

public class Payment {

    private String id;
    private double amount;

   // standard getters and setters
}

次に、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は、支払いのリストを返します。 また、application-feign.ymlファイルでリソースサーバーを構成します。

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

これで、 getPayments() APIはOAuth2承認サーバーを使用して安全になり、この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'

アクセストークンを取得したら、リクエストのAuthorizationヘッダーに設定します。

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

ここで、cURLまたはPostmanの代わりにOpenFeignを使用してセキュアAPIを呼び出します。

3. OpenFeignクライアント

3.1. 依存関係

Spring Cloud OpenFeignを使用してセキュアAPIを呼び出すには、 spring-cloud-starter-openfeignpom.xmlファイルに追加する必要があります。

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

さらに、spring-cloud-dependenciespom.xmlに追加する必要があります。

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

3.2. 構成

まず、メインクラスに@EnableFeignClientsを追加する必要があります。

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

次に、 getPayments()APIを呼び出すためのPaymentClientインターフェイスを定義します。 また、@FeignClientPaymentClientインターフェイスに追加する必要があります。

@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サポート

4.1. 依存関係

OAuth2サポートをSpring CloudOpenFeignに追加するには、 spring-security-oauth2-clientおよびspring-boot-starter-securityをに追加する必要があります。 pom.xml ファイル:

<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. 構成

次に、構成を作成します。 OpenFeignリクエストにアクセストークンを取得して追加するという考え方です。インターセプターはすべてのHTTPリクエスト/レスポンスに対してこのタスクを実行できます。 インターセプターの追加は、Feignが提供する便利な機能です。 RequestInterceptorを使用します。これは、Authorization Bearerヘッダーを追加することにより、OpenFeignクライアントのリクエストにOAuth2アクセストークンを挿入します。 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では、ClientRegistrationクラスとOAuthClientCredentialsFeignManagerクラスを使用して、oauth2クライアントを登録し、承認サーバーからアクセストークンを取得します。 これを行うには、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;
}

承認サーバーからアクセストークンを取得するには、OAuth2AuthorizeRequestクラスとOAuth2AuthorizedClientクラスを使用します。 これで、すべてのリクエストに対して、OpenFeignインターセプターがoauth2クライアントを管理し、リクエストにアクセストークンを追加します。

5. テスト

OpenFeignクライアントをテストするために、PaymentClientUnitTestクラスを作成しましょう。

@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クライアントに接続し、インターセプターを使用してアクセストークンを取得します。

6. 結論

この記事では、安全なAPIを呼び出すために必要な環境を設定します。 次に、実際の例を通じてセキュアAPIを呼び出すようにOpenFeignを構成します。 このために、OpenFeignにインターセプターを追加して構成します。 インターセプターはOAuth2クライアントを管理し、アクセストークンをリクエストに追加します。

いつものように、このチュートリアルの完全なソースコードは、GitHubから入手できます。 さらに、リソースと認証サーバーのソースコードはGitHubで入手できます。