Skip to content

Commit

Permalink
添加获取网关公钥的 readme (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-smile authored Dec 11, 2023
1 parent 2de55aa commit bb13100
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 91 deletions.
159 changes: 122 additions & 37 deletions sdks/apigw-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,78 +211,163 @@ resource_docs:

## 校验请求来自 API 网关

如果应用需要认证 API 网关传递过来的 JWT 信息,在 MIDDLEWARE 中加入:
### 场景一:Django 项目

要在后端服务中认证 API 网关传递过来的请求头 `X-Bkapi-JWT`,可以通过在 settings 中的 MIDDLEWARE 中添加以下 Django 中间件。这样,在请求处理过程中,会自动解析请求头中的 X-Bkapi-JWT,并将相关信息添加到 request 对象中。

```python
MIDDLEWARE += [
'apigw_manager.apigw.authentication.ApiGatewayJWTGenericMiddleware', # JWT 认证
'apigw_manager.apigw.authentication.ApiGatewayJWTAppMiddleware', # JWT 透传的应用信息
'apigw_manager.apigw.authentication.ApiGatewayJWTUserMiddleware', # JWT 透传的用户信息
"apigw_manager.apigw.authentication.ApiGatewayJWTGenericMiddleware", # JWT 认证,解析请求头中的 X-Bkapi-JWT,获取 request.jwt 对象
"apigw_manager.apigw.authentication.ApiGatewayJWTAppMiddleware", # 根据 request.jwt,获取 request.app 对象
]
```

> **请确保应用进程在启动前执行了 python manage.py fetch_apigw_public_key 命令,否则中间件可能无法正常工作**
> 如果因某些因素不方便使用命令自动获取网关公钥,可以在网关页面中手动获取公钥,配置到 `settings.APIGW_PUBLIC_KEY` 中。
添加以上两个中间件后,request 对象中将会添加 `request.jwt``request.app` 两个对象。这些对象包含了网关名、当前请求的蓝鲸应用 ID 等信息。具体内容可参考下文。

注意中间件的优先级,请加到其他中间件之前。
如果需要在 request 对象中获取当前请求用户 `request.user` 对象,除了上面的中间件外,还需要添加一个中间件以及 AUTHENTICATION_BACKENDS:

apigw_manager 默认提供了一个基于 User Model 实现的 authentication backend,如需使用,在 AUTHENTICATION_BACKENDS 中加入:
```python
# 添加中间件
MIDDLEWARE += [
"apigw_manager.apigw.authentication.ApiGatewayJWTUserMiddleware", # 根据 request.jwt,获取 request.user 对象
]

# 添加 AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS += [
'apigw_manager.apigw.authentication.UserModelBackend',
"apigw_manager.apigw.authentication.UserModelBackend",
]
```

### Django 中间件
注意,Django 中间件 ApiGatewayJWTGenericMiddleware 解析 `X-Bkapi-JWT` 时,需要获取网关公钥,SDK 默认从以下两个位置获取网关公钥:
- SDK model Context (库表 apigw_manager_context),需提前执行 `python manage.py fetch_apigw_public_key` 拉取并保存网关公钥
- settings.APIGW_PUBLIC_KEY,可在网关页面中手动获取公钥,并配置到 settings 中

#### Django 中间件

#### ApiGatewayJWTGenericMiddleware
认证 JWT 信息,在 `request` 中注入 `jwt` 对象,有以下属性:
##### ApiGatewayJWTGenericMiddleware
利用网关公钥,解析请求头中的 X-Bkapi-JWT,在 `request` 中注入 `jwt` 对象,有以下属性:
- `gateway_name`:传入的网关名称;

#### ApiGatewayJWTAppMiddleware
解析 JWT 中的应用信息,在 `request` 中注入 `app` 对象,有以下属性:
##### ApiGatewayJWTAppMiddleware
根据 `request.jwt`,在 `request` 中注入 `app` 对象,有以下属性:
- `bk_app_code`:调用接口的应用;
- `verified`:应用是否经过认证;

#### ApiGatewayJWTUserMiddleware
解析 JWT 中的用户信息,在 `request` 中注入 `user` 对象,该对象通过以下调用获取:
##### ApiGatewayJWTUserMiddleware
根据 `request.jwt`,在 `request` 中注入 `user` 对象:
- 如果用户通过认证:其为一个 Django User Model 对象,用户名为当前请求用户的用户名
- 如果用户未通过认证,其为一个 Django AnonymousUser 对象,用户名为当前请求用户的用户名

如果中间件 `ApiGatewayJWTUserMiddleware` 中获取用户的逻辑不满足需求,可以继承此中间件并自定义用户获取方法 `get_user`,例如::

```python
auth.authenticate(request, username=username, verified=verified)
class MyJWTUserMiddleware(ApiGatewayJWTUserMiddleware):
def get_user(self, request, gateway_name=None, bk_username=None, verified=False, **credentials):
...
return auth.authenticate(
request, gateway_name=gateway_name, bk_username=bk_username, verified=verified, **credentials
)
```

因此,请选择或实现合适的 authentication backend。
如果该中间件认证逻辑不符合应用预期,可继承此中间件,重载 `get_user` 方法进行调整;

### 用户认证后端
#### UserModelBackend
- 已认证的用户名,通过 `UserModel` 根据 `username` 获取用户,不存在时返回 `None`
- 未认证的用户名,返回 `AnonymousUser`,可通过继承后修改 `make_anonymous_user` 的实现来定制具体字段;
注意:在自定义中间件 `ApiGatewayJWTUserMiddleware` 时,如果继续使用 `auth.authenticate` 获取用户,请确保正确设置用户认证后端,以遵循 Django `AUTHENTICATION_BACKENDS` 相关规则。

### 本地开发测试
#### 用户认证后端

如果使用了 `ApiGatewayJWTGenericMiddleware` 中间件,在本地开发测试时在请求中带上合法的 JWT 是相对来说较困难的,这个时候我们可以通过使用测试用的 `JWTProvider` 来解决这个问题
##### UserModelBackend
- 已认证的用户名,根据 `UserModel` 创建一个用户对象,不存在时返回 `None`
- 未认证的用户名,返回 `AnonymousUser`,可通过继承后修改 `make_anonymous_user` 的实现来定制具体字段;

在项目根目录下创建 `local_provider.py` 文件,并提供测试用 `JWTProvider`
#### 本地开发测试

在 Django settings 中提供如下配置
本地开发测试时,接口可能未接入 API 网关,此时中间件 `ApiGatewayJWTGenericMiddleware` 无法获取请求头中的 X-Bkapi-JWT。
为方便测试,SDK 提供了一个 Dummy JWT Provider,用于根据环境变量直接构造一个 request.jwt 对象。

在项目中添加本地开发配置文件 `local_settings.py`,并将其导入到 settings;然后,在此本地开发配置文件中添加配置:
```python
BK_APIGW_JWT_PROVIDER_CLS = "apigw_manager.apigw.providers.DummyEnvPayloadJWTProvider"
```

同时提供以下环境变量

同时提供以下环境变量(非 Django settings)
```
APIGW_MANAGER_DUMMY_GATEWAY_NAME # JWT 中的网关名
APIGW_MANAGER_DUMMY_PAYLOAD_APP_CODE # JWT payload 中的 app_code
APIGW_MANAGER_DUMMY_PAYLOAD_USERNAME # JWT payload 中的 username
```

## FAQ
### 场景二:非 Django 项目

非 Django 项目,需要项目获取网关公钥,并解析请求头中的 X-Bkapi-JWT;获取网关公钥的方案请参考下文。

解析 X-Bkapi-JWT 时,可根据 jwt header 中的 kid 获取当前网关名,例如:
```
{
"iat": 1701399603,
"typ": "JWT",
"kid": "my-gateway", # 网关名称
"alg": "RS512" # 加密算法
}
```

可从 jwt 内容中获取网关认证的应用、用户信息,例如:
```
{
"user": { # 用户信息
"bk_username": "admin", # 用户名,解析时需同时支持 bk_username、username 两个 key,如 user.get("bk_username") or user.get("username", "")
"verified": true # 用户是否通过认证,true 表示通过认证,false 表示未通过认证
},
"app": { # 蓝鲸应用信息
"bk_app_code": "my-app", # 蓝鲸应用ID,解析时需同时支持 bk_app_code、app_code 两个 key,如 app.get("bk_app_code") or app.get("app_code", "")
"verified": true # 应用是否通过认证,true 表示通过认证,false 表示未通过认证
},
"exp": 1701401103, # 过期时间
"nbf": 1701399303, # Not Before 时间
"iss": "APIGW" # 签发者
}
```

### 如何获取网关公钥

后端服务如需解析 API 网关发送的请求头 X-Bkapi-JWT,需要提前获取该网关的公钥。获取网关公钥,有以下方案。

#### 1. 根据 SDK 提供的 Django Command 拉取

在同步网关数据时,直接添加以下 Command 拉取网关公钥。网关公钥将保存在 model Context 对应的库表 apigw_manager_context 中,SDK 提供的 Django 中间件将从表中读取网关公钥。

```bash
# 默认拉取 settings.BK_APIGW_NAME 对应网关的公钥
python manage.py fetch_apigw_public_key

# 拉取指定网关的公钥
python manage.py fetch_apigw_public_key --gateway-name my-gateway
```

#### 2. 直接获取网关公钥,配置到项目配置文件

服务仅需接入一些固定的网关部署环境时,可在网关管理端,网关基本信息中查询网关公钥,并配置到项目配置文件。

蓝鲸官方网关,需要自动注册并获取网关公钥,可联系蓝鲸官方运营同学,在服务部署前,由官方提前创建网关,并设置网关公钥、私钥,同时将网关公钥同步给后端服务。
具体可参考 helm-charts 仓库的 README。

#### 3. 通过网关公开接口,拉取网关公钥

API 网关提供了公钥查询接口,后端服务可按需根据接口拉取网关公钥,接口信息如下:
```bash
# 将 bkapi.example.com 替换为网关 API 地址,
# 将 gateway_name 替换为待查询公钥的网关名,
# 提供正确的蓝鲸应用账号
curl -X GET 'https://bkapi.example.com/api/bk-apigateway/prod/api/v1/apis/{gateway_name}/public_key/' \
-H 'X-Bkapi-Authorization: {"bk_app_code": "my-app", "bk_app_secret": "secret"}'
```

响应样例:

```json
{
"data": {
"public_key": "your public key"
}
}
```

#### Docker 镜像方案如何获得网关公钥
1. 可设置环境变量 `APIGW_PUBLIC_KEY_PATH`(默认值:apigateway.pub),同步后可读取该文件获取;
2. 可设置环境变量 `DATABASE_URL`,指定外部数据库,同步后可通过执行以下 SQL 查询:
```sql
select value from apigw_manager_context where scope="public_key" and key="<BK_APIGW_NAME>";
```
注意事项:
- 拉取公钥时,不能实时拉取,需要添加缓存(实时拉取会导致整体接口性能下降)
Loading

0 comments on commit bb13100

Please sign in to comment.