第一次提交

This commit is contained in:
2026-03-13 16:40:08 +08:00
commit 0a49bcfb2d
277 changed files with 61890 additions and 0 deletions

628
authz-service/src/README.md Normal file
View File

@ -0,0 +1,628 @@
# Authz-service
## 概览
这是一个基于 FastAPI 的认证授权服务,当前实现主要提供 4 类能力:
1. 后端实例注册与管理
2. 用户注册时自动绑定/创建后端
3. 基于 OAuth 2.0 `client_credentials` 的 access token 签发
4. token 内省、权限配置、Outlook 配置管理
文档内容按当前代码实现整理,接口定义来源于 [app/main.py](/home/ivan/xuan/steven_project/Authz-service/app/main.py)。
## 鉴权模型
### 1. 当前真正支持的登录方式
当前服务没有“用户名 + 密码登录”接口。
真正用于获取 access token 的方式只有一种:
- OAuth 2.0 `client_credentials`
- 调用接口:`POST /oauth/token`
- 需要提供:`client_id``client_secret``aud`
- 可选提供:`scope`
也就是说:
- `username` / `password` 只在 `POST /oauth/register` 注册阶段出现
- 其中 `password` 当前仅做“必填校验”,不会被保存,也不会参与后续登录或鉴权
### 2. 内部接口鉴权
以下接口要求请求头带内部 Bearer Token
- `GET /internal/backends/{backend_id}/settings/outlook`
- `POST /oauth/introspect`
请求头格式:
```http
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
```
服务会将 Bearer Token 与环境变量 `AUTHZ_INTERNAL_TOKEN` 做精确匹配。
### 3. Token 签名与校验
- 签名算法:`RS256`
- JWKS 地址:`GET /.well-known/jwks.json`
- OAuth 元数据地址:`GET /.well-known/oauth-authorization-server`
- access token 默认有效期:`3600` 秒,可通过环境变量 `AUTHZ_ACCESS_TOKEN_TTL_SECONDS` 调整
## 基础信息
- 默认 `issuer``http://127.0.0.1:19090`
- 默认 `token_endpoint``/oauth/token`
- 默认 `introspection_endpoint``/oauth/introspect`
- 默认只声明支持 `client_secret_post`
如果服务已启动FastAPI 默认也会提供:
- `GET /docs`
- `GET /openapi.json`
## 典型流程
### 流程 A注册用户并自动创建后端
1. 调用 `POST /oauth/register`
2. 服务创建或更新 backend
3. 服务返回 `backend_id``client_id`
4. 如果 backend 是首次创建,还会返回一次性的 `client_secret`
5. 客户端使用 `client_id + client_secret + aud` 调用 `POST /oauth/token` 换取 access token
### 流程 B单独注册 backend
1. 调用 `POST /backends/register`
2. 记录返回的 `client_id``client_secret`
3. 配置 `POST /backends/{backend_id}/permissions`
4.`POST /oauth/token` 获取 token
## 注册时需要提供什么信息
### 用户注册接口:`POST /oauth/register`
请求体字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `username` | 是 | `string` | 用户名,去掉首尾空格后不能为空 |
| `password` | 是 | `string` | 当前仅用于必填校验,不会被持久化,也不会用于后续登录 |
| `email` | 否 | `string` | 用户邮箱 |
| `name` | 否 | `string` | backend 名称的候选值之一 |
| `backend_name` | 否 | `string` | backend 名称 |
| `backend_id` | 否 | `string` | 指定 backend 唯一标识;不传则自动生成 |
| `base_url` | 否 | `string` | backend 服务地址 |
| `public_base_url` | 否 | `string` | `base_url` 的候选字段之一 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
| `backend` | 否 | `object` | 嵌套 backend 配置,优先级高于同名顶层字段 |
其中真正影响 backend 创建/更新的有效字段有:
- backend 名称:按以下优先级取值
`backend.name` -> `backend_name` -> `name` -> `username`
- backend id按以下优先级取值
`backend.backend_id` -> `backend_id` -> 自动生成
- backend 服务地址:按以下优先级取值
`backend.base_url` -> `public_base_url` -> `base_url`
- backend 前端地址:按以下优先级取值
`backend.frontend_base_url` -> `frontend_base_url`
`base_url` 最终必须能解析出有效值,否则会返回 `400 base_url is required`
#### 推荐最小请求体
```json
{
"username": "alice",
"password": "any-non-empty-value",
"email": "alice@example.com",
"backend_name": "Alice Workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com"
}
```
#### 使用嵌套 backend 的请求体
```json
{
"username": "alice",
"password": "any-non-empty-value",
"email": "alice@example.com",
"backend": {
"name": "Alice Workspace",
"backend_id": "alice-workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com"
}
}
```
#### 成功响应
```json
{
"user": {
"username": "alice",
"email": "alice@example.com",
"default_backend_id": "alice-workspace",
"created_at": "2026-03-13T08:00:00+00:00",
"updated_at": "2026-03-13T08:00:00+00:00"
},
"backend": {
"backend_id": "alice-workspace",
"client_id": "alice-workspace",
"client_secret": "generated-secret-only-when-created",
"name": "Alice Workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com",
"status": "active",
"created_at": "2026-03-13T08:00:00+00:00"
}
}
```
#### 特别说明
- 如果 `backend_id` 不存在,服务会新建 backend并返回新的 `client_secret`
- 如果 `backend_id` 已存在,服务会更新该 backend 信息,但 `client_secret` 会返回 `null`
- 用户数据当前只保存:`username``email``default_backend_id`
- 当前实现没有用户密码校验、密码存储、密码登录、刷新 token
### 后端注册接口:`POST /backends/register`
请求体字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | 是 | `string` | backend 名称 |
| `base_url` | 是 | `string` | backend 服务地址 |
| `backend_id` | 否 | `string` | 指定 backend id不传则按 `name` 自动生成 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
示例:
```json
{
"name": "Local Backend",
"base_url": "https://api.example.com",
"backend_id": "local-backend",
"frontend_base_url": "https://app.example.com"
}
```
成功响应:
```json
{
"backend_id": "local-backend",
"client_id": "local-backend",
"client_secret": "generated-secret",
"created_at": "2026-03-13T08:00:00+00:00",
"frontend_base_url": "https://app.example.com"
}
```
## 登录/鉴权时需要什么信息
### 换取 access token`POST /oauth/token`
这是当前服务唯一的“登录/鉴权入口”。
支持两种提交方式:
- `application/x-www-form-urlencoded`
- `application/json`
请求字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `grant_type` | 否 | `string` | 仅支持 `client_credentials`,默认值也是这个 |
| `client_id` | 是 | `string` | 注册 backend 后得到,默认等于 `backend_id` |
| `client_secret` | 是 | `string` | 注册或轮转密钥时得到 |
| `aud` | 是 | `string` | 目标 audience决定可申请哪些 scope |
| `scope` / `scopes` | 否 | `string` / `string[]` | 需要的权限范围;不传则自动下发该 audience 下允许的全部 scope |
说明:
- 表单模式下使用字段 `scope`,多个 scope 用空格分隔
- JSON 模式下使用字段 `scopes`,类型为字符串数组
- JSON 模式下也可以把 `aud` 换成 `resource` 吗:不可以。`resource` 只在表单解析分支里作为 `aud` 的兜底字段
#### 表单请求示例
```bash
curl -X POST http://127.0.0.1:19090/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=local-backend" \
-d "client_secret=<client-secret>" \
-d "aud=mcp:outlook" \
-d "scope=list_tools tool:mail_list_messages"
```
#### JSON 请求示例
```json
{
"grant_type": "client_credentials",
"client_id": "local-backend",
"client_secret": "<client-secret>",
"aud": "mcp:outlook",
"scopes": [
"list_tools",
"tool:mail_list_messages"
]
}
```
#### 成功响应
```json
{
"access_token": "<jwt>",
"token_type": "bearer",
"expires_in": 3600
}
```
### `aud` 和 `scope` 的校验规则
当前代码只支持两类 audience
#### 1. MCP audience
格式:
```text
mcp:<server_id>
```
例如:
```text
mcp:outlook
```
只有当权限配置满足以下条件时才能签发 token
```json
{
"mcp": {
"outlook": {
"enabled": true,
"tools": [
"mail_list_messages",
"mail_send_email"
]
}
}
}
```
这时允许的 scope 会自动变成:
- `list_tools`
- `tool:mail_list_messages`
- `tool:mail_send_email`
#### 2. A2A audience
格式:
```text
a2a:<agent_id>
```
例如:
```text
a2a:planner
```
只有当权限配置满足以下条件时才能签发 token
```json
{
"a2a": {
"enabled": true,
"agents": [
"planner"
]
}
}
```
这时允许的 scope 只有:
- `run_task`
#### 3. 请求 scope 的规则
- 如果不传 `scope/scopes`,服务会返回该 audience 下允许的全部 scope
- 如果传了 `scope/scopes`,必须是允许 scope 的子集,否则返回 `403 Requested scopes exceed backend permissions`
- 如果 audience 未启用,返回 `403 Audience is not enabled for this backend`
### Token 内省:`POST /oauth/introspect`
此接口是内部接口,必须带内部 Bearer Token。
请求头:
```http
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
```
请求体:
```json
{
"token": "<access-token>"
}
```
成功响应:
```json
{
"active": true,
"client_id": "local-backend",
"backend_id": "local-backend",
"aud": "mcp:outlook",
"scp": [
"list_tools",
"tool:mail_list_messages"
],
"exp": 1773392400
}
```
token 无效时返回:
```json
{
"active": false
}
```
## 接口清单
### 公开接口
| 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| `GET` | `/healthz` | 无 | 健康检查 |
| `GET` | `/.well-known/oauth-authorization-server` | 无 | OAuth 元数据 |
| `GET` | `/.well-known/jwks.json` | 无 | JWKS 公钥 |
| `POST` | `/backends/register` | 无 | 注册 backend |
| `GET` | `/backends` | 无 | 查询全部 backend |
| `GET` | `/backends/{backend_id}` | 无 | 查询单个 backend |
| `PUT` | `/backends/{backend_id}` | 无 | 更新 backend |
| `POST` | `/backends/{backend_id}/disable` | 无 | 禁用 backend |
| `POST` | `/backends/{backend_id}/enable` | 无 | 启用 backend |
| `POST` | `/backends/{backend_id}/rotate-secret` | 无 | 轮转 client secret |
| `GET` | `/backends/{backend_id}/permissions` | 无 | 查询权限配置 |
| `POST` | `/backends/{backend_id}/permissions` | 无 | 保存权限配置 |
| `GET` | `/backends/{backend_id}/settings/outlook` | 无 | 查询 Outlook 配置,密码会被隐藏 |
| `POST` | `/backends/{backend_id}/settings/outlook` | 无 | 保存 Outlook 配置 |
| `DELETE` | `/backends/{backend_id}/settings/outlook` | 无 | 删除 Outlook 配置 |
| `POST` | `/oauth/register` | 无 | 用户注册并绑定 backend |
| `POST` | `/oauth/token` | 无 | 使用 client credentials 获取 token |
### 内部接口
| 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| `GET` | `/internal/backends/{backend_id}/settings/outlook` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 查询完整 Outlook 配置,包含密码 |
| `POST` | `/oauth/introspect` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 内省 token |
## 详细接口说明
### 1. `GET /healthz`
响应:
```json
{
"status": "ok"
}
```
### 2. `GET /.well-known/oauth-authorization-server`
响应示例:
```json
{
"issuer": "http://127.0.0.1:19090",
"token_endpoint": "http://127.0.0.1:19090/oauth/token",
"introspection_endpoint": "http://127.0.0.1:19090/oauth/introspect",
"jwks_uri": "http://127.0.0.1:19090/.well-known/jwks.json",
"grant_types_supported": [
"client_credentials"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post"
]
}
```
### 3. `GET /.well-known/jwks.json`
返回 RSA 公钥集合,用于校验 `RS256` access token。
### 4. `GET /backends`
返回全部 backend 列表。
### 5. `GET /backends/{backend_id}`
- backend 不存在时返回 `404 Backend not found`
### 6. `PUT /backends/{backend_id}`
请求体字段:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | 否 | `string` | backend 名称 |
| `base_url` | 否 | `string` | backend 服务地址 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
说明:
- 只有非空字段才会更新
- 当前实现不支持把已有字段显式清空为 `null`
### 7. `POST /backends/{backend_id}/disable`
将 backend 状态改为 `disabled`
### 8. `POST /backends/{backend_id}/enable`
将 backend 状态改为 `active`
### 9. `POST /backends/{backend_id}/rotate-secret`
返回新的 `client_secret`,并覆盖旧的凭证。
成功响应:
```json
{
"backend_id": "local-backend",
"client_id": "local-backend",
"client_secret": "new-generated-secret",
"rotated_at": "2026-03-13T08:00:00+00:00"
}
```
### 10. `GET /backends/{backend_id}/permissions`
返回指定 backend 的权限配置;若从未配置,返回空对象 `{}`
### 11. `POST /backends/{backend_id}/permissions`
请求体是任意 JSON 对象,当前实际生效的结构建议如下:
```json
{
"mcp": {
"outlook": {
"enabled": true,
"tools": [
"mail_list_messages",
"mail_send_email"
]
}
},
"a2a": {
"enabled": true,
"agents": [
"planner"
]
}
}
```
服务会原样保存该 JSON。
### 12. `GET /backends/{backend_id}/settings/outlook`
返回 Outlook 配置,但不会暴露明文密码。
未配置时响应:
```json
{
"configured": false
}
```
已配置时响应示例:
```json
{
"configured": true,
"email": "user@example.com",
"username": "user",
"domain": "example.com",
"service_endpoint": null,
"server": "mail.example.com",
"autodiscover": false,
"default_timezone": "Asia/Shanghai",
"updated_at": "2026-03-13T08:00:00+00:00",
"password_masked": true
}
```
### 13. `POST /backends/{backend_id}/settings/outlook`
请求体字段:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `configured` | 否 | `boolean` | 默认 `true`,一般传 `true` |
| `email` | 是 | `string` | 邮箱地址 |
| `username` | 是 | `string` | Outlook 用户名 |
| `domain` | 否 | `string` | 域名 |
| `service_endpoint` | 否 | `string` | 服务地址 |
| `server` | 否 | `string` | 邮件服务器 |
| `autodiscover` | 否 | `boolean` | 是否自动发现 |
| `default_timezone` | 否 | `string` | 默认时区,默认 `Asia/Shanghai` |
| `password` | 是,但更新时可空 | `string` | 初次配置必须传;后续更新可传空字符串表示沿用旧密码 |
初次配置时如果没有密码,会返回:
```json
{
"detail": "Password is required for initial Outlook setup"
}
```
### 14. `DELETE /backends/{backend_id}/settings/outlook`
成功响应:
```json
{
"ok": true
}
```
### 15. `GET /internal/backends/{backend_id}/settings/outlook`
内部接口,返回完整 Outlook 配置,包含明文 `password`
### 16. `POST /oauth/register`
见上文“注册时需要提供什么信息”。
### 17. `POST /oauth/token`
见上文“登录/鉴权时需要什么信息”。
### 18. `POST /oauth/introspect`
见上文“Token 内省”。
## 常见错误码
| HTTP 状态码 | 场景 |
| --- | --- |
| `400` | 缺少 `username``password``base_url`,或 `grant_type` 不支持 |
| `401` | `client_id/client_secret` 无效,或内部 Bearer Token 无效 |
| `403` | backend 被禁用、audience 未启用、请求 scope 超出权限 |
| `404` | backend 不存在,或内部 Outlook 配置不存在 |
| `409` | 注册 backend 时 `backend_id` 已存在 |
## 当前实现限制
1. `/oauth/register``password` 目前不会被保存,也不会用于认证。
2. 没有用户名密码登录接口,只有 `client_credentials`
3. 没有 refresh token。
4. 公开的 backend 管理接口当前未做访问控制,调用方需自行在网关或网络层保护。