为什么使用 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 签发
- 确认
sys_oauthclient_details
表中 client ogsp 对应的authorized_grant_types
(授权类型)字段中包含client_credentials
pig 客户端模式使用
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())); }
|
- 通过客户端模式申请 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
- 使用获取到的 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 拦截替换
- 添加
oauth2-client
依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency>
|
- 添加 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 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; }
|
- 注册 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); 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
| if (CollUtil.isNotEmpty(fromHeader) && (fromHeader.contains(SecurityConstants.FROM_IN) || fromHeader.contains( SecurityConstants.FROM_CLIENT))) { return; }
|
- 后台请求调用 Feign 接口时通过
SecurityConstants.FROM
参数传递 token
1
| R<SysLDAPEntry> entryR = remoteLDAPService.getLDAPAuth(sysLDAPEntry, SecurityConstants.FROM_CLIENT);
|