add:
This commit is contained in:
@@ -0,0 +1,617 @@
|
|||||||
|
# Envoy Gateway 配置指南(带 JWT 认证)
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
|
||||||
|
1. [快速开始](#快速开始)
|
||||||
|
2. [添加新服务](#添加新服务)
|
||||||
|
3. [JWT 认证配置](#jwt-认证配置)
|
||||||
|
4. [分级访问控制](#分级访问控制)
|
||||||
|
5. [故障排查](#故障排查)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 前置条件
|
||||||
|
|
||||||
|
- K8s 集群正在运行(已验证 ✅)
|
||||||
|
- Envoy Gateway Pod 处于 Running 状态
|
||||||
|
- 所有后端服务已部署
|
||||||
|
|
||||||
|
### 当前网关状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Envoy Pod
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 查看网关 Service
|
||||||
|
kubectl get svc -n juwan envoy-gateway
|
||||||
|
|
||||||
|
# 查看 ConfigMap
|
||||||
|
kubectl get cm -n juwan envoy-config
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问网关
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通过 kubectl 端口转发(本地测试)
|
||||||
|
kubectl port-forward -n juwan svc/envoy-gateway 8080:80 &
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
curl http://localhost:8080/api/users/login
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 添加新服务
|
||||||
|
|
||||||
|
### 场景:添加 Product 服务
|
||||||
|
|
||||||
|
#### 1. 创建服务的 K8s 部署清单
|
||||||
|
|
||||||
|
编辑或创建 `deploy/k8s/service/product/product-api.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: juwan
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: product-api-config
|
||||||
|
namespace: juwan
|
||||||
|
data:
|
||||||
|
product-api.yaml: |
|
||||||
|
Name: product-api
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Database:
|
||||||
|
DataSource: postgres://user:pass@pg-dx:5432/juwan
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: product-api
|
||||||
|
namespace: juwan
|
||||||
|
labels:
|
||||||
|
app: product-api
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: product-api
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: product-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
image: your-registry/product-api:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 8890
|
||||||
|
name: http
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/product-api
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: "Asia/Shanghai"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8890
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: product-api-config
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: product-api-svc
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: product-api
|
||||||
|
ports:
|
||||||
|
- port: 8890
|
||||||
|
targetPort: 8890
|
||||||
|
name: http
|
||||||
|
type: ClusterIP
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 在 Envoy 网关中添加路由
|
||||||
|
|
||||||
|
编辑 `deploy/k8s/envoy-gateway.yaml`,在 `route_config` 的 `routes` 部分添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ... 在现有路由下方添加:
|
||||||
|
- match:
|
||||||
|
prefix: /api/products
|
||||||
|
route:
|
||||||
|
cluster: product_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 在 Envoy 网关中添加上游集群
|
||||||
|
|
||||||
|
编辑 `deploy/k8s/envoy-gateway.yaml`,在 `clusters` 部分添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: product_api_cluster
|
||||||
|
connect_timeout: 5s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: product_api_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: product-api-svc.juwan.svc.cluster.local
|
||||||
|
port_value: 8890
|
||||||
|
health_checks:
|
||||||
|
- timeout: 3s
|
||||||
|
interval: 10s
|
||||||
|
unhealthy_threshold: 2
|
||||||
|
healthy_threshold: 2
|
||||||
|
http_health_check:
|
||||||
|
path: /health
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 部署到集群
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 部署 Product API
|
||||||
|
kubectl apply -f deploy/k8s/service/product/product-api.yaml
|
||||||
|
|
||||||
|
# 更新 Envoy 配置
|
||||||
|
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||||
|
|
||||||
|
# 重启 Envoy Pod 以加载新配置
|
||||||
|
kubectl delete pods -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
kubectl get pods -n juwan
|
||||||
|
|
||||||
|
# 测试新接口
|
||||||
|
curl http://localhost:8080/api/products
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JWT 认证配置
|
||||||
|
|
||||||
|
### 1. 生成 JWT 密钥并存储
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 执行设置脚本
|
||||||
|
bash deploy/envoy/setup-jwt-auth.sh
|
||||||
|
|
||||||
|
# 或手动执行
|
||||||
|
JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
echo "保存这个密钥: $JWT_SECRET"
|
||||||
|
|
||||||
|
# 创建 K8s Secret
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=key=$JWT_SECRET \
|
||||||
|
-n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置 Envoy JWT 认证
|
||||||
|
|
||||||
|
编辑 `deploy/k8s/envoy-gateway.yaml`,更新 `http_filters` 部分:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http_filters:
|
||||||
|
# JWT 认证过滤器(必须在 router 之前)
|
||||||
|
- name: envoy.filters.http.jwt_authn
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
|
||||||
|
|
||||||
|
providers:
|
||||||
|
default:
|
||||||
|
issuer: "juwan"
|
||||||
|
audiences: "api"
|
||||||
|
# 使用 ConfigMap 中的 JWKS(已通过 volumeMount 挂载)
|
||||||
|
local_jwks:
|
||||||
|
filename: /etc/envoy/jwks.json
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# 规则1: 登录端点不需要认证
|
||||||
|
- match:
|
||||||
|
prefix: /api/users/login
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
# 规则2: 注册端点不需要认证
|
||||||
|
- match:
|
||||||
|
prefix: /api/users/register
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
# 规则3: 获取公开商品列表不需要认证
|
||||||
|
- match:
|
||||||
|
prefix: /api/products
|
||||||
|
case_sensitive: false
|
||||||
|
methods: ["GET"] # 仅 GET 不需要认证
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
# 规则4: 其他所有路由需要认证
|
||||||
|
- match:
|
||||||
|
prefix: "/"
|
||||||
|
requires:
|
||||||
|
provider_name: "default"
|
||||||
|
|
||||||
|
# 路由过滤器(在 JWT 认证之后)
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在 Envoy Deployment 中挂载 JWKS
|
||||||
|
|
||||||
|
编辑 `deploy/k8s/envoy-gateway.yaml` 的 Deployment 部分:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
# ... 其他配置 ...
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: envoy
|
||||||
|
# ... 其他配置 ...
|
||||||
|
volumeMounts:
|
||||||
|
- name: envoy-config
|
||||||
|
mountPath: /etc/envoy
|
||||||
|
- name: jwks-config # ← 新增
|
||||||
|
mountPath: /etc/envoy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: envoy-config
|
||||||
|
configMap:
|
||||||
|
name: envoy-config
|
||||||
|
- name: jwks-config # ← 新增
|
||||||
|
configMap:
|
||||||
|
name: jwks-config
|
||||||
|
items:
|
||||||
|
- key: jwks.json
|
||||||
|
path: jwks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 在 API 服务中生成 JWT Token
|
||||||
|
|
||||||
|
在 User API 的 login 端点(`app/users/api/internal/logic/user/loginlogic.go`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"app/users/api/internal/svc"
|
||||||
|
"app/users/api/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) {
|
||||||
|
// TODO: 验证用户名和密码
|
||||||
|
|
||||||
|
// 从配置中获取 JWT 密钥
|
||||||
|
jwtSecret := l.svcCtx.Config.JwtSecret
|
||||||
|
if jwtSecret == "" {
|
||||||
|
jwtSecret = "default-secret" // 开发环境默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 JWT Token
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"userId": 1, // 实际应从数据库获取
|
||||||
|
"username": req.Username,
|
||||||
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenString, err := token.SignedString([]byte(jwtSecret))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.LoginResp{
|
||||||
|
Token: tokenString,
|
||||||
|
Expires: time.Now().Add(24 * time.Hour).Unix(),
|
||||||
|
UserId: 1,
|
||||||
|
Username: req.Username,
|
||||||
|
Email: "user@example.com",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 在 API 配置中设置 JWT 密钥
|
||||||
|
|
||||||
|
编辑 `app/users/api/etc/user-api.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Name: user-api
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8888
|
||||||
|
|
||||||
|
JwtSecret: "${JWT_SECRET}" # 环境变量
|
||||||
|
|
||||||
|
Database:
|
||||||
|
DataSource: postgres://...
|
||||||
|
|
||||||
|
UserRpc:
|
||||||
|
Endpoints:
|
||||||
|
- user-rpc-svc.juwan.svc.cluster.local:50051
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `app/users/api/internal/config/config.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/zeromicro/go-zero/rest"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
rest.RestConf
|
||||||
|
JwtSecret string `json:"jwtSecret"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 K8s Deployment 中设置环境变量:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
env:
|
||||||
|
- name: JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分级访问控制
|
||||||
|
|
||||||
|
### 场景1: 获取用户信息(有权限区分)
|
||||||
|
|
||||||
|
如果用户查看自己的信息 → 返回完整数据
|
||||||
|
如果用户查看他人信息 → 返回部分数据
|
||||||
|
|
||||||
|
#### 在 RPC 服务中实现
|
||||||
|
|
||||||
|
编辑 `app/users/rpc/internal/logic/getUsersByIdLogic.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (l *GetUsersByIdLogic) GetUsersById(ctx context.Context, in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) {
|
||||||
|
// 获取请求者的 userId(由 API 层通过 context 传递)
|
||||||
|
requesterID, ok := ctx.Value("userId").(int64)
|
||||||
|
if !ok {
|
||||||
|
requesterID = 0 // 未认证用户
|
||||||
|
}
|
||||||
|
|
||||||
|
targetID := in.Id
|
||||||
|
|
||||||
|
// 查询数据库
|
||||||
|
user := l.svcCtx.UserModel.FindOne(ctx, targetID)
|
||||||
|
if user == nil {
|
||||||
|
return nil, status.Error(codes.NotFound, "user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.GetUsersByIdResp{
|
||||||
|
Users: &pb.Users{
|
||||||
|
UserId: user.UserId,
|
||||||
|
Username: user.Username,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查:自己可以看全部,别人只能看部分
|
||||||
|
if requesterID == targetID {
|
||||||
|
resp.Users.Email = user.Email // ✅ 自己可见
|
||||||
|
resp.Users.Phone = user.Phone // ✅ 自己可见
|
||||||
|
resp.Users.Passwd = "" // ❌ 密码永远不返回
|
||||||
|
}
|
||||||
|
// else: 只返回基本信息(username, userId)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在 API 层调用时传递 userId
|
||||||
|
|
||||||
|
编辑 `app/users/api/internal/logic/user/getUserInfoLogic.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) {
|
||||||
|
// 从 context 获取当前认证用户
|
||||||
|
currentUserID, ok := l.ctx.Value("userId").(int64)
|
||||||
|
if !ok {
|
||||||
|
// 未认证 → 只能查看公开信息
|
||||||
|
currentUserID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 RPC,传递 userId
|
||||||
|
ctx := context.WithValue(l.ctx, "userId", currentUserID)
|
||||||
|
rpcResp, err := l.svcCtx.UserRpc.GetUsersById(ctx, &pb.GetUsersByIdReq{
|
||||||
|
Id: req.UserId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.UserInfo{
|
||||||
|
UserId: rpcResp.Users.UserId,
|
||||||
|
Username: rpcResp.Users.Username,
|
||||||
|
Email: rpcResp.Users.Email,
|
||||||
|
Phone: rpcResp.Users.Phone,
|
||||||
|
CreateAt: rpcResp.Users.CreatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2: 修改用户信息(只能修改自己)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) {
|
||||||
|
// 获取当前认证用户
|
||||||
|
currentUserID, ok := l.ctx.Value("userId").(int64)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查:只能修改自己的信息
|
||||||
|
if currentUserID != req.UserId {
|
||||||
|
return nil, errors.New("forbidden: can only update your own info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
// ...
|
||||||
|
|
||||||
|
return &types.UpdateUserInfoResp{
|
||||||
|
Message: "更新成功",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题1: Envoy Pod 启动失败
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway --tail=100
|
||||||
|
|
||||||
|
# 常见错误及解决
|
||||||
|
# Error: "no such field"
|
||||||
|
# → YAML 字段名拼写错误或与 Envoy 版本不兼容
|
||||||
|
# → 检查 Envoy 版本并查看官方文档
|
||||||
|
|
||||||
|
# Error: "unknown cluster"
|
||||||
|
# → envoy-gateway.yaml 中缺少 cluster 定义
|
||||||
|
# → 确保添加了所有需要的 cluster 部分
|
||||||
|
|
||||||
|
# Error: "unknown extension type"
|
||||||
|
# → 使用了 Envoy 不支持的扩展类型
|
||||||
|
# → 检查 "@type" 字段是否正确
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题2: JWT 认证失败
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证 JWKS ConfigMap 是否存在
|
||||||
|
kubectl get cm -n juwan jwks-config
|
||||||
|
|
||||||
|
# 查看 JWKS 内容
|
||||||
|
kubectl get cm jwks-config -n juwan -o jsonpath='{.data.jwks\.json}'
|
||||||
|
|
||||||
|
# 验证 Envoy 能否读取 JWKS
|
||||||
|
kubectl exec -it {envoy-pod-name} -n juwan -- ls -la /etc/envoy/
|
||||||
|
|
||||||
|
# 测试没有 Token 的请求(应返回 401)
|
||||||
|
curl -v http://localhost/api/users/1
|
||||||
|
|
||||||
|
# 测试有效 Token 的请求
|
||||||
|
TOKEN="your-jwt-token"
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" http://localhost/api/users/1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题3: 后端服务无法访问
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Service 是否存在
|
||||||
|
kubectl get svc -n juwan
|
||||||
|
|
||||||
|
# 测试 DNS 解析
|
||||||
|
kubectl exec -it {pod-name} -n juwan -- \
|
||||||
|
nslookup product-api-svc.juwan.svc.cluster.local
|
||||||
|
|
||||||
|
# 查看 Pod 是否正确运行
|
||||||
|
kubectl get pods -n juwan -l app=product-api
|
||||||
|
|
||||||
|
# 查看后端服务日志
|
||||||
|
kubectl logs -n juwan -l app=product-api --tail=50
|
||||||
|
|
||||||
|
# Envoy 检查上游集群状态
|
||||||
|
kubectl exec -it {envoy-pod-name} -n juwan -- \
|
||||||
|
curl localhost:9901/clusters | grep -A5 product_api_cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题4: 跨域请求失败
|
||||||
|
|
||||||
|
如果前端遇到 CORS 问题:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 在 Envoy 配置中添加 CORS 过滤器
|
||||||
|
http_filters:
|
||||||
|
- name: envoy.filters.http.cors
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
|
||||||
|
|
||||||
|
# JWT 认证过滤器(在 CORS 之后)
|
||||||
|
- name: envoy.filters.http.jwt_authn
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置更新流程
|
||||||
|
|
||||||
|
每次修改 `envoy-gateway.yaml` 后的完整更新步骤:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 验证 YAML 语法
|
||||||
|
kubectl apply -f deploy/k8s/envoy-gateway.yaml --dry-run=client
|
||||||
|
|
||||||
|
# 2. 应用配置
|
||||||
|
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||||
|
|
||||||
|
# 3. 监控 Pod 重启(应该自动重新加载)
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway -w
|
||||||
|
|
||||||
|
# 4. 查看最新日志确认无错误
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway --tail=50
|
||||||
|
|
||||||
|
# 5. 测试新配置
|
||||||
|
curl http://localhost/api/your-new-endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
| 任务 | 文件 | 说明 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 添加新 API | `desc/api/`, `app/*/api/` | 定义接口并实现业务逻辑 |
|
||||||
|
| 添加新 RPC | `desc/rpc/`, `app/*/rpc/` | 内部服务通信(不通过网关) |
|
||||||
|
| 更新网关路由 | `deploy/k8s/envoy-gateway.yaml` | 添加路由、集群、认证规则 |
|
||||||
|
| 配置认证 | `deploy/envoy/setup-jwt-auth.sh` | 生成和管理 JWT 密钥 |
|
||||||
|
| 部署到 K8s | `deploy/k8s/service/` | 创建服务的 Deployment 和 Service |
|
||||||
|
|
||||||
|
需要更多帮助?查看 `PROJECT_GUIDE.md` 了解完整的项目架构和工作流!
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
# JWT 集成指南
|
||||||
|
|
||||||
|
指导如何将 JWT Manager 集成到 RPC Handlers 和业务逻辑中。
|
||||||
|
|
||||||
|
## 1. gRPC Unary Interceptor 实现
|
||||||
|
|
||||||
|
在 RPC 服务中添加 JWT 验证拦截器。
|
||||||
|
|
||||||
|
### 创建拦截器
|
||||||
|
|
||||||
|
创建文件 [app/users/rpc/internal/interceptor/jwt_interceptor.go](../../../app/users/rpc/internal/interceptor/jwt_interceptor.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"yourmodule/app/users/rpc/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JwtUnaryInterceptor 验证 gRPC 请求中的 JWT 令牌
|
||||||
|
func JwtUnaryInterceptor(svcCtx *svc.ServiceContext) grpc.UnaryServerInterceptor {
|
||||||
|
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||||
|
// 获取请求元数据
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Authorization 头提取令牌
|
||||||
|
tokens := md.Get("authorization")
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := tokens[0]
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
claims, err := svcCtx.JwtManager.Valid(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Token validation failed: %v", err)
|
||||||
|
|
||||||
|
// 尝试刷新令牌(如果过期但仍在 Redis 中)
|
||||||
|
newToken, refreshErr := svcCtx.JwtManager.Renew(ctx, token)
|
||||||
|
if refreshErr == nil && newToken != "" {
|
||||||
|
// 在响应头中返回新令牌
|
||||||
|
grpc.SetHeader(ctx, metadata.Pairs("authorization", newToken))
|
||||||
|
// 继续处理请求,使用原令牌的声明
|
||||||
|
// 注意:实际应用中需要重新验证新令牌
|
||||||
|
newClaims, err := svcCtx.JwtManager.Valid(ctx, newToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "token refresh failed")
|
||||||
|
}
|
||||||
|
claims = newClaims
|
||||||
|
} else {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "invalid or expired token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将声明附加到上下文,供处理器使用
|
||||||
|
newCtx := context.WithValue(ctx, "claims", claims)
|
||||||
|
|
||||||
|
return handler(newCtx, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JwtStreamInterceptor 验证流式 gRPC 请求中的 JWT 令牌
|
||||||
|
func JwtStreamInterceptor(svcCtx *svc.ServiceContext) grpc.StreamServerInterceptor {
|
||||||
|
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||||
|
md, ok := metadata.FromIncomingContext(ss.Context())
|
||||||
|
if !ok {
|
||||||
|
return status.Error(codes.Unauthenticated, "missing metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := md.Get("authorization")
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return status.Error(codes.Unauthenticated, "missing authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := tokens[0]
|
||||||
|
claims, err := svcCtx.JwtManager.Valid(ss.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
return status.Error(codes.Unauthenticated, "invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建包装流以注入上下文
|
||||||
|
wrappedStream := &WrappedStream{
|
||||||
|
ServerStream: ss,
|
||||||
|
ctx: context.WithValue(ss.Context(), "claims", claims),
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(srv, wrappedStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrappedStream 包装 grpc.ServerStream 以注入新的上下文
|
||||||
|
type WrappedStream struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WrappedStream) Context() context.Context {
|
||||||
|
return w.ctx
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 Server 中注册拦截器
|
||||||
|
|
||||||
|
修改 [app/users/rpc/usercenter/usercenter.go](../../../app/users/rpc/usercenter/usercenter.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"yourmodule/app/users/rpc/internal/config"
|
||||||
|
"yourmodule/app/users/rpc/internal/interceptor"
|
||||||
|
"yourmodule/app/users/rpc/internal/server"
|
||||||
|
"yourmodule/app/users/rpc/internal/svc"
|
||||||
|
"yourmodule/app/users/rpc/pb"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = flag.String("f", "etc/pb.yaml", "the config file")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var c config.Config
|
||||||
|
conf.MustLoad(*configFile, &c)
|
||||||
|
ctx := svc.NewServiceContext(c)
|
||||||
|
|
||||||
|
logx.DisableStat()
|
||||||
|
|
||||||
|
s := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)),
|
||||||
|
grpc.StreamInterceptor(interceptor.JwtStreamInterceptor(ctx)),
|
||||||
|
)
|
||||||
|
|
||||||
|
pb.RegisterUsercenterServer(s, server.NewUsercenterServer(ctx))
|
||||||
|
|
||||||
|
logx.Infof("Starting gRPC server on %s:%d", c.Host, c.Port)
|
||||||
|
if err := s.Serve(net.Listen("tcp", "0.0.0.0:"+fmt.Sprintf("%d", c.Port))); err != nil {
|
||||||
|
logx.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 登录 Handler 实现
|
||||||
|
|
||||||
|
实现 [app/users/api/internal/handler/user/loginHandler.go](../../../app/users/api/internal/handler/user/loginHandler.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/logic/user"
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
"yourmodule/app/users/api/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginHandler 处理用户登录
|
||||||
|
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.LoginRequest
|
||||||
|
|
||||||
|
// 解析请求体...
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用业务逻辑
|
||||||
|
resp, err := user.NewLoginLogic(r.Context(), svcCtx).Login(&req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Login failed: %v", err)
|
||||||
|
http.Error(w, "Login failed", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回令牌
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现 [app/users/api/internal/logic/user/loginLogic.go](../../../app/users/api/internal/logic/user/loginLogic.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
"yourmodule/app/users/api/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
|
||||||
|
return &LoginLogic{
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) {
|
||||||
|
// 1. 验证用户凭证(密码等)
|
||||||
|
user, err := l.svcCtx.UserModel.FindByEmail(l.ctx, req.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证密码
|
||||||
|
if !user.VerifyPassword(req.Password) {
|
||||||
|
return nil, errors.New("invalid password")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 生成 JWT 令牌
|
||||||
|
token, err := l.svcCtx.JwtManager.New(
|
||||||
|
l.ctx,
|
||||||
|
user.ID,
|
||||||
|
user.Email,
|
||||||
|
user.Name,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to generate token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 返回令牌
|
||||||
|
return &types.LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
User: types.User{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.Email,
|
||||||
|
Name: user.Name,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 在 Handlers 中使用声明
|
||||||
|
|
||||||
|
在 Protected Handlers 中提取并使用声明:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
"yourmodule/app/users/api/internal/types"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUserInfoHandler 获取当前用户信息
|
||||||
|
func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从上下文提取声明(由拦截器设置)
|
||||||
|
claims, ok := r.Context().Value("claims").(*jwt.RegisteredClaims)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用声明中的用户信息
|
||||||
|
userID := claims.Subject // 用户 ID 存储在 Subject 中
|
||||||
|
log.Printf("User %s requested their info", userID)
|
||||||
|
|
||||||
|
// 查询用户信息
|
||||||
|
user, err := svcCtx.UserModel.FindByID(r.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "User not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回用户信息
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 令牌刷新端点
|
||||||
|
|
||||||
|
实现令牌刷新端点:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
"yourmodule/app/users/api/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefreshTokenHandler 刷新过期的 JWT 令牌
|
||||||
|
func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.RefreshTokenRequest
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取旧令牌
|
||||||
|
oldToken := req.Token
|
||||||
|
|
||||||
|
// 尝试刷新令牌
|
||||||
|
newToken, err := svcCtx.JwtManager.Renew(r.Context(), oldToken)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Token refresh failed", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回新令牌
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(types.RefreshTokenResponse{
|
||||||
|
Token: newToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 登出处理
|
||||||
|
|
||||||
|
实现登出端点以撤销令牌:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogoutHandler 登出用户(撤销令牌)
|
||||||
|
func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从上下文提取声明
|
||||||
|
claims, ok := r.Context().Value("claims").(*jwt.RegisteredClaims)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := claims.Subject
|
||||||
|
|
||||||
|
// 获取用户当前令牌
|
||||||
|
currentToken := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
// 撤销令牌
|
||||||
|
err := svcCtx.JwtManager.Revoke(r.Context(), userID, currentToken)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Logout failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"message": "logged out successfully"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 特定端点的 JWT 验证
|
||||||
|
|
||||||
|
对于 REST API,在需要的 handlers 中手动验证令牌:
|
||||||
|
|
||||||
|
### 在 Routes 中配置
|
||||||
|
|
||||||
|
修改 [app/users/api/internal/handler/routes.go](../../../app/users/api/internal/handler/routes.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/middleware"
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
"yourmodule/app/users/api/internal/handler/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes 注册所有路由
|
||||||
|
func RegisterRoutes(router *http.ServeMux, svcCtx *svc.ServiceContext) {
|
||||||
|
// 公开路由
|
||||||
|
router.HandleFunc("POST /api/v1/auth/login", user.LoginHandler(svcCtx))
|
||||||
|
router.HandleFunc("POST /api/v1/auth/refresh", user.RefreshTokenHandler(svcCtx))
|
||||||
|
|
||||||
|
// 受保护的路由(需要 JWT 验证)
|
||||||
|
protected := middleware.JwtMiddleware(svcCtx)
|
||||||
|
router.HandleFunc("GET /api/v1/users/me", protected(user.GetUserInfoHandler(svcCtx)))
|
||||||
|
router.HandleFunc("POST /api/v1/users/logout", protected(user.LogoutHandler(svcCtx)))
|
||||||
|
router.HandleFunc("PUT /api/v1/users/me", protected(user.UpdateUserInfoHandler(svcCtx)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建 JWT 中间件
|
||||||
|
|
||||||
|
创建 [app/users/api/internal/middleware/jwt.go](../../../app/users/api/internal/middleware/jwt.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"yourmodule/app/users/api/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JwtMiddleware 为 HTTP 处理器添加 JWT 验证
|
||||||
|
func JwtMiddleware(svcCtx *svc.ServiceContext) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从 Authorization 头提取令牌
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 期望格式: "Bearer <token>"
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := parts[1]
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
claims, err := svcCtx.JwtManager.Valid(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
// 尝试刷新
|
||||||
|
newToken, refreshErr := svcCtx.JwtManager.Renew(r.Context(), token)
|
||||||
|
if refreshErr != nil {
|
||||||
|
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在响应头返回新令牌
|
||||||
|
w.Header().Set("X-New-Token", newToken)
|
||||||
|
|
||||||
|
// 重新验证新令牌
|
||||||
|
claims, err = svcCtx.JwtManager.Valid(r.Context(), newToken)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Token refresh failed", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将声明附加到上下文
|
||||||
|
newCtx := context.WithValue(r.Context(), "claims", claims)
|
||||||
|
next.ServeHTTP(w, r.WithContext(newCtx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 错误处理最佳实践
|
||||||
|
|
||||||
|
```go
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"yourmodule/app/users/rpc/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleJwtError 处理 JWT 相关错误
|
||||||
|
func HandleJwtError(err error) error {
|
||||||
|
if errors.Is(err, utils.ErrTokenExpired) {
|
||||||
|
log.Println("Token has expired, user needs to refresh")
|
||||||
|
return errors.New("token expired - use refresh endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, utils.ErrTokenInvalid) {
|
||||||
|
log.Println("Token is invalid or malformed")
|
||||||
|
return errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, utils.ErrTokenNotFound) {
|
||||||
|
log.Println("Token not found in Redis (revoked or expired)")
|
||||||
|
return errors.New("token revoked or expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 测试 JWT 集成
|
||||||
|
|
||||||
|
### 单元测试示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJwtUnaryInterceptor_ValidToken(t *testing.T) {
|
||||||
|
// 1. 创建有效的令牌
|
||||||
|
token, err := svcCtx.JwtManager.New(context.Background(), "user123", "user@example.com", "John")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建包含令牌的上下文
|
||||||
|
md := metadata.Pairs("authorization", token)
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
|
// 3. 调用拦截器
|
||||||
|
_, err = JwtUnaryInterceptor(svcCtx)(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJwtUnaryInterceptor_ExpiredToken(t *testing.T) {
|
||||||
|
// 1. 创建过期的令牌或使用无效令牌
|
||||||
|
token := "invalid.token.here"
|
||||||
|
|
||||||
|
// 2. 创建包含令牌的上下文
|
||||||
|
md := metadata.Pairs("authorization", token)
|
||||||
|
ctx := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
|
// 3. 调用拦截器
|
||||||
|
_, err := JwtUnaryInterceptor(svcCtx)(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 验证错误
|
||||||
|
st, ok := status.FromError(err)
|
||||||
|
if !ok || st.Code() != codes.Unauthenticated {
|
||||||
|
t.Errorf("Expected Unauthenticated error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 生产部署清单
|
||||||
|
|
||||||
|
在将 JWT 集成部署到生产环境前:
|
||||||
|
|
||||||
|
- [ ] 所有令牌端点都进行了压力测试
|
||||||
|
- [ ] 令牌刷新逻辑已验证
|
||||||
|
- [ ] 错误处理覆盖了所有 JWT 失败情况
|
||||||
|
- [ ] 审计日志记录了所有认证尝试
|
||||||
|
- [ ] 密钥轮换计划已确定
|
||||||
|
- [ ] 监控和告警已配置
|
||||||
|
- [ ] 灾难恢复流程已文档化
|
||||||
|
- [ ] 所有依赖于 JWT 的服务都已更新
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- [app/users/rpc/internal/utils/jwt.go](../../../app/users/rpc/internal/utils/jwt.go) - JWT Manager 实现
|
||||||
|
- [app/users/rpc/internal/config/config.go](../../../app/users/rpc/internal/config/config.go) - JWT 配置
|
||||||
|
- [app/users/rpc/internal/svc/serviceContext.go](../../../app/users/rpc/internal/svc/serviceContext.go) - 依赖注入
|
||||||
|
- [deploy/k8s/secrets/jwt-secret.yaml](./jwt-secret.yaml) - Secret 和 RBAC
|
||||||
|
- [deploy/k8s/secrets/DEPLOYMENT.md](./DEPLOYMENT.md) - 部署指南
|
||||||
@@ -28,7 +28,6 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
|
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
|
||||||
// todo: add your logic here and delete this line
|
|
||||||
|
|
||||||
return
|
return &types.LoginResp{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"juwan-backend/app/users/api/internal/svc"
|
"juwan-backend/app/users/api/internal/svc"
|
||||||
"juwan-backend/app/users/api/internal/types"
|
"juwan-backend/app/users/api/internal/types"
|
||||||
"juwan-backend/app/users/rpc/pb"
|
"juwan-backend/app/users/rpc/pb"
|
||||||
|
"juwan-backend/common/utils"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
@@ -31,25 +32,44 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
|
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
|
||||||
// todo: add your logic here and delete this line
|
// 检查用户是否已存在
|
||||||
user, err := l.svcCtx.UserRpc.GetUserByUsername(l.ctx, &pb.GetUserByUsernameReq{
|
existingUser, err := l.svcCtx.UserRpc.GetUserByUsername(l.ctx, &pb.GetUserByUsernameReq{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
})
|
})
|
||||||
if err == nil || user != nil {
|
if err == nil && existingUser != nil {
|
||||||
return nil, errors.New("User is exisit")
|
return nil, errors.New("用户已存在")
|
||||||
}
|
|
||||||
id, err := uuid.NewRandom()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Register is failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = l.svcCtx.UserRpc.AddUsers(l.ctx, &pb.AddUsersReq{
|
// 生成用户ID
|
||||||
UserId: id.String(),
|
userId, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("注册失败:无法生成用户ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
hashedPassword, err := utils.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("注册失败:密码加密失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
|
newUser, err := l.svcCtx.UserRpc.AddUsers(l.ctx, &pb.AddUsersReq{
|
||||||
|
UserId: userId.String(),
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Passwd: req.Password,
|
Passwd: hashedPassword,
|
||||||
Phone: req.Phone,
|
Phone: req.Phone,
|
||||||
State: true,
|
State: true,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.Errorf("AddUsers failed: %v", err)
|
||||||
|
return nil, errors.New("注册失败:创建用户失败")
|
||||||
|
}
|
||||||
|
|
||||||
return
|
// 返回响应
|
||||||
|
return &types.RegisterResp{
|
||||||
|
UserId: int64(newUser.), // RPC 返回的可能是用户信息,这里简化处理
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
Message: "注册成功",
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ ListenOn: 0.0.0.0:9001
|
|||||||
|
|
||||||
Prometheus:
|
Prometheus:
|
||||||
Host: 0.0.0.0
|
Host: 0.0.0.0
|
||||||
Port: 9001
|
Port: 4001
|
||||||
Path: /metrics
|
Path: /metrics
|
||||||
|
|
||||||
DataSource: "${DB_URI}?sslmode=disable"
|
DataSource: "${DB_URI}?sslmode=disable"
|
||||||
@@ -13,3 +13,7 @@ CacheConf:
|
|||||||
Type: cluster
|
Type: cluster
|
||||||
Pass: "${REDIS_PASSWORD}"
|
Pass: "${REDIS_PASSWORD}"
|
||||||
User: "default"
|
User: "default"
|
||||||
|
|
||||||
|
Jwt:
|
||||||
|
SecretKey: "${JWT_SECRET_KEY}"
|
||||||
|
Issuer: "juwan-user-rpc"
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import (
|
|||||||
"github.com/zeromicro/go-zero/zrpc"
|
"github.com/zeromicro/go-zero/zrpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type JwtConfig struct {
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
zrpc.RpcServerConf
|
zrpc.RpcServerConf
|
||||||
DataSource string `json:"dataSource"`
|
DataSource string `json:"dataSource"`
|
||||||
CacheConf cache.CacheConf
|
CacheConf cache.CacheConf
|
||||||
|
Jwt JwtConfig `json:"jwt"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"juwan-backend/app/users/rpc/internal/svc"
|
||||||
|
"juwan-backend/app/users/rpc/pb"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckPermissionLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
logx.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckPermissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckPermissionLogic {
|
||||||
|
return &CheckPermissionLogic{
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CheckPermissionLogic) CheckPermission(in *pb.CheckPermissionReq) (*pb.CheckPermissionResp, error) {
|
||||||
|
// todo: add your logic here and delete this line
|
||||||
|
|
||||||
|
return &pb.CheckPermissionResp{}, nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"juwan-backend/app/users/rpc/internal/svc"
|
"juwan-backend/app/users/rpc/internal/svc"
|
||||||
"juwan-backend/app/users/rpc/pb"
|
"juwan-backend/app/users/rpc/pb"
|
||||||
|
"juwan-backend/common/converter"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
)
|
)
|
||||||
@@ -23,8 +24,19 @@ func NewGetUserByUsernameLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetUserByUsernameLogic) GetUserByUsername(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) {
|
func (l *GetUserByUsernameLogic) GetUserByUsername(in *pb.GetUserByUsernameReq) (*pb.GetUserByUsernameResp, error) {
|
||||||
// todo: add your logic here and delete this line
|
// todo: add your logic here and delete this line
|
||||||
|
|
||||||
return &pb.GetUsersByIdResp{}, nil
|
user, err := l.svcCtx.UsersModel.FindOneByUsername(l.ctx, in.Username)
|
||||||
|
pbUsers := &pb.Users{}
|
||||||
|
converter.StructToStruct(user, pbUsers)
|
||||||
|
if err == nil || user != nil {
|
||||||
|
return &pb.GetUserByUsernameResp{
|
||||||
|
Users: pbUsers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if err.Error() != "not found" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pb.GetUserByUsernameResp{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"juwan-backend/app/users/rpc/internal/svc"
|
||||||
|
"juwan-backend/app/users/rpc/pb"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
logx.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
|
||||||
|
return &LoginLogic{
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
|
||||||
|
// todo: add your logic here and delete this line
|
||||||
|
|
||||||
|
return &pb.LoginResp{}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"juwan-backend/app/users/rpc/internal/svc"
|
||||||
|
"juwan-backend/app/users/rpc/pb"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValidateTokenLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
logx.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValidateTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ValidateTokenLogic {
|
||||||
|
return &ValidateTokenLogic{
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ValidateTokenLogic) ValidateToken(in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||||
|
// todo: add your logic here and delete this line
|
||||||
|
|
||||||
|
return &pb.ValidateTokenResp{}, nil
|
||||||
|
}
|
||||||
@@ -53,3 +53,18 @@ func (s *UsercenterServer) SearchUsers(ctx context.Context, in *pb.SearchUsersRe
|
|||||||
l := logic.NewSearchUsersLogic(ctx, s.svcCtx)
|
l := logic.NewSearchUsersLogic(ctx, s.svcCtx)
|
||||||
return l.SearchUsers(in)
|
return l.SearchUsers(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UsercenterServer) Login(ctx context.Context, in *pb.LoginReq) (*pb.LoginResp, error) {
|
||||||
|
l := logic.NewLoginLogic(ctx, s.svcCtx)
|
||||||
|
return l.Login(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UsercenterServer) ValidateToken(ctx context.Context, in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||||
|
l := logic.NewValidateTokenLogic(ctx, s.svcCtx)
|
||||||
|
return l.ValidateToken(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UsercenterServer) CheckPermission(ctx context.Context, in *pb.CheckPermissionReq) (*pb.CheckPermissionResp, error) {
|
||||||
|
l := logic.NewCheckPermissionLogic(ctx, s.svcCtx)
|
||||||
|
return l.CheckPermission(in)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"juwan-backend/app/users/rpc/internal/config"
|
"juwan-backend/app/users/rpc/internal/config"
|
||||||
"juwan-backend/app/users/rpc/internal/models"
|
"juwan-backend/app/users/rpc/internal/models"
|
||||||
|
"juwan-backend/app/users/rpc/internal/utils"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -15,6 +16,7 @@ type ServiceContext struct {
|
|||||||
Config config.Config
|
Config config.Config
|
||||||
UsersModel models.UsersModel
|
UsersModel models.UsersModel
|
||||||
RedisCluster *redis.ClusterClient
|
RedisCluster *redis.ClusterClient
|
||||||
|
JwtManager *utils.JwtManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceContext(c config.Config) *ServiceContext {
|
func NewServiceContext(c config.Config) *ServiceContext {
|
||||||
@@ -39,9 +41,13 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize JWT Manager
|
||||||
|
jwtManager := utils.NewJwtManager(redisCluster, c.Jwt.SecretKey, c.Jwt.Issuer)
|
||||||
|
|
||||||
return &ServiceContext{
|
return &ServiceContext{
|
||||||
Config: c,
|
Config: c,
|
||||||
UsersModel: models.NewUsersModel(conn, c.CacheConf),
|
UsersModel: models.NewUsersModel(conn, c.CacheConf),
|
||||||
RedisCluster: redisCluster,
|
RedisCluster: redisCluster,
|
||||||
|
JwtManager: jwtManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWKS (JSON Web Key Set) 结构
|
||||||
|
type JWKSKey struct {
|
||||||
|
Kty string `json:"kty"`
|
||||||
|
Use string `json:"use"`
|
||||||
|
Kid string `json:"kid"`
|
||||||
|
N string `json:"n,omitempty"`
|
||||||
|
E string `json:"e,omitempty"`
|
||||||
|
K string `json:"k,omitempty"` // 对称密钥
|
||||||
|
Alg string `json:"alg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWKS struct {
|
||||||
|
Keys []JWKSKey `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWKSFromSecret 从密钥生成 JWKS(用于对称加密 HS256)
|
||||||
|
func GenerateJWKSFromSecret(secretKey string, keyID string) *JWKS {
|
||||||
|
// 对于 HS256,将密钥进行 base64 编码
|
||||||
|
encodedSecret := base64.RawURLEncoding.EncodeToString([]byte(secretKey))
|
||||||
|
|
||||||
|
return &JWKS{
|
||||||
|
Keys: []JWKSKey{
|
||||||
|
{
|
||||||
|
Kty: "oct",
|
||||||
|
Use: "sig",
|
||||||
|
Kid: keyID,
|
||||||
|
K: encodedSecret,
|
||||||
|
Alg: "HS256",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWKSEndpoint 生成可以被 Envoy 使用的 JWKS JSON
|
||||||
|
// 此端点应在 user-rpc 中暴露,URL 为 /.well-known/jwks.json
|
||||||
|
func GenerateJWKSEndpoint(secretKey string, keyID string) (string, error) {
|
||||||
|
if secretKey == "" {
|
||||||
|
return "", fmt.Errorf("secret key cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
jwks := GenerateJWKSFromSecret(secretKey, keyID)
|
||||||
|
|
||||||
|
jsonData, err := json.MarshalIndent(jwks, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(jsonData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPayload 令牌负载
|
||||||
|
type TokenMetadata struct {
|
||||||
|
IssuedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
Subject string // userId
|
||||||
|
Issuer string
|
||||||
|
Audience string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractTokenMetadata 从 token 中提取元数据(不验证签名)
|
||||||
|
func ExtractTokenMetadata(tokenString string) (*TokenMetadata, error) {
|
||||||
|
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid token claims type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TokenMetadata{
|
||||||
|
IssuedAt: claims.IssuedAt.Time,
|
||||||
|
ExpiresAt: claims.ExpiresAt.Time,
|
||||||
|
Subject: claims.UserId,
|
||||||
|
Issuer: claims.Issuer,
|
||||||
|
Audience: "", // 如果需要,可以增加到 Claims 中
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenPayload struct {
|
type TokenPayload struct {
|
||||||
@@ -10,11 +16,16 @@ type TokenPayload struct {
|
|||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
TokenPayload
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tokenCachePrefixUser = "jwt:user:"
|
tokenCachePrefixUser = "jwt:user:"
|
||||||
tokenCachePrefixToken = "jwt:token:"
|
tokenCachePrefixToken = "jwt:token:"
|
||||||
tokenCacheTTL = 60 * 24 * time.Hour
|
tokenCacheTTL = 30 * 24 * time.Hour
|
||||||
tokenLifetime = 5 * 24 * time.Hour
|
tokenLifetime = 7 * 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,9 +33,211 @@ var (
|
|||||||
errInvalidToken = errors.New("invalid token claims")
|
errInvalidToken = errors.New("invalid token claims")
|
||||||
errTokenNotInCache = errors.New("token not found in cache")
|
errTokenNotInCache = errors.New("token not found in cache")
|
||||||
errNoRedisClient = errors.New("redis client not configured")
|
errNoRedisClient = errors.New("redis client not configured")
|
||||||
|
// errExpiredToken = errors.New("token expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewToken(payload TokenPayload) (string, error) {
|
type JwtManager struct {
|
||||||
|
redisCluster *redis.ClusterClient
|
||||||
return "", nil
|
secretKey string
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJwtManager(redisCluster *redis.ClusterClient, secretKey, issuer string) *JwtManager {
|
||||||
|
return &JwtManager{
|
||||||
|
redisCluster: redisCluster,
|
||||||
|
secretKey: secretKey,
|
||||||
|
issuer: issuer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 生成新的 JWT token
|
||||||
|
func (m *JwtManager) New(ctx context.Context, payload *TokenPayload) (string, error) {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return "", errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
expiresAt := now.Add(tokenLifetime)
|
||||||
|
|
||||||
|
claims := &Claims{
|
||||||
|
TokenPayload: *payload,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
Issuer: m.issuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenString, err := token.SignedString([]byte(m.secretKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储 token 到 Redis,TTL 为 30 天
|
||||||
|
userKey := tokenCachePrefixUser + payload.UserId
|
||||||
|
tokenKey := tokenCachePrefixToken + tokenString
|
||||||
|
|
||||||
|
tokenData, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
// 同时存储两个 key:用户 -> token 和 token -> payload
|
||||||
|
pipe := m.redisCluster.Pipeline()
|
||||||
|
pipe.Set(ctx, userKey, tokenString, tokenCacheTTL)
|
||||||
|
pipe.Set(ctx, tokenKey, string(tokenData), tokenCacheTTL)
|
||||||
|
_, err = pipe.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid 验证 token 有效性,支持自动换票
|
||||||
|
func (m *JwtManager) Valid(ctx context.Context, tokenString string) (*TokenPayload, error) {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return nil, errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenString == "" {
|
||||||
|
return nil, errMissingToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 token 是否在 Redis 中
|
||||||
|
tokenKey := tokenCachePrefixToken + tokenString
|
||||||
|
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||||
|
if err != nil && err != redis.Nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload TokenPayload
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, errTokenNotInCache
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(tokenData), &payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JWT 并验证签名和过期时间
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(m.secretKey), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return &claims.TokenPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew 换票逻辑:如果 token 过期但 Redis 中还存在,则生成新 token
|
||||||
|
func (m *JwtManager) Renew(ctx context.Context, tokenString string) (string, error) {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return "", errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 token 是否在 Redis 中(不检查过期时间)
|
||||||
|
tokenKey := tokenCachePrefixToken + tokenString
|
||||||
|
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return "", errTokenNotInCache
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload TokenPayload
|
||||||
|
err = json.Unmarshal([]byte(tokenData), &payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除旧 token 记录
|
||||||
|
userKey := tokenCachePrefixUser + payload.UserId
|
||||||
|
m.redisCluster.Del(ctx, tokenKey, userKey)
|
||||||
|
|
||||||
|
// 生成新 token
|
||||||
|
return m.New(ctx, &payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract payload from token without validating expiration (used for auto-renewal)
|
||||||
|
func (m *JwtManager) Extract(ctx context.Context, tokenString string) (*TokenPayload, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(m.secretKey), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return &claims.TokenPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if token exists in Redis (i.e. is valid and not revoked)
|
||||||
|
func (m *JwtManager) Exists(ctx context.Context, tokenString string) (bool, error) {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return false, errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenKey := tokenCachePrefixToken + tokenString
|
||||||
|
exists, err := m.redisCluster.Exists(ctx, tokenKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract payload from JWT claims
|
||||||
|
func (m *JwtManager) ClaimsToPayload(claims *Claims) *TokenPayload {
|
||||||
|
return &claims.TokenPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// revoke token by deleting both user -> token and token -> payload keys from Redis
|
||||||
|
func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := m.Extract(ctx, tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey := tokenCachePrefixUser + payload.UserId
|
||||||
|
tokenKey := tokenCachePrefixToken + tokenString
|
||||||
|
|
||||||
|
pipe := m.redisCluster.Pipeline()
|
||||||
|
pipe.Del(ctx, userKey)
|
||||||
|
pipe.Del(ctx, tokenKey)
|
||||||
|
_, err = pipe.Exec(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JwtManager) GetUserToken(ctx context.Context, userID string) (string, error) {
|
||||||
|
if m.redisCluster == nil {
|
||||||
|
return "", errNoRedisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey := tokenCachePrefixUser + userID
|
||||||
|
token, err := m.redisCluster.Get(ctx, userKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return "", fmt.Errorf("user %s has no token", userID)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+375
-12
@@ -906,6 +906,334 @@ func (x *GetUserByUsernameResp) GetUsers() *Users {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginReq struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||||
|
Passwd string `protobuf:"bytes,2,opt,name=passwd,proto3" json:"passwd,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginReq) Reset() {
|
||||||
|
*x = LoginReq{}
|
||||||
|
mi := &file_users_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginReq) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginReq) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginReq) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[13]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginReq.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginReq) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginReq) GetUsername() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Username
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginReq) GetPasswd() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Passwd
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResp struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResp) Reset() {
|
||||||
|
*x = LoginResp{}
|
||||||
|
mi := &file_users_proto_msgTypes[14]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResp) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginResp) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginResp) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[14]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginResp.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginResp) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{14}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResp) GetToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Token
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidateTokenReq struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // JWT token
|
||||||
|
UserId string `protobuf:"bytes,2,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenReq) Reset() {
|
||||||
|
*x = ValidateTokenReq{}
|
||||||
|
mi := &file_users_proto_msgTypes[15]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenReq) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ValidateTokenReq) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ValidateTokenReq) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[15]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ValidateTokenReq.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ValidateTokenReq) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{15}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenReq) GetToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Token
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenReq) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidateTokenResp struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` // token 是否有效(不在黑名单中)
|
||||||
|
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 验证失败原因
|
||||||
|
UserId string `protobuf:"bytes,3,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||||
|
RoleType int64 `protobuf:"varint,4,opt,name=roleType,proto3" json:"roleType,omitempty"` // 用户角色
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) Reset() {
|
||||||
|
*x = ValidateTokenResp{}
|
||||||
|
mi := &file_users_proto_msgTypes[16]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ValidateTokenResp) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[16]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ValidateTokenResp.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ValidateTokenResp) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{16}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) GetValid() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Valid
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) GetMessage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ValidateTokenResp) GetRoleType() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.RoleType
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckPermissionReq struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
UserId string `protobuf:"bytes,1,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||||
|
Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` // 资源 ID
|
||||||
|
Action string `protobuf:"bytes,3,opt,name=action,proto3" json:"action,omitempty"` // 操作类型: read/write/delete
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) Reset() {
|
||||||
|
*x = CheckPermissionReq{}
|
||||||
|
mi := &file_users_proto_msgTypes[17]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CheckPermissionReq) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[17]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CheckPermissionReq.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CheckPermissionReq) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{17}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) GetResource() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Resource
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionReq) GetAction() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Action
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckPermissionResp struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` // 是否有权限
|
||||||
|
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 拒绝原因
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionResp) Reset() {
|
||||||
|
*x = CheckPermissionResp{}
|
||||||
|
mi := &file_users_proto_msgTypes[18]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionResp) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CheckPermissionResp) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *CheckPermissionResp) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_users_proto_msgTypes[18]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use CheckPermissionResp.ProtoReflect.Descriptor instead.
|
||||||
|
func (*CheckPermissionResp) Descriptor() ([]byte, []int) {
|
||||||
|
return file_users_proto_rawDescGZIP(), []int{18}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionResp) GetAllowed() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Allowed
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *CheckPermissionResp) GetMessage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
var File_users_proto protoreflect.FileDescriptor
|
var File_users_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_users_proto_rawDesc = "" +
|
const file_users_proto_rawDesc = "" +
|
||||||
@@ -987,7 +1315,27 @@ const file_users_proto_rawDesc = "" +
|
|||||||
"\x14GetUserByUsernameReq\x12\x1a\n" +
|
"\x14GetUserByUsernameReq\x12\x1a\n" +
|
||||||
"\busername\x18\x01 \x01(\tR\busername\"8\n" +
|
"\busername\x18\x01 \x01(\tR\busername\"8\n" +
|
||||||
"\x15GetUserByUsernameResp\x12\x1f\n" +
|
"\x15GetUserByUsernameResp\x12\x1f\n" +
|
||||||
"\x05users\x18\x01 \x01(\v2\t.pb.UsersR\x05users2\xdf\x02\n" +
|
"\x05users\x18\x01 \x01(\v2\t.pb.UsersR\x05users\">\n" +
|
||||||
|
"\bLoginReq\x12\x1a\n" +
|
||||||
|
"\busername\x18\x01 \x01(\tR\busername\x12\x16\n" +
|
||||||
|
"\x06passwd\x18\x02 \x01(\tR\x06passwd\"!\n" +
|
||||||
|
"\tLoginResp\x12\x14\n" +
|
||||||
|
"\x05token\x18\x01 \x01(\tR\x05token\"@\n" +
|
||||||
|
"\x10ValidateTokenReq\x12\x14\n" +
|
||||||
|
"\x05token\x18\x01 \x01(\tR\x05token\x12\x16\n" +
|
||||||
|
"\x06userId\x18\x02 \x01(\tR\x06userId\"w\n" +
|
||||||
|
"\x11ValidateTokenResp\x12\x14\n" +
|
||||||
|
"\x05valid\x18\x01 \x01(\bR\x05valid\x12\x18\n" +
|
||||||
|
"\amessage\x18\x02 \x01(\tR\amessage\x12\x16\n" +
|
||||||
|
"\x06userId\x18\x03 \x01(\tR\x06userId\x12\x1a\n" +
|
||||||
|
"\broleType\x18\x04 \x01(\x03R\broleType\"`\n" +
|
||||||
|
"\x12CheckPermissionReq\x12\x16\n" +
|
||||||
|
"\x06userId\x18\x01 \x01(\tR\x06userId\x12\x1a\n" +
|
||||||
|
"\bresource\x18\x02 \x01(\tR\bresource\x12\x16\n" +
|
||||||
|
"\x06action\x18\x03 \x01(\tR\x06action\"I\n" +
|
||||||
|
"\x13CheckPermissionResp\x12\x18\n" +
|
||||||
|
"\aallowed\x18\x01 \x01(\bR\aallowed\x12\x18\n" +
|
||||||
|
"\amessage\x18\x02 \x01(\tR\amessage2\x87\x04\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"usercenter\x12-\n" +
|
"usercenter\x12-\n" +
|
||||||
"\bAddUsers\x12\x0f.pb.AddUsersReq\x1a\x10.pb.AddUsersResp\x126\n" +
|
"\bAddUsers\x12\x0f.pb.AddUsersReq\x1a\x10.pb.AddUsersResp\x126\n" +
|
||||||
@@ -995,7 +1343,10 @@ const file_users_proto_rawDesc = "" +
|
|||||||
"\bDelUsers\x12\x0f.pb.DelUsersReq\x1a\x10.pb.DelUsersResp\x129\n" +
|
"\bDelUsers\x12\x0f.pb.DelUsersReq\x1a\x10.pb.DelUsersResp\x129\n" +
|
||||||
"\fGetUsersById\x12\x13.pb.GetUsersByIdReq\x1a\x14.pb.GetUsersByIdResp\x12H\n" +
|
"\fGetUsersById\x12\x13.pb.GetUsersByIdReq\x1a\x14.pb.GetUsersByIdResp\x12H\n" +
|
||||||
"\x11GetUserByUsername\x12\x18.pb.GetUserByUsernameReq\x1a\x19.pb.GetUserByUsernameResp\x126\n" +
|
"\x11GetUserByUsername\x12\x18.pb.GetUserByUsernameReq\x1a\x19.pb.GetUserByUsernameResp\x126\n" +
|
||||||
"\vSearchUsers\x12\x12.pb.SearchUsersReq\x1a\x13.pb.SearchUsersRespB\x06Z\x04./pbb\x06proto3"
|
"\vSearchUsers\x12\x12.pb.SearchUsersReq\x1a\x13.pb.SearchUsersResp\x12$\n" +
|
||||||
|
"\x05Login\x12\f.pb.LoginReq\x1a\r.pb.LoginResp\x12<\n" +
|
||||||
|
"\rValidateToken\x12\x14.pb.ValidateTokenReq\x1a\x15.pb.ValidateTokenResp\x12B\n" +
|
||||||
|
"\x0fCheckPermission\x12\x16.pb.CheckPermissionReq\x1a\x17.pb.CheckPermissionRespB\x06Z\x04./pbb\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_users_proto_rawDescOnce sync.Once
|
file_users_proto_rawDescOnce sync.Once
|
||||||
@@ -1009,7 +1360,7 @@ func file_users_proto_rawDescGZIP() []byte {
|
|||||||
return file_users_proto_rawDescData
|
return file_users_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||||
var file_users_proto_goTypes = []any{
|
var file_users_proto_goTypes = []any{
|
||||||
(*Users)(nil), // 0: pb.Users
|
(*Users)(nil), // 0: pb.Users
|
||||||
(*AddUsersReq)(nil), // 1: pb.AddUsersReq
|
(*AddUsersReq)(nil), // 1: pb.AddUsersReq
|
||||||
@@ -1024,6 +1375,12 @@ var file_users_proto_goTypes = []any{
|
|||||||
(*SearchUsersResp)(nil), // 10: pb.SearchUsersResp
|
(*SearchUsersResp)(nil), // 10: pb.SearchUsersResp
|
||||||
(*GetUserByUsernameReq)(nil), // 11: pb.GetUserByUsernameReq
|
(*GetUserByUsernameReq)(nil), // 11: pb.GetUserByUsernameReq
|
||||||
(*GetUserByUsernameResp)(nil), // 12: pb.GetUserByUsernameResp
|
(*GetUserByUsernameResp)(nil), // 12: pb.GetUserByUsernameResp
|
||||||
|
(*LoginReq)(nil), // 13: pb.LoginReq
|
||||||
|
(*LoginResp)(nil), // 14: pb.LoginResp
|
||||||
|
(*ValidateTokenReq)(nil), // 15: pb.ValidateTokenReq
|
||||||
|
(*ValidateTokenResp)(nil), // 16: pb.ValidateTokenResp
|
||||||
|
(*CheckPermissionReq)(nil), // 17: pb.CheckPermissionReq
|
||||||
|
(*CheckPermissionResp)(nil), // 18: pb.CheckPermissionResp
|
||||||
}
|
}
|
||||||
var file_users_proto_depIdxs = []int32{
|
var file_users_proto_depIdxs = []int32{
|
||||||
0, // 0: pb.GetUsersByIdResp.users:type_name -> pb.Users
|
0, // 0: pb.GetUsersByIdResp.users:type_name -> pb.Users
|
||||||
@@ -1035,14 +1392,20 @@ var file_users_proto_depIdxs = []int32{
|
|||||||
7, // 6: pb.usercenter.GetUsersById:input_type -> pb.GetUsersByIdReq
|
7, // 6: pb.usercenter.GetUsersById:input_type -> pb.GetUsersByIdReq
|
||||||
11, // 7: pb.usercenter.GetUserByUsername:input_type -> pb.GetUserByUsernameReq
|
11, // 7: pb.usercenter.GetUserByUsername:input_type -> pb.GetUserByUsernameReq
|
||||||
9, // 8: pb.usercenter.SearchUsers:input_type -> pb.SearchUsersReq
|
9, // 8: pb.usercenter.SearchUsers:input_type -> pb.SearchUsersReq
|
||||||
2, // 9: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp
|
13, // 9: pb.usercenter.Login:input_type -> pb.LoginReq
|
||||||
4, // 10: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp
|
15, // 10: pb.usercenter.ValidateToken:input_type -> pb.ValidateTokenReq
|
||||||
6, // 11: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp
|
17, // 11: pb.usercenter.CheckPermission:input_type -> pb.CheckPermissionReq
|
||||||
8, // 12: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp
|
2, // 12: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp
|
||||||
12, // 13: pb.usercenter.GetUserByUsername:output_type -> pb.GetUserByUsernameResp
|
4, // 13: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp
|
||||||
10, // 14: pb.usercenter.SearchUsers:output_type -> pb.SearchUsersResp
|
6, // 14: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp
|
||||||
9, // [9:15] is the sub-list for method output_type
|
8, // 15: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp
|
||||||
3, // [3:9] is the sub-list for method input_type
|
12, // 16: pb.usercenter.GetUserByUsername:output_type -> pb.GetUserByUsernameResp
|
||||||
|
10, // 17: pb.usercenter.SearchUsers:output_type -> pb.SearchUsersResp
|
||||||
|
14, // 18: pb.usercenter.Login:output_type -> pb.LoginResp
|
||||||
|
16, // 19: pb.usercenter.ValidateToken:output_type -> pb.ValidateTokenResp
|
||||||
|
18, // 20: pb.usercenter.CheckPermission:output_type -> pb.CheckPermissionResp
|
||||||
|
12, // [12:21] is the sub-list for method output_type
|
||||||
|
3, // [3:12] is the sub-list for method input_type
|
||||||
3, // [3:3] is the sub-list for extension type_name
|
3, // [3:3] is the sub-list for extension type_name
|
||||||
3, // [3:3] is the sub-list for extension extendee
|
3, // [3:3] is the sub-list for extension extendee
|
||||||
0, // [0:3] is the sub-list for field type_name
|
0, // [0:3] is the sub-list for field type_name
|
||||||
@@ -1059,7 +1422,7 @@ func file_users_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_proto_rawDesc), len(file_users_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_proto_rawDesc), len(file_users_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 13,
|
NumMessages: 19,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ const (
|
|||||||
Usercenter_GetUsersById_FullMethodName = "/pb.usercenter/GetUsersById"
|
Usercenter_GetUsersById_FullMethodName = "/pb.usercenter/GetUsersById"
|
||||||
Usercenter_GetUserByUsername_FullMethodName = "/pb.usercenter/GetUserByUsername"
|
Usercenter_GetUserByUsername_FullMethodName = "/pb.usercenter/GetUserByUsername"
|
||||||
Usercenter_SearchUsers_FullMethodName = "/pb.usercenter/SearchUsers"
|
Usercenter_SearchUsers_FullMethodName = "/pb.usercenter/SearchUsers"
|
||||||
|
Usercenter_Login_FullMethodName = "/pb.usercenter/Login"
|
||||||
|
Usercenter_ValidateToken_FullMethodName = "/pb.usercenter/ValidateToken"
|
||||||
|
Usercenter_CheckPermission_FullMethodName = "/pb.usercenter/CheckPermission"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsercenterClient is the client API for Usercenter service.
|
// UsercenterClient is the client API for Usercenter service.
|
||||||
@@ -38,6 +41,9 @@ type UsercenterClient interface {
|
|||||||
GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error)
|
GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error)
|
||||||
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
||||||
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
||||||
|
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
|
||||||
|
ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error)
|
||||||
|
CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type usercenterClient struct {
|
type usercenterClient struct {
|
||||||
@@ -108,6 +114,36 @@ func (c *usercenterClient) SearchUsers(ctx context.Context, in *SearchUsersReq,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *usercenterClient) Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(LoginResp)
|
||||||
|
err := c.cc.Invoke(ctx, Usercenter_Login_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *usercenterClient) ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ValidateTokenResp)
|
||||||
|
err := c.cc.Invoke(ctx, Usercenter_ValidateToken_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *usercenterClient) CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(CheckPermissionResp)
|
||||||
|
err := c.cc.Invoke(ctx, Usercenter_CheckPermission_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UsercenterServer is the server API for Usercenter service.
|
// UsercenterServer is the server API for Usercenter service.
|
||||||
// All implementations must embed UnimplementedUsercenterServer
|
// All implementations must embed UnimplementedUsercenterServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -119,6 +155,9 @@ type UsercenterServer interface {
|
|||||||
GetUsersById(context.Context, *GetUsersByIdReq) (*GetUsersByIdResp, error)
|
GetUsersById(context.Context, *GetUsersByIdReq) (*GetUsersByIdResp, error)
|
||||||
GetUserByUsername(context.Context, *GetUserByUsernameReq) (*GetUserByUsernameResp, error)
|
GetUserByUsername(context.Context, *GetUserByUsernameReq) (*GetUserByUsernameResp, error)
|
||||||
SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error)
|
SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error)
|
||||||
|
Login(context.Context, *LoginReq) (*LoginResp, error)
|
||||||
|
ValidateToken(context.Context, *ValidateTokenReq) (*ValidateTokenResp, error)
|
||||||
|
CheckPermission(context.Context, *CheckPermissionReq) (*CheckPermissionResp, error)
|
||||||
mustEmbedUnimplementedUsercenterServer()
|
mustEmbedUnimplementedUsercenterServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +186,15 @@ func (UnimplementedUsercenterServer) GetUserByUsername(context.Context, *GetUser
|
|||||||
func (UnimplementedUsercenterServer) SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error) {
|
func (UnimplementedUsercenterServer) SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SearchUsers not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SearchUsers not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedUsercenterServer) Login(context.Context, *LoginReq) (*LoginResp, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUsercenterServer) ValidateToken(context.Context, *ValidateTokenReq) (*ValidateTokenResp, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ValidateToken not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUsercenterServer) CheckPermission(context.Context, *CheckPermissionReq) (*CheckPermissionResp, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method CheckPermission not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedUsercenterServer) mustEmbedUnimplementedUsercenterServer() {}
|
func (UnimplementedUsercenterServer) mustEmbedUnimplementedUsercenterServer() {}
|
||||||
func (UnimplementedUsercenterServer) testEmbeddedByValue() {}
|
func (UnimplementedUsercenterServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -276,6 +324,60 @@ func _Usercenter_SearchUsers_Handler(srv interface{}, ctx context.Context, dec f
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Usercenter_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(LoginReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UsercenterServer).Login(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Usercenter_Login_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UsercenterServer).Login(ctx, req.(*LoginReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Usercenter_ValidateToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ValidateTokenReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UsercenterServer).ValidateToken(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Usercenter_ValidateToken_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UsercenterServer).ValidateToken(ctx, req.(*ValidateTokenReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Usercenter_CheckPermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(CheckPermissionReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UsercenterServer).CheckPermission(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Usercenter_CheckPermission_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UsercenterServer).CheckPermission(ctx, req.(*CheckPermissionReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Usercenter_ServiceDesc is the grpc.ServiceDesc for Usercenter service.
|
// Usercenter_ServiceDesc is the grpc.ServiceDesc for Usercenter service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -307,6 +409,18 @@ var Usercenter_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SearchUsers",
|
MethodName: "SearchUsers",
|
||||||
Handler: _Usercenter_SearchUsers_Handler,
|
Handler: _Usercenter_SearchUsers_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Login",
|
||||||
|
Handler: _Usercenter_Login_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ValidateToken",
|
||||||
|
Handler: _Usercenter_ValidateToken_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "CheckPermission",
|
||||||
|
Handler: _Usercenter_CheckPermission_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "users.proto",
|
Metadata: "users.proto",
|
||||||
|
|||||||
@@ -16,17 +16,23 @@ import (
|
|||||||
type (
|
type (
|
||||||
AddUsersReq = pb.AddUsersReq
|
AddUsersReq = pb.AddUsersReq
|
||||||
AddUsersResp = pb.AddUsersResp
|
AddUsersResp = pb.AddUsersResp
|
||||||
|
CheckPermissionReq = pb.CheckPermissionReq
|
||||||
|
CheckPermissionResp = pb.CheckPermissionResp
|
||||||
DelUsersReq = pb.DelUsersReq
|
DelUsersReq = pb.DelUsersReq
|
||||||
DelUsersResp = pb.DelUsersResp
|
DelUsersResp = pb.DelUsersResp
|
||||||
GetUserByUsernameReq = pb.GetUserByUsernameReq
|
GetUserByUsernameReq = pb.GetUserByUsernameReq
|
||||||
GetUserByUsernameResp = pb.GetUserByUsernameResp
|
GetUserByUsernameResp = pb.GetUserByUsernameResp
|
||||||
GetUsersByIdReq = pb.GetUsersByIdReq
|
GetUsersByIdReq = pb.GetUsersByIdReq
|
||||||
GetUsersByIdResp = pb.GetUsersByIdResp
|
GetUsersByIdResp = pb.GetUsersByIdResp
|
||||||
|
LoginReq = pb.LoginReq
|
||||||
|
LoginResp = pb.LoginResp
|
||||||
SearchUsersReq = pb.SearchUsersReq
|
SearchUsersReq = pb.SearchUsersReq
|
||||||
SearchUsersResp = pb.SearchUsersResp
|
SearchUsersResp = pb.SearchUsersResp
|
||||||
UpdateUsersReq = pb.UpdateUsersReq
|
UpdateUsersReq = pb.UpdateUsersReq
|
||||||
UpdateUsersResp = pb.UpdateUsersResp
|
UpdateUsersResp = pb.UpdateUsersResp
|
||||||
Users = pb.Users
|
Users = pb.Users
|
||||||
|
ValidateTokenReq = pb.ValidateTokenReq
|
||||||
|
ValidateTokenResp = pb.ValidateTokenResp
|
||||||
|
|
||||||
Usercenter interface {
|
Usercenter interface {
|
||||||
// -----------------------users-----------------------
|
// -----------------------users-----------------------
|
||||||
@@ -36,6 +42,9 @@ type (
|
|||||||
GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error)
|
GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error)
|
||||||
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
||||||
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
||||||
|
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
|
||||||
|
ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error)
|
||||||
|
CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultUsercenter struct {
|
defaultUsercenter struct {
|
||||||
@@ -79,3 +88,18 @@ func (m *defaultUsercenter) SearchUsers(ctx context.Context, in *SearchUsersReq,
|
|||||||
client := pb.NewUsercenterClient(m.cli.Conn())
|
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||||
return client.SearchUsers(ctx, in, opts...)
|
return client.SearchUsers(ctx, in, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *defaultUsercenter) Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error) {
|
||||||
|
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||||
|
return client.Login(ctx, in, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultUsercenter) ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error) {
|
||||||
|
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||||
|
return client.ValidateToken(ctx, in, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultUsercenter) CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error) {
|
||||||
|
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||||
|
return client.CheckPermission(ctx, in, opts...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Converter - 通用结构体转换工具
|
||||||
|
|
||||||
|
利用 Go 反射机制,实现自动的 model 到 protobuf 结构体转换。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
✅ **自动字段映射** - 自动匹配同名字段并赋值
|
||||||
|
✅ **智能类型转换** - 自动处理常见类型转换
|
||||||
|
✅ **通用设计** - 支持任何 model 和 pb 结构体,无需手动编写
|
||||||
|
✅ **灵活扩展** - 支持自定义类型转换规则
|
||||||
|
|
||||||
|
## 支持的类型转换
|
||||||
|
|
||||||
|
| 源类型 | 目标类型 | 说明 |
|
||||||
|
|-------|---------|------|
|
||||||
|
| `time.Time` | `int64` | 转换为 Unix 时间戳 |
|
||||||
|
| `sql.NullTime` | `int64` | 有效时自动转换,无效则为 0 |
|
||||||
|
| `sql.NullTime` | `time.Time` | 有效时自动转换,无效则为零值 |
|
||||||
|
| `sql.NullInt64` | `int64` | 有效时自动转换,无效则为 0 |
|
||||||
|
| `sql.NullString` | `string` | 有效时自动转换,无效则为空字符串 |
|
||||||
|
| `sql.NullBool` | `bool` | 有效时自动转换,无效则为 false |
|
||||||
|
| `int` | `int64` | 自动转换 |
|
||||||
|
| `int64` | `int` | 自动转换 |
|
||||||
|
| 相同类型 | 相同类型 | 直接复制 |
|
||||||
|
|
||||||
|
## 核心函数
|
||||||
|
|
||||||
|
### 1. StructToStruct - 单个结构体转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
func StructToStruct(src, dst interface{}) error
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `src` - 源结构体(可以是指针或值类型)
|
||||||
|
- `dst` - 目标结构体(必须是指针)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "app/common/converter"
|
||||||
|
|
||||||
|
// 单个 model 转 pb
|
||||||
|
user, _ := m.FindOne(ctx, userId)
|
||||||
|
pbUser := &pb.Users{}
|
||||||
|
converter.StructToStruct(user, pbUser)
|
||||||
|
|
||||||
|
// 或直接点对点转换
|
||||||
|
pbUser := &pb.Users{}
|
||||||
|
_ = converter.StructToStruct(user, pbUser)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SliceToSlice - 切片转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `src` - 源切片
|
||||||
|
- `dstSliceType` - 目标切片类型(用于推导元素类型)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 多个 model 转 pb
|
||||||
|
users := []*models.Users{user1, user2, user3}
|
||||||
|
pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{})
|
||||||
|
pbUsers := pbUsersIface.([]*pb.Users)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. UserModelToPb - Users 专用转换(推荐)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func UserModelToPb(user *models.Users) *pb.Users
|
||||||
|
```
|
||||||
|
|
||||||
|
简化的 Users model 转 pb 的快捷函数。
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
user, _ := m.FindOne(ctx, userId)
|
||||||
|
pbUser := converter.UserModelToPb(user)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. UserModelsToPb - Users 批量转换(推荐)
|
||||||
|
|
||||||
|
```go
|
||||||
|
func UserModelsToPb(users []*models.Users) []*pb.Users
|
||||||
|
```
|
||||||
|
|
||||||
|
简化的批量转换快捷函数。
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
users, _ := m.FindAll(ctx)
|
||||||
|
pbUsers := converter.UserModelsToPb(users)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 场景 1:在 Logic 层直接转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"app/common/converter"
|
||||||
|
"app/users/rpc/internal/models"
|
||||||
|
"app/users/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetUserByIdLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetUserByIdLogic) GetUserById(req *pb.GetUserByIdReq) (*pb.Users, error) {
|
||||||
|
// 查询数据库
|
||||||
|
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接转换,无需手动赋值每个字段
|
||||||
|
pbUser := converter.UserModelToPb(user)
|
||||||
|
|
||||||
|
return pbUser, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:批量操作
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (l *ListUsersLogic) ListUsers(req *pb.ListUsersReq) (*pb.ListUsersResp, error) {
|
||||||
|
users, err := l.svcCtx.UsersModel.FindAll(l.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量转换
|
||||||
|
pbUsers := converter.UserModelsToPb(users)
|
||||||
|
|
||||||
|
return &pb.ListUsersResp{
|
||||||
|
Users: pbUsers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:搜索/过滤结果
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (l *SearchUsersLogic) SearchUsers(req *pb.SearchReq) (*pb.SearchResp, error) {
|
||||||
|
// 搜索数据库
|
||||||
|
results, err := l.svcCtx.UsersModel.SearchByKeyword(l.ctx, req.Keyword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUsers := converter.UserModelsToPb(results)
|
||||||
|
|
||||||
|
return &pb.SearchResp{
|
||||||
|
Results: pbUsers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 处理特殊字段
|
||||||
|
|
||||||
|
### NULLable 字段
|
||||||
|
|
||||||
|
当源字段是 `sql.NullTime` 或其他 `sql.Null*` 类型时,转换器会自动处理:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// sql.NullTime -> int64(有效情况)
|
||||||
|
user.DeletedAt = sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
// 转换后 pb.Users.DeletedAt 会包含 Unix 时间戳
|
||||||
|
|
||||||
|
// sql.NullTime -> int64(无效情况)
|
||||||
|
user.DeletedAt = sql.NullTime{
|
||||||
|
Valid: false,
|
||||||
|
}
|
||||||
|
// 转换后 pb.Users.DeletedAt 为 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间戳字段
|
||||||
|
|
||||||
|
数据库中的 `time.Time` 字段会自动转换为 protobuf 中的 `int64` Unix 时间戳:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Model
|
||||||
|
type Users struct {
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
DeletedAt sql.NullTime `db:"deleted_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protobuf
|
||||||
|
type Users struct {
|
||||||
|
CreatedAt int64 // 自动转换为 Unix 时间戳
|
||||||
|
UpdatedAt int64 // 自动转换为 Unix 时间戳
|
||||||
|
DeletedAt int64 // 有效时转换,无效时为 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展 - 添加自定义类型转换
|
||||||
|
|
||||||
|
如果需要支持新的类型转换,可以在 `generic.go` 的 `assignValue` 函数中添加:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 处理自定义类型 MyType -> int32 的转换
|
||||||
|
if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 {
|
||||||
|
mt := srcField.Interface().(MyType)
|
||||||
|
dstField.SetInt(int64(mt.SomeIntField))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
- 反射操作相对于直接赋值会有性能开销(通常很小)
|
||||||
|
- 如果需要转换大量数据(>10000 条),考虑性能测试
|
||||||
|
- 对于热点代码路径,可以写针对性的转换函数
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := converter.StructToStruct(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("转换失败: %v", err)
|
||||||
|
// 处理错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: 字段名必须完全相同吗?**
|
||||||
|
A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 `UserId`,pb 字段也必须是 `UserId`。
|
||||||
|
|
||||||
|
**Q: 如果某个字段转换失败怎么办?**
|
||||||
|
A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。
|
||||||
|
|
||||||
|
**Q: 能否自定义字段映射规则(比如 `db_name` -> `pbName`)?**
|
||||||
|
A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。
|
||||||
|
|
||||||
|
**Q: 转换速度快吗?**
|
||||||
|
A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `generic.go` - 通用转换函数核心实现
|
||||||
|
- `user_converter.go` - Users model 专用转换函数(示例)
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StructToStruct 通用结构体转换函数,利用反射将源结构体的字段值复制到目标结构体
|
||||||
|
// src: 源结构体(通常是 model)
|
||||||
|
// dst: 目标结构体(通常是 pb),必须是指针
|
||||||
|
// 支持的自动转换:
|
||||||
|
// - time.Time -> int64 (Unix 时间戳)
|
||||||
|
// - sql.NullTime -> int64 (如果有效)
|
||||||
|
// - sql.NullInt64 -> int64
|
||||||
|
// - sql.NullString -> string
|
||||||
|
// - 相同名称和兼容类型的字段
|
||||||
|
func StructToStruct(src, dst interface{}) error {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srcVal := reflect.ValueOf(src)
|
||||||
|
dstVal := reflect.ValueOf(dst)
|
||||||
|
|
||||||
|
// 确保 dst 是指针
|
||||||
|
if dstVal.Kind() != reflect.Ptr {
|
||||||
|
return newError("destination must be a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
dstVal = dstVal.Elem()
|
||||||
|
|
||||||
|
// 如果 src 是指针,解引用
|
||||||
|
if srcVal.Kind() == reflect.Ptr {
|
||||||
|
srcVal = srcVal.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 都必须是结构体
|
||||||
|
if srcVal.Kind() != reflect.Struct || dstVal.Kind() != reflect.Struct {
|
||||||
|
return newError("both source and destination must be structs")
|
||||||
|
}
|
||||||
|
|
||||||
|
srcType := srcVal.Type()
|
||||||
|
|
||||||
|
// 遍历源结构体的所有字段
|
||||||
|
for i := 0; i < srcVal.NumField(); i++ {
|
||||||
|
srcField := srcVal.Field(i)
|
||||||
|
srcFieldName := srcType.Field(i).Name
|
||||||
|
|
||||||
|
// 在目标结构体中查找同名字段
|
||||||
|
dstField := dstVal.FieldByName(srcFieldName)
|
||||||
|
if !dstField.IsValid() || !dstField.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行类型转换和赋值
|
||||||
|
if err := assignValue(srcField, dstField); err != nil {
|
||||||
|
continue // 如果单个字段转换失败,继续处理其他字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignValue 尝试将源字段值赋给目标字段
|
||||||
|
func assignValue(srcField, dstField reflect.Value) error {
|
||||||
|
// 如果是可直接赋值的类型
|
||||||
|
if srcField.Type() == dstField.Type() {
|
||||||
|
dstField.Set(srcField)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srcType := srcField.Type()
|
||||||
|
dstType := dstField.Type()
|
||||||
|
|
||||||
|
// 处理 time.Time -> int64 的转换
|
||||||
|
if srcType == reflect.TypeOf(time.Time{}) && dstType.Kind() == reflect.Int64 {
|
||||||
|
t := srcField.Interface().(time.Time)
|
||||||
|
dstField.SetInt(t.Unix())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sql.NullTime -> int64 的转换
|
||||||
|
if srcType == reflect.TypeOf(sql.NullTime{}) && dstType.Kind() == reflect.Int64 {
|
||||||
|
nt := srcField.Interface().(sql.NullTime)
|
||||||
|
if nt.Valid {
|
||||||
|
dstField.SetInt(nt.Time.Unix())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sql.NullTime -> time.Time 的转换
|
||||||
|
if srcType == reflect.TypeOf(sql.NullTime{}) && dstType == reflect.TypeOf(time.Time{}) {
|
||||||
|
nt := srcField.Interface().(sql.NullTime)
|
||||||
|
if nt.Valid {
|
||||||
|
dstField.Set(reflect.ValueOf(nt.Time))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sql.NullInt64 -> int64 的转换
|
||||||
|
if srcType == reflect.TypeOf(sql.NullInt64{}) && dstType.Kind() == reflect.Int64 {
|
||||||
|
ni := srcField.Interface().(sql.NullInt64)
|
||||||
|
if ni.Valid {
|
||||||
|
dstField.SetInt(ni.Int64)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sql.NullString -> string 的转换
|
||||||
|
if srcType == reflect.TypeOf(sql.NullString{}) && dstType.Kind() == reflect.String {
|
||||||
|
ns := srcField.Interface().(sql.NullString)
|
||||||
|
if ns.Valid {
|
||||||
|
dstField.SetString(ns.String)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 sql.NullBool -> bool 的转换
|
||||||
|
if srcType == reflect.TypeOf(sql.NullBool{}) && dstType.Kind() == reflect.Bool {
|
||||||
|
nb := srcField.Interface().(sql.NullBool)
|
||||||
|
if nb.Valid {
|
||||||
|
dstField.SetBool(nb.Bool)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 int -> int64 的转换
|
||||||
|
if srcType.Kind() == reflect.Int && dstType.Kind() == reflect.Int64 {
|
||||||
|
dstField.SetInt(int64(srcField.Int()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 int64 -> int 的转换
|
||||||
|
if srcType.Kind() == reflect.Int64 && dstType.Kind() == reflect.Int {
|
||||||
|
dstField.SetInt(srcField.Int())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 string -> string(某些情况下可能存在复制)
|
||||||
|
if srcType.Kind() == reflect.String && dstType.Kind() == reflect.String {
|
||||||
|
dstField.SetString(srcField.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 bool -> bool
|
||||||
|
if srcType.Kind() == reflect.Bool && dstType.Kind() == reflect.Bool {
|
||||||
|
dstField.SetBool(srcField.Bool())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return newError("unsupported type conversion from " + srcType.String() + " to " + dstType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceToSlice 通用切片转换函数,使用 StructToStruct 转换每个元素
|
||||||
|
func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error) {
|
||||||
|
srcVal := reflect.ValueOf(src)
|
||||||
|
|
||||||
|
// src 必须是切片
|
||||||
|
if srcVal.Kind() != reflect.Slice {
|
||||||
|
return nil, newError("source must be a slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始 dst slice type
|
||||||
|
dstSliceVal := reflect.ValueOf(dstSliceType)
|
||||||
|
if dstSliceVal.Kind() != reflect.Slice {
|
||||||
|
return nil, newError("dstSliceType must be a slice type")
|
||||||
|
}
|
||||||
|
|
||||||
|
dstSliceElemType := dstSliceVal.Type().Elem()
|
||||||
|
|
||||||
|
// 创建新的目标切片
|
||||||
|
dstSlice := reflect.MakeSlice(dstSliceVal.Type(), srcVal.Len(), srcVal.Len())
|
||||||
|
|
||||||
|
// 逐个转换元素
|
||||||
|
for i := 0; i < srcVal.Len(); i++ {
|
||||||
|
srcElem := srcVal.Index(i)
|
||||||
|
dstElem := reflect.New(dstSliceElemType)
|
||||||
|
|
||||||
|
// 如果 src 元素是指针,需要解引用
|
||||||
|
if srcElem.Kind() == reflect.Ptr {
|
||||||
|
srcElem = srcElem.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换单个元素
|
||||||
|
dstElemIface := dstElem.Interface()
|
||||||
|
if err := StructToStruct(srcElem.Interface(), dstElemIface); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstSlice.Index(i).Set(dstElem.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstSlice.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func newError(msg string) error {
|
||||||
|
return &Error{msg: msg}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"app/users/rpc/internal/models"
|
||||||
|
"app/users/rpc/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserModelToPb 将 Users Model 转换为 protobuf Users
|
||||||
|
// 使用通用转换函数,自动处理所有字段
|
||||||
|
func UserModelToPb(user *models.Users) *pb.Users {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pbUser := &pb.Users{}
|
||||||
|
_ = StructToStruct(user, pbUser)
|
||||||
|
return pbUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserModelsToPb 将多个 Users Model 转换为 protobuf Users
|
||||||
|
// 使用通用转换函数,自动处理所有元素
|
||||||
|
func UserModelsToPb(users []*models.Users) []*pb.Users {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return []*pb.Users{}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := SliceToSlice(users, []*pb.Users{})
|
||||||
|
return result.([]*pb.Users)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// bcrypt 密钥成本
|
||||||
|
bcryptCost = bcrypt.DefaultCost
|
||||||
|
)
|
||||||
|
|
||||||
|
// HashPassword 对密码进行哈希加密
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword 验证密码是否正确
|
||||||
|
func VerifyPassword(hashedPassword, password string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFFTCCAv2gAwIBAgIUXj+1vyqKDhTsubwSmcHY61+YvmQwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMB4XDTI2MDIyMzExNTYxNloX
|
||||||
|
DTI3MDIyMzExNTYxNlowGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMIICIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkn7Jw5f0awGFbGL3ZHEPJanZO9Yk
|
||||||
|
JDUklLF3kABiXqawSFpM6pXfKMa4VHE6/MfpREQeX2lkvtBseOf/vhC4DLACui8g
|
||||||
|
yslUObv77xGSXmIwjFcXZzLPQ/gEs2lxikxeoI4Su9qpsUQNzUD10rvWMx0iea8Z
|
||||||
|
47Z4RI6fIlA5xC5N4VfUFQdE/VN670HdiTZ7YAFIg9F/ZJQMH+hPVNSLgY6J0RdU
|
||||||
|
3gqKAkAvmCQZyQKWG1eRqKauw4CIvk6d7N+nOzmwDb6clueFj7Kx4h4IAFHCQthn
|
||||||
|
eXrf21uBCVwVjs64ilnTVwFfklr79euYRHPmRqR5eswbIGpDEFOaf1smu4hrkK9s
|
||||||
|
tQ8YWey8TICymBaXr1hI+WjSVEQFN8xPoVQwiKJRdu7lIosDjbH8V/ooKGMhCHgl
|
||||||
|
5C995L3sKsMyCMkw90viYNy2jUuSNu2X3eK+QJip2D2smfSM2tBsFtiXyEk+WeyY
|
||||||
|
cRDlwB7+6vvVwCHqz0+4lr0HHBEky43m3NgUtZoulfRwv4znGXcMqvxVUm4pwoBf
|
||||||
|
lo7zVuXh+cXrEzzCksQiCBzBM115itb3la8RX8A4bRUs38XG6Bz+Qfr6RQspppV1
|
||||||
|
vNd5mUOyBYNeVfErf59PnFsdMI3kD0UgwpLkkGdSGdzDKykdt7vffNRpV8jOYuuO
|
||||||
|
LxH+2WlebCv1N90CAwEAAaNTMFEwHQYDVR0OBBYEFF80R0EZORGRXrZTVrAfaatK
|
||||||
|
eNi9MB8GA1UdIwQYMBaAFF80R0EZORGRXrZTVrAfaatKeNi9MA8GA1UdEwEB/wQF
|
||||||
|
MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHFZUflyNOCJqV+RghOAaVFDc7wqtZJ1
|
||||||
|
d2dpIs28kKd43Nd+xjSZLmSmVhcntQNwqC8AHIuJKKNDmM5BRnzls1ZO+OLc+YcC
|
||||||
|
kXzO2aBrNz8a0S0nYGzgR+CoTPvd61RGGHbqQNvZiroWsC4NaR+7NYPzsORNaN+1
|
||||||
|
p/xqZygOYLOcD5tP5iNlgBugD+nPEHL0cylE0XpoZ059MIITdlvsrdPgHhFn9Nvv
|
||||||
|
McPZp4nzpJvyUmVjkbT7ZbKIJFrOQ6qJ9U2y55F4xuHzvnaAsOGnGx1tyBHtvkA1
|
||||||
|
IIovrku4su3TmMsBs/6ikT8XSR20gcsDq3N2RcFtgU5LONsWvUL9CTp7P7lMlIfg
|
||||||
|
v1RelzXDE2mESlZEbzbFyVVGAoEPZA4t6kgBV4zObxxp4YmimqGWmVs3qQ/A6wbV
|
||||||
|
OO4rLYW7NZeJLLvsGOabVK+jyFCMyB3YOS6nZ9q48SaWCHlFTZveluP5n/8Y5LGc
|
||||||
|
ppjaZbsG2/apCqlown6jKT7hkP84eu3a+HyQ6ZXpCa6P9c9OZ8bVlP8dXi4mRuhU
|
||||||
|
lINwIKA0HbFAzwhyArMkLFWsw26ImusLZH1KUjHabzbfxnDgb9hwIlSGyPrcHcYY
|
||||||
|
lXTlThSXL0ERoqafQTE9tpPFXC+LCneytAKUgM2TZ1KhRlisA9Tb3i0X4y/yJba7
|
||||||
|
T2Eqz8rRnaIe
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCSfsnDl/RrAYVs
|
||||||
|
YvdkcQ8lqdk71iQkNSSUsXeQAGJeprBIWkzqld8oxrhUcTr8x+lERB5faWS+0Gx4
|
||||||
|
5/++ELgMsAK6LyDKyVQ5u/vvEZJeYjCMVxdnMs9D+ASzaXGKTF6gjhK72qmxRA3N
|
||||||
|
QPXSu9YzHSJ5rxnjtnhEjp8iUDnELk3hV9QVB0T9U3rvQd2JNntgAUiD0X9klAwf
|
||||||
|
6E9U1IuBjonRF1TeCooCQC+YJBnJApYbV5Gopq7DgIi+Tp3s36c7ObANvpyW54WP
|
||||||
|
srHiHggAUcJC2Gd5et/bW4EJXBWOzriKWdNXAV+SWvv165hEc+ZGpHl6zBsgakMQ
|
||||||
|
U5p/Wya7iGuQr2y1DxhZ7LxMgLKYFpevWEj5aNJURAU3zE+hVDCIolF27uUiiwON
|
||||||
|
sfxX+igoYyEIeCXkL33kvewqwzIIyTD3S+Jg3LaNS5I27Zfd4r5AmKnYPayZ9Iza
|
||||||
|
0GwW2JfIST5Z7JhxEOXAHv7q+9XAIerPT7iWvQccESTLjebc2BS1mi6V9HC/jOcZ
|
||||||
|
dwyq/FVSbinCgF+WjvNW5eH5xesTPMKSxCIIHMEzXXmK1veVrxFfwDhtFSzfxcbo
|
||||||
|
HP5B+vpFCymmlXW813mZQ7IFg15V8St/n0+cWx0wjeQPRSDCkuSQZ1IZ3MMrKR23
|
||||||
|
u9981GlXyM5i644vEf7ZaV5sK/U33QIDAQABAoICAA34ohDxm8mdxEYFPT9ayf1H
|
||||||
|
UNS0VE+QsuusbjDxXHBW+N55oDbKMtV+eENzZhMIFM7iKTxjvow1L/cq9xi/GvJ4
|
||||||
|
0dXEW14Dq/DypPEUra8rMaKcxrpcnehHTdl3f7DXHjo1OoOoc8EYcrGF1bvylpfa
|
||||||
|
2jgdMzykoR02teYNnSjA2sQYPn1/6zw2uzV4xGJK7CLIlIwfzYS/2tUrMG+wcpqZ
|
||||||
|
R7sFfN5NRoK28OMTZFMnmD3E0Psy5F14U3JE6KpX3SjYlFoHOQNqUrJU8kKUpyIy
|
||||||
|
qfJ6lYnAJnvS4wBLxDGRtQda0D1Ov/jjDP8T6Dp1DDvmAUDtGNQzVjCHLKejP5MD
|
||||||
|
ltUjTDiFqSXzcRmEV2Jq8y/DjqWieM3BGGl77W6W8eksYqLSo2Ik4fJLqoTm5TSw
|
||||||
|
QY6d8/9gZAP+0E64MWnu0cxpMEXikPPrcjhcTFASxNBoxVhxKseRt+tgkgP4krPu
|
||||||
|
hG2WsWY7n5B0iuO5Dxi0yttT5LfpKcrmRlQXqs0Jdn6nxA7us62WgegxBCXPHCpE
|
||||||
|
rMHlsbrmJECkvnQ11P7eRnpD56b5uD7Kg4uMcUVdY+EKESjm2SwK0FrSBJRvQ/mg
|
||||||
|
JKC7rf2tx7XB3tiKPrmygtLzwyU18+drCMI7fpcrf7wgwyuSdH3klkqnjF/xchQ9
|
||||||
|
RkT3ZDR6mpxhv/ytoXrhAoIBAQDISEKgj/Z+2bNLhryvRfQACzvLHVp59oyI5Xa6
|
||||||
|
MxLIVtozpq05wJxUgY4iPVXLx1Vm9/osHhXQtsFwMQTG2RI0tcz4N7YXrH4acmlm
|
||||||
|
ErdoORtcRX15mEVAl7Mwac4LVyllOZ1D9woKboHDmlBO2L8FUXy8RiLdUT0jgK7k
|
||||||
|
ShWb35twbqwQDLezLEiMnxKFCarVFVBTxVn2bhRA5jcPU+9S2oK9Qx/Mei7QlKKE
|
||||||
|
uTXLOTtNGSvY/7h0dExzS8nXwRDvsVCrWCT5pca1KfmR/JPOPbM6I/vwziSIqMNk
|
||||||
|
GfZWe5IlsQRtyZ49DpA+eDQxzMhxjZhWQ6JR5iQFWUCtXKIhAoIBAQC7P+pPEf9l
|
||||||
|
KOhdPJu6p9NPQu6+hMTi5rTyCDsH5VggvoLKDZTJ1BSqWtW1K09UafRWP5vOxm7u
|
||||||
|
fBYcnqu0W5RSUuoQTZiu03ZhYLBV5vbR+Icx0Hc2BDl8eEIyevyqsmm8+w5i28Ar
|
||||||
|
knep4sP7/n+q2EAK1B2ZlNXXz6f47CMQMkVvZp3FR0F7R6yJoS32tTL5wDOxuOFG
|
||||||
|
LYQOG/yI5JWXwBXov83zrpc+C7kl4gV3xAOk7fZ0exoRdmuvdLS96Ans85L7J8RW
|
||||||
|
ELSfhmGahM+SQ1oJMcV/wYqF2qeLL2F8DZbjR5izLZgkNz4a/VMl/A6YHtuTBXAY
|
||||||
|
+5PXXUOX+9Y9AoIBACI+II4dLxLPG9WM6tO4zRf407dNhHuXyL1bJip9svdnyhTM
|
||||||
|
qY9XPCNCp095VyLpKNPbD/3dAvPVW0tYRi3NTUyPzMSfmdWAW2sgJp8aEhuSr/fd
|
||||||
|
ta9Fdomtpihf3qeXtm8lI5tMMH5KGIud5Z8ldbtuDDqQb0ORsTdRuBU2CW3GFGhr
|
||||||
|
s6Vm1z2eE6VfSSZP2dJmu34nHtOATJwwADfxrNhonbPINzaZqUlmMEcq92SQm2/6
|
||||||
|
HsISLrJSdAO+cHsf+kpQ8a7p+iBo1ImC7LWmDotTh0IohtnMFPj8ibOisLhmlj01
|
||||||
|
f8FZmGFuDQFxQdNF5PttLx+InscL5xq3ANTjIqECggEADpdtd9nsMALfEJzveb0o
|
||||||
|
P0308s2/1fqqcQ3pI7Vgh7Sw1nP2ez/WmGvZqXOFjAtxqeLtDlDyRg1PX82Rjc1x
|
||||||
|
InUpnjmdw0nhOLdjJl6IL1aRmnUnRQNRQ3zPk8V3uQmMKdjahyOetwaD4q40HYf4
|
||||||
|
hOSzIOTkpZoui9G3wjMMjG+Ob57sfnoOBUBRlqwDu+zk2wd6P8grbd+QIdVWeYhu
|
||||||
|
i9PBIVEJCIs7Z+9b7zLMwEd7DTgp82vAXUoAHD0Y9I+HbnqQope3ugk1OhUrt/HP
|
||||||
|
hxNOidbiEBGR7NpcIgGAND2O24kxwgy0hWX0pf/FofkhXgNRkwRidt/r5mVzJf3O
|
||||||
|
9QKCAQAcPXczJY1gynUA8uD/1ODmjpDjWAk0EKBEWY5X2oULv2+xGMNTbT8pwE3f
|
||||||
|
1rszdtF3ckDPoBn7XS9OJwHnVHfXZNJHBtq9utLu0ccE+29HRG0pLCzATsvtoBWi
|
||||||
|
MEwZ2mPqhVpktfqEnL27l/QHkP7dNOyh+halVCHMfy1aNMY6hsKrOcmVmYHVARX0
|
||||||
|
Np2sG9zQszE0+t2mf8Pfd7cEvVuSTIfYZnW+77+PaVkICXXX0rrvwXVh/DVXwmWH
|
||||||
|
kYbDIdiNs9NEFwCmIvzLVsCp0qGUuq9txYo/ML5PMzJhN6X3U+rV42GkkT7KxwH2
|
||||||
|
Izss0+mp4ijKEFQuCGCkxjFmxUEq
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# Envoy Gateway 配置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Envoy Gateway 作为 API 统一入口,提供以下功能:
|
||||||
|
- **JWT 身份验证**:所有 API 请求(除登录/注册)都需要有效的 JWT token
|
||||||
|
- **CSRF 防护**:防止跨站点请求伪造攻击
|
||||||
|
- **速率限制**:防止 DDoS 攻击
|
||||||
|
- **TLS 加密**:所有通信都加密
|
||||||
|
- **负载均衡**:分担后端服务的流量
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Client │
|
||||||
|
└──────┬──────┘
|
||||||
|
│ HTTP/HTTPS (Port 80/443)
|
||||||
|
│
|
||||||
|
┌──────▼────────────────┐
|
||||||
|
│ Envoy Gateway │
|
||||||
|
│ ┌────────────────┐ │
|
||||||
|
│ │ JWT Validator │ │ ◄─── JWT Verification (offline)
|
||||||
|
│ │ CSRF Filter │ │
|
||||||
|
│ │ Rate Limiter │ │
|
||||||
|
│ │ Router │ │
|
||||||
|
│ └────────────────┘ │
|
||||||
|
└────────┬─────────────┘
|
||||||
|
│ gRPC/HTTP
|
||||||
|
┌────┴────┬──────────┐
|
||||||
|
│ │ │
|
||||||
|
┌───▼──┐ ┌──▼────┐ ┌──▼─────┐
|
||||||
|
│User │ │Order │ │User │
|
||||||
|
│API │ │API │ │RPC │
|
||||||
|
└──────┘ └───────┘ └────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署步骤
|
||||||
|
|
||||||
|
### 1. 生成 TLS 证书
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 为 Envoy 生成自签名证书(生产环境应使用正式证书)
|
||||||
|
kubectl create secret tls envoy-tls \
|
||||||
|
--cert=path/to/tls.crt \
|
||||||
|
--key=path/to/tls.key \
|
||||||
|
-n juwan
|
||||||
|
|
||||||
|
# 或生成自签名证书(仅用于测试)
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
|
||||||
|
-days 365 -nodes -subj "/CN=api.juwan.local"
|
||||||
|
|
||||||
|
kubectl create secret tls envoy-tls \
|
||||||
|
--cert=tls.crt \
|
||||||
|
--key=tls.key \
|
||||||
|
-n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 部署 Envoy Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用 Envoy 配置
|
||||||
|
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||||
|
|
||||||
|
# 查看部署状态
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
kubectl get svc -n juwan envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 配置 JWKS 端点
|
||||||
|
|
||||||
|
在 user-rpc 中暴露 JWKS 端点,供 Envoy 验证 JWT:
|
||||||
|
|
||||||
|
#### 在 `app/users/rpc` 中添加 HTTP 路由(go-zero)
|
||||||
|
|
||||||
|
编辑 `app/users/rpc/internal/handler/` 或在 `main.go` 中:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在 rpc server 启动时,添加 HTTP 端点用于暴露 JWKS
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"juwan-backend/app/users/rpc/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在 main 函数中
|
||||||
|
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||||
|
if secretKey == "" {
|
||||||
|
secretKey = "your-default-secret-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
jwksJSON, err := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate JWKS", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(jwksJSON))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在单独的 goroutine 中启动 HTTP 服务器
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
**或使用 Echo 框架(更推荐)**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在 main.go 中
|
||||||
|
import "github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
e.GET("/.well-known/jwks.json", func(c echo.Context) error {
|
||||||
|
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||||
|
jwksJSON, _ := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||||
|
return c.JSONBlob(http.StatusOK, []byte(jwksJSON))
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
e.Start(":8080")
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 更新环境变量
|
||||||
|
|
||||||
|
在 K8s Secret 中配置 JWT_SECRET_KEY:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret
|
||||||
|
namespace: juwan
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
JWT_SECRET_KEY: "$(echo -n 'your-secret-key-change-this' | base64)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 验证 JWKS 端点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 端口转发
|
||||||
|
kubectl port-forward -n juwan svc/user-rpc-svc 9001:9001
|
||||||
|
|
||||||
|
# 验证 JWKS 端点可访问
|
||||||
|
curl http://localhost:9001/.well-known/jwks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT 验证流程
|
||||||
|
|
||||||
|
### 1. 登录获取 Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://api.juwan.local/api/v1/users/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "password123"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 响应:
|
||||||
|
# {
|
||||||
|
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
# "expires": 1708780800
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用 Token 访问受保护资源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
https://api.juwan.local/api/v1/users/123
|
||||||
|
|
||||||
|
# Envoy 验证步骤:
|
||||||
|
# 1. 从 Authorization header 提取 token
|
||||||
|
# 2. 从 JWKS 端点获取公钥(缓存 5 分钟)
|
||||||
|
# 3. 验证 token 签名
|
||||||
|
# 4. 检查 token 过期时间
|
||||||
|
# 5. 将验证后的用户信息添加到请求头(X-USER-ID)
|
||||||
|
# 6. 转发请求到 user-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSRF 防护
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
Envoy 的 CSRF 过滤器检查:
|
||||||
|
- 只对 POST/PUT/DELETE/PATCH 请求进行检查
|
||||||
|
- 检查 `Origin` 和 `Referer` header
|
||||||
|
- 验证请求来自已知域名
|
||||||
|
|
||||||
|
### 跨域请求配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Envoy 配置中允许的来源
|
||||||
|
additional_origins:
|
||||||
|
- exact: "https://admin.juwan.local"
|
||||||
|
- exact: "https://web.juwan.local"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token 黑名单检查(可选)
|
||||||
|
|
||||||
|
如果需要验证 token 未被撤销,可启用额外的 RPC 验证:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# envoy-gateway.yaml 中取消注释
|
||||||
|
- name: envoy.filters.http.ext_authz
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
|
||||||
|
grpc_service:
|
||||||
|
envoy_grpc:
|
||||||
|
cluster_name: user_rpc_cluster
|
||||||
|
failure_mode_allow: false
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 user-rpc 中实现 ValidateToken RPC:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc ValidateToken(ValidateTokenReq) returns(ValidateTokenResp);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 1. JWT 验证失败
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
|
||||||
|
# 验证 JWKS 端点是否可访问
|
||||||
|
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||||
|
curl http://user-rpc-svc:9001/.well-known/jwks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 无法连接到后端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证服务发现
|
||||||
|
kubectl get endpoints -n juwan
|
||||||
|
|
||||||
|
# 验证网络策略
|
||||||
|
kubectl get networkpolicy -n juwan
|
||||||
|
|
||||||
|
# 测试连接
|
||||||
|
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||||
|
curl http://user-api-svc:8888/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CSRF 错误
|
||||||
|
|
||||||
|
- 确保设置了 `Origin` 和 `Referer` header
|
||||||
|
- 检查 `additional_origins` 配置是否包含你的域名
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. JWKS 缓存
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cache_ttl:
|
||||||
|
seconds: 300 # 缓存 5 分钟,减少 RPC 调用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 连接池
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http2_protocol_options: {} # 启用 HTTP/2 多路复用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 速率限制调整
|
||||||
|
|
||||||
|
根据实际流量调整令牌桶参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
token_bucket:
|
||||||
|
max_tokens: 10000 # 最大令牌数
|
||||||
|
tokens_per_fill: 10000 # 每次填充的令牌数
|
||||||
|
fill_interval: 1s # 填充间隔
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和日志
|
||||||
|
|
||||||
|
### 访问日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看访问日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway --follow
|
||||||
|
|
||||||
|
# 格式包含:
|
||||||
|
# - 请求时间、方法、路径
|
||||||
|
# - 响应状态码、字节数
|
||||||
|
# - 上游服务信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prometheus 指标
|
||||||
|
|
||||||
|
Envoy 在 `:9901/stats` 暴露 Prometheus 指标:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n juwan svc/envoy-gateway 9901:9901
|
||||||
|
curl localhost:9901/stats | grep jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生产环境检查清单
|
||||||
|
|
||||||
|
- [ ] 使用正式 TLS 证书(不是自签名)
|
||||||
|
- [ ] 配置正确的 JWT_SECRET_KEY(强密码)
|
||||||
|
- [ ] 启用 HTTPS(关闭 HTTP)
|
||||||
|
- [ ] 配置网络策略限制访问
|
||||||
|
- [ ] 启用访问日志和监控
|
||||||
|
- [ ] 设置合理的速率限制
|
||||||
|
- [ ] 测试 token 过期和刷新流程
|
||||||
|
- [ ] 配置告警规则
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [Envoy JWT Authentication](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto)
|
||||||
|
- [Envoy CSRF Protection](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/csrf/v3/csrf.proto)
|
||||||
|
- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
# Envoy 配置完整清单
|
||||||
|
|
||||||
|
## 📋 配置文件清单
|
||||||
|
|
||||||
|
### 1. Proto 更新
|
||||||
|
- **文件**: [desc/rpc/users.proto](../../desc/rpc/users.proto)
|
||||||
|
- **更改**: 添加了两个 RPC 方法
|
||||||
|
- `ValidateToken()`: 验证 token 是否有效(检查黑名单)
|
||||||
|
- `CheckPermission()`: 检查用户权限
|
||||||
|
|
||||||
|
### 2. Envoy 部署
|
||||||
|
- **配置文件**: [envoy.yaml](./envoy.yaml)
|
||||||
|
- HTTP 监听器(端口 8080)
|
||||||
|
- HTTPS 监听器(端口 8443)
|
||||||
|
- JWT 验证过滤器
|
||||||
|
- CSRF 防护过滤器
|
||||||
|
- 速率限制(DDoS 防护)
|
||||||
|
- 路由配置
|
||||||
|
|
||||||
|
- **K8s 部署**: [../k8s/envoy-gateway.yaml](../k8s/envoy-gateway.yaml)
|
||||||
|
- 2 个副本
|
||||||
|
- 负载均衡器服务
|
||||||
|
- Service Account 和 RBAC
|
||||||
|
- Network Policy
|
||||||
|
- ConfigMap 用于配置管理
|
||||||
|
|
||||||
|
### 3. 工具代码
|
||||||
|
- **JWKS 生成**: [app/users/rpc/internal/utils/jwks.go](../../app/users/rpc/internal/utils/jwks.go)
|
||||||
|
- `GenerateJWKSFromSecret()`: 从 JWT 密钥生成 JWKS
|
||||||
|
- `GenerateJWKSEndpoint()`: 生成 JSON 输出供 Envoy 使用
|
||||||
|
- `ExtractTokenMetadata()`: 提取 token 元数据
|
||||||
|
|
||||||
|
### 4. Dockerfile
|
||||||
|
- **文件**: [Dockerfile](./Dockerfile)
|
||||||
|
- **用途**: 构建 Envoy 容器镜像
|
||||||
|
|
||||||
|
### 5. 脚本
|
||||||
|
- **文件**: [generate-jwks.sh](./generate-jwks.sh)
|
||||||
|
- **用途**: 快速生成 JWKS JSON 文件
|
||||||
|
|
||||||
|
### 6. 文档
|
||||||
|
- **文件**: [ENVOY_CONFIG_GUIDE.md](./ENVOY_CONFIG_GUIDE.md)
|
||||||
|
- **内容**: 详细的配置和部署指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速部署步骤
|
||||||
|
|
||||||
|
### 步骤 1: 生成 TLS 证书
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试环境:生成自签名证书
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
|
||||||
|
-days 365 -nodes -subj "/CN=api.juwan.local"
|
||||||
|
|
||||||
|
# 创建 K8s Secret
|
||||||
|
kubectl create secret tls envoy-tls \
|
||||||
|
--cert=tls.crt \
|
||||||
|
--key=tls.key \
|
||||||
|
-n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2: 部署 Envoy Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用部署文件
|
||||||
|
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||||
|
|
||||||
|
# 验证部署
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
kubectl get svc -n juwan envoy-gateway
|
||||||
|
|
||||||
|
# 等待 LoadBalancer 获取外部 IP
|
||||||
|
kubectl get svc -n juwan envoy-gateway -w
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3: 在 User RPC 中暴露 JWKS 端点
|
||||||
|
|
||||||
|
编辑 `app/users/rpc/rpcserver.go` 或 `main.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"http"
|
||||||
|
"os"
|
||||||
|
"juwan-backend/app/users/rpc/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在启动 RPC server 前,添加 HTTP 端点
|
||||||
|
func setupJWKSEndpoint() {
|
||||||
|
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||||
|
if secretKey == "" {
|
||||||
|
secretKey = "your-default-secret-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
jwksJSON, err := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to generate JWKS", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=300") // 缓存 5 分钟
|
||||||
|
w.Write([]byte(jwksJSON))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在独立的 goroutine 中启动 HTTP 服务器
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
setupJWKSEndpoint()
|
||||||
|
|
||||||
|
// ... 其他 RPC 启动代码 ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4: 更新 User RPC 配置
|
||||||
|
|
||||||
|
编辑 `app/users/rpc/etc/pb.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Name: pb.rpc
|
||||||
|
ListenOn: 0.0.0.0:9001
|
||||||
|
|
||||||
|
Prometheus:
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 4001
|
||||||
|
Path: /metrics
|
||||||
|
|
||||||
|
# ... 其他配置 ...
|
||||||
|
|
||||||
|
Jwt:
|
||||||
|
SecretKey: "${JWT_SECRET_KEY:your-secret-jwt-key-change-this-in-production}"
|
||||||
|
Issuer: "juwan-user-rpc"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 5: 构建并推送容器镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建 User API 镜像
|
||||||
|
docker build -t your-registry/user-api:latest ./app/users/api/
|
||||||
|
docker push your-registry/user-api:latest
|
||||||
|
|
||||||
|
# 构建 User RPC 镜像
|
||||||
|
docker build -t your-registry/user-rpc:latest ./app/users/rpc/
|
||||||
|
docker push your-registry/user-rpc:latest
|
||||||
|
|
||||||
|
# 构建 Envoy 镜像
|
||||||
|
docker build -f deploy/envoy/Dockerfile -t your-registry/envoy-gateway:latest .
|
||||||
|
docker push your-registry/envoy-gateway:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 6: 更新 K8s 部署
|
||||||
|
|
||||||
|
更新 `deploy/k8s/service/user/user-api.yaml` 和 `user-rpc.yaml`,确保使用正确的镜像和环境变量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试流程
|
||||||
|
|
||||||
|
### 1. 登录获取 Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取 Envoy 外部 IP
|
||||||
|
ENVOY_IP=$(kubectl get svc -n juwan envoy-gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||||
|
|
||||||
|
# 登录
|
||||||
|
curl -k -X POST "https://$ENVOY_IP:443/api/v1/users/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "password123"
|
||||||
|
}' | jq .
|
||||||
|
|
||||||
|
# 示例响应:
|
||||||
|
# {
|
||||||
|
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
# "expires": 1708780800
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用 Token 访问受保护资源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
curl -k -X GET "https://$ENVOY_IP:443/api/v1/users/123" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证 CSRF 防护
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# POST 请求必须有正确的 Origin/Referer
|
||||||
|
curl -k -X POST "https://$ENVOY_IP:443/api/v1/users/logout" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Origin: https://api.juwan.local" \
|
||||||
|
-H "Referer: https://api.juwan.local/" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"userId": 123}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 验证检查清单
|
||||||
|
|
||||||
|
- [ ] Envoy 容器运行正常
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] JWKS 端点可访问
|
||||||
|
```bash
|
||||||
|
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||||
|
curl http://user-rpc-svc:9001/.well-known/jwks.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] 后端服务健康
|
||||||
|
```bash
|
||||||
|
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||||
|
curl http://user-api-svc:8888/health
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] JWT 验证工作
|
||||||
|
```bash
|
||||||
|
# 不带 token 访问受保护资源应返回 401
|
||||||
|
curl -k https://api.juwan.local/api/v1/users/123
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] CSRF 防护生效
|
||||||
|
```bash
|
||||||
|
# 缺少 Origin header 的 POST 应被拒绝
|
||||||
|
curl -k -X POST https://api.juwan.local/api/v1/users/logout \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 配置调整
|
||||||
|
|
||||||
|
### 修改 JWT 密钥
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 更新 K8s Secret
|
||||||
|
kubectl patch secret jwt-secret -n juwan \
|
||||||
|
-p '{"data":{"JWT_SECRET_KEY":"'$(echo -n 'new-secret-key' | base64)'"}}'
|
||||||
|
|
||||||
|
# 2. 重启 User RPC 和 Envoy
|
||||||
|
kubectl rollout restart deployment/user-rpc-svc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调整 Envoy 速率限制
|
||||||
|
|
||||||
|
编辑 ConfigMap:
|
||||||
|
```bash
|
||||||
|
kubectl edit cm envoy-config -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 `token_bucket` 参数:
|
||||||
|
```yaml
|
||||||
|
token_bucket:
|
||||||
|
max_tokens: 5000 # 降低限制
|
||||||
|
tokens_per_fill: 5000
|
||||||
|
fill_interval: 1s
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加信任的 CSRF 来源
|
||||||
|
|
||||||
|
编辑 ConfigMap:
|
||||||
|
```yaml
|
||||||
|
additional_origins:
|
||||||
|
- exact: "https://admin.juwan.local"
|
||||||
|
- exact: "https://web.juwan.local"
|
||||||
|
- prefix: "https://app.juwan.local" # 支持前缀匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 监控和日志
|
||||||
|
|
||||||
|
### 查看 Envoy 统计
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n juwan svc/envoy-gateway 9901:9901
|
||||||
|
curl localhost:9901/stats | grep -E "(jwt_authn|csrf|http_ratelimit)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实时日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
|
||||||
|
# 查看特定日志行
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway | grep "401\|403"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监控指标(集成 Prometheus)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# prometheus-scrape-config.yaml
|
||||||
|
- job_name: 'envoy-gateway'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['envoy-gateway.juwan.svc.cluster.local:9901']
|
||||||
|
metrics_path: '/stats/prometheus'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文件位置
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/
|
||||||
|
├── envoy/
|
||||||
|
│ ├── envoy.yaml ← Envoy 核心配置
|
||||||
|
│ ├── ENVOY_CONFIG_GUIDE.md ← 详细指南
|
||||||
|
│ ├── generate-jwks.sh ← JWKS 生成脚本
|
||||||
|
│ ├── Dockerfile ← Envoy 镜像
|
||||||
|
│ └── QUICK_REFERENCE.md ← 本文件
|
||||||
|
├── k8s/
|
||||||
|
│ ├── envoy-gateway.yaml ← K8s 部署清单
|
||||||
|
│ └── secrets/jwt-secret.yaml ← JWT 密钥配置
|
||||||
|
└── script/
|
||||||
|
└── init-secrets.sh ← 初始化脚本
|
||||||
|
|
||||||
|
app/users/
|
||||||
|
├── rpc/
|
||||||
|
│ ├── internal/utils/
|
||||||
|
│ │ ├── jwks.go ← JWKS 生成工具
|
||||||
|
│ │ └── jwt.go ← JWT 管理器
|
||||||
|
│ └── etc/pb.yaml ← RPC 配置
|
||||||
|
└── api/
|
||||||
|
└── etc/user-api.yaml ← API 配置
|
||||||
|
|
||||||
|
desc/
|
||||||
|
└── rpc/users.proto ← Proto 定义(已更新)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 常见问题
|
||||||
|
|
||||||
|
1. **Envoy 无法连接到后端服务**
|
||||||
|
- 检查 K8s Service DNS: `user-api-svc.juwan.svc.cluster.local`
|
||||||
|
- 验证 NetworkPolicy 允许流量
|
||||||
|
|
||||||
|
2. **JWT 验证失败**
|
||||||
|
- 确保 JWT_SECRET_KEY 一致
|
||||||
|
- 检查 JWKS 端点是否可访问
|
||||||
|
- 查看 Envoy 日志: `grep "jwt_authn" envoy.log`
|
||||||
|
|
||||||
|
3. **CSRF 防护过于严格**
|
||||||
|
- 在 `additional_origins` 中添加允许的来源
|
||||||
|
- 对于单页应用,确保发送 `Origin` header
|
||||||
|
|
||||||
|
4. **速率限制阻止正常流量**
|
||||||
|
- 增加 `max_tokens` 和 `tokens_per_fill`
|
||||||
|
- 针对特定客户端配置不同的限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 获取帮助
|
||||||
|
|
||||||
|
- 查看 [Envoy 官方文档](https://www.envoyproxy.io/docs)
|
||||||
|
- 查看 [JWT 规范](https://tools.ietf.org/html/rfc7519)
|
||||||
|
- 检查 [CSRF 防护最佳实践](https://owasp.org/www-community/attacks/csrf)
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Envoy 快速部署脚本
|
||||||
|
# 用途:自动化部署 Envoy Gateway 到 Kubernetes
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
NAMESPACE="${NAMESPACE:-juwan}"
|
||||||
|
RELEASE_NAME="${RELEASE_NAME:-envoy-gateway}"
|
||||||
|
TIMEOUT="${TIMEOUT:-300s}"
|
||||||
|
CONTEXT="${CONTEXT:-}"
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}ℹ${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}✓${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}⚠${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}✗${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
check_dependencies() {
|
||||||
|
log_info "检查依赖..."
|
||||||
|
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
missing_deps+=("kubectl")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v openssl &> /dev/null; then
|
||||||
|
missing_deps+=("openssl")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||||
|
log_error "缺少以下依赖: ${missing_deps[*]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "所有依赖已安装"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成 TLS 证书
|
||||||
|
generate_tls_cert() {
|
||||||
|
log_info "生成 TLS 证书..."
|
||||||
|
|
||||||
|
local cert_dir="certs"
|
||||||
|
local key_file="$cert_dir/tls.key"
|
||||||
|
local cert_file="$cert_dir/tls.crt"
|
||||||
|
|
||||||
|
# 创建 certs 目录
|
||||||
|
mkdir -p "$cert_dir"
|
||||||
|
|
||||||
|
# 检查是否已存在证书
|
||||||
|
if [ -f "$cert_file" ] && [ -f "$key_file" ]; then
|
||||||
|
log_warn "证书已存在: $cert_file, $key_file"
|
||||||
|
read -p "是否要重新生成? (y/n) " -t 10 -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
log_success "使用现有证书"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成自签名证书(仅用于测试)
|
||||||
|
openssl req -x509 -newkey rsa:4096 \
|
||||||
|
-keyout "$key_file" \
|
||||||
|
-out "$cert_file" \
|
||||||
|
-days 365 -nodes \
|
||||||
|
-subj "/CN=api.juwan.local" \
|
||||||
|
-addext "subjectAltName=DNS:api.juwan.local,DNS:*.juwan.local" \
|
||||||
|
> /dev/null 2>&1
|
||||||
|
|
||||||
|
log_success "TLS 证书已生成"
|
||||||
|
log_warn "警告: 这是自签名证书,仅用于测试环境"
|
||||||
|
log_warn "生产环境应使用正式的 CA 签发证书"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建命名空间
|
||||||
|
create_namespace() {
|
||||||
|
log_info "创建 Kubernetes 命名空间..."
|
||||||
|
|
||||||
|
if kubectl get namespace "$NAMESPACE" &> /dev/null; then
|
||||||
|
log_warn "命名空间已存在: $NAMESPACE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl create namespace "$NAMESPACE"
|
||||||
|
log_success "命名空间已创建: $NAMESPACE"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建 TLS Secret
|
||||||
|
create_tls_secret() {
|
||||||
|
log_info "创建 TLS Secret..."
|
||||||
|
|
||||||
|
local cert_dir="certs"
|
||||||
|
local key_file="$cert_dir/tls.key"
|
||||||
|
local cert_file="$cert_dir/tls.crt"
|
||||||
|
|
||||||
|
# 检查证书文件
|
||||||
|
if [ ! -f "$cert_file" ] || [ ! -f "$key_file" ]; then
|
||||||
|
log_error "证书文件不存在"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 Secret 是否已存在
|
||||||
|
if kubectl get secret envoy-tls -n "$NAMESPACE" &> /dev/null; then
|
||||||
|
log_warn "Secret 已存在,删除后重建"
|
||||||
|
kubectl delete secret envoy-tls -n "$NAMESPACE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建 Secret
|
||||||
|
kubectl create secret tls envoy-tls \
|
||||||
|
-n "$NAMESPACE" \
|
||||||
|
--cert="$cert_file" \
|
||||||
|
--key="$key_file"
|
||||||
|
|
||||||
|
log_success "TLS Secret 已创建: envoy-tls"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 部署 Envoy Gateway
|
||||||
|
deploy_envoy() {
|
||||||
|
log_info "部署 Envoy Gateway..."
|
||||||
|
|
||||||
|
local manifest_file="deploy/k8s/envoy-gateway.yaml"
|
||||||
|
|
||||||
|
if [ ! -f "$manifest_file" ]; then
|
||||||
|
log_error "找不到部署清单: $manifest_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 应用部署
|
||||||
|
if [ -n "$CONTEXT" ]; then
|
||||||
|
kubectl apply -f "$manifest_file" --context="$CONTEXT"
|
||||||
|
else
|
||||||
|
kubectl apply -f "$manifest_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Envoy Gateway 部署清单已应用"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 等待部署完成
|
||||||
|
wait_deployment() {
|
||||||
|
log_info "等待部署完成(超时: $TIMEOUT)..."
|
||||||
|
|
||||||
|
kubectl rollout status deployment/envoy-gateway \
|
||||||
|
-n "$NAMESPACE" \
|
||||||
|
--timeout="$TIMEOUT" || {
|
||||||
|
log_error "部署超时"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success "部署已完成"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证部署
|
||||||
|
verify_deployment() {
|
||||||
|
log_info "验证部署..."
|
||||||
|
|
||||||
|
# 检查 Pod
|
||||||
|
local pod_count=$(kubectl get pods -n "$NAMESPACE" \
|
||||||
|
-l app=envoy-gateway \
|
||||||
|
-o jsonpath='{.items | length}')
|
||||||
|
|
||||||
|
if [ "$pod_count" -eq 0 ]; then
|
||||||
|
log_error "未找到 Envoy 容器"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "找到 $pod_count 个 Envoy 容器"
|
||||||
|
|
||||||
|
# 检查 Service
|
||||||
|
local svc_status=$(kubectl get svc -n "$NAMESPACE" |
|
||||||
|
grep envoy-gateway || echo "")
|
||||||
|
|
||||||
|
if [ -z "$svc_status" ]; then
|
||||||
|
log_error "未找到 Service"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Service 已创建"
|
||||||
|
|
||||||
|
# 显示 LoadBalancer IP
|
||||||
|
log_info "等待 LoadBalancer IP..."
|
||||||
|
local lb_ip=""
|
||||||
|
for i in {1..30}; do
|
||||||
|
lb_ip=$(kubectl get svc envoy-gateway -n "$NAMESPACE" \
|
||||||
|
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -n "$lb_ip" ] && [ "$lb_ip" != "null" ]; then
|
||||||
|
log_success "LoadBalancer IP: $lb_ip"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $i -eq 30 ]; then
|
||||||
|
log_warn "未获得 LoadBalancer IP(可能在内网环境或使用 NodePort)"
|
||||||
|
kubectl get svc -n "$NAMESPACE" envoy-gateway
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示部署信息
|
||||||
|
show_summary() {
|
||||||
|
log_info "部署摘要"
|
||||||
|
echo ""
|
||||||
|
echo " Namespace: $NAMESPACE"
|
||||||
|
echo " Release: $RELEASE_NAME"
|
||||||
|
echo ""
|
||||||
|
echo " Pods:"
|
||||||
|
kubectl get pods -n "$NAMESPACE" -l app=envoy-gateway \
|
||||||
|
-o custom-columns=NAME:.metadata.name,STATUS:.status.phase,IP:.status.podIP \
|
||||||
|
| sed 's/^/ /'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " Service:"
|
||||||
|
kubectl get svc -n "$NAMESPACE" envoy-gateway \
|
||||||
|
-o custom-columns=NAME:.metadata.name,TYPE:.spec.type,IP:.spec.clusterIP,EXTERNAL_IP:.status.loadBalancer.ingress[0].ip \
|
||||||
|
| sed 's/^/ /'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " 后续步骤:"
|
||||||
|
echo " 1. 在 User RPC 中暴露 JWKS 端点 (/.well-known/jwks.json)"
|
||||||
|
echo " 2. 配置 JWT_SECRET_KEY 环境变量"
|
||||||
|
echo " 3. 测试 JWT 验证: curl -k https://<ENVOY_IP>/api/v1/users/login"
|
||||||
|
echo ""
|
||||||
|
echo " 文档:"
|
||||||
|
echo " - 配置指南: deploy/envoy/ENVOY_CONFIG_GUIDE.md"
|
||||||
|
echo " - 快速参考: deploy/envoy/QUICK_REFERENCE.md"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 清理部署
|
||||||
|
cleanup() {
|
||||||
|
log_warn "清理 Envoy Gateway..."
|
||||||
|
|
||||||
|
kubectl delete -f deploy/k8s/envoy-gateway.yaml -n "$NAMESPACE" || true
|
||||||
|
kubectl delete secret envoy-tls -n "$NAMESPACE" || true
|
||||||
|
|
||||||
|
log_success "清理完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ Envoy Gateway 快速部署脚本 ║${NC}"
|
||||||
|
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 解析命令行参数
|
||||||
|
local cmd="${1:-deploy}"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
deploy)
|
||||||
|
check_dependencies || exit 1
|
||||||
|
generate_tls_cert || exit 1
|
||||||
|
create_namespace || exit 1
|
||||||
|
create_tls_secret || exit 1
|
||||||
|
deploy_envoy || exit 1
|
||||||
|
wait_deployment || exit 1
|
||||||
|
verify_deployment || exit 1
|
||||||
|
show_summary
|
||||||
|
log_success "Envoy Gateway 已成功部署!"
|
||||||
|
;;
|
||||||
|
cleanup)
|
||||||
|
cleanup
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
log_info "部署状态"
|
||||||
|
kubectl get all -n "$NAMESPACE" -l app=envoy-gateway
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
log_info "Envoy 日志"
|
||||||
|
kubectl logs -n "$NAMESPACE" -l app=envoy-gateway -f
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 <命令>"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " deploy 部署 Envoy Gateway(默认)"
|
||||||
|
echo " cleanup 移除部署"
|
||||||
|
echo " status 查看部署状态"
|
||||||
|
echo " logs 查看 Envoy 日志"
|
||||||
|
echo ""
|
||||||
|
echo "环境变量:"
|
||||||
|
echo " NAMESPACE K8s 命名空间(默认: juwan)"
|
||||||
|
echo " RELEASE_NAME 发布名称(默认: envoy-gateway)"
|
||||||
|
echo " TIMEOUT 部署超时(默认: 300s)"
|
||||||
|
echo " CONTEXT K8s 上下文(可选)"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
# HTTP 监听器(重定向到 HTTPS)
|
||||||
|
- name: listener_http
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_number: 8080
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
http_filters:
|
||||||
|
# CSRF 防护过滤器
|
||||||
|
- name: envoy.filters.http.local_ratelimit
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
|
||||||
|
stat_prefix: http_local_rate_limiter
|
||||||
|
token_bucket:
|
||||||
|
max_tokens: 1000
|
||||||
|
tokens_per_fill: 1000
|
||||||
|
fill_interval: 1s
|
||||||
|
filter_enabled:
|
||||||
|
runtime_key: local_rate_limit_enabled
|
||||||
|
default_value:
|
||||||
|
numerator: 100
|
||||||
|
denominator: HUNDRED
|
||||||
|
filter_enforced:
|
||||||
|
runtime_key: local_rate_limit_enforced
|
||||||
|
default_value:
|
||||||
|
numerator: 100
|
||||||
|
denominator: HUNDRED
|
||||||
|
|
||||||
|
# 路由过滤器
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: backend
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
# 登录端点 - 不需要 JWT
|
||||||
|
- match:
|
||||||
|
path: /api/v1/users/login
|
||||||
|
headers:
|
||||||
|
- name: ":method"
|
||||||
|
string_match:
|
||||||
|
exact: "POST"
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
# 注册端点 - 不需要 JWT
|
||||||
|
- match:
|
||||||
|
path: /api/v1/users/register
|
||||||
|
headers:
|
||||||
|
- name: ":method"
|
||||||
|
string_match:
|
||||||
|
exact: "POST"
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
# 其他所有用户 API 端点 - 需要 JWT
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/users
|
||||||
|
headers:
|
||||||
|
- name: ":method"
|
||||||
|
string_match:
|
||||||
|
exact: "GET"
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
request_headers_to_add:
|
||||||
|
- header:
|
||||||
|
key: "x-verified-user"
|
||||||
|
value: "%REQ(X-USER-ID)%"
|
||||||
|
|
||||||
|
# 订单 API - 需要 JWT
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/orders
|
||||||
|
route:
|
||||||
|
cluster: order_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
request_headers_to_add:
|
||||||
|
- header:
|
||||||
|
key: "x-verified-user"
|
||||||
|
value: "%REQ(X-USER-ID)%"
|
||||||
|
|
||||||
|
# 健康检查端点
|
||||||
|
- match:
|
||||||
|
path: /health
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
|
# 默认路由
|
||||||
|
- match:
|
||||||
|
prefix: /
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
direct_response:
|
||||||
|
status: 404
|
||||||
|
body:
|
||||||
|
inline_string: "Not Found"
|
||||||
|
|
||||||
|
# HTTPS 监听器(需要配置 TLS 证书)
|
||||||
|
- name: listener_https
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_number: 8443
|
||||||
|
filter_chains:
|
||||||
|
- transport_socket:
|
||||||
|
name: envoy.transport_sockets.tls
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
|
||||||
|
common_tls_context:
|
||||||
|
tls_certificates:
|
||||||
|
- certificate_chain:
|
||||||
|
filename: /etc/envoy/certs/tls.crt
|
||||||
|
private_key:
|
||||||
|
filename: /etc/envoy/certs/tls.key
|
||||||
|
filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||||
|
stat_prefix: ingress_https
|
||||||
|
access_log:
|
||||||
|
- name: envoy.access_loggers.file
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
|
||||||
|
path: /var/log/envoy/access.log
|
||||||
|
format: |
|
||||||
|
[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
|
||||||
|
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT%
|
||||||
|
"%DURATION%" "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
|
||||||
|
"%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"
|
||||||
|
|
||||||
|
http_filters:
|
||||||
|
# JWT 验证过滤器
|
||||||
|
- name: envoy.filters.http.jwt_authn
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
|
||||||
|
providers:
|
||||||
|
jwt_provider:
|
||||||
|
issuer: "juwan-user-rpc"
|
||||||
|
audiences: "api.juwan.local"
|
||||||
|
# 本地验证(离线模式)- 需要在 ConfigMap 中配置公钥
|
||||||
|
local_jwks:
|
||||||
|
inline_string: |
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"kty": "oct",
|
||||||
|
"k": "YOUR-BASE64-ENCODED-SECRET-KEY"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# 也可以使用远程 JWKS(更推荐)
|
||||||
|
# remote_jwks:
|
||||||
|
# http_uri:
|
||||||
|
# uri: "http://user-rpc-svc:9001/.well-known/jwks.json"
|
||||||
|
# cluster: user_rpc_cluster
|
||||||
|
# timeout: 5s
|
||||||
|
# cache_ttl:
|
||||||
|
# seconds: 300
|
||||||
|
# payload_in_metadata: "JWT_PAYLOAD"
|
||||||
|
rules:
|
||||||
|
# 不需要验证的路由
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/users/login
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/users/register
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
- match:
|
||||||
|
path: /health
|
||||||
|
allow_missing_or_failed: true
|
||||||
|
|
||||||
|
# 所有其他路由都需要有效的 JWT
|
||||||
|
- match:
|
||||||
|
prefix: /
|
||||||
|
requires:
|
||||||
|
provider_name: jwt_provider
|
||||||
|
|
||||||
|
# CSRF 防护过滤器
|
||||||
|
- name: envoy.filters.http.csrf
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy
|
||||||
|
filter_enabled:
|
||||||
|
default_value:
|
||||||
|
numerator: 100
|
||||||
|
denominator: HUNDRED
|
||||||
|
runtime_key: csrf_filter_enabled
|
||||||
|
shadow_enabled:
|
||||||
|
default_value:
|
||||||
|
numerator: 0
|
||||||
|
denominator: HUNDRED
|
||||||
|
runtime_key: csrf_filter_shadow_enabled
|
||||||
|
additional_origins:
|
||||||
|
- exact: "https://admin.juwan.local"
|
||||||
|
ignore_method_matches:
|
||||||
|
- google_re2:
|
||||||
|
regex: "^(GET|HEAD|OPTIONS|TRACE)$"
|
||||||
|
|
||||||
|
# 代理验证过滤器(可选 - 调用 RPC 验证 token 黑名单)
|
||||||
|
# - name: envoy.filters.http.ext_authz
|
||||||
|
# typed_config:
|
||||||
|
# "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
|
||||||
|
# grpc_service:
|
||||||
|
# envoy_grpc:
|
||||||
|
# cluster_name: user_rpc_cluster
|
||||||
|
# failure_mode_allow: false
|
||||||
|
# with_request_body:
|
||||||
|
# max_request_bytes: 8192
|
||||||
|
# allow_partial_message: false
|
||||||
|
|
||||||
|
# 本地速率限制(DDOS 防护)
|
||||||
|
- name: envoy.filters.http.local_ratelimit
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
|
||||||
|
stat_prefix: https_local_rate_limiter
|
||||||
|
token_bucket:
|
||||||
|
max_tokens: 10000
|
||||||
|
tokens_per_fill: 10000
|
||||||
|
fill_interval: 1s
|
||||||
|
filter_enabled:
|
||||||
|
runtime_key: local_rate_limit_enabled
|
||||||
|
default_value:
|
||||||
|
numerator: 100
|
||||||
|
denominator: HUNDRED
|
||||||
|
|
||||||
|
# 路由过滤器
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
|
||||||
|
route_config:
|
||||||
|
name: https_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: backend
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
# 登录和注册不需要 JWT
|
||||||
|
- match:
|
||||||
|
path: /api/v1/users/login
|
||||||
|
headers:
|
||||||
|
- name: ":method"
|
||||||
|
string_match:
|
||||||
|
exact: "POST"
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
- match:
|
||||||
|
path: /api/v1/users/register
|
||||||
|
headers:
|
||||||
|
- name: ":method"
|
||||||
|
string_match:
|
||||||
|
exact: "POST"
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
# 用户 API(带 JWT 验证)
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/users
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
request_headers_to_add:
|
||||||
|
- header:
|
||||||
|
key: "x-verified-user"
|
||||||
|
value: "%REQ(X-USER-ID)%"
|
||||||
|
|
||||||
|
# 订单 API(带 JWT 验证)
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/orders
|
||||||
|
route:
|
||||||
|
cluster: order_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
request_headers_to_add:
|
||||||
|
- header:
|
||||||
|
key: "x-verified-user"
|
||||||
|
value: "%REQ(X-USER-ID)%"
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
- match:
|
||||||
|
path: /health
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
|
# 默认路由
|
||||||
|
- match:
|
||||||
|
prefix: /
|
||||||
|
direct_response:
|
||||||
|
status: 404
|
||||||
|
body:
|
||||||
|
inline_string: "Not Found"
|
||||||
|
|
||||||
|
clusters:
|
||||||
|
# User API 集群
|
||||||
|
- name: user_api_cluster
|
||||||
|
connect_timeout: 10s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: user_api_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: user-api-svc
|
||||||
|
port_number: 8888
|
||||||
|
health_checks:
|
||||||
|
- timeout: 5s
|
||||||
|
interval: 10s
|
||||||
|
unhealthy_threshold: 2
|
||||||
|
healthy_threshold: 2
|
||||||
|
http_health_check:
|
||||||
|
path: /health
|
||||||
|
expected_statuses:
|
||||||
|
- start: 200
|
||||||
|
end: 299
|
||||||
|
|
||||||
|
# Order API 集群
|
||||||
|
- name: order_api_cluster
|
||||||
|
connect_timeout: 10s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: order_api_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: order-api-svc
|
||||||
|
port_number: 8889
|
||||||
|
health_checks:
|
||||||
|
- timeout: 5s
|
||||||
|
interval: 10s
|
||||||
|
unhealthy_threshold: 2
|
||||||
|
healthy_threshold: 2
|
||||||
|
http_health_check:
|
||||||
|
path: /health
|
||||||
|
expected_statuses:
|
||||||
|
- start: 200
|
||||||
|
end: 299
|
||||||
|
|
||||||
|
# User RPC 集群(用于 ext_authz 调用)
|
||||||
|
- name: user_rpc_cluster
|
||||||
|
connect_timeout: 10s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: user_rpc_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: user-rpc-svc
|
||||||
|
port_number: 9001
|
||||||
|
http2_protocol_options: {}
|
||||||
|
|
||||||
|
admin:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_number: 9901
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 生成 JWKS JSON 文件的脚本
|
||||||
|
# 用于 Envoy JWT 验证
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 参数
|
||||||
|
JWT_SECRET_KEY="${1:-your-secret-key-change-this-in-production}"
|
||||||
|
OUTPUT_FILE="${2:-jwks.json}"
|
||||||
|
KEY_ID="${3:-default-key-id}"
|
||||||
|
|
||||||
|
echo "生成 JWKS JSON..."
|
||||||
|
echo "- Secret Key: ${JWT_SECRET_KEY:0:10}..."
|
||||||
|
echo "- Key ID: $KEY_ID"
|
||||||
|
echo "- Output: $OUTPUT_FILE"
|
||||||
|
|
||||||
|
# 对密钥进行 base64 编码(URL-safe 无填充)
|
||||||
|
ENCODED_KEY=$(echo -n "$JWT_SECRET_KEY" | base64 | tr '+/' '-_' | sed 's/=//g')
|
||||||
|
|
||||||
|
# 生成 JWKS JSON
|
||||||
|
cat > "$OUTPUT_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"kty": "oct",
|
||||||
|
"use": "sig",
|
||||||
|
"kid": "$KEY_ID",
|
||||||
|
"k": "$ENCODED_KEY",
|
||||||
|
"alg": "HS256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✓ JWKS 文件已生成: $OUTPUT_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "内容预览:"
|
||||||
|
cat "$OUTPUT_FILE"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "配置说明:"
|
||||||
|
echo "1. 在 user-rpc 的 .well-known/jwks.json 端点暴露此文件"
|
||||||
|
echo "2. 在 Envoy 中配置远程 JWKS URI:"
|
||||||
|
echo " remote_jwks:"
|
||||||
|
echo " http_uri:"
|
||||||
|
echo " uri: http://user-rpc-svc:9001/.well-known/jwks.json"
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# JWT 和认证配置完整设置脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔐 Juwan JWT 认证配置脚本"
|
||||||
|
echo "===================================="
|
||||||
|
|
||||||
|
NAMESPACE="juwan"
|
||||||
|
JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
JWKS_KEY_ID="juwan-key-2026"
|
||||||
|
|
||||||
|
echo "✅ 生成 JWT 密钥..."
|
||||||
|
echo " Secret: $JWT_SECRET"
|
||||||
|
|
||||||
|
# Step 1: 创建 JWT Secret
|
||||||
|
echo ""
|
||||||
|
echo "📝 创建 K8s Secret..."
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=key=$JWT_SECRET \
|
||||||
|
-n $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# Step 2: 生成 JWKS JSON(包含公钥)
|
||||||
|
# 注意:对于 HMAC 算法,JWKS 包含密钥本身
|
||||||
|
JWKS_JSON=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"kty": "oct",
|
||||||
|
"kid": "$JWKS_KEY_ID",
|
||||||
|
"k": "$(echo -n $JWT_SECRET | base64 -w 0)",
|
||||||
|
"alg": "HS256",
|
||||||
|
"use": "sig"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "📝 创建 JWKS ConfigMap..."
|
||||||
|
kubectl create configmap jwks-config \
|
||||||
|
--from-literal=jwks.json="$JWKS_JSON" \
|
||||||
|
-n $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ JWT 认证配置完成!"
|
||||||
|
echo ""
|
||||||
|
echo "后续步骤:"
|
||||||
|
echo "1. 更新 Envoy ConfigMap,挂载 JWKS 文件"
|
||||||
|
echo "2. 在各 API 服务中配置 JWT_SECRET 环境变量"
|
||||||
|
echo "3. 登录端点使用此密钥签名 Token"
|
||||||
|
echo ""
|
||||||
|
echo "JWT 密钥已保存到 K8s Secret: jwt-secret"
|
||||||
|
echo "JWKS 已保存到 K8s ConfigMap: jwks-config"
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: juwan
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: envoy-config
|
||||||
|
namespace: juwan
|
||||||
|
data:
|
||||||
|
envoy.yaml: |
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_http
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 8080
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
access_log:
|
||||||
|
- name: envoy.access_loggers.stdout
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||||
|
http_filters:
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: backend
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/users
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
- match:
|
||||||
|
prefix: /api/v1/orders
|
||||||
|
route:
|
||||||
|
cluster: order_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
- match:
|
||||||
|
prefix: /health
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 10s
|
||||||
|
- match:
|
||||||
|
prefix: /
|
||||||
|
route:
|
||||||
|
cluster: user_api_cluster
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
clusters:
|
||||||
|
- name: user_api_cluster
|
||||||
|
connect_timeout: 5s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: user_api_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: user-api-svc.juwan.svc.cluster.local
|
||||||
|
port_value: 8888
|
||||||
|
health_checks:
|
||||||
|
- timeout: 3s
|
||||||
|
interval: 10s
|
||||||
|
unhealthy_threshold: 2
|
||||||
|
healthy_threshold: 2
|
||||||
|
http_health_check:
|
||||||
|
path: /health
|
||||||
|
|
||||||
|
- name: order_api_cluster
|
||||||
|
connect_timeout: 5s
|
||||||
|
type: STRICT_DNS
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: order_api_cluster
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: order-api-svc.juwan.svc.cluster.local
|
||||||
|
port_value: 8889
|
||||||
|
health_checks:
|
||||||
|
- timeout: 3s
|
||||||
|
interval: 10s
|
||||||
|
unhealthy_threshold: 2
|
||||||
|
healthy_threshold: 2
|
||||||
|
http_health_check:
|
||||||
|
path: /health
|
||||||
|
|
||||||
|
admin:
|
||||||
|
access_log_path: /tmp/admin_access.log
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 9901
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- name: admin
|
||||||
|
port: 9901
|
||||||
|
targetPort: 9901
|
||||||
|
protocol: TCP
|
||||||
|
selector:
|
||||||
|
app: envoy-gateway
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
labels:
|
||||||
|
app: envoy-gateway
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: envoy-gateway
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: envoy-gateway
|
||||||
|
spec:
|
||||||
|
serviceAccountName: envoy-gateway
|
||||||
|
containers:
|
||||||
|
- name: envoy
|
||||||
|
image: envoyproxy/envoy:v1.27-latest
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
- name: admin
|
||||||
|
containerPort: 9901
|
||||||
|
volumeMounts:
|
||||||
|
- name: envoy-config
|
||||||
|
mountPath: /etc/envoy
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /stats
|
||||||
|
port: 9901
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /stats
|
||||||
|
port: 9901
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: envoy-config
|
||||||
|
configMap:
|
||||||
|
name: envoy-config
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["endpoints"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["discovery.k8s.io"]
|
||||||
|
resources: ["endpointslices"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["services"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: envoy-gateway
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway-network-policy
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: envoy-gateway
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
- Egress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector: {}
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector: {}
|
||||||
|
ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- to:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: user-api
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: order-api
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8888
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8889
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: envoy-config
|
|
||||||
namespace: juwan
|
|
||||||
data:
|
|
||||||
envoy.yaml: |
|
|
||||||
static_resources:
|
|
||||||
listeners:
|
|
||||||
- name: http_listener
|
|
||||||
address:
|
|
||||||
socket_address:
|
|
||||||
address: 0.0.0.0
|
|
||||||
port_value: 8080
|
|
||||||
filter_chains:
|
|
||||||
- filters:
|
|
||||||
- name: envoy.filters.network.http_connection_manager
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
|
||||||
stat_prefix: ingress_http
|
|
||||||
codec_type: AUTO
|
|
||||||
route_config:
|
|
||||||
name: local_route
|
|
||||||
virtual_hosts:
|
|
||||||
- name: gozero_services
|
|
||||||
domains: ["*"]
|
|
||||||
routes:
|
|
||||||
- match:
|
|
||||||
prefix: "/"
|
|
||||||
route:
|
|
||||||
cluster: user-api
|
|
||||||
http_filters:
|
|
||||||
- name: envoy.filters.http.lua
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
|
||||||
inline_code: |
|
|
||||||
math.randomseed(os.time())
|
|
||||||
|
|
||||||
local function is_safe(method)
|
|
||||||
return method == "GET" or method == "HEAD" or method == "OPTIONS"
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parse_cookie(cookie_header)
|
|
||||||
if not cookie_header then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
for cookie in string.gmatch(cookie_header, "([^;]+)") do
|
|
||||||
local k, v = cookie:match("^%s*([^=]+)=?(.*)$")
|
|
||||||
if k == "csrf_token" then
|
|
||||||
return v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function random_token()
|
|
||||||
local t = {}
|
|
||||||
for i = 1, 32 do
|
|
||||||
t[i] = string.format("%x", math.random(0, 15))
|
|
||||||
end
|
|
||||||
return table.concat(t)
|
|
||||||
end
|
|
||||||
|
|
||||||
function envoy_on_request(request_handle)
|
|
||||||
local headers = request_handle:headers()
|
|
||||||
local method = headers:get(":method") or ""
|
|
||||||
local cookie = parse_cookie(headers:get("cookie"))
|
|
||||||
local csrf_header = headers:get("x-csrf-token")
|
|
||||||
|
|
||||||
if is_safe(method) then
|
|
||||||
if not cookie then
|
|
||||||
local token = random_token()
|
|
||||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "token", token)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not cookie or not csrf_header or cookie ~= csrf_header then
|
|
||||||
request_handle:respond({[":status"] = "403"}, "CSRF validation failed")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function envoy_on_response(response_handle)
|
|
||||||
local md = response_handle:streamInfo():dynamicMetadata():get("csrf") or {}
|
|
||||||
local token = md["token"]
|
|
||||||
if token then
|
|
||||||
response_handle:headers():add("set-cookie", "csrf_token=" .. token .. "; Path=/; SameSite=Strict")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
- name: envoy.filters.http.router
|
|
||||||
typed_config:
|
|
||||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
|
||||||
clusters:
|
|
||||||
- name: user-api
|
|
||||||
connect_timeout: 2s
|
|
||||||
type: STRICT_DNS
|
|
||||||
lb_policy: ROUND_ROBIN
|
|
||||||
load_assignment:
|
|
||||||
cluster_name: user-api
|
|
||||||
endpoints:
|
|
||||||
- lb_endpoints:
|
|
||||||
- endpoint:
|
|
||||||
address:
|
|
||||||
socket_address:
|
|
||||||
address: user-api-svc.juwan.svc.cluster.local
|
|
||||||
port_value: 8888
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: envoy-gateway
|
|
||||||
namespace: juwan
|
|
||||||
labels:
|
|
||||||
app: envoy-gateway
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: envoy-gateway
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: envoy-gateway
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: envoy
|
|
||||||
image: envoyproxy/envoy:v1.32.2
|
|
||||||
args:
|
|
||||||
- "-c"
|
|
||||||
- "/etc/envoy/envoy.yaml"
|
|
||||||
- "--log-level"
|
|
||||||
- "info"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8080
|
|
||||||
volumeMounts:
|
|
||||||
- name: config
|
|
||||||
mountPath: /etc/envoy
|
|
||||||
volumes:
|
|
||||||
- name: config
|
|
||||||
configMap:
|
|
||||||
name: envoy-config
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: envoy-gateway
|
|
||||||
namespace: juwan
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: envoy-gateway
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
# JWT Secret + ETCD Encryption Deployment Guide
|
||||||
|
|
||||||
|
完整的 JWT 认证系统部署指南,包括密钥管理、RBAC 权限控制和 ETCD 加密。
|
||||||
|
|
||||||
|
## 部署顺序
|
||||||
|
|
||||||
|
### 第1步:创建 Secret 和 RBAC(必需)
|
||||||
|
|
||||||
|
创建 JWT 秘钥和服务账户权限:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证创建成功:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Secret
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts
|
||||||
|
kubectl get sa user-rpc -n juwan
|
||||||
|
kubectl get sa envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 检查 RBAC 权限
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
kubectl get rolebinding -n juwan -l app=jwt-secret-reader
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第2步:更新 user-rpc 部署(依赖第1步)
|
||||||
|
|
||||||
|
已自动更新 `deploy/k8s/service/user/user-rpc.yaml`:
|
||||||
|
|
||||||
|
- ✅ 更新 `serviceAccountName` 从 `find-endpoints` → `user-rpc`
|
||||||
|
- ✅ 添加环境变量 `JWT_SECRET_KEY` 从 Secret `jwt-secret` 读取
|
||||||
|
|
||||||
|
应用更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccount 已正确绑定
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 5 serviceAccountName
|
||||||
|
|
||||||
|
# 查看 Pod 是否以 user-rpc ServiceAccount 身份运行
|
||||||
|
kubectl get pod -n juwan -l app=user-rpc -o yaml | grep serviceAccount
|
||||||
|
|
||||||
|
# 验证环境变量已注入
|
||||||
|
kubectl exec -it POD_NAME -n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第3步:更新 Envoy 网关部署(依赖第1步)
|
||||||
|
|
||||||
|
已自动更新 `deploy/k8s/envoy/envoy.yaml`:
|
||||||
|
|
||||||
|
- ✅ 添加 `serviceAccountName: envoy-gateway` 到 Deployment spec
|
||||||
|
|
||||||
|
应用更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccount 已正确绑定
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
|
||||||
|
# 检查 Pod 状态
|
||||||
|
kubectl get pod -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第4步:启用 ETCD 加密(强烈推荐用于生产环境)
|
||||||
|
|
||||||
|
这是一个集群级别的配置,需要在 Kubernetes 控制平面节点上执行。
|
||||||
|
|
||||||
|
**前提条件:**
|
||||||
|
- 具有 Kubernetes 集群管理员权限
|
||||||
|
- 可以访问控制平面节点
|
||||||
|
- 备份 ETCD 数据库
|
||||||
|
|
||||||
|
**步骤:**
|
||||||
|
|
||||||
|
1. **生成加密密钥**
|
||||||
|
```bash
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
```
|
||||||
|
记录输出的 Base64 密钥。
|
||||||
|
|
||||||
|
2. **创建加密配置文件**
|
||||||
|
|
||||||
|
在控制平面节点上,创建 `/etc/kubernetes/encryption-config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: <BASE64_ENCODED_32_BYTE_KEY>
|
||||||
|
- identity: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
替换 `<BASE64_ENCODED_32_BYTE_KEY>` 为第1步生成的密钥。
|
||||||
|
|
||||||
|
3. **修改 kube-apiserver 配置**
|
||||||
|
|
||||||
|
在控制平面节点上,编辑 `/etc/kubernetes/manifests/kube-apiserver.yaml`:
|
||||||
|
|
||||||
|
**添加参数:**
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
command:
|
||||||
|
- kube-apiserver
|
||||||
|
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**添加卷挂载:**
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
volumeMounts:
|
||||||
|
- name: encryption-config
|
||||||
|
mountPath: /etc/kubernetes
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: encryption-config
|
||||||
|
hostPath:
|
||||||
|
path: /etc/kubernetes
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **重启 kube-apiserver**
|
||||||
|
|
||||||
|
修改清单后,kubelet 会自动重启 kube-apiserver。检查状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在控制平面节点
|
||||||
|
kubectl get pods -n kube-system | grep kube-apiserver
|
||||||
|
|
||||||
|
# 监控重启过程
|
||||||
|
kubectl logs -n kube-system -l component=kube-apiserver -f
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **验证加密是否启用**
|
||||||
|
|
||||||
|
创建一个新 Secret 并检查它在 ETCD 中是否加密:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建测试 Secret
|
||||||
|
kubectl create secret generic test-secret -n juwan --from-literal=key=value
|
||||||
|
|
||||||
|
# 从 control plane 节点检查 ETCD 数据
|
||||||
|
# 如果数据不可读并包含加密标记,说明加密已启用
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **保存加密密钥**
|
||||||
|
|
||||||
|
⚠️ **关键:将加密密钥安全地保存在离线存储中**
|
||||||
|
- 密钥丢失后,无法解密 ETCD 中的数据
|
||||||
|
- 无法恢复任何 Secrets
|
||||||
|
- 建议用密码管理工具(如 HashiCorp Vault)或 HSM 存储密钥
|
||||||
|
|
||||||
|
### 第5步:验证整个系统
|
||||||
|
|
||||||
|
完整的验证清单:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查所有 Secrets 已创建
|
||||||
|
kubectl get secret -n juwan
|
||||||
|
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts 已创建
|
||||||
|
kubectl get sa -n juwan
|
||||||
|
kubectl describe sa user-rpc -n juwan
|
||||||
|
kubectl describe sa envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 检查 RBAC 权限
|
||||||
|
kubectl get role -n juwan
|
||||||
|
kubectl get rolebinding -n juwan
|
||||||
|
kubectl describe role jwt-secret-reader -n juwan
|
||||||
|
|
||||||
|
# 测试权限:user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:user-rpc --resource-name=jwt-secret -n juwan
|
||||||
|
|
||||||
|
# 测试权限:envoy-gateway 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:envoy-gateway --resource-name=jwt-secret -n juwan
|
||||||
|
|
||||||
|
# 测试权限:其他 ServiceAccount 无法读取
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:other-service -n juwan
|
||||||
|
|
||||||
|
# 检查 Deployments 已正确配置
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
|
||||||
|
# 检查 Pods 是否已启动并运行
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 查看 JWT Secret 是否已挂载到 Pod
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和日志
|
||||||
|
|
||||||
|
### 查看 Pod 日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
|
||||||
|
|
||||||
|
# Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查 Pod 事件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Pod 创建和启动事件
|
||||||
|
kubectl describe pod -n juwan -l app=user-rpc
|
||||||
|
kubectl describe pod -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限问题排查
|
||||||
|
|
||||||
|
如果 Pod 无法读取 Secret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Pod 使用的 ServiceAccount
|
||||||
|
kubectl get pod POD_NAME -n juwan -o yaml | grep serviceAccountName
|
||||||
|
|
||||||
|
# 检查 RBAC 绑定
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
|
||||||
|
# 检查 Role 权限定义
|
||||||
|
kubectl get role jwt-secret-reader -n juwan -o yaml
|
||||||
|
|
||||||
|
# 尝试用 Pod 的身份读取 Secret(需要 kubectl-user-impersonate 或类似工具)
|
||||||
|
kubectl get secret jwt-secret --as=system:serviceaccount:juwan:user-rpc -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全最佳实践
|
||||||
|
|
||||||
|
### 1. 密钥轮换
|
||||||
|
|
||||||
|
周期性更换 JWT 秘钥(建议每季度):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 生成新密钥
|
||||||
|
NEW_KEY=$(head -c 32 /dev/urandom | base64)
|
||||||
|
|
||||||
|
# 2. 更新 Secret
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=secret-key=$NEW_KEY \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# 3. 重启 Pods 以加载新密钥(滚动更新)
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 4. 验证新 Pods 已启动并运行
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 5. 已颁发的旧令牌将变为无效
|
||||||
|
# 需要用户重新登录获取新令牌
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 审计和监控
|
||||||
|
|
||||||
|
在生产环境中启用 Kubernetes 审计日志来跟踪 Secret 访问:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# kube-apiserver 审计策略示例
|
||||||
|
apiVersion: audit.k8s.io/v1
|
||||||
|
kind: Policy
|
||||||
|
rules:
|
||||||
|
# 记录 secret 资源的访问
|
||||||
|
- level: RequestResponse
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
resources: ["secrets"]
|
||||||
|
# 记录所有认证失败
|
||||||
|
- level: RequestResponse
|
||||||
|
omitStages:
|
||||||
|
- RequestReceived
|
||||||
|
userGroups: ["system:unauthenticated"]
|
||||||
|
- level: Metadata
|
||||||
|
omitStages:
|
||||||
|
- RequestReceived
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 网络策略
|
||||||
|
|
||||||
|
使用 NetworkPolicy 限制 Pods 之间的通信:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret-access
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: jwt-secret-reader
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: user-rpc
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: envoy-gateway
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 443 # API Server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 灾难恢复
|
||||||
|
|
||||||
|
### 备份 Secret 和 RBAC 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 JWT Secret
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml > jwt-secret-backup.yaml
|
||||||
|
|
||||||
|
# 备份 RBAC 配置
|
||||||
|
kubectl get role jwt-secret-reader -n juwan -o yaml > jwt-role-backup.yaml
|
||||||
|
kubectl get rolebinding -n juwan -l app=jwt-secret-reader -o yaml > jwt-rolebinding-backup.yaml
|
||||||
|
|
||||||
|
# 加密备份文件
|
||||||
|
gpg --symmetric jwt-secret-backup.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 恢复步骤
|
||||||
|
|
||||||
|
如果 Secret 被意外删除:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从备份恢复
|
||||||
|
kubectl apply -f jwt-secret-backup.yaml
|
||||||
|
|
||||||
|
# 重启 Pods 以重新加载 Secret
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: Pod 无法启动,显示 "failed to pull secret"
|
||||||
|
|
||||||
|
A: 检查:
|
||||||
|
1. Secret 是否存在:`kubectl get secret jwt-secret -n juwan`
|
||||||
|
2. ServiceAccount 是否绑定了 RBAC:`kubectl describe rolebinding -n juwan`
|
||||||
|
3. Secret 名称和命名空间是否正确
|
||||||
|
|
||||||
|
### Q: 加密后如何验证 ETCD 中的数据已加密?
|
||||||
|
|
||||||
|
A: 从 control plane 节点:
|
||||||
|
```bash
|
||||||
|
# 直接读取 ETCD(如果配置了加密,数据应该不可读)
|
||||||
|
sudo strings /var/lib/etcd/member/snap/db | grep -i secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 能否更改加密密钥而不重新创建 ETCD?
|
||||||
|
|
||||||
|
A: 可以,但流程复杂:
|
||||||
|
1. 更新 encryption-config.yaml 中的新密钥
|
||||||
|
2. 将新密钥添加到提供程序列表(保持旧密钥)
|
||||||
|
3. 重启 kube-apiserver
|
||||||
|
4. 触发重新加密:`kubectl get all --all-namespaces -o json | kubectl apply -f -`
|
||||||
|
|
||||||
|
### Q: 如何在 Minikube 中启用 ETCD 加密?
|
||||||
|
|
||||||
|
A: 参考 `ENCRYPTION.md` 中的 Minikube 特定说明部分。
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `jwt-secret.yaml` - Secret 和 RBAC 配置
|
||||||
|
- `ENCRYPTION.md` - ETCD 加密详细文档
|
||||||
|
- `README.md` - 快速参考指南
|
||||||
|
- `/deploy/k8s/service/user/user-rpc.yaml` - user-rpc Deployment 配置
|
||||||
|
- `/deploy/k8s/envoy/envoy.yaml` - Envoy 网关 Deployment 配置
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
部署完成后:
|
||||||
|
|
||||||
|
1. **集成 JWT 验证到 RPC Handlers**
|
||||||
|
- 实现 gRPC unary interceptor
|
||||||
|
- 验证令牌有效性
|
||||||
|
- 处理令牌刷新逻辑
|
||||||
|
|
||||||
|
2. **集成 JWT 验证到 Envoy**
|
||||||
|
- 扩展 Lua filter 进行令牌验证
|
||||||
|
- 返回 401(无效令牌)或 200(有效令牌)
|
||||||
|
|
||||||
|
3. **端到端测试**
|
||||||
|
- 创建用户和登录
|
||||||
|
- 生成和验证 JWT
|
||||||
|
- 测试令牌刷新和撤销
|
||||||
|
- 验证 ETCD 加密
|
||||||
|
|
||||||
|
4. **生产部署**
|
||||||
|
- 启用审计日志
|
||||||
|
- 配置密钥轮换计划
|
||||||
|
- 建立备份和恢复流程
|
||||||
|
- 监控 Secret 访问
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# ETCD Encryption Configuration for Kubernetes
|
||||||
|
|
||||||
|
To enable static encryption at rest for Kubernetes secrets in ETCD, you need to configure the API Server with an EncryptionConfiguration.
|
||||||
|
|
||||||
|
## 1. Generate an Encryption Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a 32-byte base64-encoded key
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
# Example output: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Create EncryptionConfiguration File
|
||||||
|
|
||||||
|
Create `/etc/kubernetes/encryption-config.yaml` on the control plane node:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
|
||||||
|
- identity: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Update kube-apiserver Static Pod Manifest
|
||||||
|
|
||||||
|
Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` on the control plane node:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
command:
|
||||||
|
- kube-apiserver
|
||||||
|
# ... existing flags ...
|
||||||
|
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
volumeMounts:
|
||||||
|
- name: encryption-config
|
||||||
|
mountPath: /etc/kubernetes
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: encryption-config
|
||||||
|
hostPath:
|
||||||
|
path: /etc/kubernetes
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Verify Encryption is Working
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After restarting the API server, create a secret and verify it's encrypted
|
||||||
|
kubectl create secret generic test-secret --from-literal=key=value -n juwan
|
||||||
|
|
||||||
|
# Check if the secret is encrypted in etcd
|
||||||
|
kubectl get secret test-secret -o yaml
|
||||||
|
|
||||||
|
# You can also check raw etcd data (requires etcd access):
|
||||||
|
# etcdctl --endpoints=https://127.0.0.1:2379 get /kubernetes.io/secrets/juwan/test-secret
|
||||||
|
# The data should be encrypted (not human-readable)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Important Notes
|
||||||
|
|
||||||
|
- **Backup your encryption key** in a secure location
|
||||||
|
- **Never commit encryption keys** to version control
|
||||||
|
- If you lose the key, all encrypted secrets will be unrecoverable
|
||||||
|
- After enabling encryption, existing unencrypted secrets will not be automatically encrypted
|
||||||
|
- To encrypt existing secrets, you can use: `kubectl delete secret <name> && kubectl create secret ...`
|
||||||
|
- Or use: `kubectl patch secret <name> -p '{}' --type=merge` (triggers re-encryption)
|
||||||
|
|
||||||
|
## 6. RBAC Configuration for JWT Secret
|
||||||
|
|
||||||
|
The `jwt-secret.yaml` includes RBAC rules that:
|
||||||
|
- Create a `jwt-secret` Secret in the `juwan` namespace
|
||||||
|
- Create ServiceAccounts for `user-rpc` and `envoy-gateway`
|
||||||
|
- Create a Role `jwt-secret-reader` that allows reading only the `jwt-secret` Secret
|
||||||
|
- Bind this Role to both ServiceAccounts via RoleBindings
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- Only `user-rpc` and `envoy-gateway` Pods can read the JWT secret
|
||||||
|
- Other services and users cannot access the JWT secret
|
||||||
|
- Least privilege access principle is enforced
|
||||||
|
|
||||||
|
## 7. Update Deployment to Use ServiceAccount
|
||||||
|
|
||||||
|
Make sure your Deployment references the ServiceAccount:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: user-rpc # This is important!
|
||||||
|
containers:
|
||||||
|
- name: user-rpc
|
||||||
|
env:
|
||||||
|
- name: JWT_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. For Minikube Users
|
||||||
|
|
||||||
|
If using Minikube, you can enable encryption with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minikube config set apiserver.encryption-provider-config /path/to/encryption-config.yaml
|
||||||
|
minikube start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually edit the kube-apiserver manifest after starting Minikube:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minikube ssh
|
||||||
|
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
# Add the flags and volume mounts as shown above
|
||||||
|
```
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
# 部署流程图和时间线
|
||||||
|
|
||||||
|
## 部署架构流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ JWT 认证系统部署流程 │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 1: 前置检查 (5分钟) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ✓ Kubernetes 集群版本 >= 1.24 │
|
||||||
|
│ ✓ kubectl 已配置,可访问集群 │
|
||||||
|
│ ✓ juwan namespace 已存在 │
|
||||||
|
│ ✓ redis-operator CRD 已安装 │
|
||||||
|
│ ✓ 集群管理员权限(用于 ETCD 加密) │
|
||||||
|
│ │
|
||||||
|
│ 命令检查: │
|
||||||
|
│ $ kubectl cluster-info │
|
||||||
|
│ $ kubectl get ns juwan │
|
||||||
|
│ $ kubectl get crd redisclusters.redis.redis.opstreelabs.in │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 2: 创建 Secret 和 RBAC (5分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml │
|
||||||
|
│ │
|
||||||
|
│ 创建的资源: │
|
||||||
|
│ ✓ Secret: jwt-secret (包含 JWT 秘钥) │
|
||||||
|
│ ✓ ServiceAccount: user-rpc │
|
||||||
|
│ ✓ ServiceAccount: envoy-gateway │
|
||||||
|
│ ✓ Role: jwt-secret-reader (只读权限) │
|
||||||
|
│ ✓ RoleBinding: jwt-secret-reader-user-rpc │
|
||||||
|
│ ✓ RoleBinding: jwt-secret-reader-envoy-gateway │
|
||||||
|
│ │
|
||||||
|
│ 验证: │
|
||||||
|
│ $ kubectl get secret jwt-secret -n juwan │
|
||||||
|
│ $ kubectl get sa -n juwan | grep -E "user-rpc|envoy" │
|
||||||
|
│ $ kubectl get role -n juwan │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 3: 更新 Deployments (10分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Step 3a: 更新 user-rpc Deployment │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/service/user/user-rpc.yaml │
|
||||||
|
│ │
|
||||||
|
│ 变更: │
|
||||||
|
│ - serviceAccountName: user-rpc (绑定权限) │
|
||||||
|
│ - env.JWT_SECRET_KEY (从 Secret 挂载) │
|
||||||
|
│ - 保持 Redis Cluster 配置 │
|
||||||
|
│ │
|
||||||
|
│ 等待 Pods 启动: │
|
||||||
|
│ $ kubectl rollout status deployment/user-rpc -n juwan │
|
||||||
|
│ │
|
||||||
|
│ --- │
|
||||||
|
│ │
|
||||||
|
│ Step 3b: 更新 Envoy Gateway Deployment │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/envoy/envoy.yaml │
|
||||||
|
│ │
|
||||||
|
│ 变更: │
|
||||||
|
│ - serviceAccountName: envoy-gateway (绑定权限) │
|
||||||
|
│ - 保持 CSRF Lua 防护配置 │
|
||||||
|
│ │
|
||||||
|
│ 等待 Pods 启动: │
|
||||||
|
│ $ kubectl rollout status deployment/envoy-gateway -n juwan │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 4: 验证部署 (15分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 检查清单: │
|
||||||
|
│ │
|
||||||
|
│ 1️⃣ Secret 和权限验证 │
|
||||||
|
│ $ kubectl get secret jwt-secret -n juwan │
|
||||||
|
│ $ kubectl get role jwt-secret-reader -n juwan │
|
||||||
|
│ $ kubectl get rolebinding -n juwan | grep jwt-secret │
|
||||||
|
│ │
|
||||||
|
│ 2️⃣ 权限测试 │
|
||||||
|
│ $ kubectl auth can-i get secrets \ │
|
||||||
|
│ --as=system:serviceaccount:juwan:user-rpc \ │
|
||||||
|
│ --resource-name=jwt-secret -n juwan │
|
||||||
|
│ 预期: yes │
|
||||||
|
│ │
|
||||||
|
│ 3️⃣ Pods 运行状态 │
|
||||||
|
│ $ kubectl get pods -n juwan -l app=user-rpc │
|
||||||
|
│ $ kubectl get pods -n juwan -l app=envoy-gateway │
|
||||||
|
│ 预期: 3 个 user-rpc Pods + 1 个 envoy-gateway Pod 都在 Running │
|
||||||
|
│ │
|
||||||
|
│ 4️⃣ 环境变量验证 │
|
||||||
|
│ $ kubectl exec -it <user-rpc-pod> -n juwan -- env | grep JWT │
|
||||||
|
│ 预期: JWT_SECRET_KEY=... │
|
||||||
|
│ │
|
||||||
|
│ 5️⃣ Redis 连接验证 │
|
||||||
|
│ $ kubectl run redis-cli --image=redis:latest --rm -it \ │
|
||||||
|
│ -- redis-cli -h user-redis.juwan:6379 PING │
|
||||||
|
│ 预期: PONG │
|
||||||
|
│ │
|
||||||
|
│ 详见: VERIFICATION.md 第1-8部分 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
├─────────── 生产环境额外步骤 ──────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────────────┐ ┌─────────────────────────────────────┐
|
||||||
|
│ Phase 5a: 应用集成 │ │ Phase 5b: 启用 ETCD 加密 (30分钟) │
|
||||||
|
│ (2-3 小时) ⚠️ 推荐 │ │ ⚠️ 生产推荐,需集群管理员权限 │
|
||||||
|
├──────────────────────────┤ ├─────────────────────────────────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ 实施内容: │ │ 前提条件: │
|
||||||
|
│ ✓ gRPC Interceptor │ │ ✓ Control Plane 节点访问权限 │
|
||||||
|
│ ✓ Login/Logout Handler │ │ ✓ ETCD 备份已创建 │
|
||||||
|
│ ✓ JWT Middleware │ │ ✓ 加密密钥已生成 │
|
||||||
|
│ ✓ Token Refresh Logic │ │ │
|
||||||
|
│ ✓ Error Handling │ │ 步骤: │
|
||||||
|
│ ✓ Unit Tests │ │ 1. 生成 32 字节密钥 │
|
||||||
|
│ │ │ $ head -c 32 /dev/urandom | base64
|
||||||
|
│ 参考: │ │ │
|
||||||
|
│ INTEGRATION.md │ │ 2. 创建加密配置文件 │
|
||||||
|
│ │ │ /etc/kubernetes/encryption-config.yaml
|
||||||
|
│ 时间估计: │ │ │
|
||||||
|
│ - gRPC 拦截器: 30分钟 │ │ 3. 修改 kube-apiserver manifest │
|
||||||
|
│ - Handlers: 60分钟 │ │ 添加密钥路径和卷挂载 │
|
||||||
|
│ - Middleware: 30分钟 │ │ │
|
||||||
|
│ - 测试: 60分钟 │ │ 4. 重启 kube-apiserver │
|
||||||
|
│ │ │ kubelet 自动重启 │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ 5. 验证加密已启用 │
|
||||||
|
│ │ │ kubectl create secret generic ... │
|
||||||
|
│ │ │ 检查 ETCD 中的数据是否加密 │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ 详见: ENCRYPTION.md (8个部分) │
|
||||||
|
│ │ │ 验证: VERIFICATION.md 第9部分 │
|
||||||
|
│ │ │ │
|
||||||
|
└──────────────────────────┘ └─────────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
└───────────────────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Phase 6: 完成 ✅ │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 最终检查: │
|
||||||
|
│ ✓ 所有 Pods 运行正常 │
|
||||||
|
│ ✓ RBAC 权限已验证 │
|
||||||
|
│ ✓ JWT 功能已集成 │
|
||||||
|
│ ✓ 日志和监控已配置 │
|
||||||
|
│ ✓ (可选) ETCD 加密已启用 │
|
||||||
|
│ │
|
||||||
|
│ 生产推荐: │
|
||||||
|
│ ✓ 启用审计日志 │
|
||||||
|
│ ✓ 配置密钥轮换计划(季度) │
|
||||||
|
│ ✓ 备份密钥到安全位置 │
|
||||||
|
│ ✓ 配置告警和监控 │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 时间估计和路径
|
||||||
|
|
||||||
|
```
|
||||||
|
推荐部署路径
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
🚀 最快路径 (30 分钟) - 开发环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1: 前置检查 ⏱️ 5 分钟
|
||||||
|
Phase 2: 创建 Secret 和 RBAC ⏱️ 5 分钟
|
||||||
|
Phase 3: 更新 Deployments ⏱️ 10 分钟
|
||||||
|
Phase 4: 验证部署 ⏱️ 10 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 30 分钟
|
||||||
|
|
||||||
|
|
||||||
|
📊 默认路径 (75 分钟) - 测试环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1-4: 如上 ⏱️ 30 分钟
|
||||||
|
Phase 5a: 应用集成(简单版) ⏱️ 45 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 75 分钟
|
||||||
|
|
||||||
|
|
||||||
|
🏆 完整路径 (3.5-4 小时) - 生产环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1-4: 如上 ⏱️ 30 分钟
|
||||||
|
Phase 5a: 应用集成(完整版) ⏱️ 2-3 小时
|
||||||
|
Phase 5b: ETCD 加密配置 ⏱️ 30 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 3.5-4 小时
|
||||||
|
|
||||||
|
|
||||||
|
📚 学习路径 (6-8 小时) - 从零开始理解
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
文档阅读 (SUMMARY + DEPLOYMENT + INTEGRATION) ⏱️ 1-2 小时
|
||||||
|
完整路径部署 ⏱️ 3.5-4 小时
|
||||||
|
验证和测试 ⏱️ 1-2 小时
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 6-8 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
## 并行和串行步骤
|
||||||
|
|
||||||
|
```
|
||||||
|
可以并行执行的任务
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 应用集成 (Phase 5a) │ ────┐
|
||||||
|
├─────────────────────────────────┤ │ 可选,独立
|
||||||
|
│ • gRPC interceptor │ │ 进行
|
||||||
|
│ • REST middleware │ │
|
||||||
|
│ • Handler 实现 │ │
|
||||||
|
│ • 单元测试 │ │
|
||||||
|
└─────────────────────────────────┘ │
|
||||||
|
├─ 与 Phase 2-4 并行
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────┐ │
|
||||||
|
│ ETCD 加密 (Phase 5b) │ ────┘
|
||||||
|
├─────────────────────────────────┤ │ 需要集群管理员
|
||||||
|
│ • 生成密钥(可单独进行) │ │ 权限,在
|
||||||
|
│ • 创建配置文件 │ │ Control Plane
|
||||||
|
│ • 修改 kube-apiserver │ │ 节点执行
|
||||||
|
│ • 重启 API server │ │
|
||||||
|
└─────────────────────────────────┘────┘
|
||||||
|
|
||||||
|
|
||||||
|
必须串行执行的步骤
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Phase 1 → Phase 2 → Phase 3 → Phase 4 → (Phase 5a + Phase 5b)
|
||||||
|
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
前置检查 创建资源 部署应用 验证完整 可选扩展功能
|
||||||
|
|
||||||
|
• Phase 2 必须在 Phase 1 之后(需要 namespace)
|
||||||
|
• Phase 3 必须在 Phase 2 之后(需要 RoleBinding)
|
||||||
|
• Phase 4 必须在 Phase 3 之后(需要 Pods 启动)
|
||||||
|
• Phase 5a/5b 可在 Phase 4 完成后并行进行
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键时间点
|
||||||
|
|
||||||
|
```
|
||||||
|
事件时间线
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
T+0 Phase 1: 验证前置条件
|
||||||
|
└─ 预计 5 分钟
|
||||||
|
|
||||||
|
T+5 Phase 2: kubectl apply jwt-secret.yaml
|
||||||
|
└─ 预计 1 分钟执行,5 分钟验证
|
||||||
|
|
||||||
|
T+11 Phase 3a: kubectl apply user-rpc.yaml
|
||||||
|
└─ 3 个 Pods 启动(滚动更新)
|
||||||
|
└─ 预计 ~3 分钟(取决于镜像拉取)
|
||||||
|
|
||||||
|
T+14 Phase 3b: kubectl apply envoy.yaml
|
||||||
|
└─ 1 个 Pod 启动
|
||||||
|
└─ 预计 ~2 分钟
|
||||||
|
|
||||||
|
T+16 Phase 4: 执行完整验证检查
|
||||||
|
└─ 12 个验证部分,共 ~15 分钟
|
||||||
|
|
||||||
|
T+31 ✅ 基础部署完成
|
||||||
|
|
||||||
|
T+31 (可选) Phase 5a: 应用代码集成
|
||||||
|
到 └─ 2-3 小时编码和测试
|
||||||
|
T+211
|
||||||
|
|
||||||
|
T+31 (可选) Phase 5b: ETCD 加密
|
||||||
|
到 └─ 30 分钟配置
|
||||||
|
T+61
|
||||||
|
|
||||||
|
T+211 或 T+61 🎉 全部完成
|
||||||
|
(取决于是否执行 Phase 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐的部署顺序
|
||||||
|
|
||||||
|
### 对于 DevOps/SRE
|
||||||
|
|
||||||
|
```
|
||||||
|
优先级顺序:
|
||||||
|
|
||||||
|
1️⃣ Phase 1-4 (核心部署) [必需]
|
||||||
|
└─ 时间: 30 分钟
|
||||||
|
|
||||||
|
2️⃣ Phase 5b (ETCD 加密) [生产强烈推荐]
|
||||||
|
└─ 时间: 30 分钟
|
||||||
|
└─ 开始时间: T+16 之前 (与 Phase 4 并行)
|
||||||
|
|
||||||
|
3️⃣ 密钥备份和恢复计划 [重要]
|
||||||
|
└─ 时间: 15 分钟
|
||||||
|
└─ 参考: DEPLOYMENT.md 灾难恢复
|
||||||
|
|
||||||
|
4️⃣ Phase 5a 支持 [当开发完成时]
|
||||||
|
└─ 协助开发团队集成 JWT
|
||||||
|
└─ 参考: INTEGRATION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 对于应用开发者
|
||||||
|
|
||||||
|
```
|
||||||
|
优先级顺序:
|
||||||
|
|
||||||
|
1️⃣ 了解系统架构 [了解背景]
|
||||||
|
└─ 文档: SUMMARY.md
|
||||||
|
└─ 时间: 10 分钟
|
||||||
|
|
||||||
|
2️⃣ Phase 5a: 代码集成 [并行进行]
|
||||||
|
└─ 参考: INTEGRATION.md
|
||||||
|
└─ 时间: 2-3 小时
|
||||||
|
|
||||||
|
3️⃣ 单元测试 [在开发中]
|
||||||
|
└─ 参考: INTEGRATION.md 第 9 部分
|
||||||
|
└─ 时间: 1 小时
|
||||||
|
|
||||||
|
4️⃣ 集成测试 [与运维协调]
|
||||||
|
└─ 测试完整流程
|
||||||
|
└─ 时间: 1-2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚应急步骤
|
||||||
|
|
||||||
|
如果部署失败,可以快速回滚:
|
||||||
|
|
||||||
|
```
|
||||||
|
紧急回滚
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
如果 Phase 2 (Secret) 失败:
|
||||||
|
→ kubectl delete secret jwt-secret -n juwan
|
||||||
|
→ kubectl delete sa user-rpc envoy-gateway -n juwan
|
||||||
|
→ kubectl delete role jwt-secret-reader -n juwan
|
||||||
|
→ 修正配置后重新应用
|
||||||
|
|
||||||
|
如果 Phase 3 (Deployment) 失败:
|
||||||
|
→ kubectl rollout undo deployment/user-rpc -n juwan
|
||||||
|
→ kubectl rollout undo deployment/envoy-gateway -n juwan
|
||||||
|
→ 或删除部署并使用稳定的旧版本重新部署
|
||||||
|
|
||||||
|
如果 Phase 5b (ETCD 加密) 失败:
|
||||||
|
→ 从 kube-apiserver 清单中移除加密参数
|
||||||
|
→ 重启 kube-apiserver
|
||||||
|
→ 删除 /etc/kubernetes/encryption-config.yaml
|
||||||
|
→ 从最近的 ETCD 备份恢复(如果需要)
|
||||||
|
|
||||||
|
注意: 备份是关键!每次重大操作前都应备份。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署检查点 (Go/No-Go)
|
||||||
|
|
||||||
|
```
|
||||||
|
关键检查点
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
✅ Checkpoint 1 (Phase 2 后)
|
||||||
|
- Secret 已创建
|
||||||
|
- ServiceAccounts 已创建
|
||||||
|
- Go → 继续 Phase 3
|
||||||
|
- No-Go → 检查 kubectl 权限
|
||||||
|
|
||||||
|
✅ Checkpoint 2 (Phase 3 后)
|
||||||
|
- Pods 已启动 (Running)
|
||||||
|
- No PodSchedulingFailure
|
||||||
|
- Go → 继续 Phase 4
|
||||||
|
- No-Go → 检查资源限制和镜像拉取
|
||||||
|
|
||||||
|
✅ Checkpoint 3 (Phase 4 权限测试)
|
||||||
|
- user-rpc 可以读 jwt-secret
|
||||||
|
- envoy-gateway 可以读 jwt-secret
|
||||||
|
- 其他 SA 无法读取
|
||||||
|
- Go → 继续 Phase 5
|
||||||
|
- No-Go → 检查 RBAC 配置
|
||||||
|
|
||||||
|
✅ Checkpoint 4 (Phase 4 Redis 连接)
|
||||||
|
- Redis Cluster 健康 (3/3 nodes)
|
||||||
|
- PING 返回 PONG
|
||||||
|
- Go → 继续应用集成
|
||||||
|
- No-Go → 检查 Redis Pods 和网络
|
||||||
|
|
||||||
|
✅ Checkpoint 5 (Phase 5b - ETCD 加密)
|
||||||
|
- 新创建的 Secret 在 ETCD 中已加密
|
||||||
|
- Go → 生产就绪
|
||||||
|
- No-Go → 检查 kube-apiserver 配置参数
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用这个流程图
|
||||||
|
|
||||||
|
1. **首次部署** → 从 "推荐部署路径" 选择合适的版本
|
||||||
|
2. **卡在某一步** → 查看对应的 Phase 描述和命令
|
||||||
|
3. **估算时间** → 查看 "时间估计和路径" 部分
|
||||||
|
4. **需要回滚** → 参考 "回滚应急步骤"
|
||||||
|
5. **检查进度** → 使用 "部署检查点"
|
||||||
|
|
||||||
|
详细的部署步骤见:[DEPLOYMENT.md](./DEPLOYMENT.md)
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
# JWT + ETCD 加密系统文档索引
|
||||||
|
|
||||||
|
## 📚 文档完整导航
|
||||||
|
|
||||||
|
### 快速入门 (5-15分钟)
|
||||||
|
|
||||||
|
**推荐路径:** 从上到下顺序阅读
|
||||||
|
|
||||||
|
1. **[SUMMARY.md](./SUMMARY.md)** ⭐ 从这里开始
|
||||||
|
- 📋 项目概览和架构图
|
||||||
|
- 🎯 核心特性一览
|
||||||
|
- ✅ 生产就绪检查清单
|
||||||
|
- 🚀 下一步行动计划
|
||||||
|
|
||||||
|
2. **[README.md](./README.md)**
|
||||||
|
- 🏃 4个快速部署步骤
|
||||||
|
- 🔐 安全考虑事项
|
||||||
|
- 🔄 密钥轮换程序
|
||||||
|
- 🆘 故障排查
|
||||||
|
|
||||||
|
3. **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)**
|
||||||
|
- ⚡ 一页速查表
|
||||||
|
- 📝 常见命令复制粘贴
|
||||||
|
- 🗺️ 文档地图
|
||||||
|
- 🎓 关键参数速知
|
||||||
|
|
||||||
|
### 部署实施 (30-60分钟)
|
||||||
|
|
||||||
|
4. **[DEPLOYMENT.md](./DEPLOYMENT.md)** - 最详细的部署指南
|
||||||
|
- 📦 第1步:创建 Secret 和 RBAC(必需)
|
||||||
|
- 🔄 第2步:更新 user-rpc Deployment
|
||||||
|
- 🌐 第3步:更新 Envoy Gateway Deployment
|
||||||
|
- 🔐 第4步:启用 ETCD 加密(生产推荐)
|
||||||
|
- ✔️ 第5步:验证整个系统
|
||||||
|
- 📊 监控和日志配置
|
||||||
|
- 🛠️ 安全最佳实践
|
||||||
|
- 🆘 故障排查指南
|
||||||
|
- 💾 灾难恢复流程
|
||||||
|
|
||||||
|
### 验证和监控 (20-30分钟)
|
||||||
|
|
||||||
|
5. **[VERIFICATION.md](./VERIFICATION.md)** - 完整验证清单
|
||||||
|
|
||||||
|
**12个验证部分:**
|
||||||
|
|
||||||
|
| 部分 | 用途 | 时间 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 第1部分 | Secret/RBAC 基础验证 | 2分钟 |
|
||||||
|
| 第2部分 | 权限测试(allow/deny) | 3分钟 |
|
||||||
|
| 第3部分 | Deployment 配置检查 | 2分钟 |
|
||||||
|
| 第4部分 | Redis 连接测试 | 2分钟 |
|
||||||
|
| 第5部分 | 应用启动日志 | 3分钟 |
|
||||||
|
| 第6部分 | 网络和服务发现 | 3分钟 |
|
||||||
|
| 第7部分 | Prometheus 指标 | 3分钟 |
|
||||||
|
| 第8部分 | Loki 日志聚合 | 2分钟 |
|
||||||
|
| 第9部分 | ETCD 加密验证 | 5分钟 |
|
||||||
|
| 第10部分 | JWT 功能测试 | 10分钟 |
|
||||||
|
| 第11部分 | 故障排查诊断 | 5分钟 |
|
||||||
|
| 第12部分 | 清理和总结 | 2分钟 |
|
||||||
|
|
||||||
|
### 高级话题
|
||||||
|
|
||||||
|
6. **[ENCRYPTION.md](./ENCRYPTION.md)** - ETCD 加密完整指南
|
||||||
|
- 🔑 第1部分:密钥生成
|
||||||
|
- 📋 第2部分:配置格式
|
||||||
|
- 🔧 第3部分:kube-apiserver 修改
|
||||||
|
- ✅第4部分:验证加密
|
||||||
|
- ⚠️ 第5部分:关键警告(数据不可恢复)
|
||||||
|
- 🔐 第6部分:RBAC 解释
|
||||||
|
- 📦 第7部分:Deployment 示例
|
||||||
|
- 🍎 第8部分:Minikube 特定说明
|
||||||
|
|
||||||
|
7. **[INTEGRATION.md](../api/INTEGRATION.md)** - 代码集成指南
|
||||||
|
- 🔗 第1部分:gRPC Unary Interceptor
|
||||||
|
- 🔗 第2部分:gRPC Stream Interceptor
|
||||||
|
- 👤 第3部分:登录 Handler 实现
|
||||||
|
- 🔐 第4部分:受保护 Handler 中的声明提取
|
||||||
|
- 🔄 第5部分:令牌刷新端点
|
||||||
|
- 🚪 第6部分:登出处理
|
||||||
|
- 🛣️ 第7部分:REST Routes 配置
|
||||||
|
- 🔎 第8部分:错误处理最佳实践
|
||||||
|
- 🧪 第9部分:单元测试示例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 文件结构详解
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/k8s/
|
||||||
|
├── secrets/
|
||||||
|
│ ├── jwt-secret.yaml ✅ Kubernetes 清单文件
|
||||||
|
│ │ ├── Secret: jwt-secret (JWT 秘钥数据)
|
||||||
|
│ │ ├── ServiceAccount: user-rpc
|
||||||
|
│ │ ├── ServiceAccount: envoy-gateway
|
||||||
|
│ │ ├── Role: jwt-secret-reader
|
||||||
|
│ │ ├── RoleBinding: jwt-secret-reader-user-rpc
|
||||||
|
│ │ └── RoleBinding: jwt-secret-reader-envoy-gateway
|
||||||
|
│ │
|
||||||
|
│ ├── README.md 📖 快速参考指南(5分钟入门)
|
||||||
|
│ ├── SUMMARY.md 📊 系统概览(10分钟了解全貌)
|
||||||
|
│ ├── QUICK_REFERENCE.md ⚡ 速查表(查找命令和参数)
|
||||||
|
│ ├── DEPLOYMENT.md 📦 详细部署指南(60分钟完整部署)
|
||||||
|
│ ├── ENCRYPTION.md 🔐 ETCD 加密指南(Control Plane 配置)
|
||||||
|
│ ├── VERIFICATION.md ✅ 验证清单(部署后验证)
|
||||||
|
│ └── INDEX.md 🗺️ 本文件(文档导航)
|
||||||
|
│
|
||||||
|
└── envoy/
|
||||||
|
│ └── envoy.yaml ✅ Envoy 网关配置
|
||||||
|
│ └── 已更新: serviceAccountName: envoy-gateway
|
||||||
|
│
|
||||||
|
service/user/
|
||||||
|
├── user-api.yaml ✅ user-api Service
|
||||||
|
├── user-rpc.yaml ✅ user-rpc Deployment(已更新)
|
||||||
|
│ ├── serviceAccountName: user-rpc (已更新)
|
||||||
|
│ ├── JWT_SECRET_KEY env var (已更新)
|
||||||
|
│ └── Redis Cluster configuration
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
app/users/
|
||||||
|
├── api/
|
||||||
|
│ └── INTEGRATION.md 📝 REST/gRPC 集成指南
|
||||||
|
│
|
||||||
|
└── rpc/
|
||||||
|
├── internal/utils/jwt.go ✅ JwtManager 实现(已存在)
|
||||||
|
├── internal/config/config.go ✅ JWT 配置(已存在)
|
||||||
|
├── internal/svc/
|
||||||
|
│ └── serviceContext.go ✅ 依赖注入(已存在)
|
||||||
|
└── etc/pb.yaml ✅ 运行时配置(已存在)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 按场景查找文档
|
||||||
|
|
||||||
|
### 场景 1:我想快速了解这个系统是什么
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [SUMMARY.md](./SUMMARY.md) - 项目概览(5分钟)
|
||||||
|
2. [SUMMARY.md](./SUMMARY.md) 中的架构图和特性说明
|
||||||
|
|
||||||
|
**关键信息:**
|
||||||
|
- JWT 令牌系统 + Redis 存储 + RBAC 权限 + ETCD 加密
|
||||||
|
- 支持 7 天有效期、30 天可刷新
|
||||||
|
- Envoy 网关 CSRF 防护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 2:我想立即部署到 Kubernetes
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [README.md](./README.md) - 快速参考(2分钟)
|
||||||
|
2. [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - 复制粘贴命令(3分钟)
|
||||||
|
3. 运行部署命令(5分钟)
|
||||||
|
4. [VERIFICATION.md](./VERIFICATION.md) 第1-7部分 - 验证(10分钟)
|
||||||
|
|
||||||
|
**快速命令:**
|
||||||
|
```bash
|
||||||
|
# Copy from QUICK_REFERENCE.md "部署命令" 部分
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 3:部署后验证一切正常
|
||||||
|
|
||||||
|
**推荐阅读:**
|
||||||
|
- [VERIFICATION.md](./VERIFICATION.md) - 12部分完整验证清单
|
||||||
|
- 逐部分执行验证命令
|
||||||
|
|
||||||
|
**预计时间:** 30-40分钟
|
||||||
|
|
||||||
|
**验证触发点:**
|
||||||
|
- ✅ Secrets 和 RBAC 已创建
|
||||||
|
- ✅ Pods 已启动运行
|
||||||
|
- ✅ 权限验证通过
|
||||||
|
- ✅ Redis 连接成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 4:启用 ETCD 加密(生产推荐)
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [ENCRYPTION.md](./ENCRYPTION.md) - 完整加密指南
|
||||||
|
2. 按照 8 个步骤逐一执行
|
||||||
|
3. [VERIFICATION.md](./VERIFICATION.md) 第9部分 - 加密验证
|
||||||
|
|
||||||
|
**需要的权限:**
|
||||||
|
- Control Plane 节点的 root/sudo 权限
|
||||||
|
- Kubernetes 集群管理员权限
|
||||||
|
|
||||||
|
**预计时间:** 15-20分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 5:集成 JWT 到我的应用代码中
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [INTEGRATION.md](../api/INTEGRATION.md) 第1-2部分 - gRPC 拦截器
|
||||||
|
2. 第3-4部分 - 登录和受保护 Handlers
|
||||||
|
3. 第7-8部分 - REST API 中间件
|
||||||
|
4. 第9部分 - 单元测试
|
||||||
|
|
||||||
|
**需要实现:**
|
||||||
|
- ✅ gRPC Unary/Stream Interceptors
|
||||||
|
- ✅ 登录/登出端点
|
||||||
|
- ✅ JWT Middleware for REST
|
||||||
|
- ✅ 错误处理
|
||||||
|
|
||||||
|
**预计时间:** 2-3 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 6:部署后遇到问题
|
||||||
|
|
||||||
|
**根据错误类型选择:**
|
||||||
|
|
||||||
|
| 错误类型 | 查看文档 |
|
||||||
|
|---------|--------|
|
||||||
|
| Pod 无法启动 | [VERIFICATION.md](./VERIFICATION.md) 第11部分 |
|
||||||
|
| 权限被拒绝 | [VERIFICATION.md](./VERIFICATION.md) 第2部分 + [README.md](./README.md) 故障排查 |
|
||||||
|
| Redis 连接失败 | [VERIFICATION.md](./VERIFICATION.md) 第4部分 |
|
||||||
|
| ETCD 加密失败 | [ENCRYPTION.md](./ENCRYPTION.md) 第5-6部分 |
|
||||||
|
| 配置不清楚 | [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 配置文件位置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 7:定期维护任务
|
||||||
|
|
||||||
|
#### 任务:轮换 JWT 秘钥
|
||||||
|
|
||||||
|
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 安全最佳实践 > 密钥轮换
|
||||||
|
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 密钥轮换步骤
|
||||||
|
|
||||||
|
**频率:** 季度(3个月)
|
||||||
|
|
||||||
|
#### 任务:轮换 ETCD 加密密钥
|
||||||
|
|
||||||
|
**阅读:** [ENCRYPTION.md](./ENCRYPTION.md) 第5部分
|
||||||
|
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) ETCD 加密系统
|
||||||
|
|
||||||
|
**频率:** 年度(12个月)
|
||||||
|
|
||||||
|
#### 任务:备份密钥
|
||||||
|
|
||||||
|
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 灾难恢复
|
||||||
|
**或:** [ENCRYPTION.md](./ENCRYPTION.md) 关键警告
|
||||||
|
|
||||||
|
**频率:** 立即 + 每次轮换后
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 文档深度对比
|
||||||
|
|
||||||
|
| 文档 | 深度 | 丰富度 | 代码 | 适合角色 |
|
||||||
|
|-----|------|--------|------|---------|
|
||||||
|
| README | 浅 | 概览 | - | PM/初学者 |
|
||||||
|
| SUMMARY | 浅 | 概览 | - | 决策者 |
|
||||||
|
| QUICK_REFERENCE | 中 | 速查 | 命令 | DevOps/SRE |
|
||||||
|
| DEPLOYMENT | 深 | 详细 | 示例 | DevOps/运维 |
|
||||||
|
| VERIFICATION | 深 | 详细 | 脚本 | QA/DevOps |
|
||||||
|
| ENCRYPTION | 非常深 | 极详细 | YAML | 安全/运维 |
|
||||||
|
| INTEGRATION | 非常深 | 代码级 | 完整 | 开发者 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 学习路径建议
|
||||||
|
|
||||||
|
### 对于 DevOps/SRE
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. DEPLOYMENT.md (30 分钟)
|
||||||
|
3. VERIFICATION.md (30 分钟)
|
||||||
|
4. ENCRYPTION.md (20 分钟)
|
||||||
|
5. 实践部署 (60 分钟)
|
||||||
|
|
||||||
|
**总计:** ~3 小时
|
||||||
|
|
||||||
|
### 对于应用开发者
|
||||||
|
|
||||||
|
1. SUMMARY.md > "集成点" 部分 (5 分钟)
|
||||||
|
2. INTEGRATION.md (60 分钟)
|
||||||
|
3. QUICK_REFERENCE.md > "JWT Manager API" (10 分钟)
|
||||||
|
4. 代码实现 (2-3 小时)
|
||||||
|
5. 单元测试 (INTEGRATION.md 第9部分)
|
||||||
|
|
||||||
|
**总计:** ~4 小时
|
||||||
|
|
||||||
|
### 对于安全/合规人员
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. ENCRYPTION.md (30 分钟)
|
||||||
|
3. DEPLOYMENT.md > 安全最佳实践 (15 分钟)
|
||||||
|
4. VERIFICATION.md 第9部分 (10 分钟)
|
||||||
|
|
||||||
|
**总计:** ~1 小时
|
||||||
|
|
||||||
|
### 对于项目经理
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. DEPLOYMENT.md > "部署状态示意图" (5 分钟)
|
||||||
|
3. DEPLOYMENT.md > "快速部署" (2 分钟)
|
||||||
|
|
||||||
|
**总计:** ~15 分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 学习成果预期
|
||||||
|
|
||||||
|
### 完成后,您将能够:
|
||||||
|
|
||||||
|
✅ 在 Kubernetes 中部署 JWT 认证系统
|
||||||
|
✅ 配置 RBAC 权限控制
|
||||||
|
✅ 启用 ETCD 加密保护敏感数据
|
||||||
|
✅ 在 Go-zero 应用中集成 JWT
|
||||||
|
✅ 实现令牌刷新和撤销
|
||||||
|
✅ 诊断和排查常见问题
|
||||||
|
✅ 执行密钥轮换和灾难恢复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 求助指南
|
||||||
|
|
||||||
|
### 第一步:找到相关文档
|
||||||
|
- 浏览本索引找到相关章节
|
||||||
|
- 或用 Ctrl+F 搜索关键词
|
||||||
|
|
||||||
|
### 第二步:查看文档中的相关部分
|
||||||
|
- DEPLOYMENT.md 的相关章节
|
||||||
|
- 或 VERIFICATION.md 的故障排查部分
|
||||||
|
|
||||||
|
### 第三步:运行诊断命令
|
||||||
|
- QUICK_REFERENCE.md 的 "故障排查" 部分
|
||||||
|
- 或 VERIFICATION.md 的 "故障排查" 部分
|
||||||
|
|
||||||
|
### 第四步:检查日志
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第五步:查看详细文档
|
||||||
|
如果上述步骤未能解决,查看对应的详细文档:
|
||||||
|
- 配置问题 → DEPLOYMENT.md
|
||||||
|
- 权限问题 → VERIFICATION.md 第2/11部分
|
||||||
|
- 集成问题 → INTEGRATION.md
|
||||||
|
- 加密问题 → ENCRYPTION.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 文档反馈
|
||||||
|
|
||||||
|
如果您发现:
|
||||||
|
- ❌ 文档不清楚
|
||||||
|
- ❌ 命令不工作
|
||||||
|
- ❌ 信息缺失或过时
|
||||||
|
- ❌ 错别字或格式问题
|
||||||
|
|
||||||
|
请在相应的 `.md` 文件中标记,或提交更新建议。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 关键概念快速链接
|
||||||
|
|
||||||
|
| 概念 | 详见 |
|
||||||
|
|-----|------|
|
||||||
|
| JWT 令牌生命周期 | SUMMARY.md "关键特性" |
|
||||||
|
| Redis 双键结构 | SUMMARY.md "关键特性" |
|
||||||
|
| RBAC 权限隔离 | SUMMARY.md "关键特性" |
|
||||||
|
| CSRF 防护 | SUMMARY.md "关键特性" |
|
||||||
|
| ETCD 加密 | ENCRYPTION.md |
|
||||||
|
| 错误处理 | INTEGRATION.md 第8部分 |
|
||||||
|
| 密钥轮换 | DEPLOYMENT.md "安全最佳实践" |
|
||||||
|
| 灾难恢复 | DEPLOYMENT.md "灾难恢复" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 文档特性
|
||||||
|
|
||||||
|
✅ **模块化** - 每个文档独立,但相互链接
|
||||||
|
✅ **分层** - 从快速概览到深度细节
|
||||||
|
✅ **实践导向** - 包含实际命令和代码示例
|
||||||
|
✅ **完整性** - 覆盖部署、验证、维护、故障排查
|
||||||
|
✅ **易查找** - 目录、索引、速查表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**开始阅读:** 👉 [SUMMARY.md](./SUMMARY.md)
|
||||||
|
|
||||||
|
或根据您的角色选择:
|
||||||
|
|
||||||
|
| 角色 | 开始文档 | 预计时间 |
|
||||||
|
|-----|--------|--------|
|
||||||
|
| DevOps/运维 | [DEPLOYMENT.md](./DEPLOYMENT.md) | 1-2 小时 |
|
||||||
|
| 应用开发 | [INTEGRATION.md](../api/INTEGRATION.md) | 2-3 小时 |
|
||||||
|
| 安全审查 | [ENCRYPTION.md](./ENCRYPTION.md) | 30 分钟 |
|
||||||
|
| 项目经理 | [SUMMARY.md](./SUMMARY.md) | 15 分钟 |
|
||||||
|
| 新手 | [README.md](./README.md) → [SUMMARY.md](./SUMMARY.md) | 15-20 分钟 |
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# JWT + ETCD 加密系统 - 快速参考卡片
|
||||||
|
|
||||||
|
## 一页速查表
|
||||||
|
|
||||||
|
### 部署命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建 Secret 和 RBAC
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
|
||||||
|
# 更新 Deployments
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 验证部署
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
kubectl get sa user-rpc envoy-gateway -n juwan
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
--resource-name=jwt-secret -n juwan
|
||||||
|
# 预期: yes
|
||||||
|
|
||||||
|
# 其他 SA 无法读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:default \
|
||||||
|
--resource-name=jwt-secret -n juwan
|
||||||
|
# 预期: no
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志查看
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
|
||||||
|
# Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
|
||||||
|
# 特定 Pod 日志
|
||||||
|
kubectl logs -n juwan <pod-name> --all-containers=true -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 JWT_SECRET_KEY 已注入
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) \
|
||||||
|
-n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 连接到 Redis Cluster
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 -c CLUSTER INFO
|
||||||
|
|
||||||
|
# 测试键操作
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 -c GET jwt:user:test-user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 在 Control Plane 节点生成密钥
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
|
||||||
|
# 2. 编辑 kube-apiserver 清单
|
||||||
|
sudo nano /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
|
||||||
|
# 添加参数:
|
||||||
|
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
|
||||||
|
# 3. 创建加密配置文件
|
||||||
|
cat <<EOF | sudo tee /etc/kubernetes/encryption-config.yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: <BASE64_KEY>
|
||||||
|
- identity: {}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. 验证加密
|
||||||
|
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
|
||||||
|
sudo ETCDCTL_API=3 etcdctl --cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption | od -A x -t x1z
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod 无法启动?查看事件
|
||||||
|
kubectl describe pod <pod-name> -n juwan
|
||||||
|
|
||||||
|
# 权限被拒绝?检查 RBAC
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
|
||||||
|
|
||||||
|
# 无法挂载 Secret?检查 Secret 存在性
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml
|
||||||
|
|
||||||
|
# Redis 连接错误?测试连通性
|
||||||
|
kubectl exec -it <user-rpc-pod> -n juwan -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 PING
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Manager API 速查
|
||||||
|
|
||||||
|
### JwtManager 方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 生成新令牌
|
||||||
|
token, err := svcCtx.JwtManager.New(ctx, userID, email, name)
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
claims, err := svcCtx.JwtManager.Valid(ctx, token)
|
||||||
|
|
||||||
|
// 刷新令牌(如果过期但 Redis 仍有数据)
|
||||||
|
newToken, err := svcCtx.JwtManager.Renew(ctx, token)
|
||||||
|
|
||||||
|
// 提取声明(不验证签名)
|
||||||
|
claims, err := svcCtx.JwtManager.Extract(ctx, token)
|
||||||
|
|
||||||
|
// 检查令牌是否存在于 Redis
|
||||||
|
exists, err := svcCtx.JwtManager.Exists(ctx, token)
|
||||||
|
|
||||||
|
// 撤销令牌(登出)
|
||||||
|
err := svcCtx.JwtManager.Revoke(ctx, userID, token)
|
||||||
|
|
||||||
|
// 获取用户当前令牌
|
||||||
|
token, err := svcCtx.JwtManager.GetUserToken(ctx, userID)
|
||||||
|
|
||||||
|
// 将声明转换为载荷
|
||||||
|
payload := svcCtx.JwtManager.ClaimsToPayload(claims)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置文件位置
|
||||||
|
|
||||||
|
| 配置 | 位置 | 关键参数 |
|
||||||
|
|-----|------|--------|
|
||||||
|
| JWT Secret | `deploy/k8s/secrets/jwt-secret.yaml` | `secret-key` |
|
||||||
|
| user-rpc 配置 | `app/users/rpc/etc/pb.yaml` | `JWT.SecretKey`, `REDIS_HOST` |
|
||||||
|
| Envoy 配置 | `deploy/k8s/envoy/envoy.yaml` | CSRF 验证 Lua 代码 |
|
||||||
|
| ETCD 加密 | `/etc/kubernetes/encryption-config.yaml`(Control Plane) | `secret` (32字节密钥) |
|
||||||
|
|
||||||
|
## 关键参数速查
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# JWT 令牌有效期
|
||||||
|
Token Exp: 7 days
|
||||||
|
|
||||||
|
# Redis 存储 TTL
|
||||||
|
Redis TTL: 30 days
|
||||||
|
|
||||||
|
# 可刷新时间窗口
|
||||||
|
Refresh Window: 30 days - 7 days = 23 days
|
||||||
|
|
||||||
|
# CSRF Token 位置
|
||||||
|
Cookie: csrf_token=...
|
||||||
|
Header: X-CSRF-Token: ...
|
||||||
|
|
||||||
|
# ETCD 加密算法
|
||||||
|
Algorithm: AES-CBC
|
||||||
|
Key Size: 256 bits (32 bytes)
|
||||||
|
Encoding: Base64
|
||||||
|
|
||||||
|
# Secret 挂载方式
|
||||||
|
Method: volumeMount (read-only)
|
||||||
|
或
|
||||||
|
Method: valueFrom.secretKeyRef
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题速查
|
||||||
|
|
||||||
|
| 问题 | 排查命令 | 解决方案 |
|
||||||
|
|-----|--------|--------|
|
||||||
|
| Pod 无法启动 | `kubectl describe pod` | 检查 Secret/RBAC |
|
||||||
|
| 权限被拒绝 | `kubectl auth can-i get secrets` | 验证 RBAC 绑定 |
|
||||||
|
| Redis 连接失败 | `redis-cli PING` | 检查 Redis Pods |
|
||||||
|
| JWT 验证失败 | 查看 Pod 日志 | 检查 Redis 中的令牌 |
|
||||||
|
| CSRF 验证失败 | 查看 Envoy 日志 | 检查 Cookie/Header 匹配 |
|
||||||
|
| ETCD 加密失败 | `kubectl get secret` | 检查 kube-apiserver 启动参数 |
|
||||||
|
|
||||||
|
## 部署检查清单 (5分钟版)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 第1步: 部署 Secret (10秒)
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml && sleep 5
|
||||||
|
|
||||||
|
# 第2步: 验证 Secret (10秒)
|
||||||
|
kubectl get secret jwt-secret -n juwan && echo "✓ Secret 已创建"
|
||||||
|
|
||||||
|
# 第3步: 验证 RBAC (10秒)
|
||||||
|
kubectl get role jwt-secret-reader -n juwan && echo "✓ RBAC 已配置"
|
||||||
|
|
||||||
|
# 第4步: 更新 Deployments (20秒)
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 第5步: 等待 Pods 启动 (30秒)
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 第6步: 快速功能测试 (2分钟)
|
||||||
|
# 创建一个令牌并验证可读取
|
||||||
|
REDIS_POD=$(kubectl get pod -n juwan -l redis=user-redis -o name | head -1)
|
||||||
|
kubectl exec -it $REDIS_POD -n juwan -- redis-cli KEYS "jwt:*"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 密钥轮换步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 生成新密钥
|
||||||
|
NEW_KEY=$(head -c 32 /dev/urandom | base64)
|
||||||
|
|
||||||
|
# 2. 更新 Secret
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=secret-key=$NEW_KEY \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# 3. 重启 Pods(自动挂载新 Secret)
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 4. 等待 Pods 启动
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 5. 旧令牌现在需要刷新或重新登录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档地图
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/k8s/secrets/
|
||||||
|
├── jwt-secret.yaml ← Secrets + RBAC 配置
|
||||||
|
├── README.md ← 开始阅读(快速指南)
|
||||||
|
├── SUMMARY.md ← 本文件(系统概览)
|
||||||
|
├── DEPLOYMENT.md ← 详细部署步骤(12步)
|
||||||
|
├── ENCRYPTION.md ← ETCD 加密详细指南
|
||||||
|
├── VERIFICATION.md ← 完整验证清单(12部分)
|
||||||
|
└── QUICK_REFERENCE.md ← 本快速参考卡片
|
||||||
|
|
||||||
|
app/users/api/
|
||||||
|
└── INTEGRATION.md ← JWT 代码集成指南
|
||||||
|
|
||||||
|
app/users/rpc/
|
||||||
|
├── internal/utils/jwt.go ← JwtManager 实现
|
||||||
|
├── internal/config/config.go ← JWT 配置
|
||||||
|
├── internal/svc/serviceContext.go ← 依赖注入
|
||||||
|
└── etc/pb.yaml ← 运行时配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键时间点
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 操作 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 令牌签发 | T0 | 生成 JWT,过期时间 = T0 + 7天 |
|
||||||
|
| | | 在 Redis 存储,TTL = 30天 |
|
||||||
|
| Token 过期 | T0 + 7天 | JWT 验证失败 |
|
||||||
|
| 令牌刷新 | T0 + 7天到T0 + 30天 | 如果 Redis 仍有数据,生成新令牌 |
|
||||||
|
| 完全失效 | T0 + 30天 | Redis 删除,无法再刷新 |
|
||||||
|
| 重新登录 | T0 + 30天+ | 用户需要重新登录 |
|
||||||
|
|
||||||
|
## 性能提示
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 高并发下优化 Redis 连接
|
||||||
|
# 在 pb.yaml 中调整:
|
||||||
|
CacheConf:
|
||||||
|
- Host: "user-redis.juwan:6379"
|
||||||
|
Type: "cluster"
|
||||||
|
MaxConnections: 100
|
||||||
|
ConnectionPoolSize: 50
|
||||||
|
|
||||||
|
# 监控 JWT 验证吞吐量
|
||||||
|
# 在 Prometheus 查询:
|
||||||
|
rate(jwt_validations_total[5m])
|
||||||
|
rate(jwt_refresh_total[5m])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全提示
|
||||||
|
|
||||||
|
✅ **必做**
|
||||||
|
- [ ] 定期轮换 JWT 秘钥(季度)
|
||||||
|
- [ ] 定期轮换 ETCD 加密密钥(年度)
|
||||||
|
- [ ] 备份加密密钥到安全位置
|
||||||
|
- [ ] 启用审计日志
|
||||||
|
- [ ] 监控异常的令牌验证失败
|
||||||
|
|
||||||
|
❌ **禁止**
|
||||||
|
- [ ] 不要在日志中输出 JWT 秘钥
|
||||||
|
- [ ] 不要在代码库中存储密钥
|
||||||
|
- [ ] 不要发送明文密钥到 Slack/Email
|
||||||
|
- [ ] 不要在多个环境间共享密钥
|
||||||
|
|
||||||
|
## 版本信息
|
||||||
|
|
||||||
|
```
|
||||||
|
Kubernetes: 1.24+
|
||||||
|
Go-zero: v1.10.0+
|
||||||
|
Redis: 7.0+
|
||||||
|
ETCD: 3.5+
|
||||||
|
Envoy: v1.32.2+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持和反馈
|
||||||
|
|
||||||
|
遇到问题?按优先级检查:
|
||||||
|
|
||||||
|
1. **运行验证脚本**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
./deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **查看日志**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **阅读 VERIFICATION.md**
|
||||||
|
- 第1-5部分: 基础配置
|
||||||
|
- 第6-8部分: 网络和监控
|
||||||
|
- 第9部分: ETCD 加密
|
||||||
|
- 第11部分: 故障排查
|
||||||
|
|
||||||
|
4. **详细指南**
|
||||||
|
- DEPLOYMENT.md - 完整步骤
|
||||||
|
- INTEGRATION.md - 代码集成
|
||||||
|
- ENCRYPTION.md - 加密配置
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# JWT Secret Management
|
||||||
|
|
||||||
|
This directory contains secure configuration for JWT secret key management.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `jwt-secret.yaml`: Kubernetes Secret + ServiceAccount + RBAC rules
|
||||||
|
- `ENCRYPTION.md`: Guide for enabling ETCD static encryption at rest
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create the Secret and RBAC
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
- Secret `jwt-secret` in namespace `juwan` containing the JWT secret key
|
||||||
|
- ServiceAccount `user-rpc` in namespace `juwan`
|
||||||
|
- ServiceAccount `envoy-gateway` in namespace `juwan`
|
||||||
|
- Role `jwt-secret-reader` that allows reading only `jwt-secret`
|
||||||
|
- RoleBindings to grant both ServiceAccounts read permission on the secret
|
||||||
|
|
||||||
|
### 2. Update user-rpc Deployment
|
||||||
|
|
||||||
|
Update `deploy/k8s/service/user/user-rpc.yaml` to:
|
||||||
|
|
||||||
|
1. Set the serviceAccountName:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: user-rpc
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add environment variable to load JWT secret:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-rpc
|
||||||
|
env:
|
||||||
|
- name: JWT_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update envoy-gateway Deployment
|
||||||
|
|
||||||
|
Update `deploy/k8s/envoy/envoy.yaml` to:
|
||||||
|
|
||||||
|
1. Set the serviceAccountName:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add environment variable or mount Secret:
|
||||||
|
```yaml
|
||||||
|
volumeMounts:
|
||||||
|
- name: jwt-secret
|
||||||
|
mountPath: /etc/jwt
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: jwt-secret
|
||||||
|
secret:
|
||||||
|
secretName: jwt-secret
|
||||||
|
defaultMode: 0400
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in the Envoy config:
|
||||||
|
```yaml
|
||||||
|
data:
|
||||||
|
envoy.yaml: |
|
||||||
|
# Read JWT secret from /etc/jwt/secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Enable ETCD Encryption
|
||||||
|
|
||||||
|
Follow the guide in `ENCRYPTION.md` to enable static encryption at rest for all secrets in ETCD.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Least Privilege
|
||||||
|
|
||||||
|
- Only `user-rpc` and `envoy-gateway` can read the JWT secret
|
||||||
|
- No other services or users have access
|
||||||
|
- The Role allows reading **only** the `jwt-secret`, not other secrets
|
||||||
|
|
||||||
|
### Encryption at Rest
|
||||||
|
|
||||||
|
- With ETCD encryption enabled, the secret is encrypted when stored on disk
|
||||||
|
- Even if someone gains access to the ETCD database files, they cannot read the secret without the encryption key
|
||||||
|
|
||||||
|
### Secret Rotation
|
||||||
|
|
||||||
|
To rotate the JWT secret key:
|
||||||
|
|
||||||
|
1. Generate a new key
|
||||||
|
2. Update the Secret:
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic jwt-secret --from-literal=secret-key=NEW_KEY --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
```
|
||||||
|
3. Pod mounts/env vars will be updated automatically within a few minutes
|
||||||
|
4. Old tokens will become invalid (you may need to log users out)
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [ ] ETCD encryption enabled (see ENCRYPTION.md)
|
||||||
|
- [ ] JWT secret key changed from default
|
||||||
|
- [ ] Both user-rpc and envoy-gateway Deployments use correct serviceAccountName
|
||||||
|
- [ ] Both Deployments load the secret via environment variable or volume mount
|
||||||
|
- [ ] Regular secret rotation policy implemented
|
||||||
|
- [ ] Secret backup stored in secure location (encrypted)
|
||||||
|
- [ ] RBAC audit logging enabled to track secret access
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot read jwt-secret
|
||||||
|
Check if the Pod is using the correct ServiceAccount:
|
||||||
|
```bash
|
||||||
|
kubectl get deployment user-rpc -o yaml | grep serviceAccountName
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secret not being mounted
|
||||||
|
Verify the Secret exists:
|
||||||
|
```bash
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
Check Pod logs for mounting errors:
|
||||||
|
```bash
|
||||||
|
kubectl logs -l app=user-rpc -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied error
|
||||||
|
Verify RBAC binding:
|
||||||
|
```bash
|
||||||
|
kubectl get rolebinding -n juwan
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
```
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# JWT 认证系统 + ETCD 加密 - 完整部署总结
|
||||||
|
|
||||||
|
## 项目概览
|
||||||
|
|
||||||
|
这个项目为微服务提供了一个完整的 JWT 认证系统,包括:
|
||||||
|
|
||||||
|
1. **JWT 令牌管理** - 令牌生成、验证、刷新和撤销
|
||||||
|
2. **Redis Cluster 存储** - 令牌交换缓存(30天TTL)和用户会话管理
|
||||||
|
3. **RBAC 权限控制** - 限制只有 user-rpc 和 envoy-gateway 服务可以访问 JWT 秘钥
|
||||||
|
4. **ETCD 加密** - 在 Kubernetes 集群中对所有 Secrets 进行加密
|
||||||
|
5. **网关保护** - Envoy 网关处理 CSRF 防护和请求路由
|
||||||
|
|
||||||
|
## 创建的文件清单
|
||||||
|
|
||||||
|
### 部署配置文件
|
||||||
|
|
||||||
|
#### `/deploy/k8s/secrets/`
|
||||||
|
|
||||||
|
| 文件 | 说明 | 关键内容 |
|
||||||
|
|-----|------|--------|
|
||||||
|
| `jwt-secret.yaml` | Secret + RBAC 配置 | 包含JWT秘钥、ServiceAccounts、Role、RoleBindings |
|
||||||
|
| `README.md` | 快速参考指南 | Secret 创建和 Deployment 更新说明 |
|
||||||
|
| `DEPLOYMENT.md` | 详细部署步骤 | 12个部署步骤,包括ETCD加密配置 |
|
||||||
|
| `ENCRYPTION.md` | ETCD加密完整指南 | 密钥生成、配置修改、验证流程 |
|
||||||
|
| `VERIFICATION.md` | 验证清单 | 12个部分的完整验证脚本和检查项 |
|
||||||
|
|
||||||
|
### 应用代码更新
|
||||||
|
|
||||||
|
| 文件路径 | 修改内容 |
|
||||||
|
|---------|--------|
|
||||||
|
| `/app/users/rpc/internal/utils/jwt.go` | JwtManager 实现(已存在) |
|
||||||
|
| `/app/users/rpc/internal/config/config.go` | JwtConfig 结构体(已存在) |
|
||||||
|
| `/app/users/rpc/internal/svc/serviceContext.go` | Redis Cluster + JwtManager 依赖注入(已存在) |
|
||||||
|
| `/app/users/rpc/etc/pb.yaml` | JWT 和 Redis Cluster 配置(已存在) |
|
||||||
|
| `/deploy/k8s/service/user/user-rpc.yaml` | ✅ **已更新** - 添加 serviceAccountName + JWT_SECRET_KEY 环境变量 |
|
||||||
|
| `/deploy/k8s/envoy/envoy.yaml` | ✅ **已更新** - 添加 serviceAccountName: envoy-gateway |
|
||||||
|
| `/app/users/api/INTEGRATION.md` | 🆕 **新建** - JWT 集成指南(interceptors, handlers, middleware) |
|
||||||
|
|
||||||
|
## 部署状态示意图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Kubernetes Cluster │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ juwan Namespace │ │
|
||||||
|
│ ├─────────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Secret: jwt-secret │ │ │
|
||||||
|
│ │ │ ├─ secret-key: <encrypted in ETCD> │ │ │
|
||||||
|
│ │ │ └─ Protected by RBAC Role + RoleBindings │ │ │
|
||||||
|
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ △ │ │
|
||||||
|
│ │ │ (mounted via serviceAccountName) │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │
|
||||||
|
│ │ │ user-rpc │ │ envoy-gateway │ │ │
|
||||||
|
│ │ │ Deployment │ │ Deployment │ │ │
|
||||||
|
│ │ ├─────────────────┤ ├──────────────────────┤ │ │
|
||||||
|
│ │ │ SA: user-rpc │ │ SA: envoy-gateway │ │ │
|
||||||
|
│ │ │ Replicas: 3 │ │ Replicas: 1 │ │ │
|
||||||
|
│ │ │ Port: 9001(RPC) │ │ Port: 8080(HTTP) │ │ │
|
||||||
|
│ │ │ 4001(Met) │ │ │ │ │
|
||||||
|
│ │ └─────────────────┘ └──────────────────────┘ │ │
|
||||||
|
│ │ │ JWT Manager │ CSRF Filter │ │
|
||||||
|
│ │ │ (HS256 signing) │ (X-CSRF-Token) │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ ┌──────▼──────────────────────────▼─────┐ │ │
|
||||||
|
│ │ │ user-redis (RedisCluster) │ │ │
|
||||||
|
│ │ │ 3-node cluster │ │ │
|
||||||
|
│ │ │ - Token exchange cache (30d TTL) │ │ │
|
||||||
|
│ │ │ - User session management │ │ │
|
||||||
|
│ │ └────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Control Plane (kube-apiserver) │ │
|
||||||
|
│ │ • ETCD 加密: AES-CBC 32-byte key │ │
|
||||||
|
│ │ • Secrets 自动加密存储 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心配置参数
|
||||||
|
|
||||||
|
### JWT 配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
JWT:
|
||||||
|
SecretKey: "your-secret-jwt-key-change-this-in-production"
|
||||||
|
Issuer: "your-app-name"
|
||||||
|
# Token 有效期: 7 天
|
||||||
|
# Redis TTL: 30 天(支持令牌刷新)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Cluster
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
RedisCluster:
|
||||||
|
ClusterSize: 3 🔴 主服务器 + 2 🔵 从服务器
|
||||||
|
Address: "user-redis.juwan:6379"
|
||||||
|
HighAvailability: 自动故障转移
|
||||||
|
```
|
||||||
|
|
||||||
|
### RBAC 权限
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Role: jwt-secret-reader
|
||||||
|
Resources: [secrets]
|
||||||
|
ResourceNames: [jwt-secret]
|
||||||
|
Verbs: [get] # 只读,无列表/创建/删除
|
||||||
|
Subjects:
|
||||||
|
- user-rpc (ServiceAccount)
|
||||||
|
- envoy-gateway (ServiceAccount)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署流程
|
||||||
|
|
||||||
|
### 快速部署(5分钟)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 第1步:创建 Secret 和 RBAC
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
|
||||||
|
# 第2步:更新 Deployments
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 第3步:验证
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密部署(需要集群管理员权限)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在 Control Plane 节点执行
|
||||||
|
1. 生成 32 字节密钥
|
||||||
|
2. 创建 /etc/kubernetes/encryption-config.yaml
|
||||||
|
3. 修改 /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
4. 重启 kube-apiserver
|
||||||
|
5. 验证加密已启用
|
||||||
|
```
|
||||||
|
|
||||||
|
详见:`deploy/k8s/secrets/ENCRYPTION.md`
|
||||||
|
|
||||||
|
## 关键特性
|
||||||
|
|
||||||
|
### 1. JWT 令牌生命周期
|
||||||
|
|
||||||
|
```
|
||||||
|
登录 → 生成 JWT
|
||||||
|
├─ 有效期: 7 天(exp claim)
|
||||||
|
└─ 存储到 Redis: 30 天(TTL)
|
||||||
|
|
||||||
|
Token 过期(7天+)
|
||||||
|
├─ JWT 签名验证失败
|
||||||
|
└─ 检查 Redis 是否仍有数据
|
||||||
|
├─ 有 → 生成新 Token(刷新)✅
|
||||||
|
└─ 无 → 令牌已过期,需要重新登录 ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Redis 双键结构
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt:user:{userId} → {token}
|
||||||
|
用途: 快速查询用户当前令牌
|
||||||
|
TTL: 30 天
|
||||||
|
|
||||||
|
jwt:token:{token} → {payload}
|
||||||
|
用途: 令牌验证和刷新
|
||||||
|
TTL: 30 天
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CSRF 防护(Envoy 网关)
|
||||||
|
|
||||||
|
```
|
||||||
|
安全方法 (GET/HEAD/OPTIONS)
|
||||||
|
→ 自动生成 csrf_token
|
||||||
|
→ 返回 Set-Cookie: csrf_token=...
|
||||||
|
|
||||||
|
不安全方法 (POST/PUT/DELETE/PATCH)
|
||||||
|
→ 检查 Cookie csrf_token
|
||||||
|
→ 检查 X-CSRF-Token 头
|
||||||
|
→ 两者必须相等,否则 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 权限隔离
|
||||||
|
|
||||||
|
```
|
||||||
|
Only user-rpc + envoy-gateway 可以:
|
||||||
|
✅ 读 jwt-secret
|
||||||
|
|
||||||
|
Other services 无法:
|
||||||
|
❌ 列出 secrets
|
||||||
|
❌ 获取 jwt-secret(RBAC 拒绝)
|
||||||
|
❌ 删除 secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ETCD 加密
|
||||||
|
|
||||||
|
```
|
||||||
|
未加密:
|
||||||
|
etcdctl get /registry/seca/...
|
||||||
|
→ secret-key: "plaintext-value"
|
||||||
|
|
||||||
|
已加密 (AES-CBC):
|
||||||
|
etcdctl get /registry/secrets/...
|
||||||
|
→ 二进制数据,无法读取
|
||||||
|
```
|
||||||
|
|
||||||
|
## 集成点
|
||||||
|
|
||||||
|
### 1. RPC Handler(需要实现)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在 gRPC server 中注册拦截器
|
||||||
|
s := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 拦截器会:
|
||||||
|
// 1. 提取 Authorization 头中的 Token
|
||||||
|
// 2. 调用 JwtManager.Valid()
|
||||||
|
// 3. 如果过期,尝试 JwtManager.Renew()
|
||||||
|
// 4. 将声明注入 context
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第1-2章
|
||||||
|
|
||||||
|
### 2. REST Endpoint(需要实现)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建 JWT Middleware
|
||||||
|
protected := middleware.JwtMiddleware(svcCtx)
|
||||||
|
|
||||||
|
// 应用到受保护的路由
|
||||||
|
router.HandleFunc("GET /api/v1/users/me",
|
||||||
|
protected(user.GetUserInfoHandler(svcCtx)))
|
||||||
|
|
||||||
|
// Middleware 会验证 Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第3-4章
|
||||||
|
|
||||||
|
### 3. 登录/登出流程(需要实现)
|
||||||
|
|
||||||
|
```
|
||||||
|
登录:
|
||||||
|
1. 验证用户凭证(DB 查询)
|
||||||
|
2. JwtManager.New() → 生成令牌
|
||||||
|
3. 返回令牌给客户端
|
||||||
|
|
||||||
|
登出:
|
||||||
|
1. 从上下文提取 userId
|
||||||
|
2. JwtManager.Revoke() → 删除 Redis 中的令牌
|
||||||
|
3. 用户需要重新登录获取新令牌
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第5-6章
|
||||||
|
|
||||||
|
## 文档导航
|
||||||
|
|
||||||
|
| 场景 | 推荐阅读 |
|
||||||
|
|-----|--------|
|
||||||
|
| 第一次部署 | `README.md` → `DEPLOYMENT.md` |
|
||||||
|
| 部署遇到问题 | `VERIFICATION.md` + `DEPLOYMENT.md` 故障排查部分 |
|
||||||
|
| 代码集成 | `app/users/api/INTEGRATION.md` |
|
||||||
|
| ETCD 加密配置 | `ENCRYPTION.md` |
|
||||||
|
| ETCD 加密验证 | `VERIFICATION.md` 第9部分 |
|
||||||
|
| 安全最佳实践 | `DEPLOYMENT.md` 安全最佳实践部分 |
|
||||||
|
| 灾难恢复 | `DEPLOYMENT.md` 灾难恢复部分 |
|
||||||
|
|
||||||
|
## 生产就绪检查清单
|
||||||
|
|
||||||
|
- [ ] 所有 Pods 都在 Running 状态
|
||||||
|
- [ ] JWT Secret 已创建并正确挂载
|
||||||
|
- [ ] RBAC 权限验证通过
|
||||||
|
- [ ] Redis Cluster 健康(3/3 节点)
|
||||||
|
- [ ] ETCD 加密已启用(如需要)
|
||||||
|
- [ ] 监控和日志聚合正常工作
|
||||||
|
- [ ] 密钥轮换计划已制定
|
||||||
|
- [ ] 备份和恢复流程已文档化
|
||||||
|
- [ ] 安全审计日志已启用
|
||||||
|
- [ ] 端到端测试已通过
|
||||||
|
|
||||||
|
## 下一步行动
|
||||||
|
|
||||||
|
### 短期(本周)
|
||||||
|
|
||||||
|
1. **部署 Secret 和 RBAC**
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **更新 Deployments**
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证部署**
|
||||||
|
```bash
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中期(本月)
|
||||||
|
|
||||||
|
1. **实现 JWT 集成**
|
||||||
|
- 创建 gRPC 拦截器
|
||||||
|
- 实现登录/登出端点
|
||||||
|
- 添加 JWT 中间件到 REST API
|
||||||
|
|
||||||
|
2. **端到端测试**
|
||||||
|
- 测试令牌生成和验证
|
||||||
|
- 测试令牌刷新
|
||||||
|
- 测试 CSRF 防护
|
||||||
|
|
||||||
|
### 长期(本季度)
|
||||||
|
|
||||||
|
1. **启用 ETCD 加密**
|
||||||
|
- 按照 `ENCRYPTION.md` 配置
|
||||||
|
- 验证所有 Secrets 都已加密
|
||||||
|
|
||||||
|
2. **生产部署**
|
||||||
|
- 启用审计日志
|
||||||
|
- 配置监控和告警
|
||||||
|
- 制定密钥轮换政策
|
||||||
|
|
||||||
|
## 支持
|
||||||
|
|
||||||
|
如遇到问题:
|
||||||
|
|
||||||
|
1. **检查日志**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **运行验证脚本**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
./deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **查看详细文档**
|
||||||
|
- 部署问题 → `DEPLOYMENT.md`
|
||||||
|
- 代码集成 → `INTEGRATION.md`
|
||||||
|
- ETCD 加密 → `ENCRYPTION.md`
|
||||||
|
- 诊断 → `VERIFICATION.md`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个系统为微服务提供了:
|
||||||
|
|
||||||
|
✅ **安全的身份验证** - JWT 令牌 + HS256 签名
|
||||||
|
✅ **灵活的令牌管理** - 7天有效期,30天可刷新
|
||||||
|
✅ **高可用性** - Redis Cluster 自动故障转移
|
||||||
|
✅ **权限隔离** - RBAC 限制密钥访问
|
||||||
|
✅ **数据加密** - ETCD 加密保护敏感信息
|
||||||
|
✅ **请求保护** - Envoy CSRF 双令牌验证
|
||||||
|
|
||||||
|
现在可以部署并集成到应用中了!
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
# 完整部署验证清单
|
||||||
|
|
||||||
|
完成所有部署后使用此清单验证系统是否正确配置和运行。
|
||||||
|
|
||||||
|
## 第一部分:基础设施验证
|
||||||
|
|
||||||
|
### Secret 和 RBAC 创建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Secret 已创建
|
||||||
|
kubectl get secret -n juwan | grep jwt-secret
|
||||||
|
# 预期输出: jwt-secret Created
|
||||||
|
|
||||||
|
# 查看 Secret 详情(不显示敏感数据)
|
||||||
|
kubectl describe secret jwt-secret -n juwan
|
||||||
|
# 应该看到:
|
||||||
|
# Name: jwt-secret
|
||||||
|
# Namespace: juwan
|
||||||
|
# Type: Opaque
|
||||||
|
# Data
|
||||||
|
# ====
|
||||||
|
# secret-key: <encrypted>
|
||||||
|
|
||||||
|
# 验证 Secret 内容已正确加载
|
||||||
|
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d | wc -c
|
||||||
|
# 预期输出: 应该是 32 个字符(32 字节密钥的 Base64 解码)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ServiceAccount 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 user-rpc ServiceAccount
|
||||||
|
kubectl get sa user-rpc -n juwan
|
||||||
|
kubectl describe sa user-rpc -n juwan
|
||||||
|
# 应该显示正确的 Secrets 挂载
|
||||||
|
|
||||||
|
# 检查 envoy-gateway ServiceAccount
|
||||||
|
kubectl get sa envoy-gateway -n juwan
|
||||||
|
kubectl describe sa envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### RBAC 权限验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Role 定义
|
||||||
|
kubectl get role -n juwan -l app=jwt-secret-reader
|
||||||
|
kubectl describe role jwt-secret-reader -n juwan
|
||||||
|
# 应该显示:
|
||||||
|
# PolicyRule:
|
||||||
|
# Resources Non-Resource URLs Resource Names Verbs
|
||||||
|
# --------- ----------------- -------------- -----
|
||||||
|
# secrets [] [jwt-secret] [get]
|
||||||
|
|
||||||
|
# 检查 RoleBindings
|
||||||
|
kubectl get rolebinding -n juwan | grep jwt-secret-reader
|
||||||
|
# 应该显示两个绑定: jwt-secret-reader-user-rpc 和 jwt-secret-reader-envoy-gateway
|
||||||
|
|
||||||
|
# 验证每个 RoleBinding
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二部分:权限测试
|
||||||
|
|
||||||
|
### 权限允许测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试 user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: yes
|
||||||
|
|
||||||
|
# 测试 envoy-gateway 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:envoy-gateway \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: yes
|
||||||
|
|
||||||
|
# 测试 user-rpc 无法读其他 Secrets
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: no
|
||||||
|
|
||||||
|
# 测试其他 ServiceAccount 无法读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:default \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: no
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第三部分:Deployment 配置验证
|
||||||
|
|
||||||
|
### user-rpc Deployment 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccountName 是否正确设置
|
||||||
|
kubectl get deployment user-rpc -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
|
||||||
|
# 预期输出: user-rpc
|
||||||
|
|
||||||
|
# 检查是否包含所有必需的环境变量
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 20 "env:"
|
||||||
|
# 应该包括:
|
||||||
|
# - name: JWT_SECRET_KEY
|
||||||
|
# valueFrom:
|
||||||
|
# secretKeyRef:
|
||||||
|
# name: jwt-secret
|
||||||
|
# key: secret-key
|
||||||
|
|
||||||
|
# 检查 Pod 是否正在运行
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
# 应该显示至少 3 个 Running 的 Pod
|
||||||
|
|
||||||
|
# 验证 Pod 已加载 Secret(在 Pod 中执行)
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep -i jwt
|
||||||
|
# 应该输出环境变量,例如:
|
||||||
|
# JWT_SECRET_KEY=your-secret-jwt-key-change-this-in-production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Envoy Gateway Deployment 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccountName 是否正确设置
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
|
||||||
|
# 预期输出: envoy-gateway
|
||||||
|
|
||||||
|
# 检查 Pod 是否正在运行
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
# 应该显示 Running 的 Pod
|
||||||
|
|
||||||
|
# 检查 Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway
|
||||||
|
# 应该看到启动日志,没有权限相关错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第四部分:Redis 连接验证
|
||||||
|
|
||||||
|
### Redis Cluster 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 RedisCluster CRD 状态
|
||||||
|
kubectl get rediscluster -n juwan
|
||||||
|
# 应该显示 user-redis,Status 应该是 Healthy
|
||||||
|
|
||||||
|
# 详细查看 RedisCluster 状态
|
||||||
|
kubectl describe rediscluster user-redis -n juwan
|
||||||
|
# 应该显示:
|
||||||
|
# Status:
|
||||||
|
# Cluster Status: Healthy
|
||||||
|
# Nodes Ready: 3/3
|
||||||
|
# Master: 1
|
||||||
|
# Replicas: 2
|
||||||
|
|
||||||
|
# 检查 Redis Pods
|
||||||
|
kubectl get pods -n juwan | grep redis
|
||||||
|
# 应该显示 3 个 Redis Pod,都在 Running 状态
|
||||||
|
|
||||||
|
# 测试 Redis 连接
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan -c CLUSTER INFO
|
||||||
|
# 应该看到集群信息,cluster_state:ok 表示集群健康
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第五部分:应用启动日志检查
|
||||||
|
|
||||||
|
### user-rpc 启动日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 user-rpc Pods 的启动日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc --all-containers=true
|
||||||
|
|
||||||
|
# 应该包含类似以下消息:
|
||||||
|
# - "Starting gRPC server on 0.0.0.0:9001"
|
||||||
|
# - "Redis Cluster connected successfully" 或 JWT Manager 初始化成功
|
||||||
|
# - "Listening on metrics port 4001"
|
||||||
|
|
||||||
|
# 如果有错误,查看详细日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Envoy 启动日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Envoy 启动日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 应该包含:
|
||||||
|
# - "[info] Configuration: /etc/envoy/envoy.yaml"
|
||||||
|
# - "[info] listener listening on 0.0.0.0:8080"
|
||||||
|
# - 没有权限相关错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第六部分:网络和服务发现验证
|
||||||
|
|
||||||
|
### Service 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 user-rpc-svc
|
||||||
|
kubectl get svc user-rpc-svc -n juwan
|
||||||
|
# 应该显示 ClusterIP 和两个端口 (9001/rpc 和 4001/metrics)
|
||||||
|
|
||||||
|
# 检查 Envoy Gateway Service
|
||||||
|
kubectl get svc envoy-gateway -n juwan
|
||||||
|
# 应该显示 ClusterIP 和端口 80
|
||||||
|
|
||||||
|
# 检查 Redis Service
|
||||||
|
kubectl get svc -n juwan | grep redis
|
||||||
|
# 应该显示 user-redis(ClusterIP)服务
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS 解析验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试服务名称解析
|
||||||
|
kubectl run -it --rm debug --image=busybox --restart=Never -- \
|
||||||
|
nslookup user-rpc-svc.juwan.svc.cluster.local
|
||||||
|
# 应该返回 ClusterIP 地址
|
||||||
|
|
||||||
|
kubectl run -it --rm debug --image=busybox --restart=Never -- \
|
||||||
|
nslookup user-redis.juwan.svc.cluster.local
|
||||||
|
# 应该返回 ClusterIP 地址
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第七部分:监控和指标验证
|
||||||
|
|
||||||
|
### Prometheus 指标收集
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Prometheus 是否在收集指标
|
||||||
|
kubectl port-forward -n monitoring svc/prometheus 9090:9090 &
|
||||||
|
|
||||||
|
# 打开浏览器访问 http://localhost:9090
|
||||||
|
# 查看 Status > Targets
|
||||||
|
# 应该看到 user-rpc-svc:4001 目标显示为 UP
|
||||||
|
|
||||||
|
# 查询一个指标
|
||||||
|
curl 'http://localhost:9090/api/v1/query?query=up{job="kubernetes-pods"}'
|
||||||
|
# 应该返回 user-rpc 的指标数据
|
||||||
|
|
||||||
|
# 关闭端口转发
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试源代码级指标端点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 user-rpc Pod 直接访问指标端点
|
||||||
|
kubectl port-forward -n juwan svc/user-rpc-svc 4001:4001 &
|
||||||
|
|
||||||
|
# 测试指标端点
|
||||||
|
curl http://localhost:4001/metrics
|
||||||
|
|
||||||
|
# 应该看到 Prometheus 格式的指标,例如:
|
||||||
|
# # HELP go_goroutines Number of goroutines that currently exist.
|
||||||
|
# # TYPE go_goroutines gauge
|
||||||
|
# go_goroutines 25
|
||||||
|
|
||||||
|
# 关闭端口转发
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第八部分:日志聚合验证(Loki)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Loki 是否正确接收日志
|
||||||
|
kubectl port-forward -n monitoring svc/loki 3100:3100 &
|
||||||
|
|
||||||
|
# 查询日志
|
||||||
|
curl 'http://localhost:3100/loki/api/v1/query_range?query={job="kubernetes-pods"}&start=0&end=9999999999'
|
||||||
|
|
||||||
|
# 应该返回最近的日志条目
|
||||||
|
|
||||||
|
# 检查特定应用的日志
|
||||||
|
curl 'http://localhost:3100/loki/api/v1/query_range?query={app="user-rpc"}&start=0&end=9999999999'
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第九部分:ETCD 加密验证
|
||||||
|
|
||||||
|
如果已启用 ETCD 加密,执行以下验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 control plane 节点
|
||||||
|
ssh <control-plane-node>
|
||||||
|
|
||||||
|
# 检查 ETCD 配置
|
||||||
|
sudo cat /etc/kubernetes/encryption-config.yaml | head -20
|
||||||
|
|
||||||
|
# 验证 kube-apiserver 正在使用加密配置
|
||||||
|
sudo ps aux | grep kube-apiserver | grep encryption-provider
|
||||||
|
|
||||||
|
# 创建新 Secret 进行测试
|
||||||
|
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
|
||||||
|
|
||||||
|
# 检查 ETCD 中的数据是否加密
|
||||||
|
# 注意:如果加密正确,数据应该不可读
|
||||||
|
sudo ETCDCTL_API=3 etcdctl \
|
||||||
|
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption
|
||||||
|
|
||||||
|
# 输出应该是二进制数据,不可读(表示已加密)
|
||||||
|
# 或者使用十六进制 dump
|
||||||
|
sudo ETCDCTL_API=3 etcdctl \
|
||||||
|
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption | od -A x -t x1z -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十部分:功能测试
|
||||||
|
|
||||||
|
### JWT 令牌生成和验证测试
|
||||||
|
|
||||||
|
如果已实现 JWT handlers,测试完整流程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 前向 user-api 服务
|
||||||
|
kubectl port-forward -n juwan svc/user-api-svc 8888:8888 &
|
||||||
|
|
||||||
|
# 2. 调用登录端点获取令牌
|
||||||
|
TOKEN=$(curl -X POST http://localhost:8888/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}' \
|
||||||
|
| jq -r '.token')
|
||||||
|
|
||||||
|
echo "Token: $TOKEN"
|
||||||
|
|
||||||
|
# 3. 使用令牌访问受保护的端点
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" http://localhost:8888/api/v1/users/me
|
||||||
|
|
||||||
|
# 4. 测试令牌刷新
|
||||||
|
curl -X POST http://localhost:8888/api/v1/auth/refresh \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"token\":\"$TOKEN\"}"
|
||||||
|
|
||||||
|
# 5. 测试无效令牌
|
||||||
|
curl -H "Authorization: Bearer invalid-token" http://localhost:8888/api/v1/users/me
|
||||||
|
# 应该返回 401 Unauthorized
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSRF 保护测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 前向 Envoy Gateway
|
||||||
|
kubectl port-forward -n juwan svc/envoy-gateway 8080:80 &
|
||||||
|
|
||||||
|
# 2. 获取 CSRF 令牌(安全方法)
|
||||||
|
curl -i http://localhost:8080/
|
||||||
|
|
||||||
|
# 查看响应头中的 Set-Cookie,应该包含 csrf_token
|
||||||
|
|
||||||
|
# 3. 提取 CSRF 令牌
|
||||||
|
CSRF_TOKEN=$(curl -i http://localhost:8080/ 2>/dev/null | grep -i csrf_token | sed 's/.*csrf_token=//;s/;.*//')
|
||||||
|
|
||||||
|
# 4. 使用 CSRF 令牌进行 POST 请求
|
||||||
|
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: csrf_token=$CSRF_TOKEN" \
|
||||||
|
-H "X-CSRF-Token: $CSRF_TOKEN" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}'
|
||||||
|
|
||||||
|
# 5. 测试无效 CSRF 令牌(应该返回 403)
|
||||||
|
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||||
|
-H "Cookie: csrf_token=valid_token" \
|
||||||
|
-H "X-CSRF-Token: invalid_token" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}'
|
||||||
|
# 应该返回 403 Forbidden
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十一部分:故障排查
|
||||||
|
|
||||||
|
如果任何验证失败,运行以下诊断:
|
||||||
|
|
||||||
|
### Pod 无法启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 显示 Pod 事件
|
||||||
|
kubectl describe pod <pod-name> -n juwan
|
||||||
|
|
||||||
|
# 查看完整日志(包括初始化容器)
|
||||||
|
kubectl logs <pod-name> -n juwan --all-containers=true --previous
|
||||||
|
|
||||||
|
# 检查 Pod 资源限制是否导致 OOMKilled
|
||||||
|
kubectl get event -n juwan --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限被拒绝错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证 ServiceAccount 是否正确
|
||||||
|
kubectl get pod <pod-name> -n juwan -o jsonpath='{.spec.serviceAccountName}'
|
||||||
|
|
||||||
|
# 检查 RBAC 绑定
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
|
||||||
|
# 手动测试权限
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
-n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 连接错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Redis Pods 状态
|
||||||
|
kubectl get pods -n juwan -l redis=user-redis
|
||||||
|
|
||||||
|
# 查看 Redis 日志
|
||||||
|
kubectl logs -n juwan -l redis=user-redis
|
||||||
|
|
||||||
|
# 测试 Redis 连接(从 user-rpc Pod)
|
||||||
|
kubectl exec -it <user-rpc-pod> -n juwan -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 PING
|
||||||
|
# 应该返回 PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证加密配置
|
||||||
|
kubectl get secret jwt-secret -n juwan -o json | jq '.data'
|
||||||
|
|
||||||
|
# 如果 ETCD 加密启用,直接读取 ETCD 的数据应该是二进制的
|
||||||
|
# 如果看到明文,说明加密未启用或配置不正确
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十二部分:清理测试资源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除测试 Secrets
|
||||||
|
kubectl delete secret test-encryption test-secret -n juwan --ignore-not-found
|
||||||
|
|
||||||
|
# 清理前转发的端口
|
||||||
|
lsof -i :9090 :3100 :8888 :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速检查脚本
|
||||||
|
|
||||||
|
创建 `verify-jwt-setup.sh` 进行自动化验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
namespace="juwan"
|
||||||
|
echo "=== JWT Setup Verification ==="
|
||||||
|
|
||||||
|
# 检查 Secret
|
||||||
|
echo -n "✓ JWT Secret存在: "
|
||||||
|
kubectl get secret jwt-secret -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts
|
||||||
|
echo -n "✓ user-rpc ServiceAccount: "
|
||||||
|
kubectl get sa user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway ServiceAccount: "
|
||||||
|
kubectl get sa envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 RBAC
|
||||||
|
echo -n "✓ JWT RBAC Role: "
|
||||||
|
kubectl get role jwt-secret-reader -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 Deployments
|
||||||
|
echo -n "✓ user-rpc Deployment: "
|
||||||
|
kubectl get deployment user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway Deployment: "
|
||||||
|
kubectl get deployment envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 Pods
|
||||||
|
echo -n "✓ user-rpc Pods运行中: "
|
||||||
|
[ $(kubectl get pods -n $namespace -l app=user-rpc --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ] && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway 运行中: "
|
||||||
|
kubectl get pods -n $namespace -l app=envoy-gateway --field-selector=status.phase=Running &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo "=== Verification Complete ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
运行脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x verify-jwt-setup.sh
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
所有检查项都通过后,JWT + ETCD 加密系统已准备就绪。下一步可以:
|
||||||
|
|
||||||
|
1. 集成 JWT 验证到 RPC handlers
|
||||||
|
2. 实现令牌刷新端点
|
||||||
|
3. 部署应用代码时启用 JWT 认证
|
||||||
|
4. 监控令牌生成和验证指标
|
||||||
|
5. 定期轮换加密密钥和 JWT 秘钥
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret
|
||||||
|
namespace: juwan
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
# base64 encoded: your-secret-jwt-key-change-this-in-production
|
||||||
|
secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ==
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
rules:
|
||||||
|
# JWT Secret 读取权限
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["jwt-secret"]
|
||||||
|
verbs: ["get"]
|
||||||
|
# 服务发现权限 (go-zero 框架需要)
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["endpoints"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["discovery.k8s.io"]
|
||||||
|
resources: ["endpointslices"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: user-rpc-jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: jwt-secret-reader
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway-jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: jwt-secret-reader
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
@@ -16,7 +16,8 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: user-rpc
|
app: user-rpc
|
||||||
spec:
|
spec:
|
||||||
serviceAccountName: find-endpoints
|
# serviceAccountName: find-endpoints
|
||||||
|
serviceAccountName: user-rpc
|
||||||
initContainers: # 等待数据库就绪的 Init Container 不影响资源使用但是影响调度策略(也可以忽略不计)
|
initContainers: # 等待数据库就绪的 Init Container 不影响资源使用但是影响调度策略(也可以忽略不计)
|
||||||
- name: wait-for-db
|
- name: wait-for-db
|
||||||
image: busybox:1.36
|
image: busybox:1.36
|
||||||
@@ -45,6 +46,11 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: user-redis
|
name: user-redis
|
||||||
key: password
|
key: password
|
||||||
|
- name: JWT_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret-key
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: 9001
|
port: 9001
|
||||||
|
|||||||
+36
-1
@@ -100,11 +100,43 @@ message GetUserByUsernameResp {
|
|||||||
Users users = 1; //users
|
Users users = 1; //users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LoginReq {
|
||||||
|
string username = 1;
|
||||||
|
string passwd = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResp {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ValidateTokenReq {
|
||||||
|
string token = 1; // JWT token
|
||||||
|
string userId = 2; // 用户ID
|
||||||
|
}
|
||||||
|
|
||||||
|
message ValidateTokenResp {
|
||||||
|
bool valid = 1; // token 是否有效(不在黑名单中)
|
||||||
|
string message = 2; // 验证失败原因
|
||||||
|
string userId = 3; // 用户ID
|
||||||
|
int64 roleType = 4; // 用户角色
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckPermissionReq {
|
||||||
|
string userId = 1; // 用户ID
|
||||||
|
string resource = 2; // 资源 ID
|
||||||
|
string action = 3; // 操作类型: read/write/delete
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckPermissionResp {
|
||||||
|
bool allowed = 1; // 是否有权限
|
||||||
|
string message = 2; // 拒绝原因
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Rpc Func
|
// Rpc Func
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
service usercenter{
|
service usercenter {
|
||||||
|
|
||||||
//-----------------------users-----------------------
|
//-----------------------users-----------------------
|
||||||
rpc AddUsers(AddUsersReq) returns (AddUsersResp);
|
rpc AddUsers(AddUsersReq) returns (AddUsersResp);
|
||||||
@@ -113,4 +145,7 @@ service usercenter{
|
|||||||
rpc GetUsersById(GetUsersByIdReq) returns (GetUsersByIdResp);
|
rpc GetUsersById(GetUsersByIdReq) returns (GetUsersByIdResp);
|
||||||
rpc GetUserByUsername(GetUserByUsernameReq) returns (GetUserByUsernameResp);
|
rpc GetUserByUsername(GetUserByUsernameReq) returns (GetUserByUsernameResp);
|
||||||
rpc SearchUsers(SearchUsersReq) returns (SearchUsersResp);
|
rpc SearchUsers(SearchUsersReq) returns (SearchUsersResp);
|
||||||
|
rpc Login(LoginReq) returns (LoginResp);
|
||||||
|
rpc ValidateToken(ValidateTokenReq) returns (ValidateTokenResp);
|
||||||
|
rpc CheckPermission(CheckPermissionReq) returns (CheckPermissionResp);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,424 @@
|
|||||||
|
# JWT Secret + ETCD Encryption Deployment Guide
|
||||||
|
|
||||||
|
完整的 JWT 认证系统部署指南,包括密钥管理、RBAC 权限控制和 ETCD 加密。
|
||||||
|
|
||||||
|
## 部署顺序
|
||||||
|
|
||||||
|
### 第1步:创建 Secret 和 RBAC(必需)
|
||||||
|
|
||||||
|
创建 JWT 秘钥和服务账户权限:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证创建成功:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Secret
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts
|
||||||
|
kubectl get sa user-rpc -n juwan
|
||||||
|
kubectl get sa envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 检查 RBAC 权限
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
kubectl get rolebinding -n juwan -l app=jwt-secret-reader
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第2步:更新 user-rpc 部署(依赖第1步)
|
||||||
|
|
||||||
|
已自动更新 `deploy/k8s/service/user/user-rpc.yaml`:
|
||||||
|
|
||||||
|
- ✅ 更新 `serviceAccountName` 从 `find-endpoints` → `user-rpc`
|
||||||
|
- ✅ 添加环境变量 `JWT_SECRET_KEY` 从 Secret `jwt-secret` 读取
|
||||||
|
|
||||||
|
应用更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccount 已正确绑定
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 5 serviceAccountName
|
||||||
|
|
||||||
|
# 查看 Pod 是否以 user-rpc ServiceAccount 身份运行
|
||||||
|
kubectl get pod -n juwan -l app=user-rpc -o yaml | grep serviceAccount
|
||||||
|
|
||||||
|
# 验证环境变量已注入
|
||||||
|
kubectl exec -it POD_NAME -n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第3步:更新 Envoy 网关部署(依赖第1步)
|
||||||
|
|
||||||
|
已自动更新 `deploy/k8s/envoy/envoy.yaml`:
|
||||||
|
|
||||||
|
- ✅ 添加 `serviceAccountName: envoy-gateway` 到 Deployment spec
|
||||||
|
|
||||||
|
应用更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
验证部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccount 已正确绑定
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
|
||||||
|
# 检查 Pod 状态
|
||||||
|
kubectl get pod -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第4步:启用 ETCD 加密(强烈推荐用于生产环境)
|
||||||
|
|
||||||
|
这是一个集群级别的配置,需要在 Kubernetes 控制平面节点上执行。
|
||||||
|
|
||||||
|
**前提条件:**
|
||||||
|
- 具有 Kubernetes 集群管理员权限
|
||||||
|
- 可以访问控制平面节点
|
||||||
|
- 备份 ETCD 数据库
|
||||||
|
|
||||||
|
**步骤:**
|
||||||
|
|
||||||
|
1. **生成加密密钥**
|
||||||
|
```bash
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
```
|
||||||
|
记录输出的 Base64 密钥。
|
||||||
|
|
||||||
|
2. **创建加密配置文件**
|
||||||
|
|
||||||
|
在控制平面节点上,创建 `/etc/kubernetes/encryption-config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: <BASE64_ENCODED_32_BYTE_KEY>
|
||||||
|
- identity: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
替换 `<BASE64_ENCODED_32_BYTE_KEY>` 为第1步生成的密钥。
|
||||||
|
|
||||||
|
3. **修改 kube-apiserver 配置**
|
||||||
|
|
||||||
|
在控制平面节点上,编辑 `/etc/kubernetes/manifests/kube-apiserver.yaml`:
|
||||||
|
|
||||||
|
**添加参数:**
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
command:
|
||||||
|
- kube-apiserver
|
||||||
|
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**添加卷挂载:**
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
volumeMounts:
|
||||||
|
- name: encryption-config
|
||||||
|
mountPath: /etc/kubernetes
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: encryption-config
|
||||||
|
hostPath:
|
||||||
|
path: /etc/kubernetes
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **重启 kube-apiserver**
|
||||||
|
|
||||||
|
修改清单后,kubelet 会自动重启 kube-apiserver。检查状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在控制平面节点
|
||||||
|
kubectl get pods -n kube-system | grep kube-apiserver
|
||||||
|
|
||||||
|
# 监控重启过程
|
||||||
|
kubectl logs -n kube-system -l component=kube-apiserver -f
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **验证加密是否启用**
|
||||||
|
|
||||||
|
创建一个新 Secret 并检查它在 ETCD 中是否加密:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建测试 Secret
|
||||||
|
kubectl create secret generic test-secret -n juwan --from-literal=key=value
|
||||||
|
|
||||||
|
# 从 control plane 节点检查 ETCD 数据
|
||||||
|
# 如果数据不可读并包含加密标记,说明加密已启用
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **保存加密密钥**
|
||||||
|
|
||||||
|
⚠️ **关键:将加密密钥安全地保存在离线存储中**
|
||||||
|
- 密钥丢失后,无法解密 ETCD 中的数据
|
||||||
|
- 无法恢复任何 Secrets
|
||||||
|
- 建议用密码管理工具(如 HashiCorp Vault)或 HSM 存储密钥
|
||||||
|
|
||||||
|
### 第5步:验证整个系统
|
||||||
|
|
||||||
|
完整的验证清单:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查所有 Secrets 已创建
|
||||||
|
kubectl get secret -n juwan
|
||||||
|
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts 已创建
|
||||||
|
kubectl get sa -n juwan
|
||||||
|
kubectl describe sa user-rpc -n juwan
|
||||||
|
kubectl describe sa envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 检查 RBAC 权限
|
||||||
|
kubectl get role -n juwan
|
||||||
|
kubectl get rolebinding -n juwan
|
||||||
|
kubectl describe role jwt-secret-reader -n juwan
|
||||||
|
|
||||||
|
# 测试权限:user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:user-rpc --resource-name=jwt-secret -n juwan
|
||||||
|
|
||||||
|
# 测试权限:envoy-gateway 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:envoy-gateway --resource-name=jwt-secret -n juwan
|
||||||
|
|
||||||
|
# 测试权限:其他 ServiceAccount 无法读取
|
||||||
|
kubectl auth can-i get secrets --as=system:serviceaccount:juwan:other-service -n juwan
|
||||||
|
|
||||||
|
# 检查 Deployments 已正确配置
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName
|
||||||
|
|
||||||
|
# 检查 Pods 是否已启动并运行
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 查看 JWT Secret 是否已挂载到 Pod
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和日志
|
||||||
|
|
||||||
|
### 查看 Pod 日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
|
||||||
|
|
||||||
|
# Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查 Pod 事件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Pod 创建和启动事件
|
||||||
|
kubectl describe pod -n juwan -l app=user-rpc
|
||||||
|
kubectl describe pod -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限问题排查
|
||||||
|
|
||||||
|
如果 Pod 无法读取 Secret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Pod 使用的 ServiceAccount
|
||||||
|
kubectl get pod POD_NAME -n juwan -o yaml | grep serviceAccountName
|
||||||
|
|
||||||
|
# 检查 RBAC 绑定
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
|
||||||
|
# 检查 Role 权限定义
|
||||||
|
kubectl get role jwt-secret-reader -n juwan -o yaml
|
||||||
|
|
||||||
|
# 尝试用 Pod 的身份读取 Secret(需要 kubectl-user-impersonate 或类似工具)
|
||||||
|
kubectl get secret jwt-secret --as=system:serviceaccount:juwan:user-rpc -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全最佳实践
|
||||||
|
|
||||||
|
### 1. 密钥轮换
|
||||||
|
|
||||||
|
周期性更换 JWT 秘钥(建议每季度):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 生成新密钥
|
||||||
|
NEW_KEY=$(head -c 32 /dev/urandom | base64)
|
||||||
|
|
||||||
|
# 2. 更新 Secret
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=secret-key=$NEW_KEY \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# 3. 重启 Pods 以加载新密钥(滚动更新)
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 4. 验证新 Pods 已启动并运行
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 5. 已颁发的旧令牌将变为无效
|
||||||
|
# 需要用户重新登录获取新令牌
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 审计和监控
|
||||||
|
|
||||||
|
在生产环境中启用 Kubernetes 审计日志来跟踪 Secret 访问:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# kube-apiserver 审计策略示例
|
||||||
|
apiVersion: audit.k8s.io/v1
|
||||||
|
kind: Policy
|
||||||
|
rules:
|
||||||
|
# 记录 secret 资源的访问
|
||||||
|
- level: RequestResponse
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
resources: ["secrets"]
|
||||||
|
# 记录所有认证失败
|
||||||
|
- level: RequestResponse
|
||||||
|
omitStages:
|
||||||
|
- RequestReceived
|
||||||
|
userGroups: ["system:unauthenticated"]
|
||||||
|
- level: Metadata
|
||||||
|
omitStages:
|
||||||
|
- RequestReceived
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 网络策略
|
||||||
|
|
||||||
|
使用 NetworkPolicy 限制 Pods 之间的通信:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret-access
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: jwt-secret-reader
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: user-rpc
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: envoy-gateway
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 443 # API Server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 灾难恢复
|
||||||
|
|
||||||
|
### 备份 Secret 和 RBAC 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 JWT Secret
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml > jwt-secret-backup.yaml
|
||||||
|
|
||||||
|
# 备份 RBAC 配置
|
||||||
|
kubectl get role jwt-secret-reader -n juwan -o yaml > jwt-role-backup.yaml
|
||||||
|
kubectl get rolebinding -n juwan -l app=jwt-secret-reader -o yaml > jwt-rolebinding-backup.yaml
|
||||||
|
|
||||||
|
# 加密备份文件
|
||||||
|
gpg --symmetric jwt-secret-backup.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 恢复步骤
|
||||||
|
|
||||||
|
如果 Secret 被意外删除:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从备份恢复
|
||||||
|
kubectl apply -f jwt-secret-backup.yaml
|
||||||
|
|
||||||
|
# 重启 Pods 以重新加载 Secret
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: Pod 无法启动,显示 "failed to pull secret"
|
||||||
|
|
||||||
|
A: 检查:
|
||||||
|
1. Secret 是否存在:`kubectl get secret jwt-secret -n juwan`
|
||||||
|
2. ServiceAccount 是否绑定了 RBAC:`kubectl describe rolebinding -n juwan`
|
||||||
|
3. Secret 名称和命名空间是否正确
|
||||||
|
|
||||||
|
### Q: 加密后如何验证 ETCD 中的数据已加密?
|
||||||
|
|
||||||
|
A: 从 control plane 节点:
|
||||||
|
```bash
|
||||||
|
# 直接读取 ETCD(如果配置了加密,数据应该不可读)
|
||||||
|
sudo strings /var/lib/etcd/member/snap/db | grep -i secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 能否更改加密密钥而不重新创建 ETCD?
|
||||||
|
|
||||||
|
A: 可以,但流程复杂:
|
||||||
|
1. 更新 encryption-config.yaml 中的新密钥
|
||||||
|
2. 将新密钥添加到提供程序列表(保持旧密钥)
|
||||||
|
3. 重启 kube-apiserver
|
||||||
|
4. 触发重新加密:`kubectl get all --all-namespaces -o json | kubectl apply -f -`
|
||||||
|
|
||||||
|
### Q: 如何在 Minikube 中启用 ETCD 加密?
|
||||||
|
|
||||||
|
A: 参考 `ENCRYPTION.md` 中的 Minikube 特定说明部分。
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `jwt-secret.yaml` - Secret 和 RBAC 配置
|
||||||
|
- `ENCRYPTION.md` - ETCD 加密详细文档
|
||||||
|
- `README.md` - 快速参考指南
|
||||||
|
- `/deploy/k8s/service/user/user-rpc.yaml` - user-rpc Deployment 配置
|
||||||
|
- `/deploy/k8s/envoy/envoy.yaml` - Envoy 网关 Deployment 配置
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
部署完成后:
|
||||||
|
|
||||||
|
1. **集成 JWT 验证到 RPC Handlers**
|
||||||
|
- 实现 gRPC unary interceptor
|
||||||
|
- 验证令牌有效性
|
||||||
|
- 处理令牌刷新逻辑
|
||||||
|
|
||||||
|
2. **集成 JWT 验证到 Envoy**
|
||||||
|
- 扩展 Lua filter 进行令牌验证
|
||||||
|
- 返回 401(无效令牌)或 200(有效令牌)
|
||||||
|
|
||||||
|
3. **端到端测试**
|
||||||
|
- 创建用户和登录
|
||||||
|
- 生成和验证 JWT
|
||||||
|
- 测试令牌刷新和撤销
|
||||||
|
- 验证 ETCD 加密
|
||||||
|
|
||||||
|
4. **生产部署**
|
||||||
|
- 启用审计日志
|
||||||
|
- 配置密钥轮换计划
|
||||||
|
- 建立备份和恢复流程
|
||||||
|
- 监控 Secret 访问
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# ETCD Encryption Configuration for Kubernetes
|
||||||
|
|
||||||
|
To enable static encryption at rest for Kubernetes secrets in ETCD, you need to configure the API Server with an EncryptionConfiguration.
|
||||||
|
|
||||||
|
## 1. Generate an Encryption Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a 32-byte base64-encoded key
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
# Example output: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Create EncryptionConfiguration File
|
||||||
|
|
||||||
|
Create `/etc/kubernetes/encryption-config.yaml` on the control plane node:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4=
|
||||||
|
- identity: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Update kube-apiserver Static Pod Manifest
|
||||||
|
|
||||||
|
Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` on the control plane node:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: kube-apiserver
|
||||||
|
command:
|
||||||
|
- kube-apiserver
|
||||||
|
# ... existing flags ...
|
||||||
|
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
volumeMounts:
|
||||||
|
- name: encryption-config
|
||||||
|
mountPath: /etc/kubernetes
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: encryption-config
|
||||||
|
hostPath:
|
||||||
|
path: /etc/kubernetes
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Verify Encryption is Working
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After restarting the API server, create a secret and verify it's encrypted
|
||||||
|
kubectl create secret generic test-secret --from-literal=key=value -n juwan
|
||||||
|
|
||||||
|
# Check if the secret is encrypted in etcd
|
||||||
|
kubectl get secret test-secret -o yaml
|
||||||
|
|
||||||
|
# You can also check raw etcd data (requires etcd access):
|
||||||
|
# etcdctl --endpoints=https://127.0.0.1:2379 get /kubernetes.io/secrets/juwan/test-secret
|
||||||
|
# The data should be encrypted (not human-readable)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Important Notes
|
||||||
|
|
||||||
|
- **Backup your encryption key** in a secure location
|
||||||
|
- **Never commit encryption keys** to version control
|
||||||
|
- If you lose the key, all encrypted secrets will be unrecoverable
|
||||||
|
- After enabling encryption, existing unencrypted secrets will not be automatically encrypted
|
||||||
|
- To encrypt existing secrets, you can use: `kubectl delete secret <name> && kubectl create secret ...`
|
||||||
|
- Or use: `kubectl patch secret <name> -p '{}' --type=merge` (triggers re-encryption)
|
||||||
|
|
||||||
|
## 6. RBAC Configuration for JWT Secret
|
||||||
|
|
||||||
|
The `jwt-secret.yaml` includes RBAC rules that:
|
||||||
|
- Create a `jwt-secret` Secret in the `juwan` namespace
|
||||||
|
- Create ServiceAccounts for `user-rpc` and `envoy-gateway`
|
||||||
|
- Create a Role `jwt-secret-reader` that allows reading only the `jwt-secret` Secret
|
||||||
|
- Bind this Role to both ServiceAccounts via RoleBindings
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- Only `user-rpc` and `envoy-gateway` Pods can read the JWT secret
|
||||||
|
- Other services and users cannot access the JWT secret
|
||||||
|
- Least privilege access principle is enforced
|
||||||
|
|
||||||
|
## 7. Update Deployment to Use ServiceAccount
|
||||||
|
|
||||||
|
Make sure your Deployment references the ServiceAccount:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: user-rpc # This is important!
|
||||||
|
containers:
|
||||||
|
- name: user-rpc
|
||||||
|
env:
|
||||||
|
- name: JWT_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. For Minikube Users
|
||||||
|
|
||||||
|
If using Minikube, you can enable encryption with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minikube config set apiserver.encryption-provider-config /path/to/encryption-config.yaml
|
||||||
|
minikube start
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually edit the kube-apiserver manifest after starting Minikube:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
minikube ssh
|
||||||
|
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
# Add the flags and volume mounts as shown above
|
||||||
|
```
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
# 部署流程图和时间线
|
||||||
|
|
||||||
|
## 部署架构流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ JWT 认证系统部署流程 │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 1: 前置检查 (5分钟) │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ✓ Kubernetes 集群版本 >= 1.24 │
|
||||||
|
│ ✓ kubectl 已配置,可访问集群 │
|
||||||
|
│ ✓ juwan namespace 已存在 │
|
||||||
|
│ ✓ redis-operator CRD 已安装 │
|
||||||
|
│ ✓ 集群管理员权限(用于 ETCD 加密) │
|
||||||
|
│ │
|
||||||
|
│ 命令检查: │
|
||||||
|
│ $ kubectl cluster-info │
|
||||||
|
│ $ kubectl get ns juwan │
|
||||||
|
│ $ kubectl get crd redisclusters.redis.redis.opstreelabs.in │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 2: 创建 Secret 和 RBAC (5分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml │
|
||||||
|
│ │
|
||||||
|
│ 创建的资源: │
|
||||||
|
│ ✓ Secret: jwt-secret (包含 JWT 秘钥) │
|
||||||
|
│ ✓ ServiceAccount: user-rpc │
|
||||||
|
│ ✓ ServiceAccount: envoy-gateway │
|
||||||
|
│ ✓ Role: jwt-secret-reader (只读权限) │
|
||||||
|
│ ✓ RoleBinding: jwt-secret-reader-user-rpc │
|
||||||
|
│ ✓ RoleBinding: jwt-secret-reader-envoy-gateway │
|
||||||
|
│ │
|
||||||
|
│ 验证: │
|
||||||
|
│ $ kubectl get secret jwt-secret -n juwan │
|
||||||
|
│ $ kubectl get sa -n juwan | grep -E "user-rpc|envoy" │
|
||||||
|
│ $ kubectl get role -n juwan │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 3: 更新 Deployments (10分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Step 3a: 更新 user-rpc Deployment │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/service/user/user-rpc.yaml │
|
||||||
|
│ │
|
||||||
|
│ 变更: │
|
||||||
|
│ - serviceAccountName: user-rpc (绑定权限) │
|
||||||
|
│ - env.JWT_SECRET_KEY (从 Secret 挂载) │
|
||||||
|
│ - 保持 Redis Cluster 配置 │
|
||||||
|
│ │
|
||||||
|
│ 等待 Pods 启动: │
|
||||||
|
│ $ kubectl rollout status deployment/user-rpc -n juwan │
|
||||||
|
│ │
|
||||||
|
│ --- │
|
||||||
|
│ │
|
||||||
|
│ Step 3b: 更新 Envoy Gateway Deployment │
|
||||||
|
│ 执行: │
|
||||||
|
│ $ kubectl apply -f deploy/k8s/envoy/envoy.yaml │
|
||||||
|
│ │
|
||||||
|
│ 变更: │
|
||||||
|
│ - serviceAccountName: envoy-gateway (绑定权限) │
|
||||||
|
│ - 保持 CSRF Lua 防护配置 │
|
||||||
|
│ │
|
||||||
|
│ 等待 Pods 启动: │
|
||||||
|
│ $ kubectl rollout status deployment/envoy-gateway -n juwan │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase 4: 验证部署 (15分钟) ⚡ 必需 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 检查清单: │
|
||||||
|
│ │
|
||||||
|
│ 1️⃣ Secret 和权限验证 │
|
||||||
|
│ $ kubectl get secret jwt-secret -n juwan │
|
||||||
|
│ $ kubectl get role jwt-secret-reader -n juwan │
|
||||||
|
│ $ kubectl get rolebinding -n juwan | grep jwt-secret │
|
||||||
|
│ │
|
||||||
|
│ 2️⃣ 权限测试 │
|
||||||
|
│ $ kubectl auth can-i get secrets \ │
|
||||||
|
│ --as=system:serviceaccount:juwan:user-rpc \ │
|
||||||
|
│ --resource-name=jwt-secret -n juwan │
|
||||||
|
│ 预期: yes │
|
||||||
|
│ │
|
||||||
|
│ 3️⃣ Pods 运行状态 │
|
||||||
|
│ $ kubectl get pods -n juwan -l app=user-rpc │
|
||||||
|
│ $ kubectl get pods -n juwan -l app=envoy-gateway │
|
||||||
|
│ 预期: 3 个 user-rpc Pods + 1 个 envoy-gateway Pod 都在 Running │
|
||||||
|
│ │
|
||||||
|
│ 4️⃣ 环境变量验证 │
|
||||||
|
│ $ kubectl exec -it <user-rpc-pod> -n juwan -- env | grep JWT │
|
||||||
|
│ 预期: JWT_SECRET_KEY=... │
|
||||||
|
│ │
|
||||||
|
│ 5️⃣ Redis 连接验证 │
|
||||||
|
│ $ kubectl run redis-cli --image=redis:latest --rm -it \ │
|
||||||
|
│ -- redis-cli -h user-redis.juwan:6379 PING │
|
||||||
|
│ 预期: PONG │
|
||||||
|
│ │
|
||||||
|
│ 详见: VERIFICATION.md 第1-8部分 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
├─────────── 生产环境额外步骤 ──────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────────────┐ ┌─────────────────────────────────────┐
|
||||||
|
│ Phase 5a: 应用集成 │ │ Phase 5b: 启用 ETCD 加密 (30分钟) │
|
||||||
|
│ (2-3 小时) ⚠️ 推荐 │ │ ⚠️ 生产推荐,需集群管理员权限 │
|
||||||
|
├──────────────────────────┤ ├─────────────────────────────────────┤
|
||||||
|
│ │ │ │
|
||||||
|
│ 实施内容: │ │ 前提条件: │
|
||||||
|
│ ✓ gRPC Interceptor │ │ ✓ Control Plane 节点访问权限 │
|
||||||
|
│ ✓ Login/Logout Handler │ │ ✓ ETCD 备份已创建 │
|
||||||
|
│ ✓ JWT Middleware │ │ ✓ 加密密钥已生成 │
|
||||||
|
│ ✓ Token Refresh Logic │ │ │
|
||||||
|
│ ✓ Error Handling │ │ 步骤: │
|
||||||
|
│ ✓ Unit Tests │ │ 1. 生成 32 字节密钥 │
|
||||||
|
│ │ │ $ head -c 32 /dev/urandom | base64
|
||||||
|
│ 参考: │ │ │
|
||||||
|
│ INTEGRATION.md │ │ 2. 创建加密配置文件 │
|
||||||
|
│ │ │ /etc/kubernetes/encryption-config.yaml
|
||||||
|
│ 时间估计: │ │ │
|
||||||
|
│ - gRPC 拦截器: 30分钟 │ │ 3. 修改 kube-apiserver manifest │
|
||||||
|
│ - Handlers: 60分钟 │ │ 添加密钥路径和卷挂载 │
|
||||||
|
│ - Middleware: 30分钟 │ │ │
|
||||||
|
│ - 测试: 60分钟 │ │ 4. 重启 kube-apiserver │
|
||||||
|
│ │ │ kubelet 自动重启 │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ 5. 验证加密已启用 │
|
||||||
|
│ │ │ kubectl create secret generic ... │
|
||||||
|
│ │ │ 检查 ETCD 中的数据是否加密 │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ 详见: ENCRYPTION.md (8个部分) │
|
||||||
|
│ │ │ 验证: VERIFICATION.md 第9部分 │
|
||||||
|
│ │ │ │
|
||||||
|
└──────────────────────────┘ └─────────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
└───────────────────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Phase 6: 完成 ✅ │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 最终检查: │
|
||||||
|
│ ✓ 所有 Pods 运行正常 │
|
||||||
|
│ ✓ RBAC 权限已验证 │
|
||||||
|
│ ✓ JWT 功能已集成 │
|
||||||
|
│ ✓ 日志和监控已配置 │
|
||||||
|
│ ✓ (可选) ETCD 加密已启用 │
|
||||||
|
│ │
|
||||||
|
│ 生产推荐: │
|
||||||
|
│ ✓ 启用审计日志 │
|
||||||
|
│ ✓ 配置密钥轮换计划(季度) │
|
||||||
|
│ ✓ 备份密钥到安全位置 │
|
||||||
|
│ ✓ 配置告警和监控 │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 时间估计和路径
|
||||||
|
|
||||||
|
```
|
||||||
|
推荐部署路径
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
🚀 最快路径 (30 分钟) - 开发环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1: 前置检查 ⏱️ 5 分钟
|
||||||
|
Phase 2: 创建 Secret 和 RBAC ⏱️ 5 分钟
|
||||||
|
Phase 3: 更新 Deployments ⏱️ 10 分钟
|
||||||
|
Phase 4: 验证部署 ⏱️ 10 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 30 分钟
|
||||||
|
|
||||||
|
|
||||||
|
📊 默认路径 (75 分钟) - 测试环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1-4: 如上 ⏱️ 30 分钟
|
||||||
|
Phase 5a: 应用集成(简单版) ⏱️ 45 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 75 分钟
|
||||||
|
|
||||||
|
|
||||||
|
🏆 完整路径 (3.5-4 小时) - 生产环境
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
Phase 1-4: 如上 ⏱️ 30 分钟
|
||||||
|
Phase 5a: 应用集成(完整版) ⏱️ 2-3 小时
|
||||||
|
Phase 5b: ETCD 加密配置 ⏱️ 30 分钟
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 3.5-4 小时
|
||||||
|
|
||||||
|
|
||||||
|
📚 学习路径 (6-8 小时) - 从零开始理解
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
文档阅读 (SUMMARY + DEPLOYMENT + INTEGRATION) ⏱️ 1-2 小时
|
||||||
|
完整路径部署 ⏱️ 3.5-4 小时
|
||||||
|
验证和测试 ⏱️ 1-2 小时
|
||||||
|
────────────────────────────────────────────────────────────────
|
||||||
|
总计: 6-8 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
## 并行和串行步骤
|
||||||
|
|
||||||
|
```
|
||||||
|
可以并行执行的任务
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 应用集成 (Phase 5a) │ ────┐
|
||||||
|
├─────────────────────────────────┤ │ 可选,独立
|
||||||
|
│ • gRPC interceptor │ │ 进行
|
||||||
|
│ • REST middleware │ │
|
||||||
|
│ • Handler 实现 │ │
|
||||||
|
│ • 单元测试 │ │
|
||||||
|
└─────────────────────────────────┘ │
|
||||||
|
├─ 与 Phase 2-4 并行
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────┐ │
|
||||||
|
│ ETCD 加密 (Phase 5b) │ ────┘
|
||||||
|
├─────────────────────────────────┤ │ 需要集群管理员
|
||||||
|
│ • 生成密钥(可单独进行) │ │ 权限,在
|
||||||
|
│ • 创建配置文件 │ │ Control Plane
|
||||||
|
│ • 修改 kube-apiserver │ │ 节点执行
|
||||||
|
│ • 重启 API server │ │
|
||||||
|
└─────────────────────────────────┘────┘
|
||||||
|
|
||||||
|
|
||||||
|
必须串行执行的步骤
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Phase 1 → Phase 2 → Phase 3 → Phase 4 → (Phase 5a + Phase 5b)
|
||||||
|
|
||||||
|
↓ ↓ ↓ ↓
|
||||||
|
前置检查 创建资源 部署应用 验证完整 可选扩展功能
|
||||||
|
|
||||||
|
• Phase 2 必须在 Phase 1 之后(需要 namespace)
|
||||||
|
• Phase 3 必须在 Phase 2 之后(需要 RoleBinding)
|
||||||
|
• Phase 4 必须在 Phase 3 之后(需要 Pods 启动)
|
||||||
|
• Phase 5a/5b 可在 Phase 4 完成后并行进行
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键时间点
|
||||||
|
|
||||||
|
```
|
||||||
|
事件时间线
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
T+0 Phase 1: 验证前置条件
|
||||||
|
└─ 预计 5 分钟
|
||||||
|
|
||||||
|
T+5 Phase 2: kubectl apply jwt-secret.yaml
|
||||||
|
└─ 预计 1 分钟执行,5 分钟验证
|
||||||
|
|
||||||
|
T+11 Phase 3a: kubectl apply user-rpc.yaml
|
||||||
|
└─ 3 个 Pods 启动(滚动更新)
|
||||||
|
└─ 预计 ~3 分钟(取决于镜像拉取)
|
||||||
|
|
||||||
|
T+14 Phase 3b: kubectl apply envoy.yaml
|
||||||
|
└─ 1 个 Pod 启动
|
||||||
|
└─ 预计 ~2 分钟
|
||||||
|
|
||||||
|
T+16 Phase 4: 执行完整验证检查
|
||||||
|
└─ 12 个验证部分,共 ~15 分钟
|
||||||
|
|
||||||
|
T+31 ✅ 基础部署完成
|
||||||
|
|
||||||
|
T+31 (可选) Phase 5a: 应用代码集成
|
||||||
|
到 └─ 2-3 小时编码和测试
|
||||||
|
T+211
|
||||||
|
|
||||||
|
T+31 (可选) Phase 5b: ETCD 加密
|
||||||
|
到 └─ 30 分钟配置
|
||||||
|
T+61
|
||||||
|
|
||||||
|
T+211 或 T+61 🎉 全部完成
|
||||||
|
(取决于是否执行 Phase 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 推荐的部署顺序
|
||||||
|
|
||||||
|
### 对于 DevOps/SRE
|
||||||
|
|
||||||
|
```
|
||||||
|
优先级顺序:
|
||||||
|
|
||||||
|
1️⃣ Phase 1-4 (核心部署) [必需]
|
||||||
|
└─ 时间: 30 分钟
|
||||||
|
|
||||||
|
2️⃣ Phase 5b (ETCD 加密) [生产强烈推荐]
|
||||||
|
└─ 时间: 30 分钟
|
||||||
|
└─ 开始时间: T+16 之前 (与 Phase 4 并行)
|
||||||
|
|
||||||
|
3️⃣ 密钥备份和恢复计划 [重要]
|
||||||
|
└─ 时间: 15 分钟
|
||||||
|
└─ 参考: DEPLOYMENT.md 灾难恢复
|
||||||
|
|
||||||
|
4️⃣ Phase 5a 支持 [当开发完成时]
|
||||||
|
└─ 协助开发团队集成 JWT
|
||||||
|
└─ 参考: INTEGRATION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 对于应用开发者
|
||||||
|
|
||||||
|
```
|
||||||
|
优先级顺序:
|
||||||
|
|
||||||
|
1️⃣ 了解系统架构 [了解背景]
|
||||||
|
└─ 文档: SUMMARY.md
|
||||||
|
└─ 时间: 10 分钟
|
||||||
|
|
||||||
|
2️⃣ Phase 5a: 代码集成 [并行进行]
|
||||||
|
└─ 参考: INTEGRATION.md
|
||||||
|
└─ 时间: 2-3 小时
|
||||||
|
|
||||||
|
3️⃣ 单元测试 [在开发中]
|
||||||
|
└─ 参考: INTEGRATION.md 第 9 部分
|
||||||
|
└─ 时间: 1 小时
|
||||||
|
|
||||||
|
4️⃣ 集成测试 [与运维协调]
|
||||||
|
└─ 测试完整流程
|
||||||
|
└─ 时间: 1-2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚应急步骤
|
||||||
|
|
||||||
|
如果部署失败,可以快速回滚:
|
||||||
|
|
||||||
|
```
|
||||||
|
紧急回滚
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
如果 Phase 2 (Secret) 失败:
|
||||||
|
→ kubectl delete secret jwt-secret -n juwan
|
||||||
|
→ kubectl delete sa user-rpc envoy-gateway -n juwan
|
||||||
|
→ kubectl delete role jwt-secret-reader -n juwan
|
||||||
|
→ 修正配置后重新应用
|
||||||
|
|
||||||
|
如果 Phase 3 (Deployment) 失败:
|
||||||
|
→ kubectl rollout undo deployment/user-rpc -n juwan
|
||||||
|
→ kubectl rollout undo deployment/envoy-gateway -n juwan
|
||||||
|
→ 或删除部署并使用稳定的旧版本重新部署
|
||||||
|
|
||||||
|
如果 Phase 5b (ETCD 加密) 失败:
|
||||||
|
→ 从 kube-apiserver 清单中移除加密参数
|
||||||
|
→ 重启 kube-apiserver
|
||||||
|
→ 删除 /etc/kubernetes/encryption-config.yaml
|
||||||
|
→ 从最近的 ETCD 备份恢复(如果需要)
|
||||||
|
|
||||||
|
注意: 备份是关键!每次重大操作前都应备份。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署检查点 (Go/No-Go)
|
||||||
|
|
||||||
|
```
|
||||||
|
关键检查点
|
||||||
|
═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
✅ Checkpoint 1 (Phase 2 后)
|
||||||
|
- Secret 已创建
|
||||||
|
- ServiceAccounts 已创建
|
||||||
|
- Go → 继续 Phase 3
|
||||||
|
- No-Go → 检查 kubectl 权限
|
||||||
|
|
||||||
|
✅ Checkpoint 2 (Phase 3 后)
|
||||||
|
- Pods 已启动 (Running)
|
||||||
|
- No PodSchedulingFailure
|
||||||
|
- Go → 继续 Phase 4
|
||||||
|
- No-Go → 检查资源限制和镜像拉取
|
||||||
|
|
||||||
|
✅ Checkpoint 3 (Phase 4 权限测试)
|
||||||
|
- user-rpc 可以读 jwt-secret
|
||||||
|
- envoy-gateway 可以读 jwt-secret
|
||||||
|
- 其他 SA 无法读取
|
||||||
|
- Go → 继续 Phase 5
|
||||||
|
- No-Go → 检查 RBAC 配置
|
||||||
|
|
||||||
|
✅ Checkpoint 4 (Phase 4 Redis 连接)
|
||||||
|
- Redis Cluster 健康 (3/3 nodes)
|
||||||
|
- PING 返回 PONG
|
||||||
|
- Go → 继续应用集成
|
||||||
|
- No-Go → 检查 Redis Pods 和网络
|
||||||
|
|
||||||
|
✅ Checkpoint 5 (Phase 5b - ETCD 加密)
|
||||||
|
- 新创建的 Secret 在 ETCD 中已加密
|
||||||
|
- Go → 生产就绪
|
||||||
|
- No-Go → 检查 kube-apiserver 配置参数
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用这个流程图
|
||||||
|
|
||||||
|
1. **首次部署** → 从 "推荐部署路径" 选择合适的版本
|
||||||
|
2. **卡在某一步** → 查看对应的 Phase 描述和命令
|
||||||
|
3. **估算时间** → 查看 "时间估计和路径" 部分
|
||||||
|
4. **需要回滚** → 参考 "回滚应急步骤"
|
||||||
|
5. **检查进度** → 使用 "部署检查点"
|
||||||
|
|
||||||
|
详细的部署步骤见:[DEPLOYMENT.md](./DEPLOYMENT.md)
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
# JWT + ETCD 加密系统文档索引
|
||||||
|
|
||||||
|
## 📚 文档完整导航
|
||||||
|
|
||||||
|
### 快速入门 (5-15分钟)
|
||||||
|
|
||||||
|
**推荐路径:** 从上到下顺序阅读
|
||||||
|
|
||||||
|
1. **[SUMMARY.md](./SUMMARY.md)** ⭐ 从这里开始
|
||||||
|
- 📋 项目概览和架构图
|
||||||
|
- 🎯 核心特性一览
|
||||||
|
- ✅ 生产就绪检查清单
|
||||||
|
- 🚀 下一步行动计划
|
||||||
|
|
||||||
|
2. **[README.md](./README.md)**
|
||||||
|
- 🏃 4个快速部署步骤
|
||||||
|
- 🔐 安全考虑事项
|
||||||
|
- 🔄 密钥轮换程序
|
||||||
|
- 🆘 故障排查
|
||||||
|
|
||||||
|
3. **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)**
|
||||||
|
- ⚡ 一页速查表
|
||||||
|
- 📝 常见命令复制粘贴
|
||||||
|
- 🗺️ 文档地图
|
||||||
|
- 🎓 关键参数速知
|
||||||
|
|
||||||
|
### 部署实施 (30-60分钟)
|
||||||
|
|
||||||
|
4. **[DEPLOYMENT.md](./DEPLOYMENT.md)** - 最详细的部署指南
|
||||||
|
- 📦 第1步:创建 Secret 和 RBAC(必需)
|
||||||
|
- 🔄 第2步:更新 user-rpc Deployment
|
||||||
|
- 🌐 第3步:更新 Envoy Gateway Deployment
|
||||||
|
- 🔐 第4步:启用 ETCD 加密(生产推荐)
|
||||||
|
- ✔️ 第5步:验证整个系统
|
||||||
|
- 📊 监控和日志配置
|
||||||
|
- 🛠️ 安全最佳实践
|
||||||
|
- 🆘 故障排查指南
|
||||||
|
- 💾 灾难恢复流程
|
||||||
|
|
||||||
|
### 验证和监控 (20-30分钟)
|
||||||
|
|
||||||
|
5. **[VERIFICATION.md](./VERIFICATION.md)** - 完整验证清单
|
||||||
|
|
||||||
|
**12个验证部分:**
|
||||||
|
|
||||||
|
| 部分 | 用途 | 时间 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 第1部分 | Secret/RBAC 基础验证 | 2分钟 |
|
||||||
|
| 第2部分 | 权限测试(allow/deny) | 3分钟 |
|
||||||
|
| 第3部分 | Deployment 配置检查 | 2分钟 |
|
||||||
|
| 第4部分 | Redis 连接测试 | 2分钟 |
|
||||||
|
| 第5部分 | 应用启动日志 | 3分钟 |
|
||||||
|
| 第6部分 | 网络和服务发现 | 3分钟 |
|
||||||
|
| 第7部分 | Prometheus 指标 | 3分钟 |
|
||||||
|
| 第8部分 | Loki 日志聚合 | 2分钟 |
|
||||||
|
| 第9部分 | ETCD 加密验证 | 5分钟 |
|
||||||
|
| 第10部分 | JWT 功能测试 | 10分钟 |
|
||||||
|
| 第11部分 | 故障排查诊断 | 5分钟 |
|
||||||
|
| 第12部分 | 清理和总结 | 2分钟 |
|
||||||
|
|
||||||
|
### 高级话题
|
||||||
|
|
||||||
|
6. **[ENCRYPTION.md](./ENCRYPTION.md)** - ETCD 加密完整指南
|
||||||
|
- 🔑 第1部分:密钥生成
|
||||||
|
- 📋 第2部分:配置格式
|
||||||
|
- 🔧 第3部分:kube-apiserver 修改
|
||||||
|
- ✅第4部分:验证加密
|
||||||
|
- ⚠️ 第5部分:关键警告(数据不可恢复)
|
||||||
|
- 🔐 第6部分:RBAC 解释
|
||||||
|
- 📦 第7部分:Deployment 示例
|
||||||
|
- 🍎 第8部分:Minikube 特定说明
|
||||||
|
|
||||||
|
7. **[INTEGRATION.md](../api/INTEGRATION.md)** - 代码集成指南
|
||||||
|
- 🔗 第1部分:gRPC Unary Interceptor
|
||||||
|
- 🔗 第2部分:gRPC Stream Interceptor
|
||||||
|
- 👤 第3部分:登录 Handler 实现
|
||||||
|
- 🔐 第4部分:受保护 Handler 中的声明提取
|
||||||
|
- 🔄 第5部分:令牌刷新端点
|
||||||
|
- 🚪 第6部分:登出处理
|
||||||
|
- 🛣️ 第7部分:REST Routes 配置
|
||||||
|
- 🔎 第8部分:错误处理最佳实践
|
||||||
|
- 🧪 第9部分:单元测试示例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 文件结构详解
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/k8s/
|
||||||
|
├── secrets/
|
||||||
|
│ ├── jwt-secret.yaml ✅ Kubernetes 清单文件
|
||||||
|
│ │ ├── Secret: jwt-secret (JWT 秘钥数据)
|
||||||
|
│ │ ├── ServiceAccount: user-rpc
|
||||||
|
│ │ ├── ServiceAccount: envoy-gateway
|
||||||
|
│ │ ├── Role: jwt-secret-reader
|
||||||
|
│ │ ├── RoleBinding: jwt-secret-reader-user-rpc
|
||||||
|
│ │ └── RoleBinding: jwt-secret-reader-envoy-gateway
|
||||||
|
│ │
|
||||||
|
│ ├── README.md 📖 快速参考指南(5分钟入门)
|
||||||
|
│ ├── SUMMARY.md 📊 系统概览(10分钟了解全貌)
|
||||||
|
│ ├── QUICK_REFERENCE.md ⚡ 速查表(查找命令和参数)
|
||||||
|
│ ├── DEPLOYMENT.md 📦 详细部署指南(60分钟完整部署)
|
||||||
|
│ ├── ENCRYPTION.md 🔐 ETCD 加密指南(Control Plane 配置)
|
||||||
|
│ ├── VERIFICATION.md ✅ 验证清单(部署后验证)
|
||||||
|
│ └── INDEX.md 🗺️ 本文件(文档导航)
|
||||||
|
│
|
||||||
|
└── envoy/
|
||||||
|
│ └── envoy.yaml ✅ Envoy 网关配置
|
||||||
|
│ └── 已更新: serviceAccountName: envoy-gateway
|
||||||
|
│
|
||||||
|
service/user/
|
||||||
|
├── user-api.yaml ✅ user-api Service
|
||||||
|
├── user-rpc.yaml ✅ user-rpc Deployment(已更新)
|
||||||
|
│ ├── serviceAccountName: user-rpc (已更新)
|
||||||
|
│ ├── JWT_SECRET_KEY env var (已更新)
|
||||||
|
│ └── Redis Cluster configuration
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
app/users/
|
||||||
|
├── api/
|
||||||
|
│ └── INTEGRATION.md 📝 REST/gRPC 集成指南
|
||||||
|
│
|
||||||
|
└── rpc/
|
||||||
|
├── internal/utils/jwt.go ✅ JwtManager 实现(已存在)
|
||||||
|
├── internal/config/config.go ✅ JWT 配置(已存在)
|
||||||
|
├── internal/svc/
|
||||||
|
│ └── serviceContext.go ✅ 依赖注入(已存在)
|
||||||
|
└── etc/pb.yaml ✅ 运行时配置(已存在)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 按场景查找文档
|
||||||
|
|
||||||
|
### 场景 1:我想快速了解这个系统是什么
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [SUMMARY.md](./SUMMARY.md) - 项目概览(5分钟)
|
||||||
|
2. [SUMMARY.md](./SUMMARY.md) 中的架构图和特性说明
|
||||||
|
|
||||||
|
**关键信息:**
|
||||||
|
- JWT 令牌系统 + Redis 存储 + RBAC 权限 + ETCD 加密
|
||||||
|
- 支持 7 天有效期、30 天可刷新
|
||||||
|
- Envoy 网关 CSRF 防护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 2:我想立即部署到 Kubernetes
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [README.md](./README.md) - 快速参考(2分钟)
|
||||||
|
2. [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - 复制粘贴命令(3分钟)
|
||||||
|
3. 运行部署命令(5分钟)
|
||||||
|
4. [VERIFICATION.md](./VERIFICATION.md) 第1-7部分 - 验证(10分钟)
|
||||||
|
|
||||||
|
**快速命令:**
|
||||||
|
```bash
|
||||||
|
# Copy from QUICK_REFERENCE.md "部署命令" 部分
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 3:部署后验证一切正常
|
||||||
|
|
||||||
|
**推荐阅读:**
|
||||||
|
- [VERIFICATION.md](./VERIFICATION.md) - 12部分完整验证清单
|
||||||
|
- 逐部分执行验证命令
|
||||||
|
|
||||||
|
**预计时间:** 30-40分钟
|
||||||
|
|
||||||
|
**验证触发点:**
|
||||||
|
- ✅ Secrets 和 RBAC 已创建
|
||||||
|
- ✅ Pods 已启动运行
|
||||||
|
- ✅ 权限验证通过
|
||||||
|
- ✅ Redis 连接成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 4:启用 ETCD 加密(生产推荐)
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [ENCRYPTION.md](./ENCRYPTION.md) - 完整加密指南
|
||||||
|
2. 按照 8 个步骤逐一执行
|
||||||
|
3. [VERIFICATION.md](./VERIFICATION.md) 第9部分 - 加密验证
|
||||||
|
|
||||||
|
**需要的权限:**
|
||||||
|
- Control Plane 节点的 root/sudo 权限
|
||||||
|
- Kubernetes 集群管理员权限
|
||||||
|
|
||||||
|
**预计时间:** 15-20分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 5:集成 JWT 到我的应用代码中
|
||||||
|
|
||||||
|
**推荐阅读顺序:**
|
||||||
|
1. [INTEGRATION.md](../api/INTEGRATION.md) 第1-2部分 - gRPC 拦截器
|
||||||
|
2. 第3-4部分 - 登录和受保护 Handlers
|
||||||
|
3. 第7-8部分 - REST API 中间件
|
||||||
|
4. 第9部分 - 单元测试
|
||||||
|
|
||||||
|
**需要实现:**
|
||||||
|
- ✅ gRPC Unary/Stream Interceptors
|
||||||
|
- ✅ 登录/登出端点
|
||||||
|
- ✅ JWT Middleware for REST
|
||||||
|
- ✅ 错误处理
|
||||||
|
|
||||||
|
**预计时间:** 2-3 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 6:部署后遇到问题
|
||||||
|
|
||||||
|
**根据错误类型选择:**
|
||||||
|
|
||||||
|
| 错误类型 | 查看文档 |
|
||||||
|
|---------|--------|
|
||||||
|
| Pod 无法启动 | [VERIFICATION.md](./VERIFICATION.md) 第11部分 |
|
||||||
|
| 权限被拒绝 | [VERIFICATION.md](./VERIFICATION.md) 第2部分 + [README.md](./README.md) 故障排查 |
|
||||||
|
| Redis 连接失败 | [VERIFICATION.md](./VERIFICATION.md) 第4部分 |
|
||||||
|
| ETCD 加密失败 | [ENCRYPTION.md](./ENCRYPTION.md) 第5-6部分 |
|
||||||
|
| 配置不清楚 | [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 配置文件位置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景 7:定期维护任务
|
||||||
|
|
||||||
|
#### 任务:轮换 JWT 秘钥
|
||||||
|
|
||||||
|
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 安全最佳实践 > 密钥轮换
|
||||||
|
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 密钥轮换步骤
|
||||||
|
|
||||||
|
**频率:** 季度(3个月)
|
||||||
|
|
||||||
|
#### 任务:轮换 ETCD 加密密钥
|
||||||
|
|
||||||
|
**阅读:** [ENCRYPTION.md](./ENCRYPTION.md) 第5部分
|
||||||
|
**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) ETCD 加密系统
|
||||||
|
|
||||||
|
**频率:** 年度(12个月)
|
||||||
|
|
||||||
|
#### 任务:备份密钥
|
||||||
|
|
||||||
|
**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 灾难恢复
|
||||||
|
**或:** [ENCRYPTION.md](./ENCRYPTION.md) 关键警告
|
||||||
|
|
||||||
|
**频率:** 立即 + 每次轮换后
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 文档深度对比
|
||||||
|
|
||||||
|
| 文档 | 深度 | 丰富度 | 代码 | 适合角色 |
|
||||||
|
|-----|------|--------|------|---------|
|
||||||
|
| README | 浅 | 概览 | - | PM/初学者 |
|
||||||
|
| SUMMARY | 浅 | 概览 | - | 决策者 |
|
||||||
|
| QUICK_REFERENCE | 中 | 速查 | 命令 | DevOps/SRE |
|
||||||
|
| DEPLOYMENT | 深 | 详细 | 示例 | DevOps/运维 |
|
||||||
|
| VERIFICATION | 深 | 详细 | 脚本 | QA/DevOps |
|
||||||
|
| ENCRYPTION | 非常深 | 极详细 | YAML | 安全/运维 |
|
||||||
|
| INTEGRATION | 非常深 | 代码级 | 完整 | 开发者 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 学习路径建议
|
||||||
|
|
||||||
|
### 对于 DevOps/SRE
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. DEPLOYMENT.md (30 分钟)
|
||||||
|
3. VERIFICATION.md (30 分钟)
|
||||||
|
4. ENCRYPTION.md (20 分钟)
|
||||||
|
5. 实践部署 (60 分钟)
|
||||||
|
|
||||||
|
**总计:** ~3 小时
|
||||||
|
|
||||||
|
### 对于应用开发者
|
||||||
|
|
||||||
|
1. SUMMARY.md > "集成点" 部分 (5 分钟)
|
||||||
|
2. INTEGRATION.md (60 分钟)
|
||||||
|
3. QUICK_REFERENCE.md > "JWT Manager API" (10 分钟)
|
||||||
|
4. 代码实现 (2-3 小时)
|
||||||
|
5. 单元测试 (INTEGRATION.md 第9部分)
|
||||||
|
|
||||||
|
**总计:** ~4 小时
|
||||||
|
|
||||||
|
### 对于安全/合规人员
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. ENCRYPTION.md (30 分钟)
|
||||||
|
3. DEPLOYMENT.md > 安全最佳实践 (15 分钟)
|
||||||
|
4. VERIFICATION.md 第9部分 (10 分钟)
|
||||||
|
|
||||||
|
**总计:** ~1 小时
|
||||||
|
|
||||||
|
### 对于项目经理
|
||||||
|
|
||||||
|
1. SUMMARY.md (5 分钟)
|
||||||
|
2. DEPLOYMENT.md > "部署状态示意图" (5 分钟)
|
||||||
|
3. DEPLOYMENT.md > "快速部署" (2 分钟)
|
||||||
|
|
||||||
|
**总计:** ~15 分钟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 学习成果预期
|
||||||
|
|
||||||
|
### 完成后,您将能够:
|
||||||
|
|
||||||
|
✅ 在 Kubernetes 中部署 JWT 认证系统
|
||||||
|
✅ 配置 RBAC 权限控制
|
||||||
|
✅ 启用 ETCD 加密保护敏感数据
|
||||||
|
✅ 在 Go-zero 应用中集成 JWT
|
||||||
|
✅ 实现令牌刷新和撤销
|
||||||
|
✅ 诊断和排查常见问题
|
||||||
|
✅ 执行密钥轮换和灾难恢复
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 求助指南
|
||||||
|
|
||||||
|
### 第一步:找到相关文档
|
||||||
|
- 浏览本索引找到相关章节
|
||||||
|
- 或用 Ctrl+F 搜索关键词
|
||||||
|
|
||||||
|
### 第二步:查看文档中的相关部分
|
||||||
|
- DEPLOYMENT.md 的相关章节
|
||||||
|
- 或 VERIFICATION.md 的故障排查部分
|
||||||
|
|
||||||
|
### 第三步:运行诊断命令
|
||||||
|
- QUICK_REFERENCE.md 的 "故障排查" 部分
|
||||||
|
- 或 VERIFICATION.md 的 "故障排查" 部分
|
||||||
|
|
||||||
|
### 第四步:检查日志
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第五步:查看详细文档
|
||||||
|
如果上述步骤未能解决,查看对应的详细文档:
|
||||||
|
- 配置问题 → DEPLOYMENT.md
|
||||||
|
- 权限问题 → VERIFICATION.md 第2/11部分
|
||||||
|
- 集成问题 → INTEGRATION.md
|
||||||
|
- 加密问题 → ENCRYPTION.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 文档反馈
|
||||||
|
|
||||||
|
如果您发现:
|
||||||
|
- ❌ 文档不清楚
|
||||||
|
- ❌ 命令不工作
|
||||||
|
- ❌ 信息缺失或过时
|
||||||
|
- ❌ 错别字或格式问题
|
||||||
|
|
||||||
|
请在相应的 `.md` 文件中标记,或提交更新建议。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 关键概念快速链接
|
||||||
|
|
||||||
|
| 概念 | 详见 |
|
||||||
|
|-----|------|
|
||||||
|
| JWT 令牌生命周期 | SUMMARY.md "关键特性" |
|
||||||
|
| Redis 双键结构 | SUMMARY.md "关键特性" |
|
||||||
|
| RBAC 权限隔离 | SUMMARY.md "关键特性" |
|
||||||
|
| CSRF 防护 | SUMMARY.md "关键特性" |
|
||||||
|
| ETCD 加密 | ENCRYPTION.md |
|
||||||
|
| 错误处理 | INTEGRATION.md 第8部分 |
|
||||||
|
| 密钥轮换 | DEPLOYMENT.md "安全最佳实践" |
|
||||||
|
| 灾难恢复 | DEPLOYMENT.md "灾难恢复" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 文档特性
|
||||||
|
|
||||||
|
✅ **模块化** - 每个文档独立,但相互链接
|
||||||
|
✅ **分层** - 从快速概览到深度细节
|
||||||
|
✅ **实践导向** - 包含实际命令和代码示例
|
||||||
|
✅ **完整性** - 覆盖部署、验证、维护、故障排查
|
||||||
|
✅ **易查找** - 目录、索引、速查表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**开始阅读:** 👉 [SUMMARY.md](./SUMMARY.md)
|
||||||
|
|
||||||
|
或根据您的角色选择:
|
||||||
|
|
||||||
|
| 角色 | 开始文档 | 预计时间 |
|
||||||
|
|-----|--------|--------|
|
||||||
|
| DevOps/运维 | [DEPLOYMENT.md](./DEPLOYMENT.md) | 1-2 小时 |
|
||||||
|
| 应用开发 | [INTEGRATION.md](../api/INTEGRATION.md) | 2-3 小时 |
|
||||||
|
| 安全审查 | [ENCRYPTION.md](./ENCRYPTION.md) | 30 分钟 |
|
||||||
|
| 项目经理 | [SUMMARY.md](./SUMMARY.md) | 15 分钟 |
|
||||||
|
| 新手 | [README.md](./README.md) → [SUMMARY.md](./SUMMARY.md) | 15-20 分钟 |
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# JWT + ETCD 加密系统 - 快速参考卡片
|
||||||
|
|
||||||
|
## 一页速查表
|
||||||
|
|
||||||
|
### 部署命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建 Secret 和 RBAC
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
|
||||||
|
# 更新 Deployments
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 验证部署
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
kubectl get sa user-rpc envoy-gateway -n juwan
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
--resource-name=jwt-secret -n juwan
|
||||||
|
# 预期: yes
|
||||||
|
|
||||||
|
# 其他 SA 无法读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:default \
|
||||||
|
--resource-name=jwt-secret -n juwan
|
||||||
|
# 预期: no
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日志查看
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# user-rpc 日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
|
||||||
|
# Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
|
||||||
|
# 特定 Pod 日志
|
||||||
|
kubectl logs -n juwan <pod-name> --all-containers=true -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 JWT_SECRET_KEY 已注入
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) \
|
||||||
|
-n juwan -- env | grep JWT_SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 连接到 Redis Cluster
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 -c CLUSTER INFO
|
||||||
|
|
||||||
|
# 测试键操作
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 -c GET jwt:user:test-user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 在 Control Plane 节点生成密钥
|
||||||
|
head -c 32 /dev/urandom | base64
|
||||||
|
|
||||||
|
# 2. 编辑 kube-apiserver 清单
|
||||||
|
sudo nano /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
|
||||||
|
# 添加参数:
|
||||||
|
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
|
||||||
|
|
||||||
|
# 3. 创建加密配置文件
|
||||||
|
cat <<EOF | sudo tee /etc/kubernetes/encryption-config.yaml
|
||||||
|
apiVersion: apiserver.config.k8s.io/v1
|
||||||
|
kind: EncryptionConfiguration
|
||||||
|
resources:
|
||||||
|
- resources:
|
||||||
|
- secrets
|
||||||
|
providers:
|
||||||
|
- aescbc:
|
||||||
|
keys:
|
||||||
|
- name: key1
|
||||||
|
secret: <BASE64_KEY>
|
||||||
|
- identity: {}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. 验证加密
|
||||||
|
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
|
||||||
|
sudo ETCDCTL_API=3 etcdctl --cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption | od -A x -t x1z
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pod 无法启动?查看事件
|
||||||
|
kubectl describe pod <pod-name> -n juwan
|
||||||
|
|
||||||
|
# 权限被拒绝?检查 RBAC
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
|
||||||
|
|
||||||
|
# 无法挂载 Secret?检查 Secret 存在性
|
||||||
|
kubectl get secret jwt-secret -n juwan -o yaml
|
||||||
|
|
||||||
|
# Redis 连接错误?测试连通性
|
||||||
|
kubectl exec -it <user-rpc-pod> -n juwan -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 PING
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Manager API 速查
|
||||||
|
|
||||||
|
### JwtManager 方法
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 生成新令牌
|
||||||
|
token, err := svcCtx.JwtManager.New(ctx, userID, email, name)
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
claims, err := svcCtx.JwtManager.Valid(ctx, token)
|
||||||
|
|
||||||
|
// 刷新令牌(如果过期但 Redis 仍有数据)
|
||||||
|
newToken, err := svcCtx.JwtManager.Renew(ctx, token)
|
||||||
|
|
||||||
|
// 提取声明(不验证签名)
|
||||||
|
claims, err := svcCtx.JwtManager.Extract(ctx, token)
|
||||||
|
|
||||||
|
// 检查令牌是否存在于 Redis
|
||||||
|
exists, err := svcCtx.JwtManager.Exists(ctx, token)
|
||||||
|
|
||||||
|
// 撤销令牌(登出)
|
||||||
|
err := svcCtx.JwtManager.Revoke(ctx, userID, token)
|
||||||
|
|
||||||
|
// 获取用户当前令牌
|
||||||
|
token, err := svcCtx.JwtManager.GetUserToken(ctx, userID)
|
||||||
|
|
||||||
|
// 将声明转换为载荷
|
||||||
|
payload := svcCtx.JwtManager.ClaimsToPayload(claims)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置文件位置
|
||||||
|
|
||||||
|
| 配置 | 位置 | 关键参数 |
|
||||||
|
|-----|------|--------|
|
||||||
|
| JWT Secret | `deploy/k8s/secrets/jwt-secret.yaml` | `secret-key` |
|
||||||
|
| user-rpc 配置 | `app/users/rpc/etc/pb.yaml` | `JWT.SecretKey`, `REDIS_HOST` |
|
||||||
|
| Envoy 配置 | `deploy/k8s/envoy/envoy.yaml` | CSRF 验证 Lua 代码 |
|
||||||
|
| ETCD 加密 | `/etc/kubernetes/encryption-config.yaml`(Control Plane) | `secret` (32字节密钥) |
|
||||||
|
|
||||||
|
## 关键参数速查
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# JWT 令牌有效期
|
||||||
|
Token Exp: 7 days
|
||||||
|
|
||||||
|
# Redis 存储 TTL
|
||||||
|
Redis TTL: 30 days
|
||||||
|
|
||||||
|
# 可刷新时间窗口
|
||||||
|
Refresh Window: 30 days - 7 days = 23 days
|
||||||
|
|
||||||
|
# CSRF Token 位置
|
||||||
|
Cookie: csrf_token=...
|
||||||
|
Header: X-CSRF-Token: ...
|
||||||
|
|
||||||
|
# ETCD 加密算法
|
||||||
|
Algorithm: AES-CBC
|
||||||
|
Key Size: 256 bits (32 bytes)
|
||||||
|
Encoding: Base64
|
||||||
|
|
||||||
|
# Secret 挂载方式
|
||||||
|
Method: volumeMount (read-only)
|
||||||
|
或
|
||||||
|
Method: valueFrom.secretKeyRef
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题速查
|
||||||
|
|
||||||
|
| 问题 | 排查命令 | 解决方案 |
|
||||||
|
|-----|--------|--------|
|
||||||
|
| Pod 无法启动 | `kubectl describe pod` | 检查 Secret/RBAC |
|
||||||
|
| 权限被拒绝 | `kubectl auth can-i get secrets` | 验证 RBAC 绑定 |
|
||||||
|
| Redis 连接失败 | `redis-cli PING` | 检查 Redis Pods |
|
||||||
|
| JWT 验证失败 | 查看 Pod 日志 | 检查 Redis 中的令牌 |
|
||||||
|
| CSRF 验证失败 | 查看 Envoy 日志 | 检查 Cookie/Header 匹配 |
|
||||||
|
| ETCD 加密失败 | `kubectl get secret` | 检查 kube-apiserver 启动参数 |
|
||||||
|
|
||||||
|
## 部署检查清单 (5分钟版)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 第1步: 部署 Secret (10秒)
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml && sleep 5
|
||||||
|
|
||||||
|
# 第2步: 验证 Secret (10秒)
|
||||||
|
kubectl get secret jwt-secret -n juwan && echo "✓ Secret 已创建"
|
||||||
|
|
||||||
|
# 第3步: 验证 RBAC (10秒)
|
||||||
|
kubectl get role jwt-secret-reader -n juwan && echo "✓ RBAC 已配置"
|
||||||
|
|
||||||
|
# 第4步: 更新 Deployments (20秒)
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 第5步: 等待 Pods 启动 (30秒)
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 第6步: 快速功能测试 (2分钟)
|
||||||
|
# 创建一个令牌并验证可读取
|
||||||
|
REDIS_POD=$(kubectl get pod -n juwan -l redis=user-redis -o name | head -1)
|
||||||
|
kubectl exec -it $REDIS_POD -n juwan -- redis-cli KEYS "jwt:*"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 密钥轮换步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 生成新密钥
|
||||||
|
NEW_KEY=$(head -c 32 /dev/urandom | base64)
|
||||||
|
|
||||||
|
# 2. 更新 Secret
|
||||||
|
kubectl create secret generic jwt-secret \
|
||||||
|
--from-literal=secret-key=$NEW_KEY \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
# 3. 重启 Pods(自动挂载新 Secret)
|
||||||
|
kubectl rollout restart deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 4. 等待 Pods 启动
|
||||||
|
kubectl rollout status deployment/user-rpc -n juwan
|
||||||
|
kubectl rollout status deployment/envoy-gateway -n juwan
|
||||||
|
|
||||||
|
# 5. 旧令牌现在需要刷新或重新登录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档地图
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/k8s/secrets/
|
||||||
|
├── jwt-secret.yaml ← Secrets + RBAC 配置
|
||||||
|
├── README.md ← 开始阅读(快速指南)
|
||||||
|
├── SUMMARY.md ← 本文件(系统概览)
|
||||||
|
├── DEPLOYMENT.md ← 详细部署步骤(12步)
|
||||||
|
├── ENCRYPTION.md ← ETCD 加密详细指南
|
||||||
|
├── VERIFICATION.md ← 完整验证清单(12部分)
|
||||||
|
└── QUICK_REFERENCE.md ← 本快速参考卡片
|
||||||
|
|
||||||
|
app/users/api/
|
||||||
|
└── INTEGRATION.md ← JWT 代码集成指南
|
||||||
|
|
||||||
|
app/users/rpc/
|
||||||
|
├── internal/utils/jwt.go ← JwtManager 实现
|
||||||
|
├── internal/config/config.go ← JWT 配置
|
||||||
|
├── internal/svc/serviceContext.go ← 依赖注入
|
||||||
|
└── etc/pb.yaml ← 运行时配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键时间点
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 操作 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 令牌签发 | T0 | 生成 JWT,过期时间 = T0 + 7天 |
|
||||||
|
| | | 在 Redis 存储,TTL = 30天 |
|
||||||
|
| Token 过期 | T0 + 7天 | JWT 验证失败 |
|
||||||
|
| 令牌刷新 | T0 + 7天到T0 + 30天 | 如果 Redis 仍有数据,生成新令牌 |
|
||||||
|
| 完全失效 | T0 + 30天 | Redis 删除,无法再刷新 |
|
||||||
|
| 重新登录 | T0 + 30天+ | 用户需要重新登录 |
|
||||||
|
|
||||||
|
## 性能提示
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 高并发下优化 Redis 连接
|
||||||
|
# 在 pb.yaml 中调整:
|
||||||
|
CacheConf:
|
||||||
|
- Host: "user-redis.juwan:6379"
|
||||||
|
Type: "cluster"
|
||||||
|
MaxConnections: 100
|
||||||
|
ConnectionPoolSize: 50
|
||||||
|
|
||||||
|
# 监控 JWT 验证吞吐量
|
||||||
|
# 在 Prometheus 查询:
|
||||||
|
rate(jwt_validations_total[5m])
|
||||||
|
rate(jwt_refresh_total[5m])
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全提示
|
||||||
|
|
||||||
|
✅ **必做**
|
||||||
|
- [ ] 定期轮换 JWT 秘钥(季度)
|
||||||
|
- [ ] 定期轮换 ETCD 加密密钥(年度)
|
||||||
|
- [ ] 备份加密密钥到安全位置
|
||||||
|
- [ ] 启用审计日志
|
||||||
|
- [ ] 监控异常的令牌验证失败
|
||||||
|
|
||||||
|
❌ **禁止**
|
||||||
|
- [ ] 不要在日志中输出 JWT 秘钥
|
||||||
|
- [ ] 不要在代码库中存储密钥
|
||||||
|
- [ ] 不要发送明文密钥到 Slack/Email
|
||||||
|
- [ ] 不要在多个环境间共享密钥
|
||||||
|
|
||||||
|
## 版本信息
|
||||||
|
|
||||||
|
```
|
||||||
|
Kubernetes: 1.24+
|
||||||
|
Go-zero: v1.10.0+
|
||||||
|
Redis: 7.0+
|
||||||
|
ETCD: 3.5+
|
||||||
|
Envoy: v1.32.2+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持和反馈
|
||||||
|
|
||||||
|
遇到问题?按优先级检查:
|
||||||
|
|
||||||
|
1. **运行验证脚本**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
./deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **查看日志**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **阅读 VERIFICATION.md**
|
||||||
|
- 第1-5部分: 基础配置
|
||||||
|
- 第6-8部分: 网络和监控
|
||||||
|
- 第9部分: ETCD 加密
|
||||||
|
- 第11部分: 故障排查
|
||||||
|
|
||||||
|
4. **详细指南**
|
||||||
|
- DEPLOYMENT.md - 完整步骤
|
||||||
|
- INTEGRATION.md - 代码集成
|
||||||
|
- ENCRYPTION.md - 加密配置
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# JWT Secret Management
|
||||||
|
|
||||||
|
This directory contains secure configuration for JWT secret key management.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `jwt-secret.yaml`: Kubernetes Secret + ServiceAccount + RBAC rules
|
||||||
|
- `ENCRYPTION.md`: Guide for enabling ETCD static encryption at rest
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create the Secret and RBAC
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
- Secret `jwt-secret` in namespace `juwan` containing the JWT secret key
|
||||||
|
- ServiceAccount `user-rpc` in namespace `juwan`
|
||||||
|
- ServiceAccount `envoy-gateway` in namespace `juwan`
|
||||||
|
- Role `jwt-secret-reader` that allows reading only `jwt-secret`
|
||||||
|
- RoleBindings to grant both ServiceAccounts read permission on the secret
|
||||||
|
|
||||||
|
### 2. Update user-rpc Deployment
|
||||||
|
|
||||||
|
Update `deploy/k8s/service/user/user-rpc.yaml` to:
|
||||||
|
|
||||||
|
1. Set the serviceAccountName:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: user-rpc
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add environment variable to load JWT secret:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-rpc
|
||||||
|
env:
|
||||||
|
- name: JWT_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update envoy-gateway Deployment
|
||||||
|
|
||||||
|
Update `deploy/k8s/envoy/envoy.yaml` to:
|
||||||
|
|
||||||
|
1. Set the serviceAccountName:
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: envoy-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add environment variable or mount Secret:
|
||||||
|
```yaml
|
||||||
|
volumeMounts:
|
||||||
|
- name: jwt-secret
|
||||||
|
mountPath: /etc/jwt
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: jwt-secret
|
||||||
|
secret:
|
||||||
|
secretName: jwt-secret
|
||||||
|
defaultMode: 0400
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in the Envoy config:
|
||||||
|
```yaml
|
||||||
|
data:
|
||||||
|
envoy.yaml: |
|
||||||
|
# Read JWT secret from /etc/jwt/secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Enable ETCD Encryption
|
||||||
|
|
||||||
|
Follow the guide in `ENCRYPTION.md` to enable static encryption at rest for all secrets in ETCD.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Least Privilege
|
||||||
|
|
||||||
|
- Only `user-rpc` and `envoy-gateway` can read the JWT secret
|
||||||
|
- No other services or users have access
|
||||||
|
- The Role allows reading **only** the `jwt-secret`, not other secrets
|
||||||
|
|
||||||
|
### Encryption at Rest
|
||||||
|
|
||||||
|
- With ETCD encryption enabled, the secret is encrypted when stored on disk
|
||||||
|
- Even if someone gains access to the ETCD database files, they cannot read the secret without the encryption key
|
||||||
|
|
||||||
|
### Secret Rotation
|
||||||
|
|
||||||
|
To rotate the JWT secret key:
|
||||||
|
|
||||||
|
1. Generate a new key
|
||||||
|
2. Update the Secret:
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic jwt-secret --from-literal=secret-key=NEW_KEY --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
```
|
||||||
|
3. Pod mounts/env vars will be updated automatically within a few minutes
|
||||||
|
4. Old tokens will become invalid (you may need to log users out)
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
- [ ] ETCD encryption enabled (see ENCRYPTION.md)
|
||||||
|
- [ ] JWT secret key changed from default
|
||||||
|
- [ ] Both user-rpc and envoy-gateway Deployments use correct serviceAccountName
|
||||||
|
- [ ] Both Deployments load the secret via environment variable or volume mount
|
||||||
|
- [ ] Regular secret rotation policy implemented
|
||||||
|
- [ ] Secret backup stored in secure location (encrypted)
|
||||||
|
- [ ] RBAC audit logging enabled to track secret access
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot read jwt-secret
|
||||||
|
Check if the Pod is using the correct ServiceAccount:
|
||||||
|
```bash
|
||||||
|
kubectl get deployment user-rpc -o yaml | grep serviceAccountName
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secret not being mounted
|
||||||
|
Verify the Secret exists:
|
||||||
|
```bash
|
||||||
|
kubectl get secret jwt-secret -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
Check Pod logs for mounting errors:
|
||||||
|
```bash
|
||||||
|
kubectl logs -l app=user-rpc -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied error
|
||||||
|
Verify RBAC binding:
|
||||||
|
```bash
|
||||||
|
kubectl get rolebinding -n juwan
|
||||||
|
kubectl get role jwt-secret-reader -n juwan
|
||||||
|
```
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# JWT 认证系统 + ETCD 加密 - 完整部署总结
|
||||||
|
|
||||||
|
## 项目概览
|
||||||
|
|
||||||
|
这个项目为微服务提供了一个完整的 JWT 认证系统,包括:
|
||||||
|
|
||||||
|
1. **JWT 令牌管理** - 令牌生成、验证、刷新和撤销
|
||||||
|
2. **Redis Cluster 存储** - 令牌交换缓存(30天TTL)和用户会话管理
|
||||||
|
3. **RBAC 权限控制** - 限制只有 user-rpc 和 envoy-gateway 服务可以访问 JWT 秘钥
|
||||||
|
4. **ETCD 加密** - 在 Kubernetes 集群中对所有 Secrets 进行加密
|
||||||
|
5. **网关保护** - Envoy 网关处理 CSRF 防护和请求路由
|
||||||
|
|
||||||
|
## 创建的文件清单
|
||||||
|
|
||||||
|
### 部署配置文件
|
||||||
|
|
||||||
|
#### `/deploy/k8s/secrets/`
|
||||||
|
|
||||||
|
| 文件 | 说明 | 关键内容 |
|
||||||
|
|-----|------|--------|
|
||||||
|
| `jwt-secret.yaml` | Secret + RBAC 配置 | 包含JWT秘钥、ServiceAccounts、Role、RoleBindings |
|
||||||
|
| `README.md` | 快速参考指南 | Secret 创建和 Deployment 更新说明 |
|
||||||
|
| `DEPLOYMENT.md` | 详细部署步骤 | 12个部署步骤,包括ETCD加密配置 |
|
||||||
|
| `ENCRYPTION.md` | ETCD加密完整指南 | 密钥生成、配置修改、验证流程 |
|
||||||
|
| `VERIFICATION.md` | 验证清单 | 12个部分的完整验证脚本和检查项 |
|
||||||
|
|
||||||
|
### 应用代码更新
|
||||||
|
|
||||||
|
| 文件路径 | 修改内容 |
|
||||||
|
|---------|--------|
|
||||||
|
| `/app/users/rpc/internal/utils/jwt.go` | JwtManager 实现(已存在) |
|
||||||
|
| `/app/users/rpc/internal/config/config.go` | JwtConfig 结构体(已存在) |
|
||||||
|
| `/app/users/rpc/internal/svc/serviceContext.go` | Redis Cluster + JwtManager 依赖注入(已存在) |
|
||||||
|
| `/app/users/rpc/etc/pb.yaml` | JWT 和 Redis Cluster 配置(已存在) |
|
||||||
|
| `/deploy/k8s/service/user/user-rpc.yaml` | ✅ **已更新** - 添加 serviceAccountName + JWT_SECRET_KEY 环境变量 |
|
||||||
|
| `/deploy/k8s/envoy/envoy.yaml` | ✅ **已更新** - 添加 serviceAccountName: envoy-gateway |
|
||||||
|
| `/app/users/api/INTEGRATION.md` | 🆕 **新建** - JWT 集成指南(interceptors, handlers, middleware) |
|
||||||
|
|
||||||
|
## 部署状态示意图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Kubernetes Cluster │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ juwan Namespace │ │
|
||||||
|
│ ├─────────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Secret: jwt-secret │ │ │
|
||||||
|
│ │ │ ├─ secret-key: <encrypted in ETCD> │ │ │
|
||||||
|
│ │ │ └─ Protected by RBAC Role + RoleBindings │ │ │
|
||||||
|
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ △ │ │
|
||||||
|
│ │ │ (mounted via serviceAccountName) │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │
|
||||||
|
│ │ │ user-rpc │ │ envoy-gateway │ │ │
|
||||||
|
│ │ │ Deployment │ │ Deployment │ │ │
|
||||||
|
│ │ ├─────────────────┤ ├──────────────────────┤ │ │
|
||||||
|
│ │ │ SA: user-rpc │ │ SA: envoy-gateway │ │ │
|
||||||
|
│ │ │ Replicas: 3 │ │ Replicas: 1 │ │ │
|
||||||
|
│ │ │ Port: 9001(RPC) │ │ Port: 8080(HTTP) │ │ │
|
||||||
|
│ │ │ 4001(Met) │ │ │ │ │
|
||||||
|
│ │ └─────────────────┘ └──────────────────────┘ │ │
|
||||||
|
│ │ │ JWT Manager │ CSRF Filter │ │
|
||||||
|
│ │ │ (HS256 signing) │ (X-CSRF-Token) │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ ┌──────▼──────────────────────────▼─────┐ │ │
|
||||||
|
│ │ │ user-redis (RedisCluster) │ │ │
|
||||||
|
│ │ │ 3-node cluster │ │ │
|
||||||
|
│ │ │ - Token exchange cache (30d TTL) │ │ │
|
||||||
|
│ │ │ - User session management │ │ │
|
||||||
|
│ │ └────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Control Plane (kube-apiserver) │ │
|
||||||
|
│ │ • ETCD 加密: AES-CBC 32-byte key │ │
|
||||||
|
│ │ • Secrets 自动加密存储 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心配置参数
|
||||||
|
|
||||||
|
### JWT 配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
JWT:
|
||||||
|
SecretKey: "your-secret-jwt-key-change-this-in-production"
|
||||||
|
Issuer: "your-app-name"
|
||||||
|
# Token 有效期: 7 天
|
||||||
|
# Redis TTL: 30 天(支持令牌刷新)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis Cluster
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
RedisCluster:
|
||||||
|
ClusterSize: 3 🔴 主服务器 + 2 🔵 从服务器
|
||||||
|
Address: "user-redis.juwan:6379"
|
||||||
|
HighAvailability: 自动故障转移
|
||||||
|
```
|
||||||
|
|
||||||
|
### RBAC 权限
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Role: jwt-secret-reader
|
||||||
|
Resources: [secrets]
|
||||||
|
ResourceNames: [jwt-secret]
|
||||||
|
Verbs: [get] # 只读,无列表/创建/删除
|
||||||
|
Subjects:
|
||||||
|
- user-rpc (ServiceAccount)
|
||||||
|
- envoy-gateway (ServiceAccount)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署流程
|
||||||
|
|
||||||
|
### 快速部署(5分钟)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 第1步:创建 Secret 和 RBAC
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
|
||||||
|
# 第2步:更新 Deployments
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
|
||||||
|
# 第3步:验证
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密部署(需要集群管理员权限)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在 Control Plane 节点执行
|
||||||
|
1. 生成 32 字节密钥
|
||||||
|
2. 创建 /etc/kubernetes/encryption-config.yaml
|
||||||
|
3. 修改 /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
|
4. 重启 kube-apiserver
|
||||||
|
5. 验证加密已启用
|
||||||
|
```
|
||||||
|
|
||||||
|
详见:`deploy/k8s/secrets/ENCRYPTION.md`
|
||||||
|
|
||||||
|
## 关键特性
|
||||||
|
|
||||||
|
### 1. JWT 令牌生命周期
|
||||||
|
|
||||||
|
```
|
||||||
|
登录 → 生成 JWT
|
||||||
|
├─ 有效期: 7 天(exp claim)
|
||||||
|
└─ 存储到 Redis: 30 天(TTL)
|
||||||
|
|
||||||
|
Token 过期(7天+)
|
||||||
|
├─ JWT 签名验证失败
|
||||||
|
└─ 检查 Redis 是否仍有数据
|
||||||
|
├─ 有 → 生成新 Token(刷新)✅
|
||||||
|
└─ 无 → 令牌已过期,需要重新登录 ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Redis 双键结构
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt:user:{userId} → {token}
|
||||||
|
用途: 快速查询用户当前令牌
|
||||||
|
TTL: 30 天
|
||||||
|
|
||||||
|
jwt:token:{token} → {payload}
|
||||||
|
用途: 令牌验证和刷新
|
||||||
|
TTL: 30 天
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CSRF 防护(Envoy 网关)
|
||||||
|
|
||||||
|
```
|
||||||
|
安全方法 (GET/HEAD/OPTIONS)
|
||||||
|
→ 自动生成 csrf_token
|
||||||
|
→ 返回 Set-Cookie: csrf_token=...
|
||||||
|
|
||||||
|
不安全方法 (POST/PUT/DELETE/PATCH)
|
||||||
|
→ 检查 Cookie csrf_token
|
||||||
|
→ 检查 X-CSRF-Token 头
|
||||||
|
→ 两者必须相等,否则 403
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 权限隔离
|
||||||
|
|
||||||
|
```
|
||||||
|
Only user-rpc + envoy-gateway 可以:
|
||||||
|
✅ 读 jwt-secret
|
||||||
|
|
||||||
|
Other services 无法:
|
||||||
|
❌ 列出 secrets
|
||||||
|
❌ 获取 jwt-secret(RBAC 拒绝)
|
||||||
|
❌ 删除 secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ETCD 加密
|
||||||
|
|
||||||
|
```
|
||||||
|
未加密:
|
||||||
|
etcdctl get /registry/seca/...
|
||||||
|
→ secret-key: "plaintext-value"
|
||||||
|
|
||||||
|
已加密 (AES-CBC):
|
||||||
|
etcdctl get /registry/secrets/...
|
||||||
|
→ 二进制数据,无法读取
|
||||||
|
```
|
||||||
|
|
||||||
|
## 集成点
|
||||||
|
|
||||||
|
### 1. RPC Handler(需要实现)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在 gRPC server 中注册拦截器
|
||||||
|
s := grpc.NewServer(
|
||||||
|
grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 拦截器会:
|
||||||
|
// 1. 提取 Authorization 头中的 Token
|
||||||
|
// 2. 调用 JwtManager.Valid()
|
||||||
|
// 3. 如果过期,尝试 JwtManager.Renew()
|
||||||
|
// 4. 将声明注入 context
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第1-2章
|
||||||
|
|
||||||
|
### 2. REST Endpoint(需要实现)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建 JWT Middleware
|
||||||
|
protected := middleware.JwtMiddleware(svcCtx)
|
||||||
|
|
||||||
|
// 应用到受保护的路由
|
||||||
|
router.HandleFunc("GET /api/v1/users/me",
|
||||||
|
protected(user.GetUserInfoHandler(svcCtx)))
|
||||||
|
|
||||||
|
// Middleware 会验证 Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第3-4章
|
||||||
|
|
||||||
|
### 3. 登录/登出流程(需要实现)
|
||||||
|
|
||||||
|
```
|
||||||
|
登录:
|
||||||
|
1. 验证用户凭证(DB 查询)
|
||||||
|
2. JwtManager.New() → 生成令牌
|
||||||
|
3. 返回令牌给客户端
|
||||||
|
|
||||||
|
登出:
|
||||||
|
1. 从上下文提取 userId
|
||||||
|
2. JwtManager.Revoke() → 删除 Redis 中的令牌
|
||||||
|
3. 用户需要重新登录获取新令牌
|
||||||
|
```
|
||||||
|
|
||||||
|
参考:`app/users/api/INTEGRATION.md` 第5-6章
|
||||||
|
|
||||||
|
## 文档导航
|
||||||
|
|
||||||
|
| 场景 | 推荐阅读 |
|
||||||
|
|-----|--------|
|
||||||
|
| 第一次部署 | `README.md` → `DEPLOYMENT.md` |
|
||||||
|
| 部署遇到问题 | `VERIFICATION.md` + `DEPLOYMENT.md` 故障排查部分 |
|
||||||
|
| 代码集成 | `app/users/api/INTEGRATION.md` |
|
||||||
|
| ETCD 加密配置 | `ENCRYPTION.md` |
|
||||||
|
| ETCD 加密验证 | `VERIFICATION.md` 第9部分 |
|
||||||
|
| 安全最佳实践 | `DEPLOYMENT.md` 安全最佳实践部分 |
|
||||||
|
| 灾难恢复 | `DEPLOYMENT.md` 灾难恢复部分 |
|
||||||
|
|
||||||
|
## 生产就绪检查清单
|
||||||
|
|
||||||
|
- [ ] 所有 Pods 都在 Running 状态
|
||||||
|
- [ ] JWT Secret 已创建并正确挂载
|
||||||
|
- [ ] RBAC 权限验证通过
|
||||||
|
- [ ] Redis Cluster 健康(3/3 节点)
|
||||||
|
- [ ] ETCD 加密已启用(如需要)
|
||||||
|
- [ ] 监控和日志聚合正常工作
|
||||||
|
- [ ] 密钥轮换计划已制定
|
||||||
|
- [ ] 备份和恢复流程已文档化
|
||||||
|
- [ ] 安全审计日志已启用
|
||||||
|
- [ ] 端到端测试已通过
|
||||||
|
|
||||||
|
## 下一步行动
|
||||||
|
|
||||||
|
### 短期(本周)
|
||||||
|
|
||||||
|
1. **部署 Secret 和 RBAC**
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **更新 Deployments**
|
||||||
|
```bash
|
||||||
|
kubectl apply -f deploy/k8s/service/user/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证部署**
|
||||||
|
```bash
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 中期(本月)
|
||||||
|
|
||||||
|
1. **实现 JWT 集成**
|
||||||
|
- 创建 gRPC 拦截器
|
||||||
|
- 实现登录/登出端点
|
||||||
|
- 添加 JWT 中间件到 REST API
|
||||||
|
|
||||||
|
2. **端到端测试**
|
||||||
|
- 测试令牌生成和验证
|
||||||
|
- 测试令牌刷新
|
||||||
|
- 测试 CSRF 防护
|
||||||
|
|
||||||
|
### 长期(本季度)
|
||||||
|
|
||||||
|
1. **启用 ETCD 加密**
|
||||||
|
- 按照 `ENCRYPTION.md` 配置
|
||||||
|
- 验证所有 Secrets 都已加密
|
||||||
|
|
||||||
|
2. **生产部署**
|
||||||
|
- 启用审计日志
|
||||||
|
- 配置监控和告警
|
||||||
|
- 制定密钥轮换政策
|
||||||
|
|
||||||
|
## 支持
|
||||||
|
|
||||||
|
如遇到问题:
|
||||||
|
|
||||||
|
1. **检查日志**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **运行验证脚本**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
./deploy/k8s/secrets/verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **查看详细文档**
|
||||||
|
- 部署问题 → `DEPLOYMENT.md`
|
||||||
|
- 代码集成 → `INTEGRATION.md`
|
||||||
|
- ETCD 加密 → `ENCRYPTION.md`
|
||||||
|
- 诊断 → `VERIFICATION.md`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这个系统为微服务提供了:
|
||||||
|
|
||||||
|
✅ **安全的身份验证** - JWT 令牌 + HS256 签名
|
||||||
|
✅ **灵活的令牌管理** - 7天有效期,30天可刷新
|
||||||
|
✅ **高可用性** - Redis Cluster 自动故障转移
|
||||||
|
✅ **权限隔离** - RBAC 限制密钥访问
|
||||||
|
✅ **数据加密** - ETCD 加密保护敏感信息
|
||||||
|
✅ **请求保护** - Envoy CSRF 双令牌验证
|
||||||
|
|
||||||
|
现在可以部署并集成到应用中了!
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
# 完整部署验证清单
|
||||||
|
|
||||||
|
完成所有部署后使用此清单验证系统是否正确配置和运行。
|
||||||
|
|
||||||
|
## 第一部分:基础设施验证
|
||||||
|
|
||||||
|
### Secret 和 RBAC 创建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Secret 已创建
|
||||||
|
kubectl get secret -n juwan | grep jwt-secret
|
||||||
|
# 预期输出: jwt-secret Created
|
||||||
|
|
||||||
|
# 查看 Secret 详情(不显示敏感数据)
|
||||||
|
kubectl describe secret jwt-secret -n juwan
|
||||||
|
# 应该看到:
|
||||||
|
# Name: jwt-secret
|
||||||
|
# Namespace: juwan
|
||||||
|
# Type: Opaque
|
||||||
|
# Data
|
||||||
|
# ====
|
||||||
|
# secret-key: <encrypted>
|
||||||
|
|
||||||
|
# 验证 Secret 内容已正确加载
|
||||||
|
kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d | wc -c
|
||||||
|
# 预期输出: 应该是 32 个字符(32 字节密钥的 Base64 解码)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ServiceAccount 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 user-rpc ServiceAccount
|
||||||
|
kubectl get sa user-rpc -n juwan
|
||||||
|
kubectl describe sa user-rpc -n juwan
|
||||||
|
# 应该显示正确的 Secrets 挂载
|
||||||
|
|
||||||
|
# 检查 envoy-gateway ServiceAccount
|
||||||
|
kubectl get sa envoy-gateway -n juwan
|
||||||
|
kubectl describe sa envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### RBAC 权限验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Role 定义
|
||||||
|
kubectl get role -n juwan -l app=jwt-secret-reader
|
||||||
|
kubectl describe role jwt-secret-reader -n juwan
|
||||||
|
# 应该显示:
|
||||||
|
# PolicyRule:
|
||||||
|
# Resources Non-Resource URLs Resource Names Verbs
|
||||||
|
# --------- ----------------- -------------- -----
|
||||||
|
# secrets [] [jwt-secret] [get]
|
||||||
|
|
||||||
|
# 检查 RoleBindings
|
||||||
|
kubectl get rolebinding -n juwan | grep jwt-secret-reader
|
||||||
|
# 应该显示两个绑定: jwt-secret-reader-user-rpc 和 jwt-secret-reader-envoy-gateway
|
||||||
|
|
||||||
|
# 验证每个 RoleBinding
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan
|
||||||
|
kubectl describe rolebinding jwt-secret-reader-envoy-gateway -n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二部分:权限测试
|
||||||
|
|
||||||
|
### 权限允许测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试 user-rpc 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: yes
|
||||||
|
|
||||||
|
# 测试 envoy-gateway 可以读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:envoy-gateway \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: yes
|
||||||
|
|
||||||
|
# 测试 user-rpc 无法读其他 Secrets
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: no
|
||||||
|
|
||||||
|
# 测试其他 ServiceAccount 无法读 jwt-secret
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:default \
|
||||||
|
--resource-name=jwt-secret \
|
||||||
|
-n juwan
|
||||||
|
# 预期输出: no
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第三部分:Deployment 配置验证
|
||||||
|
|
||||||
|
### user-rpc Deployment 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccountName 是否正确设置
|
||||||
|
kubectl get deployment user-rpc -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
|
||||||
|
# 预期输出: user-rpc
|
||||||
|
|
||||||
|
# 检查是否包含所有必需的环境变量
|
||||||
|
kubectl get deployment user-rpc -n juwan -o yaml | grep -A 20 "env:"
|
||||||
|
# 应该包括:
|
||||||
|
# - name: JWT_SECRET_KEY
|
||||||
|
# valueFrom:
|
||||||
|
# secretKeyRef:
|
||||||
|
# name: jwt-secret
|
||||||
|
# key: secret-key
|
||||||
|
|
||||||
|
# 检查 Pod 是否正在运行
|
||||||
|
kubectl get pods -n juwan -l app=user-rpc
|
||||||
|
# 应该显示至少 3 个 Running 的 Pod
|
||||||
|
|
||||||
|
# 验证 Pod 已加载 Secret(在 Pod 中执行)
|
||||||
|
kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep -i jwt
|
||||||
|
# 应该输出环境变量,例如:
|
||||||
|
# JWT_SECRET_KEY=your-secret-jwt-key-change-this-in-production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Envoy Gateway Deployment 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 ServiceAccountName 是否正确设置
|
||||||
|
kubectl get deployment envoy-gateway -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}'
|
||||||
|
# 预期输出: envoy-gateway
|
||||||
|
|
||||||
|
# 检查 Pod 是否正在运行
|
||||||
|
kubectl get pods -n juwan -l app=envoy-gateway
|
||||||
|
# 应该显示 Running 的 Pod
|
||||||
|
|
||||||
|
# 检查 Envoy 日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway
|
||||||
|
# 应该看到启动日志,没有权限相关错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第四部分:Redis 连接验证
|
||||||
|
|
||||||
|
### Redis Cluster 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 RedisCluster CRD 状态
|
||||||
|
kubectl get rediscluster -n juwan
|
||||||
|
# 应该显示 user-redis,Status 应该是 Healthy
|
||||||
|
|
||||||
|
# 详细查看 RedisCluster 状态
|
||||||
|
kubectl describe rediscluster user-redis -n juwan
|
||||||
|
# 应该显示:
|
||||||
|
# Status:
|
||||||
|
# Cluster Status: Healthy
|
||||||
|
# Nodes Ready: 3/3
|
||||||
|
# Master: 1
|
||||||
|
# Replicas: 2
|
||||||
|
|
||||||
|
# 检查 Redis Pods
|
||||||
|
kubectl get pods -n juwan | grep redis
|
||||||
|
# 应该显示 3 个 Redis Pod,都在 Running 状态
|
||||||
|
|
||||||
|
# 测试 Redis 连接
|
||||||
|
kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \
|
||||||
|
redis-cli -h user-redis.juwan -c CLUSTER INFO
|
||||||
|
# 应该看到集群信息,cluster_state:ok 表示集群健康
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第五部分:应用启动日志检查
|
||||||
|
|
||||||
|
### user-rpc 启动日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 user-rpc Pods 的启动日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc --all-containers=true
|
||||||
|
|
||||||
|
# 应该包含类似以下消息:
|
||||||
|
# - "Starting gRPC server on 0.0.0.0:9001"
|
||||||
|
# - "Redis Cluster connected successfully" 或 JWT Manager 初始化成功
|
||||||
|
# - "Listening on metrics port 4001"
|
||||||
|
|
||||||
|
# 如果有错误,查看详细日志
|
||||||
|
kubectl logs -n juwan -l app=user-rpc -f --all-containers=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Envoy 启动日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Envoy 启动日志
|
||||||
|
kubectl logs -n juwan -l app=envoy-gateway
|
||||||
|
|
||||||
|
# 应该包含:
|
||||||
|
# - "[info] Configuration: /etc/envoy/envoy.yaml"
|
||||||
|
# - "[info] listener listening on 0.0.0.0:8080"
|
||||||
|
# - 没有权限相关错误
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第六部分:网络和服务发现验证
|
||||||
|
|
||||||
|
### Service 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 user-rpc-svc
|
||||||
|
kubectl get svc user-rpc-svc -n juwan
|
||||||
|
# 应该显示 ClusterIP 和两个端口 (9001/rpc 和 4001/metrics)
|
||||||
|
|
||||||
|
# 检查 Envoy Gateway Service
|
||||||
|
kubectl get svc envoy-gateway -n juwan
|
||||||
|
# 应该显示 ClusterIP 和端口 80
|
||||||
|
|
||||||
|
# 检查 Redis Service
|
||||||
|
kubectl get svc -n juwan | grep redis
|
||||||
|
# 应该显示 user-redis(ClusterIP)服务
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS 解析验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试服务名称解析
|
||||||
|
kubectl run -it --rm debug --image=busybox --restart=Never -- \
|
||||||
|
nslookup user-rpc-svc.juwan.svc.cluster.local
|
||||||
|
# 应该返回 ClusterIP 地址
|
||||||
|
|
||||||
|
kubectl run -it --rm debug --image=busybox --restart=Never -- \
|
||||||
|
nslookup user-redis.juwan.svc.cluster.local
|
||||||
|
# 应该返回 ClusterIP 地址
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第七部分:监控和指标验证
|
||||||
|
|
||||||
|
### Prometheus 指标收集
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Prometheus 是否在收集指标
|
||||||
|
kubectl port-forward -n monitoring svc/prometheus 9090:9090 &
|
||||||
|
|
||||||
|
# 打开浏览器访问 http://localhost:9090
|
||||||
|
# 查看 Status > Targets
|
||||||
|
# 应该看到 user-rpc-svc:4001 目标显示为 UP
|
||||||
|
|
||||||
|
# 查询一个指标
|
||||||
|
curl 'http://localhost:9090/api/v1/query?query=up{job="kubernetes-pods"}'
|
||||||
|
# 应该返回 user-rpc 的指标数据
|
||||||
|
|
||||||
|
# 关闭端口转发
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试源代码级指标端点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 user-rpc Pod 直接访问指标端点
|
||||||
|
kubectl port-forward -n juwan svc/user-rpc-svc 4001:4001 &
|
||||||
|
|
||||||
|
# 测试指标端点
|
||||||
|
curl http://localhost:4001/metrics
|
||||||
|
|
||||||
|
# 应该看到 Prometheus 格式的指标,例如:
|
||||||
|
# # HELP go_goroutines Number of goroutines that currently exist.
|
||||||
|
# # TYPE go_goroutines gauge
|
||||||
|
# go_goroutines 25
|
||||||
|
|
||||||
|
# 关闭端口转发
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第八部分:日志聚合验证(Loki)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Loki 是否正确接收日志
|
||||||
|
kubectl port-forward -n monitoring svc/loki 3100:3100 &
|
||||||
|
|
||||||
|
# 查询日志
|
||||||
|
curl 'http://localhost:3100/loki/api/v1/query_range?query={job="kubernetes-pods"}&start=0&end=9999999999'
|
||||||
|
|
||||||
|
# 应该返回最近的日志条目
|
||||||
|
|
||||||
|
# 检查特定应用的日志
|
||||||
|
curl 'http://localhost:3100/loki/api/v1/query_range?query={app="user-rpc"}&start=0&end=9999999999'
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第九部分:ETCD 加密验证
|
||||||
|
|
||||||
|
如果已启用 ETCD 加密,执行以下验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从 control plane 节点
|
||||||
|
ssh <control-plane-node>
|
||||||
|
|
||||||
|
# 检查 ETCD 配置
|
||||||
|
sudo cat /etc/kubernetes/encryption-config.yaml | head -20
|
||||||
|
|
||||||
|
# 验证 kube-apiserver 正在使用加密配置
|
||||||
|
sudo ps aux | grep kube-apiserver | grep encryption-provider
|
||||||
|
|
||||||
|
# 创建新 Secret 进行测试
|
||||||
|
kubectl create secret generic test-encryption -n juwan --from-literal=key=value
|
||||||
|
|
||||||
|
# 检查 ETCD 中的数据是否加密
|
||||||
|
# 注意:如果加密正确,数据应该不可读
|
||||||
|
sudo ETCDCTL_API=3 etcdctl \
|
||||||
|
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption
|
||||||
|
|
||||||
|
# 输出应该是二进制数据,不可读(表示已加密)
|
||||||
|
# 或者使用十六进制 dump
|
||||||
|
sudo ETCDCTL_API=3 etcdctl \
|
||||||
|
--cert=/etc/kubernetes/pki/etcd/server.crt \
|
||||||
|
--key=/etc/kubernetes/pki/etcd/server.key \
|
||||||
|
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
||||||
|
--endpoints=127.0.0.1:2379 \
|
||||||
|
get /registry/secrets/juwan/test-encryption | od -A x -t x1z -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十部分:功能测试
|
||||||
|
|
||||||
|
### JWT 令牌生成和验证测试
|
||||||
|
|
||||||
|
如果已实现 JWT handlers,测试完整流程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 前向 user-api 服务
|
||||||
|
kubectl port-forward -n juwan svc/user-api-svc 8888:8888 &
|
||||||
|
|
||||||
|
# 2. 调用登录端点获取令牌
|
||||||
|
TOKEN=$(curl -X POST http://localhost:8888/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}' \
|
||||||
|
| jq -r '.token')
|
||||||
|
|
||||||
|
echo "Token: $TOKEN"
|
||||||
|
|
||||||
|
# 3. 使用令牌访问受保护的端点
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" http://localhost:8888/api/v1/users/me
|
||||||
|
|
||||||
|
# 4. 测试令牌刷新
|
||||||
|
curl -X POST http://localhost:8888/api/v1/auth/refresh \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"token\":\"$TOKEN\"}"
|
||||||
|
|
||||||
|
# 5. 测试无效令牌
|
||||||
|
curl -H "Authorization: Bearer invalid-token" http://localhost:8888/api/v1/users/me
|
||||||
|
# 应该返回 401 Unauthorized
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSRF 保护测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 前向 Envoy Gateway
|
||||||
|
kubectl port-forward -n juwan svc/envoy-gateway 8080:80 &
|
||||||
|
|
||||||
|
# 2. 获取 CSRF 令牌(安全方法)
|
||||||
|
curl -i http://localhost:8080/
|
||||||
|
|
||||||
|
# 查看响应头中的 Set-Cookie,应该包含 csrf_token
|
||||||
|
|
||||||
|
# 3. 提取 CSRF 令牌
|
||||||
|
CSRF_TOKEN=$(curl -i http://localhost:8080/ 2>/dev/null | grep -i csrf_token | sed 's/.*csrf_token=//;s/;.*//')
|
||||||
|
|
||||||
|
# 4. 使用 CSRF 令牌进行 POST 请求
|
||||||
|
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: csrf_token=$CSRF_TOKEN" \
|
||||||
|
-H "X-CSRF-Token: $CSRF_TOKEN" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}'
|
||||||
|
|
||||||
|
# 5. 测试无效 CSRF 令牌(应该返回 403)
|
||||||
|
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||||
|
-H "Cookie: csrf_token=valid_token" \
|
||||||
|
-H "X-CSRF-Token: invalid_token" \
|
||||||
|
-d '{"email":"user@example.com","password":"password"}'
|
||||||
|
# 应该返回 403 Forbidden
|
||||||
|
|
||||||
|
kill %1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十一部分:故障排查
|
||||||
|
|
||||||
|
如果任何验证失败,运行以下诊断:
|
||||||
|
|
||||||
|
### Pod 无法启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 显示 Pod 事件
|
||||||
|
kubectl describe pod <pod-name> -n juwan
|
||||||
|
|
||||||
|
# 查看完整日志(包括初始化容器)
|
||||||
|
kubectl logs <pod-name> -n juwan --all-containers=true --previous
|
||||||
|
|
||||||
|
# 检查 Pod 资源限制是否导致 OOMKilled
|
||||||
|
kubectl get event -n juwan --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限被拒绝错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证 ServiceAccount 是否正确
|
||||||
|
kubectl get pod <pod-name> -n juwan -o jsonpath='{.spec.serviceAccountName}'
|
||||||
|
|
||||||
|
# 检查 RBAC 绑定
|
||||||
|
kubectl get rolebinding -n juwan -o wide
|
||||||
|
|
||||||
|
# 手动测试权限
|
||||||
|
kubectl auth can-i get secrets \
|
||||||
|
--as=system:serviceaccount:juwan:user-rpc \
|
||||||
|
-n juwan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis 连接错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Redis Pods 状态
|
||||||
|
kubectl get pods -n juwan -l redis=user-redis
|
||||||
|
|
||||||
|
# 查看 Redis 日志
|
||||||
|
kubectl logs -n juwan -l redis=user-redis
|
||||||
|
|
||||||
|
# 测试 Redis 连接(从 user-rpc Pod)
|
||||||
|
kubectl exec -it <user-rpc-pod> -n juwan -- \
|
||||||
|
redis-cli -h user-redis.juwan:6379 PING
|
||||||
|
# 应该返回 PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
### ETCD 加密问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证加密配置
|
||||||
|
kubectl get secret jwt-secret -n juwan -o json | jq '.data'
|
||||||
|
|
||||||
|
# 如果 ETCD 加密启用,直接读取 ETCD 的数据应该是二进制的
|
||||||
|
# 如果看到明文,说明加密未启用或配置不正确
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第十二部分:清理测试资源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除测试 Secrets
|
||||||
|
kubectl delete secret test-encryption test-secret -n juwan --ignore-not-found
|
||||||
|
|
||||||
|
# 清理前转发的端口
|
||||||
|
lsof -i :9090 :3100 :8888 :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速检查脚本
|
||||||
|
|
||||||
|
创建 `verify-jwt-setup.sh` 进行自动化验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
namespace="juwan"
|
||||||
|
echo "=== JWT Setup Verification ==="
|
||||||
|
|
||||||
|
# 检查 Secret
|
||||||
|
echo -n "✓ JWT Secret存在: "
|
||||||
|
kubectl get secret jwt-secret -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 ServiceAccounts
|
||||||
|
echo -n "✓ user-rpc ServiceAccount: "
|
||||||
|
kubectl get sa user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway ServiceAccount: "
|
||||||
|
kubectl get sa envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 RBAC
|
||||||
|
echo -n "✓ JWT RBAC Role: "
|
||||||
|
kubectl get role jwt-secret-reader -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 Deployments
|
||||||
|
echo -n "✓ user-rpc Deployment: "
|
||||||
|
kubectl get deployment user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway Deployment: "
|
||||||
|
kubectl get deployment envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
# 检查 Pods
|
||||||
|
echo -n "✓ user-rpc Pods运行中: "
|
||||||
|
[ $(kubectl get pods -n $namespace -l app=user-rpc --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ] && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo -n "✓ envoy-gateway 运行中: "
|
||||||
|
kubectl get pods -n $namespace -l app=envoy-gateway --field-selector=status.phase=Running &>/dev/null && echo "✓" || echo "✗"
|
||||||
|
|
||||||
|
echo "=== Verification Complete ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
运行脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x verify-jwt-setup.sh
|
||||||
|
./verify-jwt-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
所有检查项都通过后,JWT + ETCD 加密系统已准备就绪。下一步可以:
|
||||||
|
|
||||||
|
1. 集成 JWT 验证到 RPC handlers
|
||||||
|
2. 实现令牌刷新端点
|
||||||
|
3. 部署应用代码时启用 JWT 认证
|
||||||
|
4. 监控令牌生成和验证指标
|
||||||
|
5. 定期轮换加密密钥和 JWT 秘钥
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret
|
||||||
|
namespace: juwan
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
# base64 encoded: your-secret-jwt-key-change-this-in-production
|
||||||
|
secret-key: eW91ci1zZWNyZXQtand0LWtleS1jaGFuZ2UtdGhpcy1pbi1wcm9kdWN0aW9u
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
resourceNames: ["jwt-secret"]
|
||||||
|
verbs: ["get"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: user-rpc-jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: jwt-secret-reader
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: user-rpc
|
||||||
|
namespace: juwan
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: envoy-gateway-jwt-secret-reader
|
||||||
|
namespace: juwan
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: jwt-secret-reader
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: envoy-gateway
|
||||||
|
namespace: juwan
|
||||||
Reference in New Issue
Block a user