Pig 适配 OAuth 2.0 客户端模式

为什么使用 OAuth 客户端模式

pig 框架内部服务间免鉴权采用的是 @Inner 注解 + FROM_IN header 的方式:

  • 外部请求携带 token 经过 Gateway ,token 逐层传递
  • 外部请求未携带 token (用户注册等)经过 Gateway ,通过security.oauth2.client.ignore-urls配置跳过鉴权
  • 内部请求携带 token 通过 feign 访问 ,token 逐层传递
  • 内部请求未携带 token(定时任务等),通过@RequestHeader(SecurityConstants.FROM),在过滤器中跳过验证携带特定 header 且使用了 @Inner (vlaue=true)注解的接口请求

在 Spring 体系下的服务可以兼容这套流程,但是其他语言框架下的服务如果想通过 OAuth 鉴权(如原子能力获取用户信息)的话,就需要重复造轮子以适配最后一种场景的逻辑

微服务内部之间流量安全的情况下,免鉴权可以解决大部分场景需求。但是基于零信任原则,每个服务的接口安全性不应该依赖于内部网络配置,每次微服务通信都应该经过身份验证

每个服务单独配置的免鉴权接口安全性难以保证,后期不易维护,增加开发人员心智负担

Inner注解

Feign

什么是 OAuth 客户端模式

1
2
3
4
5
6
7
+---------+                                  +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+

当客户端请求访问它所控制的,或者事先与授权服务器协商(所采用的方法超出了本规范的范围)的其他资源所有者的受保护资源,客户端可以只使用它的客户端凭据(或者其他受支持的身份验证方法)请求访问令牌。

https://datatracker.ietf.org/doc/html/rfc6749#section-4.4

客户端模式可以解决的问题

  • 定时任务、异步请求等方法跨服务调用时的 token 签发
  • 异构技术框架下的微服务鉴权统一入口(gateway),无需单独配置 api-key 或者各自实现鉴权逻辑
  • 开发新服务接口时无需要考虑权限,由业务调用来源(用户 or 服务端)决定 token 类型
  • 避免伪造 header 之类的攻击,增强系统安全性
  • 解决下游原子服务之间(可能存在)的鉴权需求

客户端模式存在的风险

  • 请求发起方和接收方都需要通过 OAuth 鉴权,会出现单点故障
  • 由于鉴权增加的网络开销,网络请求会变成 2n 次
  • 客户端模式签发的 token 不包含 refresh_token,需要根据需求自动延期

启用客户端模式

Client Credentials Token 签发

  1. 确认sys_oauthclient_details表中 client ogsp 对应的authorized_grant_types(授权类型)字段中包含client_credentials

pig 客户端模式使用

  1. AuthorizationServerConfiguration 中添加 Spring 官方的客户端模式转换器 OAuth2ClientCredentialsAuthenticationConverter()
1
2
3
4
5
6
7
8
private AuthenticationConverter accessTokenRequestConverter() {  
return new DelegatingAuthenticationConverter(
Arrays.asList(
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
  1. 通过客户端模式申请 token
1
2
3
4
curl --location --request POST 'http://127.0.0.1:3000/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic dGVzdDp0ZXN0' \
--data-urlencode 'grant_type=client_credentials'
  • 客户端模式申请 token 时通过 form 传参,pig 密码鉴权通过 url 传参的方式不符合 rfc 标准

https://gitee.com/log4j/pig/issues/I9FUG9

  • Basic 认证时使用sys_oauth_client_details表中配置的client_idclient_secret作为身份认证

  • scope 参数作为 token 的权限范围,暂未使用

  1. 使用获取到的 access_token 继续调用其他服务
1
2
3
4
5
6
{
"access_token": "cb5132d5-1815-4ada-9998-803d4148cdfb",
"scope": "server",
"token_type": "Bearer",
"expires_in": "43199"
}

Feign token 拦截替换

  1. 添加 oauth2-client 依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
  1. 添加 Spring Security 客户端信息配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
security:
oauth2:
client:
registration:
ogsp:
provider: spring
client-id: ogsp
client-secret: ogsp
scope: server
authorization-grant-type: client_credentials
provider:
spring:
token-uri: http://127.0.0.1:9999/auth/oauth2/token
  1. 注册客户端模式接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
private final ClientRegistrationRepository clientRegistrationRepository;

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager() {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();

AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
  1. 注册 Feign 拦截器替换 token
1
2
3
4
@Bean
public RequestInterceptor oauthFeignRequestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager) {
return new OgspFeignRequestClientCredentialsInterceptor(authorizedClientManager);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RequiredArgsConstructor
public class OgspFeignRequestClientCredentialsInterceptor implements RequestInterceptor {

public static final String CLIENT_ID = "ogsp";

private final OAuth2AuthorizedClientManager authorizedClientManager;

@Override
public void apply(RequestTemplate template) {

Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
// 客户端认证替换 Authorization
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_CLIENT)) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(CLIENT_ID)
.principal(CLIENT_ID)
.build();

OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

if (authorizedClient != null && authorizedClient.getAccessToken() != null) {
template.header("Authorization", "Bearer " + authorizedClient.getAccessToken().getTokenValue());
}
}
}
}
1
2
3
4
5
// 带from 请求直接跳过
if (CollUtil.isNotEmpty(fromHeader) && (fromHeader.contains(SecurityConstants.FROM_IN) || fromHeader.contains(
SecurityConstants.FROM_CLIENT))) {
return;
}
  • 继承原 pig 使用 from header 判断请求来源的方式

  • from=C 表示客户端请求

  • 通过 OAuth 获取客户端模式 token 替换请求 token

  1. 后台请求调用 Feign 接口时通过SecurityConstants.FROM参数传递 token
1
R<SysLDAPEntry> entryR = remoteLDAPService.getLDAPAuth(sysLDAPEntry, SecurityConstants.FROM_CLIENT);