# Envoy Gateway 配置指南(带 JWT 认证) ## 📋 目录 1. [快速开始](#快速开始) 2. [添加新服务](#添加新服务) 3. [JWT 认证配置](#jwt-认证配置) 4. [分级访问控制](#分级访问控制) 5. [故障排查](#故障排查) 6. [当前实现说明(与仓库配置对齐)](#当前实现说明与仓库配置对齐) 7. [ext_authz 适配方案](#ext_authz-适配方案) 8. [前端接入示例(邮箱验证码)](#前端接入示例邮箱验证码) --- ## 当前实现说明(与仓库配置对齐) > 本节对应当前实际配置文件:`deploy/k8s/envoy/envoy.yaml`。 ### 0) 当前公共路由(不需要登录) 当前网关对以下路径做了“公共放行”: - `/healthz`(直返 200,用于探针) - `POST /api/users/login` - `POST /api/users/register` - `POST /api/email/verification-code/send`(注册/登录前发送验证码) 实现方式: - 在 `jwt_authn.rules` 中将上述路径加入白名单(不要求 JWT) - 在路由层对上述路径关闭 `ext_authz`(避免公共接口和探针被二次鉴权拦截) ### 1) 用户认证后,`UserId` 放在哪里? 当前 Envoy 使用 `envoy.filters.http.jwt_authn` 做 JWT 校验,校验通过后通过 `claim_to_headers` 将 claim 注入到转发请求头: - `UserId` -> `x-auth-user-id` - `IsAdmin` -> `x-auth-is-admin` 也就是说,后端 API(如 user-api/email-api)拿到的是 HTTP Header,不是 Envoy 动态元数据。 ### 2) 当前配置是否实现了“换票”(token renew)? 没有。 当前 Envoy 配置仅负责: - 从 Cookie `JToken` 提取 JWT - 用 HS256 + `issuer: juwan-user-rpc` 验签与过期检查 当 token 过期时,`jwt_authn` 会直接拒绝请求,不会调用 user-rpc 的 `JwtManager.Renew`。 ### 3) Envoy 能否直接调用 `user-rpc.ValidateToken`? 结论:不能“直接”用现有 `ValidateToken` protobuf 接口接入 Envoy 认证链。 原因: - Envoy 内置认证过滤器(如 `jwt_authn`、`ext_authz`)要求固定协议。 - `ext_authz` 的 gRPC 需要实现 Envoy 标准服务 `envoy.service.auth.v3.Authorization`,不是业务自定义的 `pb.usercenter/ValidateToken`。 可行方案(推荐顺序): 1. **推荐**:新增一个 `authz-adapter` 服务,实现 Envoy `ext_authz` 协议,内部再调用 `user-rpc.ValidateToken`。 2. 备选:提供一个内部 HTTP 鉴权端点(例如 user-api internal route),Envoy 通过 `ext_authz` HTTP 模式或 Lua `httpCall()` 调用。 如果要走方案 1(推荐),你需要补齐: - `authz-adapter` 服务(实现 Envoy `CheckRequest/CheckResponse`) - Envoy 新增 `ext_authz` filter 与对应 cluster - 鉴权透传头约定(至少 `x-auth-user-id`、`x-auth-is-admin`) - 失败码与错误体规范(401/403) - 性能与可用性策略(超时、失败回退、缓存) ### 4) 与你现有 `ValidateTokenLogic` 的一致性提醒 当前 `app/users/rpc/internal/logic/validateTokenLogic.go` 中: - 代码使用 `jwt:%v` 格式拼接 `redisKey` - 但 `JwtManager.Valid()` 需要传入的是 **JWT token 字符串本身** 这意味着若后续接入 `ext_authz` 并调用该逻辑,建议先修正这段逻辑,避免认证结果偏差。 --- ## ext_authz 适配方案 如果你希望 Envoy 在鉴权阶段调用 `user-rpc.ValidateToken`,请看完整落地文档: - [ENVOY_EXT_AUTHZ_ADAPTER.md](ENVOY_EXT_AUTHZ_ADAPTER.md) 该文档包含: - Envoy 标准 `Authorization.Check` 最小实现骨架 - 调用 `user-rpc.ValidateToken` 的适配逻辑示例 - 可直接嵌入现有网关的 `ext_authz` filter + cluster 配置片段 --- ## 前端接入示例(邮箱验证码) 以下示例基于你当前网关与服务配置: - 登录:`POST /api/users/login`(公共放行) - 发送验证码:`POST /api/email/verification-code/send`(公共放行,无需登录) - CSRF 头:`XSRF-TOKEN`(请求头) - CSRF Cookie:`__Host-XSRF-TOKEN`(可读) - JWT Cookie:`JToken`(`HttpOnly`,前端不可读,但会随请求自动携带) > 注意:当前 Envoy 给 CSRF Cookie 设置了 `Secure` + `SameSite=Strict`。前端必须走 **HTTPS 且同站点** 才能稳定工作。 ### 接入流程 1. 先发一个安全方法请求(GET),让 Envoy 下发 XSRF 双 Cookie。 2. 注册场景可直接调用发送验证码接口,仅需携带 `XSRF-TOKEN`。 3. 登录场景可先调用登录接口拿 `JToken`,后续访问受保护接口时自动携带。 ### 前端示例(TypeScript + fetch) ```ts const API_BASE = "https://your-gateway-domain"; function getCookie(name: string): string { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : ""; } async function primeXsrfCookies() { await fetch(`${API_BASE}/healthz`, { method: "GET", credentials: "include", }); } async function login(username: string, password: string) { const xsrfToken = getCookie("__Host-XSRF-TOKEN"); const res = await fetch(`${API_BASE}/api/users/login`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "XSRF-TOKEN": xsrfToken, }, body: JSON.stringify({ username, password }), }); if (!res.ok) { const text = await res.text(); throw new Error(`login failed: ${res.status} ${text}`); } return res.json(); } type SendCodeReq = { email: string; scene: "register" | "login" | "reset_password" | "bind_email"; }; async function sendVerificationCode(req: SendCodeReq) { const xsrfToken = getCookie("__Host-XSRF-TOKEN"); const res = await fetch(`${API_BASE}/api/email/verification-code/send`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "XSRF-TOKEN": xsrfToken, }, body: JSON.stringify(req), }); if (!res.ok) { const text = await res.text(); throw new Error(`send code failed: ${res.status} ${text}`); } return res.json() as Promise<{ requestId: string; expireInSec: number; message: string; }>; } // 页面初始化时建议执行一次 await primeXsrfCookies(); // 注册场景:无需登录即可发送验证码 const data = await sendVerificationCode({ email: "alice@example.com", scene: "register", }); console.log("code request:", data); // 如需调用受保护接口,再执行登录 await login("alice", "P@ssw0rd"); ``` ### 常见前端坑位 - 必须加 `credentials: "include"`,否则 Cookie 不会带上。 - `JToken` 是 `HttpOnly`,前端读不到,这是正常设计。 - 如果你前后端跨站点,`SameSite=Strict` 会导致 Cookie 不发送;需要改网关 Cookie 策略。 - 本地 `http://localhost` 下,`Secure` Cookie 不会生效;建议本地也走 HTTPS(例如反向代理或证书)。 --- ## 快速开始 ### 前置条件 - 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` 了解完整的项目架构和工作流!