From fdbcde13b2d83a46d8e43e347c107e43a4cf835e Mon Sep 17 00:00:00 2001 From: wwweww <2646787260@qq.com> Date: Mon, 23 Feb 2026 20:36:21 +0800 Subject: [PATCH] add: --- ENVOY_GATEWAY_GUIDE.md | 617 ++++++++++ app/users/api/INTEGRATION.md | 601 ++++++++++ .../api/internal/logic/user/loginLogic.go | 3 +- .../api/internal/logic/user/registerLogic.go | 44 +- app/users/rpc/etc/pb.yaml | 6 +- app/users/rpc/internal/config/config.go | 6 + .../internal/logic/checkPermissionLogic.go | 30 + .../internal/logic/getUserByUsernameLogic.go | 16 +- app/users/rpc/internal/logic/loginLogic.go | 30 + .../rpc/internal/logic/validateTokenLogic.go | 30 + .../rpc/internal/server/usercenterServer.go | 15 + app/users/rpc/internal/svc/serviceContext.go | 6 + app/users/rpc/internal/utils/jwks.go | 90 ++ app/users/rpc/internal/utils/jwt.go | 223 +++- app/users/rpc/pb/users.pb.go | 387 ++++++- app/users/rpc/pb/users_grpc.pb.go | 114 ++ app/users/rpc/usercenter/usercenter.go | 24 + common/converter/README.md | 260 +++++ common/converter/generic.go | 207 ++++ common/converter/user_converter.go | 29 + common/utils/password.go | 25 + deploy/certs/tls.crt | 30 + deploy/certs/tls.key | 52 + deploy/envoy/ENVOY_CONFIG_GUIDE.md | 320 +++++ deploy/envoy/QUICK_REFERENCE.md | 371 ++++++ deploy/envoy/deploy.sh | 331 ++++++ deploy/envoy/envoy.yaml | 385 ++++++ deploy/envoy/generate-jwks.sh | 48 + deploy/envoy/setup-jwt-auth.sh | 55 + deploy/k8s/envoy-gateway.yaml | 262 +++++ deploy/k8s/envoy/envoy.yaml | 157 --- deploy/k8s/secrets/DEPLOYMENT.md | 424 +++++++ deploy/k8s/secrets/ENCRYPTION.md | 129 +++ deploy/k8s/secrets/FLOWCHART.md | 415 +++++++ deploy/k8s/secrets/INDEX.md | 399 +++++++ deploy/k8s/secrets/QUICK_REFERENCE.md | 350 ++++++ deploy/k8s/secrets/README.md | 148 +++ deploy/k8s/secrets/SUMMARY.md | 366 ++++++ deploy/k8s/secrets/VERIFICATION.md | 507 ++++++++ deploy/k8s/secrets/jwt-secret.yaml | 68 ++ deploy/k8s/service/user/user-rpc.yaml | 8 +- desc/rpc/users.proto | 39 +- docs/PROJECT_GUIDE.md | 1032 +++++++++++++++++ docs/secrets/DEPLOYMENT.md | 424 +++++++ docs/secrets/ENCRYPTION.md | 129 +++ docs/secrets/FLOWCHART.md | 415 +++++++ docs/secrets/INDEX.md | 399 +++++++ docs/secrets/QUICK_REFERENCE.md | 350 ++++++ docs/secrets/README.md | 148 +++ docs/secrets/SUMMARY.md | 366 ++++++ docs/secrets/VERIFICATION.md | 507 ++++++++ docs/secrets/jwt-secret.yaml | 60 + 52 files changed, 11263 insertions(+), 194 deletions(-) create mode 100644 ENVOY_GATEWAY_GUIDE.md create mode 100644 app/users/api/INTEGRATION.md create mode 100644 app/users/rpc/internal/logic/checkPermissionLogic.go create mode 100644 app/users/rpc/internal/logic/loginLogic.go create mode 100644 app/users/rpc/internal/logic/validateTokenLogic.go create mode 100644 app/users/rpc/internal/utils/jwks.go create mode 100644 common/converter/README.md create mode 100644 common/converter/generic.go create mode 100644 common/converter/user_converter.go create mode 100644 common/utils/password.go create mode 100644 deploy/certs/tls.crt create mode 100644 deploy/certs/tls.key create mode 100644 deploy/envoy/ENVOY_CONFIG_GUIDE.md create mode 100644 deploy/envoy/QUICK_REFERENCE.md create mode 100644 deploy/envoy/deploy.sh create mode 100644 deploy/envoy/envoy.yaml create mode 100644 deploy/envoy/generate-jwks.sh create mode 100644 deploy/envoy/setup-jwt-auth.sh create mode 100644 deploy/k8s/envoy-gateway.yaml delete mode 100644 deploy/k8s/envoy/envoy.yaml create mode 100644 deploy/k8s/secrets/DEPLOYMENT.md create mode 100644 deploy/k8s/secrets/ENCRYPTION.md create mode 100644 deploy/k8s/secrets/FLOWCHART.md create mode 100644 deploy/k8s/secrets/INDEX.md create mode 100644 deploy/k8s/secrets/QUICK_REFERENCE.md create mode 100644 deploy/k8s/secrets/README.md create mode 100644 deploy/k8s/secrets/SUMMARY.md create mode 100644 deploy/k8s/secrets/VERIFICATION.md create mode 100644 deploy/k8s/secrets/jwt-secret.yaml create mode 100644 docs/PROJECT_GUIDE.md create mode 100644 docs/secrets/DEPLOYMENT.md create mode 100644 docs/secrets/ENCRYPTION.md create mode 100644 docs/secrets/FLOWCHART.md create mode 100644 docs/secrets/INDEX.md create mode 100644 docs/secrets/QUICK_REFERENCE.md create mode 100644 docs/secrets/README.md create mode 100644 docs/secrets/SUMMARY.md create mode 100644 docs/secrets/VERIFICATION.md create mode 100644 docs/secrets/jwt-secret.yaml diff --git a/ENVOY_GATEWAY_GUIDE.md b/ENVOY_GATEWAY_GUIDE.md new file mode 100644 index 0000000..d990caf --- /dev/null +++ b/ENVOY_GATEWAY_GUIDE.md @@ -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` 了解完整的项目架构和工作流! diff --git a/app/users/api/INTEGRATION.md b/app/users/api/INTEGRATION.md new file mode 100644 index 0000000..5741915 --- /dev/null +++ b/app/users/api/INTEGRATION.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 " + 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) - 部署指南 diff --git a/app/users/api/internal/logic/user/loginLogic.go b/app/users/api/internal/logic/user/loginLogic.go index 08739d5..2a79ab3 100644 --- a/app/users/api/internal/logic/user/loginLogic.go +++ b/app/users/api/internal/logic/user/loginLogic.go @@ -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) { - // todo: add your logic here and delete this line - return + return &types.LoginResp{}, nil } diff --git a/app/users/api/internal/logic/user/registerLogic.go b/app/users/api/internal/logic/user/registerLogic.go index 7ae4f5e..7fe56d5 100644 --- a/app/users/api/internal/logic/user/registerLogic.go +++ b/app/users/api/internal/logic/user/registerLogic.go @@ -10,6 +10,7 @@ import ( "juwan-backend/app/users/api/internal/svc" "juwan-backend/app/users/api/internal/types" "juwan-backend/app/users/rpc/pb" + "juwan-backend/common/utils" "github.com/google/uuid" "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) { - // 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, }) - if err == nil || user != nil { - return nil, errors.New("User is exisit") - } - id, err := uuid.NewRandom() - if err != nil { - return nil, errors.New("Register is failed") + if err == nil && existingUser != nil { + return nil, errors.New("用户已存在") } - _, err = l.svcCtx.UserRpc.AddUsers(l.ctx, &pb.AddUsersReq{ - UserId: id.String(), + // 生成用户ID + 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, - Passwd: req.Password, + Passwd: hashedPassword, Phone: req.Phone, 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 } diff --git a/app/users/rpc/etc/pb.yaml b/app/users/rpc/etc/pb.yaml index d1cde6a..9fb28a0 100644 --- a/app/users/rpc/etc/pb.yaml +++ b/app/users/rpc/etc/pb.yaml @@ -3,7 +3,7 @@ ListenOn: 0.0.0.0:9001 Prometheus: Host: 0.0.0.0 - Port: 9001 + Port: 4001 Path: /metrics DataSource: "${DB_URI}?sslmode=disable" @@ -13,3 +13,7 @@ CacheConf: Type: cluster Pass: "${REDIS_PASSWORD}" User: "default" + +Jwt: + SecretKey: "${JWT_SECRET_KEY}" + Issuer: "juwan-user-rpc" diff --git a/app/users/rpc/internal/config/config.go b/app/users/rpc/internal/config/config.go index 4672a02..2c12696 100644 --- a/app/users/rpc/internal/config/config.go +++ b/app/users/rpc/internal/config/config.go @@ -5,8 +5,14 @@ import ( "github.com/zeromicro/go-zero/zrpc" ) +type JwtConfig struct { + SecretKey string `json:"secretKey"` + Issuer string `json:"issuer"` +} + type Config struct { zrpc.RpcServerConf DataSource string `json:"dataSource"` CacheConf cache.CacheConf + Jwt JwtConfig `json:"jwt"` } diff --git a/app/users/rpc/internal/logic/checkPermissionLogic.go b/app/users/rpc/internal/logic/checkPermissionLogic.go new file mode 100644 index 0000000..744df2b --- /dev/null +++ b/app/users/rpc/internal/logic/checkPermissionLogic.go @@ -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 +} diff --git a/app/users/rpc/internal/logic/getUserByUsernameLogic.go b/app/users/rpc/internal/logic/getUserByUsernameLogic.go index 50ff037..e8b0b9c 100644 --- a/app/users/rpc/internal/logic/getUserByUsernameLogic.go +++ b/app/users/rpc/internal/logic/getUserByUsernameLogic.go @@ -5,6 +5,7 @@ import ( "juwan-backend/app/users/rpc/internal/svc" "juwan-backend/app/users/rpc/pb" + "juwan-backend/common/converter" "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 - 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 } diff --git a/app/users/rpc/internal/logic/loginLogic.go b/app/users/rpc/internal/logic/loginLogic.go new file mode 100644 index 0000000..879143b --- /dev/null +++ b/app/users/rpc/internal/logic/loginLogic.go @@ -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 +} diff --git a/app/users/rpc/internal/logic/validateTokenLogic.go b/app/users/rpc/internal/logic/validateTokenLogic.go new file mode 100644 index 0000000..f388342 --- /dev/null +++ b/app/users/rpc/internal/logic/validateTokenLogic.go @@ -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 +} diff --git a/app/users/rpc/internal/server/usercenterServer.go b/app/users/rpc/internal/server/usercenterServer.go index 1504ca8..98fc8a1 100644 --- a/app/users/rpc/internal/server/usercenterServer.go +++ b/app/users/rpc/internal/server/usercenterServer.go @@ -53,3 +53,18 @@ func (s *UsercenterServer) SearchUsers(ctx context.Context, in *pb.SearchUsersRe l := logic.NewSearchUsersLogic(ctx, s.svcCtx) 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) +} diff --git a/app/users/rpc/internal/svc/serviceContext.go b/app/users/rpc/internal/svc/serviceContext.go index 01ac790..b0297f3 100644 --- a/app/users/rpc/internal/svc/serviceContext.go +++ b/app/users/rpc/internal/svc/serviceContext.go @@ -4,6 +4,7 @@ import ( "context" "juwan-backend/app/users/rpc/internal/config" "juwan-backend/app/users/rpc/internal/models" + "juwan-backend/app/users/rpc/internal/utils" "time" "github.com/redis/go-redis/v9" @@ -15,6 +16,7 @@ type ServiceContext struct { Config config.Config UsersModel models.UsersModel RedisCluster *redis.ClusterClient + JwtManager *utils.JwtManager } 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{ Config: c, UsersModel: models.NewUsersModel(conn, c.CacheConf), RedisCluster: redisCluster, + JwtManager: jwtManager, } } diff --git a/app/users/rpc/internal/utils/jwks.go b/app/users/rpc/internal/utils/jwks.go new file mode 100644 index 0000000..e23ddc1 --- /dev/null +++ b/app/users/rpc/internal/utils/jwks.go @@ -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 +} diff --git a/app/users/rpc/internal/utils/jwt.go b/app/users/rpc/internal/utils/jwt.go index 8e8ff90..578f3ad 100644 --- a/app/users/rpc/internal/utils/jwt.go +++ b/app/users/rpc/internal/utils/jwt.go @@ -1,8 +1,14 @@ package utils import ( + "context" + "encoding/json" "errors" + "fmt" "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/redis/go-redis/v9" ) type TokenPayload struct { @@ -10,11 +16,16 @@ type TokenPayload struct { IsAdmin bool } +type Claims struct { + TokenPayload + jwt.RegisteredClaims +} + const ( tokenCachePrefixUser = "jwt:user:" tokenCachePrefixToken = "jwt:token:" - tokenCacheTTL = 60 * 24 * time.Hour - tokenLifetime = 5 * 24 * time.Hour + tokenCacheTTL = 30 * 24 * time.Hour + tokenLifetime = 7 * 24 * time.Hour ) var ( @@ -22,9 +33,211 @@ var ( errInvalidToken = errors.New("invalid token claims") errTokenNotInCache = errors.New("token not found in cache") errNoRedisClient = errors.New("redis client not configured") + // errExpiredToken = errors.New("token expired") ) -func NewToken(payload TokenPayload) (string, error) { - - return "", nil +type JwtManager struct { + redisCluster *redis.ClusterClient + 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 } diff --git a/app/users/rpc/pb/users.pb.go b/app/users/rpc/pb/users.pb.go index 17e62cf..a1296f8 100644 --- a/app/users/rpc/pb/users.pb.go +++ b/app/users/rpc/pb/users.pb.go @@ -906,6 +906,334 @@ func (x *GetUserByUsernameResp) GetUsers() *Users { 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 const file_users_proto_rawDesc = "" + @@ -987,7 +1315,27 @@ const file_users_proto_rawDesc = "" + "\x14GetUserByUsernameReq\x12\x1a\n" + "\busername\x18\x01 \x01(\tR\busername\"8\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" + "usercenter\x12-\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" + "\fGetUsersById\x12\x13.pb.GetUsersByIdReq\x1a\x14.pb.GetUsersByIdResp\x12H\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 ( file_users_proto_rawDescOnce sync.Once @@ -1009,7 +1360,7 @@ func file_users_proto_rawDescGZIP() []byte { 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{ (*Users)(nil), // 0: pb.Users (*AddUsersReq)(nil), // 1: pb.AddUsersReq @@ -1024,6 +1375,12 @@ var file_users_proto_goTypes = []any{ (*SearchUsersResp)(nil), // 10: pb.SearchUsersResp (*GetUserByUsernameReq)(nil), // 11: pb.GetUserByUsernameReq (*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{ 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 11, // 7: pb.usercenter.GetUserByUsername:input_type -> pb.GetUserByUsernameReq 9, // 8: pb.usercenter.SearchUsers:input_type -> pb.SearchUsersReq - 2, // 9: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp - 4, // 10: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp - 6, // 11: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp - 8, // 12: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp - 12, // 13: pb.usercenter.GetUserByUsername:output_type -> pb.GetUserByUsernameResp - 10, // 14: pb.usercenter.SearchUsers:output_type -> pb.SearchUsersResp - 9, // [9:15] is the sub-list for method output_type - 3, // [3:9] is the sub-list for method input_type + 13, // 9: pb.usercenter.Login:input_type -> pb.LoginReq + 15, // 10: pb.usercenter.ValidateToken:input_type -> pb.ValidateTokenReq + 17, // 11: pb.usercenter.CheckPermission:input_type -> pb.CheckPermissionReq + 2, // 12: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp + 4, // 13: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp + 6, // 14: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp + 8, // 15: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp + 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 extendee 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(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_proto_rawDesc), len(file_users_proto_rawDesc)), NumEnums: 0, - NumMessages: 13, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/app/users/rpc/pb/users_grpc.pb.go b/app/users/rpc/pb/users_grpc.pb.go index 389e6cb..83321a7 100644 --- a/app/users/rpc/pb/users_grpc.pb.go +++ b/app/users/rpc/pb/users_grpc.pb.go @@ -25,6 +25,9 @@ const ( Usercenter_GetUsersById_FullMethodName = "/pb.usercenter/GetUsersById" Usercenter_GetUserByUsername_FullMethodName = "/pb.usercenter/GetUserByUsername" 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. @@ -38,6 +41,9 @@ type UsercenterClient interface { GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error) GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, 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 { @@ -108,6 +114,36 @@ func (c *usercenterClient) SearchUsers(ctx context.Context, in *SearchUsersReq, 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. // All implementations must embed UnimplementedUsercenterServer // for forward compatibility. @@ -119,6 +155,9 @@ type UsercenterServer interface { GetUsersById(context.Context, *GetUsersByIdReq) (*GetUsersByIdResp, error) GetUserByUsername(context.Context, *GetUserByUsernameReq) (*GetUserByUsernameResp, 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() } @@ -147,6 +186,15 @@ func (UnimplementedUsercenterServer) GetUserByUsername(context.Context, *GetUser func (UnimplementedUsercenterServer) SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error) { 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) testEmbeddedByValue() {} @@ -276,6 +324,60 @@ func _Usercenter_SearchUsers_Handler(srv interface{}, ctx context.Context, dec f 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. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -307,6 +409,18 @@ var Usercenter_ServiceDesc = grpc.ServiceDesc{ MethodName: "SearchUsers", 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{}, Metadata: "users.proto", diff --git a/app/users/rpc/usercenter/usercenter.go b/app/users/rpc/usercenter/usercenter.go index f633e14..96f7ca0 100644 --- a/app/users/rpc/usercenter/usercenter.go +++ b/app/users/rpc/usercenter/usercenter.go @@ -16,17 +16,23 @@ import ( type ( AddUsersReq = pb.AddUsersReq AddUsersResp = pb.AddUsersResp + CheckPermissionReq = pb.CheckPermissionReq + CheckPermissionResp = pb.CheckPermissionResp DelUsersReq = pb.DelUsersReq DelUsersResp = pb.DelUsersResp GetUserByUsernameReq = pb.GetUserByUsernameReq GetUserByUsernameResp = pb.GetUserByUsernameResp GetUsersByIdReq = pb.GetUsersByIdReq GetUsersByIdResp = pb.GetUsersByIdResp + LoginReq = pb.LoginReq + LoginResp = pb.LoginResp SearchUsersReq = pb.SearchUsersReq SearchUsersResp = pb.SearchUsersResp UpdateUsersReq = pb.UpdateUsersReq UpdateUsersResp = pb.UpdateUsersResp Users = pb.Users + ValidateTokenReq = pb.ValidateTokenReq + ValidateTokenResp = pb.ValidateTokenResp Usercenter interface { // -----------------------users----------------------- @@ -36,6 +42,9 @@ type ( GetUsersById(ctx context.Context, in *GetUsersByIdReq, opts ...grpc.CallOption) (*GetUsersByIdResp, error) GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, 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 { @@ -79,3 +88,18 @@ func (m *defaultUsercenter) SearchUsers(ctx context.Context, in *SearchUsersReq, client := pb.NewUsercenterClient(m.cli.Conn()) 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...) +} diff --git a/common/converter/README.md b/common/converter/README.md new file mode 100644 index 0000000..df38334 --- /dev/null +++ b/common/converter/README.md @@ -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 专用转换函数(示例) diff --git a/common/converter/generic.go b/common/converter/generic.go new file mode 100644 index 0000000..ca2f31b --- /dev/null +++ b/common/converter/generic.go @@ -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} +} diff --git a/common/converter/user_converter.go b/common/converter/user_converter.go new file mode 100644 index 0000000..ba5e1f2 --- /dev/null +++ b/common/converter/user_converter.go @@ -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) +} diff --git a/common/utils/password.go b/common/utils/password.go new file mode 100644 index 0000000..96851d3 --- /dev/null +++ b/common/utils/password.go @@ -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 +} diff --git a/deploy/certs/tls.crt b/deploy/certs/tls.crt new file mode 100644 index 0000000..af4004d --- /dev/null +++ b/deploy/certs/tls.crt @@ -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----- diff --git a/deploy/certs/tls.key b/deploy/certs/tls.key new file mode 100644 index 0000000..35d20b8 --- /dev/null +++ b/deploy/certs/tls.key @@ -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----- diff --git a/deploy/envoy/ENVOY_CONFIG_GUIDE.md b/deploy/envoy/ENVOY_CONFIG_GUIDE.md new file mode 100644 index 0000000..71c4059 --- /dev/null +++ b/deploy/envoy/ENVOY_CONFIG_GUIDE.md @@ -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 -- \ + 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 -- \ + 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) diff --git a/deploy/envoy/QUICK_REFERENCE.md b/deploy/envoy/QUICK_REFERENCE.md new file mode 100644 index 0000000..ec43fc8 --- /dev/null +++ b/deploy/envoy/QUICK_REFERENCE.md @@ -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 -- \ + curl http://user-rpc-svc:9001/.well-known/jwks.json + ``` + +- [ ] 后端服务健康 + ```bash + kubectl exec -it -n juwan -- \ + 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) diff --git a/deploy/envoy/deploy.sh b/deploy/envoy/deploy.sh new file mode 100644 index 0000000..289eba5 --- /dev/null +++ b/deploy/envoy/deploy.sh @@ -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:///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 "$@" diff --git a/deploy/envoy/envoy.yaml b/deploy/envoy/envoy.yaml new file mode 100644 index 0000000..cf4492c --- /dev/null +++ b/deploy/envoy/envoy.yaml @@ -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 diff --git a/deploy/envoy/generate-jwks.sh b/deploy/envoy/generate-jwks.sh new file mode 100644 index 0000000..e80840a --- /dev/null +++ b/deploy/envoy/generate-jwks.sh @@ -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" < + - identity: {} + ``` + + 替换 `` 为第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 访问 diff --git a/deploy/k8s/secrets/ENCRYPTION.md b/deploy/k8s/secrets/ENCRYPTION.md new file mode 100644 index 0000000..973ed36 --- /dev/null +++ b/deploy/k8s/secrets/ENCRYPTION.md @@ -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 && kubectl create secret ...` + - Or use: `kubectl patch secret -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 +``` diff --git a/deploy/k8s/secrets/FLOWCHART.md b/deploy/k8s/secrets/FLOWCHART.md new file mode 100644 index 0000000..4cf5bcc --- /dev/null +++ b/deploy/k8s/secrets/FLOWCHART.md @@ -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 -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) diff --git a/deploy/k8s/secrets/INDEX.md b/deploy/k8s/secrets/INDEX.md new file mode 100644 index 0000000..d4d165d --- /dev/null +++ b/deploy/k8s/secrets/INDEX.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 分钟 | diff --git a/deploy/k8s/secrets/QUICK_REFERENCE.md b/deploy/k8s/secrets/QUICK_REFERENCE.md new file mode 100644 index 0000000..085ebfa --- /dev/null +++ b/deploy/k8s/secrets/QUICK_REFERENCE.md @@ -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 --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 < + - 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 -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 -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 - 加密配置 diff --git a/deploy/k8s/secrets/README.md b/deploy/k8s/secrets/README.md new file mode 100644 index 0000000..7a4ed4b --- /dev/null +++ b/deploy/k8s/secrets/README.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 +``` diff --git a/deploy/k8s/secrets/SUMMARY.md b/deploy/k8s/secrets/SUMMARY.md new file mode 100644 index 0000000..1e1247d --- /dev/null +++ b/deploy/k8s/secrets/SUMMARY.md @@ -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: │ │ │ +│ │ │ └─ 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 双令牌验证 + +现在可以部署并集成到应用中了! diff --git a/deploy/k8s/secrets/VERIFICATION.md b/deploy/k8s/secrets/VERIFICATION.md new file mode 100644 index 0000000..16bedf5 --- /dev/null +++ b/deploy/k8s/secrets/VERIFICATION.md @@ -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: + +# 验证 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 + +# 检查 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 -n juwan + +# 查看完整日志(包括初始化容器) +kubectl logs -n juwan --all-containers=true --previous + +# 检查 Pod 资源限制是否导致 OOMKilled +kubectl get event -n juwan --sort-by='.lastTimestamp' +``` + +### 权限被拒绝错误 + +```bash +# 验证 ServiceAccount 是否正确 +kubectl get pod -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 -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 秘钥 diff --git a/deploy/k8s/secrets/jwt-secret.yaml b/deploy/k8s/secrets/jwt-secret.yaml new file mode 100644 index 0000000..90342a9 --- /dev/null +++ b/deploy/k8s/secrets/jwt-secret.yaml @@ -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 diff --git a/deploy/k8s/service/user/user-rpc.yaml b/deploy/k8s/service/user/user-rpc.yaml index 7a38133..dbee4d2 100644 --- a/deploy/k8s/service/user/user-rpc.yaml +++ b/deploy/k8s/service/user/user-rpc.yaml @@ -16,7 +16,8 @@ spec: labels: app: user-rpc spec: - serviceAccountName: find-endpoints + # serviceAccountName: find-endpoints + serviceAccountName: user-rpc initContainers: # 等待数据库就绪的 Init Container 不影响资源使用但是影响调度策略(也可以忽略不计) - name: wait-for-db image: busybox:1.36 @@ -45,6 +46,11 @@ spec: secretKeyRef: name: user-redis key: password + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: jwt-secret + key: secret-key readinessProbe: tcpSocket: port: 9001 diff --git a/desc/rpc/users.proto b/desc/rpc/users.proto index e30164e..36f39a4 100644 --- a/desc/rpc/users.proto +++ b/desc/rpc/users.proto @@ -100,11 +100,43 @@ message GetUserByUsernameResp { 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 // ------------------------------------ -service usercenter{ +service usercenter { //-----------------------users----------------------- rpc AddUsers(AddUsersReq) returns (AddUsersResp); @@ -113,4 +145,7 @@ service usercenter{ rpc GetUsersById(GetUsersByIdReq) returns (GetUsersByIdResp); rpc GetUserByUsername(GetUserByUsernameReq) returns (GetUserByUsernameResp); rpc SearchUsers(SearchUsersReq) returns (SearchUsersResp); -} + rpc Login(LoginReq) returns (LoginResp); + rpc ValidateToken(ValidateTokenReq) returns (ValidateTokenResp); + rpc CheckPermission(CheckPermissionReq) returns (CheckPermissionResp); +} \ No newline at end of file diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md new file mode 100644 index 0000000..c65ebcf --- /dev/null +++ b/docs/PROJECT_GUIDE.md @@ -0,0 +1,1032 @@ +# Juwan 后端项目完整使用指南 + +## 项目概述 + +``` +Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用以下架构: + +┌─────────────────────────────────────────────────────────────────────┐ +│ Envoy Gateway (负载均衡、认证) │ +│ 端口: 80 (HTTP) │ +└──────────────┬──────────────────────────────────────────────────────┘ + │ + ┌───────┴──────────┐ + │ │ + ┌───▼────────┐ ┌───▼────────┐ + │ User API │ │ Order API │ + │ (8888) │ │ (8889) │ + └───┬────────┘ └────────────┘ + │ + ┌───▼────────────────────┐ + │ User RPC (内部使用) │ + │ gRPC (不暴露) │ + └────────────────────────┘ + │ + ┌───▼────────────────────┐ + │ PostgreSQL Database │ + └────────────────────────┘ +``` + +**关键特性:** +- ✅ API 层通过 Envoy Gateway 暴露给外部 +- ✅ RPC 层仅限集群内部通信(通过 K8s Service Discovery) +- ✅ JWT 认证(可选路由) +- ✅ 密码加密存储 +- ✅ 用户会话管理 + +--- + +## 1️⃣ 添加新服务(完整步骤) + +### 示例:添加一个 Product API 服务 + +#### Step 1: 定义 API 接口 + +创建 `desc/api/product.api`: + +```goctl +syntax = "v1" + +type ( + Product { + ProductId int64 `json:"productId"` + Name string `json:"name"` + Description string `json:"description"` + Price float64 `json:"price"` + Stock int32 `json:"stock"` + CreateAt int64 `json:"createAt"` + } + + CreateProductReq { + Name string `json:"name" binding:"required,min=2"` + Description string `json:"description"` + Price float64 `json:"price" binding:"required,gt=0"` + Stock int32 `json:"stock" binding:"required,gte=0"` + } + + CreateProductResp { + ProductId int64 `json:"productId"` + Message string `json:"message"` + } + + GetProductReq { + ProductId int64 `path:"productId" binding:"required,gt=0"` + } + + ListProductsReq { + Page int64 `form:"page" binding:"required,gt=0"` + Limit int64 `form:"limit" binding:"required,gt=0,lte=100"` + } + + ListProductsResp { + Total int64 `json:"total"` + Products []Product `json:"products"` + } +) + +@server ( + group: product + prefix: /api/products + middleware: Logger +) +service product-api { + @doc (summary: "创建商品") + @handler CreateProduct + post / (CreateProductReq) returns (CreateProductResp) + + @doc (summary: "获取商品详情") + @handler GetProduct + get /:productId (GetProductReq) returns (Product) + + @doc (summary: "列出所有商品") + @handler ListProducts + get / (ListProductsReq) returns (ListProductsResp) +} +``` + +#### Step 2: 创建 RPC 定义(内部服务) + +创建 `desc/rpc/product.proto`: + +```proto +syntax = "proto3"; + +option go_package = "./pb"; + +package pb; + +message Product { + int64 productId = 1; + string name = 2; + string description = 3; + double price = 4; + int32 stock = 5; + int64 createAt = 6; +} + +message GetProductReq { + int64 productId = 1; +} + +message GetProductResp { + Product product = 1; +} + +message UpdateStockReq { + int64 productId = 1; + int32 delta = 2; // 正数增加库存,负数减少 +} + +message UpdateStockResp { + int32 newStock = 1; +} + +service ProductCenter { + rpc GetProduct(GetProductReq) returns (GetProductResp); + rpc UpdateStock(UpdateStockReq) returns (UpdateStockResp); +} +``` + +#### Step 3: 生成代码 + +```bash +# 生成 API 服务代码 +goctl api go -api desc/api/product.api -dir app/product/api --style=goZero + +# 生成 RPC 服务代码 +goctl rpc protoc desc/rpc/product.proto \ + --go_out=./app/product/rpc \ + --go-grpc_out=./app/product/rpc \ + --zrpc_out=./app/product/rpc \ + --style=goZero +``` + +#### Step 4: 实现业务逻辑 + +编辑 `app/product/api/internal/logic/product/createproduct.go`: + +```go +package product + +import ( + "context" + "product-api/app/product/api/internal/svc" + "product-api/app/product/api/internal/types" +) + +type CreateProductLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateProductLogic { + return &CreateProductLogic{ + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateProductLogic) CreateProduct(req *types.CreateProductReq) (*types.CreateProductResp, error) { + // TODO: 调用 RPC 或数据库创建商品 + return &types.CreateProductResp{ + ProductId: 1, + Message: "创建成功", + }, nil +} +``` + +#### Step 5: 配置 K8s 部署 + +创建 `deploy/k8s/service/product/product-api.yaml`: + +```yaml +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 +spec: + replicas: 2 + selector: + matchLabels: + app: product-api + template: + metadata: + labels: + app: product-api + spec: + containers: + - name: product-api + image: your-registry/product-api:latest + ports: + - containerPort: 8890 + volumeMounts: + - name: config + mountPath: /etc/product-api + env: + - name: TZ + value: "Asia/Shanghai" + 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 + type: ClusterIP +``` + +#### Step 6: 更新 Envoy 网关配置 + +在 `deploy/k8s/envoy-gateway.yaml` 中添加: + +```yaml +# 在 http_filters->route_config->virtual_hosts->routes 添加: +- match: + prefix: /api/products + route: + cluster: product_api_cluster + timeout: 30s + +# 在 clusters 添加: +- 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 +``` + +#### Step 7: 部署到集群 + +```bash +# 应用 K8s 配置 +kubectl apply -f deploy/k8s/service/product/product-api.yaml + +# 更新 Envoy Gateway 配置 +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/api/products +``` + +--- + +## 2️⃣ RPC 服务内部隔离(不暴露给外部) + +### 当前架构(推荐) + +``` +┌────────────────────────────────────────────┐ +│ 外部客户端 (互联网) │ +└────────────┬─────────────────────────────┘ + │ + ┌────▼──────────┐ + │ Envoy Gateway│ + │ (仅 HTTP) │ + └────┬──────────┘ + │ + ┌────────┴─────────────┐ + │ │ +┌───▼───────────┐ ┌──────▼─────────┐ +│ User API │ │ Product API │ +│ (8888) │ │ (8890) │ +└───┬───────────┘ └────────────────┘ + │ +┌───▼──────────────────────────────────┐ +│ User RPC (完全隐藏) │ +│ - 不暴露端口 │ +│ - gRPC 通信 │ +│ - K8s Service DNS 发现 │ +│ - NetworkPolicy 限制通信 │ +└──────────────────────────────────────┘ +``` + +### 实现步骤 + +#### 1. 创建仅内部的 Service(无 port 暴露) + +在 `deploy/k8s/service/user/user-rpc.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: user-rpc-svc + namespace: juwan +spec: + selector: + app: user-rpc + ports: + - name: grpc + port: 50051 + targetPort: 50051 + type: ClusterIP # 📌 仅限集群内部访问,不暴露 NodePort 或 LoadBalancer + sessionAffinity: None +``` + +#### 2. 配置 NetworkPolicy(进一步限制) + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: user-rpc-access + namespace: juwan +spec: + podSelector: + matchLabels: + app: user-rpc + policyTypes: + - Ingress + ingress: + # 仅允许 API 和其他服务(如 Order API)访问 + - from: + - podSelector: + matchLabels: + app: user-api + - podSelector: + matchLabels: + app: order-api + - podSelector: + matchLabels: + app: product-api + ports: + - protocol: TCP + port: 50051 +``` + +#### 3. API 服务中调用 RPC + +在 `app/users/api/internal/svc/servicecontext.go`: + +```go +package svc + +import ( + "github.com/zeromicro/go-zero/zrpc" + "app/users/api/internal/config" + "app/users/rpc/pb" +) + +type ServiceContext struct { + Config config.Config + UserRpc pb.UsercenterClient // RPC 客户端 +} + +func NewServiceContext(c config.Config) *ServiceContext { + userRpcClient := zrpc.MustNewClient(c.UserRpc) + return &ServiceContext{ + Config: c, + UserRpc: pb.NewUsercenterClient(userRpcClient.Conn()), + } +} +``` + +配置文件 `app/users/api/etc/user-api.yaml`: + +```yaml +Name: user-api +Host: 0.0.0.0 +Port: 8888 + +# RPC 配置(使用 K8s DNS) +UserRpc: + Endpoints: + - user-rpc-svc.juwan.svc.cluster.local:50051 + +Database: + DataSource: postgres://user:pass@pg-dx:5432/juwan +``` + +#### 4. RPC 不在 Envoy 中配置路由 + +❌ **不要**在 `envoy-gateway.yaml` 中添加 RPC 集群: + +```yaml +# ❌❌❌ 不要这样做 ❌❌❌ +# routes: +# - match: +# prefix: /juwan.pb.Usercenter/ +# route: +# cluster: user_rpc_cluster # 这样会暴露 RPC +``` + +--- + +## 3️⃣ JWT 认证与分级访问控制 + +### 实现逻辑 + +``` +请求到达 Envoy → JWT 验证 + +┌──────────────────────────────────────┐ +│ 无效或缺省 Token │ +└──────────┬───────────────────────────┘ + │ + 公开路由 (允许) ──→ /api/users/login + /api/users/register + /api/products (列表、详情) + + 受保护路由 (拒绝) ──→ 返回 401 Unauthorized + │ + ┌──────▼───────────────────────────┐ + │ 有效 Token │ + │ (已登录) │ + └──────┬───────────────────────────┘ + │ + ┌──────▼──────────────────────────────────────────────┐ + │ 完整数据访问 (取决于后端 RPC) │ + │ - 用户信息 (包括隐私信息) │ + │ - 订单历史 │ + │ - 收藏列表 │ + └──────────────────────────────────────────────────────┘ +``` + +### 配置步骤 + +#### 1. 生成 JWT 密钥并存储为 K8s Secret + +```bash +# 生成 HMAC 密钥(用于签名) +openssl rand -hex 32 + +# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 + +# 创建 Secret +kubectl create secret generic jwt-secret \ + --from-literal=key=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \ + -n juwan + +# 验证 +kubectl get secret jwt-secret -n juwan -o yaml +``` + +#### 2. 更新 Envoy 配置(添加 JWT 验证) + +编辑 `deploy/k8s/envoy-gateway.yaml`: + +```yaml +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 + http_filters: + # JWT 认证过滤器(在 router 之前) + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + + # 定义 JWT 提供者 + providers: + my-provider: + issuer: "juwan" + audiences: "api" + # 使用文件系统上的密钥 + local_jwks: + filename: /etc/envoy/jwks.json + + # 定义受保护的路由 + rules: + # 规则1: 登录和注册不需要认证 + - match: + prefix: /api/users/login + allow_missing_or_failed: true # 允许缺省/失败的 token + - match: + prefix: /api/users/register + allow_missing_or_failed: true + # 规则2: 重定向认证失败请求 + - match: + prefix: "/" + requires: + provider_name: "my-provider" + # 如果认证失败,Envoy 直接拒绝,返回 401 + + # 路由过滤器 + - 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/users + route: + cluster: user_api_cluster + - match: + prefix: /api/products + route: + cluster: product_api_cluster + # ... 其他路由 ... + + # ... clusters 定义保持不变 ... +``` + +#### 3. 在 API 服务中添加认证中间件 + +创建 `app/users/api/internal/middleware/authmiddleware.go`: + +```go +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v4" +) + +type AuthMiddleware struct { + JwtSecret string +} + +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 获取 Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + // 如果认证失败,Envoy 会返回 401, + // 但我们可以在 API 层添加自定义逻辑 + next(w, r) + return + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid authorization header", http.StatusUnauthorized) + return + } + + token := parts[1] + + // 验证 token + _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(m.JwtSecret), nil + }) + + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Token 有效,继续处理 + next(w, r) + } +} +``` + +#### 4. 登录端点返回 JWT Token + +编辑 `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 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(l.svcCtx.Config.JwtSecret)) + if err != nil { + return nil, err + } + + return &types.LoginResp{ + Token: tokenString, + Expires: time.Now().Add(24 * time.Hour).Unix(), + }, nil +} +``` + +### JWT 认证时的分级访问 + +**后端 RPC 可处理分级访问:** + +```go +// 在 User RPC 中实现 +func (s *UsercenterServer) GetUserInfo(ctx context.Context, req *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { + // 获取请求者的 userId(从 context 中取,由 API 层传递) + requesterID := ctx.Value("userId").(int64) + targetID := req.Id + + // 查询用户信息 + user := s.getUserFromDB(targetID) + + if requesterID == targetID { + // 自己查看自己 → 返回完整信息(包含隐私信息) + return &pb.GetUsersByIdResp{ + Users: &pb.Users{ + UserId: user.UserId, + Username: user.Username, + Email: user.Email, // ✅ 包含 + Phone: user.Phone, // ✅ 包含 + // ... 所有字段 + }, + }, nil + } else { + // 查看别人 → 返回部分信息 + return &pb.GetUsersByIdResp{ + Users: &pb.Users{ + UserId: user.UserId, + Username: user.Username, + // ❌ 不返回 Email、Phone 等隐私信息 + }, + }, nil + } +} +``` + +**或在 API 层处理:** + +```go +func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { + // 从 context 获取当前认证用户 + currentUser := l.ctx.Value("userId").(int64) + + // 调用 RPC + rpcResp, err := l.svcCtx.UserRpc.GetUsersById(l.ctx, &pb.GetUsersByIdReq{ + Id: req.UserId, + }) + + if currentUser == req.UserId { + // 自己查看自己 → 返回所有信息 + return &types.UserInfo{ + UserId: rpcResp.Users.UserId, + Username: rpcResp.Users.Username, + Email: rpcResp.Users.Email, + Phone: rpcResp.Users.Phone, + }, nil + } else { + // 查看别人 → 仅返回公开信息 + return &types.UserInfo{ + UserId: rpcResp.Users.UserId, + Username: rpcResp.Users.Username, + }, nil + } +} +``` + +--- + +## 4️⃣ 认证失败处理策略 + +### 当前配置 + +| 路由 | 认证要求 | 认证失败 | 示例 | +|-----|---------|--------|------| +| `/api/users/login` | ❌ 不需要 | 放行 | `POST /api/users/login` (允许) | +| `/api/users/register` | ❌ 不需要 | 放行 | `POST /api/users/register` (允许) | +| `/api/users/:userId` | ✅ 需要 | 拒绝 401 | `GET /api/users/123` (无 token → 401) | +| `/api/users/:userId/password` | ✅ 需要 | 拒绝 401 | `PUT /api/users/123/password` (无 token → 401) | +| `/api/products` | ❌ 不需要 | 放行 | `GET /api/products` (允许) | +| `/api/products/:id` | ❌ 不需要 | 放行 | `GET /api/products/1` (允许) | + +### 自定义错误响应 + +创建 `deploy/k8s/envoy-gateway.yaml` 的自定义拒绝响应: + +```yaml +# 在 jwt_authn 过滤器中配置 +http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + my-provider: + issuer: "juwan" + audiences: "api" + local_jwks: + filename: /etc/envoy/jwks.json + rules: + - match: + prefix: /api/users/login + allow_missing_or_failed: true + - match: + prefix: /api/users/register + allow_missing_or_failed: true + - match: + prefix: / + requires: + provider_name: "my-provider" + # 认证失败后的行为 + bypass_cors_preflight: false # 允许 OPTIONS 跨域请求 +``` + +**认证失败时 Envoy 返回 401:** + +```json +HTTP/1.1 401 Unauthorized +content-type: text/html + +Jwt verification fails. +``` + +### 在 API 层添加自定义错误处理 + +如果希望自定义错误响应,可以在每个受保护的 handler 中添加检查: + +```go +func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { + // 获取当前请求者的身份 + ctx := context.WithValue(l.ctx, "currentUserId", req.UserId) + + // 如果没有匹配的 token,API 层可以添加自定义错误 + if req.UserId <= 0 { + return nil, errors.New("invalid user id") + } + + // 继续处理 + return &types.UpdateUserInfoResp{Message: "更新成功"}, nil +} +``` + +--- + +## 5️⃣ 完整工作流示例 + +### 场景:用户注册 → 登录 → 获取用户信息 + +#### 步骤 1: 注册用户(无需认证) + +```bash +curl -X POST http://localhost/api/users/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "password": "securePass123", + "email": "john@example.com", + "phone": "13800138001" + }' + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "message": "用户注册成功" +} +``` + +#### 步骤 2: 登录获取 Token(无需认证) + +```bash +curl -X POST http://localhost/api/users/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "password": "securePass123" + }' + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires": 1708694400 +} +``` + +#### 步骤 3: 获取用户信息(需要 Token) + +```bash +# ❌ 无 Token → 401 Unauthorized +curl http://localhost/api/users/1 +# Jwt verification fails. + +# ✅ 有 Token → 成功 +curl http://localhost/api/users/1 \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "phone": "13800138001", + "avatar": "https://...", + "status": 1, + "createAt": 1708608000, + "updateAt": 1708608000 +} +``` + +#### 步骤 4: 查看他人信息(部分数据) + +```bash +# 用户1 查看用户2 的信息(已登录) +curl http://localhost/api/users/2 \ + -H "Authorization: Bearer eyJhbGc..." + +# 响应(仅公开信息) +{ + "userId": 2, + "username": "jane_doe", + # ❌ 不包含 email, phone 等隐私信息 +} +``` + +--- + +## 6️⃣ 部署检查清单 + +在部署新服务或更新配置前,使用此清单: + +- [ ] **API 定义** - `desc/api/*.api` 文件已创建 +- [ ] **RPC 定义** - `desc/rpc/*.proto` 文件已创建(如需内部通信) +- [ ] **代码生成** - 运行 `goctl api/rpc` 命令生成代码 +- [ ] **业务逻辑** - 编辑 `app/*/api/internal/logic/` 实现功能 +- [ ] **K8s 部署清单** - 创建 `deploy/k8s/service/*/` 文件 + - [ ] ConfigMap(配置) + - [ ] Deployment(部署) + - [ ] Service(K8s 服务发现) + - [ ] NetworkPolicy(网络隔离,可选) +- [ ] **Envoy 更新** - 修改 `deploy/k8s/envoy-gateway.yaml` + - [ ] 添加路由规则 + - [ ] 添加上游集群 + - [ ] 验证健康检查地址 +- [ ] **测试验证** + ```bash + kubectl apply -f deploy/k8s/service/{name}/ + kubectl apply -f deploy/k8s/envoy-gateway.yaml + kubectl delete pods -n juwan -l app=envoy-gateway + curl http://localhost/api/{path} # 端口转发后测试 + ``` + +--- + +## 7️⃣ 故障排查 + +### 问题 1: 新服务无法访问 + +```bash +# 检查 Pod 状态 +kubectl get pods -n juwan + +# 查看 Pod 日志 +kubectl logs -n juwan -l app=your-service --tail=100 + +# 检查 Service +kubectl get svc -n juwan + +# 测试 DNS 解析 +kubectl exec -it {pod-name} -n juwan -- nslookup your-service-svc.juwan.svc.cluster.local +``` + +### 问题 2: Envoy 配置错误 + +```bash +# 查看 Envoy Pod 日志 +kubectl logs -n juwan -l app=envoy-gateway --tail=50 + +# 常见错误 +# - "no such field" → YAML 字段名与 Envoy 版本不兼容 +# - "Unknown cluster" → Envoy 配置中缺少 cluster 定义 +# - "Connection refused" → 后端服务未启动或 DNS 无法解析 +``` + +### 问题 3: JWT 认证失败 + +```bash +# 检查 JWT 配置是否正确 +kubectl get configmap envoy-config -n juwan -o yaml + +# 查看 jwks.json 是否存在 +kubectl exec -it {envoy-pod} -n juwan -- cat /etc/envoy/jwks.json + +# 验证 Token 格式 +curl -H "Authorization: Bearer {token}" http://localhost/api/users/1 +``` + +--- + +## 文件组织总结 + +``` +project-root/ +├── desc/ # 接口定义 +│ ├── api/ +│ │ ├── users.api # User API 定义 +│ │ └── product.api # ← 新增:Product API 定义 +│ ├── rpc/ +│ │ ├── users.proto # User RPC 定义 +│ │ └── product.proto # ← 新增:Product RPC 定义 +│ └── sql/ +│ +├── app/ # 应用代码 +│ ├── users/ +│ │ ├── api/ # User API 实现 +│ │ └── rpc/ # User RPC 实现(内部) +│ └── product/ # ← 新增:Product 服务 +│ ├── api/ +│ └── rpc/ +│ +├── deploy/ # K8s 部署 +│ ├── k8s/ +│ │ ├── envoy-gateway.yaml # ← 更新:添加新路由 +│ │ ├── base/ +│ │ └── service/ +│ │ ├── user/ +│ │ │ ├── user-api.yaml +│ │ │ └── user-rpc.yaml +│ │ └── product/ # ← 新增:Product 部署文件 +│ │ ├── product-api.yaml +│ │ └── product-rpc.yaml +│ └── envoy/ # 参考配置 +│ +└── docs/ # 文档 + └── PROJECT_GUIDE.md # ← 本文件 +``` + +--- + +希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝 diff --git a/docs/secrets/DEPLOYMENT.md b/docs/secrets/DEPLOYMENT.md new file mode 100644 index 0000000..938a60a --- /dev/null +++ b/docs/secrets/DEPLOYMENT.md @@ -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: + - identity: {} + ``` + + 替换 `` 为第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 访问 diff --git a/docs/secrets/ENCRYPTION.md b/docs/secrets/ENCRYPTION.md new file mode 100644 index 0000000..973ed36 --- /dev/null +++ b/docs/secrets/ENCRYPTION.md @@ -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 && kubectl create secret ...` + - Or use: `kubectl patch secret -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 +``` diff --git a/docs/secrets/FLOWCHART.md b/docs/secrets/FLOWCHART.md new file mode 100644 index 0000000..4cf5bcc --- /dev/null +++ b/docs/secrets/FLOWCHART.md @@ -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 -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) diff --git a/docs/secrets/INDEX.md b/docs/secrets/INDEX.md new file mode 100644 index 0000000..d4d165d --- /dev/null +++ b/docs/secrets/INDEX.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 分钟 | diff --git a/docs/secrets/QUICK_REFERENCE.md b/docs/secrets/QUICK_REFERENCE.md new file mode 100644 index 0000000..085ebfa --- /dev/null +++ b/docs/secrets/QUICK_REFERENCE.md @@ -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 --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 < + - 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 -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 -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 - 加密配置 diff --git a/docs/secrets/README.md b/docs/secrets/README.md new file mode 100644 index 0000000..7a4ed4b --- /dev/null +++ b/docs/secrets/README.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 +``` diff --git a/docs/secrets/SUMMARY.md b/docs/secrets/SUMMARY.md new file mode 100644 index 0000000..1e1247d --- /dev/null +++ b/docs/secrets/SUMMARY.md @@ -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: │ │ │ +│ │ │ └─ 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 双令牌验证 + +现在可以部署并集成到应用中了! diff --git a/docs/secrets/VERIFICATION.md b/docs/secrets/VERIFICATION.md new file mode 100644 index 0000000..16bedf5 --- /dev/null +++ b/docs/secrets/VERIFICATION.md @@ -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: + +# 验证 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 + +# 检查 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 -n juwan + +# 查看完整日志(包括初始化容器) +kubectl logs -n juwan --all-containers=true --previous + +# 检查 Pod 资源限制是否导致 OOMKilled +kubectl get event -n juwan --sort-by='.lastTimestamp' +``` + +### 权限被拒绝错误 + +```bash +# 验证 ServiceAccount 是否正确 +kubectl get pod -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 -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 秘钥 diff --git a/docs/secrets/jwt-secret.yaml b/docs/secrets/jwt-secret.yaml new file mode 100644 index 0000000..65f02c2 --- /dev/null +++ b/docs/secrets/jwt-secret.yaml @@ -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