2. 服务间认证

服务间认证是API安全领域的热门话题。我们可以使用mTLS或JWT为REST API提供认证机制,但OAuth2协议才是保护API的事实标准。假设我们要用另一个服务(客户端角色)调用一个安全服务(服务端角色),这种场景下适合使用客户端凭证授权模式。客户端凭证通常用于两个API或系统间的认证,无需终端用户参与。下图展示了该授权模式中的主要参与者:

openfeign client credential1

在客户端凭证模式中:

  1. 客户端服务通过token接口从授权服务器获取访问令牌
  2. 使用该令牌访问资源服务器保护的资源
  3. 资源服务器验证令牌有效性,通过后处理请求

2.1. 授权服务器

我们先搭建一个授权服务器来签发访问令牌。为简化演示,我们将使用嵌入Spring Boot的Keycloak。假设使用GitHub上可用的授权服务器项目。首先在嵌入的Keycloak服务器的master域中定义payment-app客户端:

openfeign payment client1

设置Access Type为credential,并启用Service Accounts Enabled选项。然后将域配置导出为feign-realm.json,在application-feign.yml中配置:

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

现在授权服务器已就绪。使用--spring.profiles.active=feign参数启动应用即可。由于本文重点在OpenFeign的OAuth2支持,此处不再深入细节。

2.2. 资源服务器

配置好授权服务器后,我们来搭建资源服务器。使用GitHub上可用的资源服务器项目。首先定义Payment资源类:

public class Payment {

    private String id;
    private double amount;

   // 标准getter/setter
}

在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保护,调用时必须提供有效访问令牌:

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' 

现在我们要用OpenFeign替代cURL或Postman来调用这个安全API。

3. OpenFeign客户端

3.1. 依赖项

在pom.xml中添加以下依赖使用Spring Cloud OpenFeign:

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

同时添加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. 配置

首先在主类添加@EnableFeignClients注解:

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

然后定义PaymentClient接口调用getPayments() API,并添加@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的关键参数是configuration属性,它为OpenFeign提供OAuth2支持。接着创建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. 依赖项

在pom.xml中添加以下依赖启用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. 配置

核心思路是获取访问令牌并添加到OpenFeign请求中拦截器能为每个HTTP请求/响应完成这个任务。Feign提供的拦截器功能非常实用。我们将使用RequestInterceptor,通过添加Authorization Bearer请求头,将OAuth2访问令牌注入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中,使用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. 测试

创建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客户端并获取访问令牌。

6. 总结

本文搭建了调用安全API所需的环境,通过实际示例配置OpenFeign调用安全API。关键步骤是为OpenFeign添加并配置拦截器,该拦截器管理OAuth2客户端并将访问令牌添加到请求中。

完整源代码可在GitHub获取。授权服务器和资源服务器源代码可在GitHub获取


原始标题:Provide an OAuth2 Token to a Feign Client | Baeldung