a-novel/service-json-keys
GitHub: a-novel/service-json-keys
基于 JWK/JWE/JWS 标准的集中式密钥管理服务,通过 gRPC 私有 API 签名 Token、REST 公开 API 分发公钥,实现微服务架构下安全的密钥托管与本地验证。
Stars: 1 | Forks: 0
# JSON Keys 服务
A-Novel 平台的集中式签名密钥管理器:它保管所有私钥,通过私有 gRPC API 签名 token,并通过 REST 提供匹配的公钥,以便调用者在本地进行验证。




[](https://codecov.io/gh/a-novel/service-json-keys)

## 它的功能
服务注册命名的**用途**(`auth`、`auth-refresh` 等),每个用途都有各自的签名算法、轮换计划和声明参数。JSON Keys 保管所有私钥并代表调用者进行签名——密钥材料永远不会离开服务器。消费者只需获取一次匹配的公钥即可在本地验证 token,无需为每个 token 进行网络往返。
两个 API:
- **私有 gRPC API** —— 签名、密钥检索、状态 —— 用于内部服务间通信。所有涉及私钥的操作都在此处。服务器没有应用层身份验证;访问控制是外部的(网络策略、ingress、service mesh)。
- **公开 REST API** —— 公钥获取、健康检查 —— 供任何验证 token 的人使用。
## 部署
该服务以发布的 OCI 镜像和 PostgreSQL 数据库的形式运行。两个服务器都是无状态的,因此您可以在负载均衡器后根据需要扩展到任意数量的副本;所有状态都保存在 Postgres 中。
| 镜像 | 角色 |
| ----------------------------------- | -------------------------------------------------------------------------------- |
| `service-json-keys/grpc` | 私有签名 + 密钥管理 API。仅限内部网络。 |
| `service-json-keys/rest` | 公钥获取 + 健康 API。 |
| `service-json-keys/jobs/migrations` | 一次性 schema 迁移作业;在服务器启动前运行至完成。 |
| `service-json-keys/jobs/rotatekeys` | 定时密钥轮换作业(参见[贡献指南](./CONTRIBUTING.md#key-rotation))。 |
| `service-json-keys/database` | 预先调优的 PostgreSQL 镜像 —— 或者使用您自带的 Postgres。 |
将每个镜像固定到相同的发布标签——参见[最新发布](https://github.com/a-novel/service-json-keys/releases/latest)。生产环境部署首先运行 `database`,接着运行迁移作业至完成,然后运行任意数量的 `grpc` 和/或 `rest` 副本:
```
services:
postgres-json-keys:
image: ghcr.io/a-novel/service-json-keys/database:v2.5.0
networks: [api]
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: --auth=scram-sha-256
volumes:
- json-keys-postgres-data:/var/lib/postgresql/
migrations-json-keys:
image: ghcr.io/a-novel/service-json-keys/jobs/migrations:v2.3.2
depends_on:
postgres-json-keys: { condition: service_healthy }
environment:
POSTGRES_DSN: "postgres://postgres:postgres@postgres-json-keys:5432/postgres?sslmode=disable"
networks: [api]
service-json-keys:
image: ghcr.io/a-novel/service-json-keys/grpc:v2.5.0 # or .../rest:v2.3.2 for the public REST API
ports: ["${GRPC_PORT}:8080"] # the container always listens on 8080; map ${REST_PORT} for the rest image
depends_on:
postgres-json-keys: { condition: service_healthy }
migrations-json-keys: { condition: service_completed_successfully }
environment:
POSTGRES_DSN: "postgres://postgres:postgres@postgres-json-keys:5432/postgres?sslmode=disable"
APP_MASTER_KEY: ""
networks: [api]
networks:
api:
volumes:
json-keys-postgres-data:
```
通过添加一个重用相同数据库和迁移并使用 `rest` 镜像的第二个服务来运行这两个服务器。密钥轮换是一个独立的定时作业——在计时器上运行 `service-json-keys/jobs/rotatekeys` 镜像(参见[贡献指南](./CONTRIBUTING.md#key-rotation));如果没有它,活动的密钥最终会过期并停止签名。
### 配置
每个变量均从进程环境中读取。
| 名称 | 描述 | 镜像 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `POSTGRES_DSN` | PostgreSQL 连接字符串。**必填。** | 全部 |
| `APP_MASTER_KEY` | 用于加密静态私钥的 32 字节十六进制编码密钥。所有涉及私钥的镜像均**必填**。**切勿轮换**,除非您能承受使所有现有密钥失效的后果——参见[贡献指南](./CONTRIBUTING.md#master-key-encryption)。 | `grpc`
`rest`
`standalone-grpc`
`standalone-rest`
`jobs/rotatekeys` | gRPC 服务器暴露私钥操作,必须运行在隔离的、受访问控制的网络中——服务器本身不对调用者进行身份验证。
## 使用客户端包
该服务附带了两个客户端。每个代码片段都是**最小可行调用**;完整的接口内容请查阅您的编辑器的智能提示、[pkg.go.dev](https://pkg.go.dev/github.com/a-novel/service-json-keys/v2) 和 [API 参考](https://a-novel.github.io/service-json-keys-v2)。
- **Go** 通过 gRPC 通信——在签名 token 或使用缓存的公钥验证它们的后端服务中使用它。
- **JavaScript / TypeScript** 通过 REST 通信——在仅需公钥进行本地验证的前端或 Node 服务中使用它。
### Go (gRPC)
```
go get github.com/a-novel/service-json-keys/v2
```
```
package main
import (
"context"
"log"
"github.com/a-novel-kit/golib/grpcf"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
servicejsonkeys "github.com/a-novel/service-json-keys/v2/pkg/go"
)
type MyClaims struct {
UserID string `json:"userID"`
}
func main() {
ctx := context.Background()
// In production, swap insecure.NewCredentials() for a TLS or mTLS credential — the
// server has no application-layer auth, so transport security is the only thing
// protecting private-key operations from a network adversary.
client, err := servicejsonkeys.NewClient(
"service-json-keys:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Sign claims under the auth usage.
payload, err := grpcf.MarshalJSONAsAny(MyClaims{UserID: "user-1"})
if err != nil {
log.Fatal(err)
}
res, err := client.ClaimsSign(ctx, &servicejsonkeys.ClaimsSignRequest{
Usage: servicejsonkeys.KeyUsageAuth,
Payload: payload,
})
if err != nil {
log.Fatal(err)
}
// Verify locally — no extra network call per token.
verifier := servicejsonkeys.NewClaimsVerifier[MyClaims](client)
claims, err := verifier.VerifyClaims(ctx, &servicejsonkeys.VerifyClaimsRequest{
Usage: servicejsonkeys.KeyUsageAuth,
AccessToken: res.GetToken(),
})
if err != nil {
log.Fatal(err)
}
log.Printf("verified claims for user %s", claims.UserID)
}
```
### JavaScript / TypeScript (REST)
该包发布在 GitHub Packages 上,即使是公共包也需要具有 `read:packages` 权限范围的 Personal Access Token([原因](https://github.com/orgs/community/discussions/23386#discussioncomment-3240193))。将其添加到 `.npmrc`(项目根目录或 `$HOME`):
```
@a-novel:registry=https://npm.pkg.github.com
@a-novel-kit:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${YOUR_PERSONAL_ACCESS_TOKEN}
```
```
pnpm add @a-novel/service-json-keys-rest
```
```
import { JsonKeysApi, jwkList } from "@a-novel/service-json-keys-rest";
const api = new JsonKeysApi("http://service-json-keys:8080");
// Fetch the active public keys for a usage; cache them client-side and verify locally.
const keys = await jwkList(api, "auth");
```
API 参考:[a-novel.github.io/service-json-keys-v2](https://a-novel.github.io/service-json-keys-v2)。
## 本地运行
对于没有开发工具链的一次性实例,**standalone** 镜像将服务器和迁移捆绑在一个容器中。它们在每次启动时都会运行迁移——非常适合快速启动,但在多副本生产环境重启下是不安全的。
```
services:
postgres-json-keys:
image: ghcr.io/a-novel/service-json-keys/database:v2.5.0
networks: [api]
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
POSTGRES_INITDB_ARGS: --auth=scram-sha-256
service-json-keys:
image: ghcr.io/a-novel/service-json-keys/standalone-grpc:v2.5.0 # or standalone-rest
ports: ["${GRPC_PORT}:8080"] # map ${REST_PORT} for the standalone-rest image
depends_on:
postgres-json-keys: { condition: service_healthy }
environment:
POSTGRES_DSN: "postgres://postgres:postgres@postgres-json-keys:5432/postgres?sslmode=disable"
APP_MASTER_KEY: ""
networks: [api]
networks:
api:
```
要在服务本身上进行开发?请使用 `a-novel` CLI(`a-novel run start service-json-keys/grpc`)——参见[贡献指南](./CONTRIBUTING.md)。
## 贡献
平台设置和日常命令位于[开发者入门指南](https://github.com/a-novel-kit/.github/blob/master/README.md)中。特定于服务的概念和本地交互位于 [CONTRIBUTING.md](./CONTRIBUTING.md) 中。
`rest`
`standalone-grpc`
`standalone-rest`
`jobs/rotatekeys` | gRPC 服务器暴露私钥操作,必须运行在隔离的、受访问控制的网络中——服务器本身不对调用者进行身份验证。
可选配置(REST 调优,OpenTelemetry)
REST 调优(镜像 `rest`、`standalone-rest`): | 名称 | 描述 | 默认值 | | ----------------------------- | ------------------------------------ | ---------------- | | `REST_MAX_REQUEST_SIZE` | 最大请求体大小,单位为字节。 | `2097152` (2MiB) | | `REST_TIMEOUT_READ` | 读取超时。 | `15s` | | `REST_TIMEOUT_READ_HEADER` | Header 读取超时。 | `3s` | | `REST_TIMEOUT_WRITE` | 写入超时。 | `30s` | | `REST_TIMEOUT_IDLE` | 空闲 keep-alive 超时。 | `60s` | | `REST_TIMEOUT_REQUEST` | 单次请求超时。 | `60s` | | `REST_CORS_ALLOWED_ORIGINS` | CORS 允许的来源。 | `*` | | `REST_CORS_ALLOWED_HEADERS` | CORS 允许的 Header。 | `*` | | `REST_CORS_ALLOW_CREDENTIALS` | CORS 允许凭证标志。 | `false` | | `REST_CORS_MAX_AGE` | CORS 最大有效期,单位为秒。 | `3600` | 数据库连接池(服务器镜像)。这些限制是**针对每个进程**的。数据库的 `max_connections` 必须覆盖每个副本以及迁移作业;标准的 `postgres` 默认值为 100。 | 名称 | 描述 | 默认值 | | ------------------------- | ----------------------------------------- | ------- | | `POSTGRES_MAX_OPEN_CONNS` | 到数据库的最大打开连接数。 | `20` | | `POSTGRES_MAX_IDLE_CONNS` | 空闲时保持打开的最大连接数。 | `20` | 日志和追踪——OpenTelemetry 支持 stdout 和 Google Cloud 导出器(所有服务器镜像): | 名称 | 描述 | 默认值 | | ------------------- | --------------------------------------------------------------------- | ------------------- | | `OTEL` | 启用 OTel 追踪;以下变量用于选择导出器。 | `false` | | `GCLOUD_PROJECT_ID` | Google Cloud 项目 ID。设置后,会将 OTel 导出器切换到 GCP。 | | | `APP_NAME` | 附加到追踪和日志的应用名称。 | `service-json-keys` |标签:EVTX分析, Go, gRPC, JWT, PostgreSQL, Python工具, RESTful API, Ruby工具, 提示词优化, 日志审计, 测试用例, 用户代理