2. 服务间认证
服务间认证是API安全领域的热门话题。我们可以使用mTLS或JWT为REST API提供认证机制,但OAuth2协议才是保护API的事实标准。假设我们要用另一个服务(客户端角色)调用一个安全服务(服务端角色),这种场景下适合使用客户端凭证授权模式。客户端凭证通常用于两个API或系统间的认证,无需终端用户参与。下图展示了该授权模式中的主要参与者:
在客户端凭证模式中:
- 客户端服务通过token接口从授权服务器获取访问令牌
- 使用该令牌访问资源服务器保护的资源
- 资源服务器验证令牌有效性,通过后处理请求
2.1. 授权服务器
我们先搭建一个授权服务器来签发访问令牌。为简化演示,我们将使用嵌入Spring Boot的Keycloak。假设使用GitHub上可用的授权服务器项目。首先在嵌入的Keycloak服务器的master域中定义payment-app客户端:
设置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客户端并将访问令牌添加到请求中。