21 KiB
Envoy Gateway 配置指南(带 JWT 认证)
📋 目录
当前实现说明(与仓库配置对齐)
本节对应当前实际配置文件:
deploy/k8s/envoy/envoy.yaml。
0) 当前公共路由(不需要登录)
当前网关对以下路径做了“公共放行”:
/healthz(直返 200,用于探针)POST /api/v1/auth/loginPOST /api/v1/auth/registerPOST /api/v1/auth/forgot-passwordPOST /api/v1/auth/reset-passwordPOST /api/v1/auth/forgot-password/sendPOST /api/v1/email/verification-code/send
此外,当前配置还对一批只读接口做了 JWT 白名单豁免,例如 GET /api/v1/games*、GET /api/v1/players*、GET /api/v1/services*、GET /api/v1/posts*、GET /api/v1/shops* 以及部分 GET /api/v1/users/* 子路径。精确范围以 deploy/k8s/envoy/envoy.yaml 中的 jwt_authn.rules 为准。
实现方式:
- 在
jwt_authn.rules中将上述路径加入白名单(不要求 JWT) - 在路由层对上述路径关闭
ext_authz(避免公共接口和探针被二次鉴权拦截)
1) 用户认证后,UserId 放在哪里?
当前 Envoy 使用 envoy.filters.http.jwt_authn 做 JWT 校验,校验通过后通过 claim_to_headers 将 claim 注入到转发请求头:
UserId->x-auth-user-idIsAdmin->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。
当前仓库已经采用 authz-adapter 方案:Envoy 先做 jwt_authn,再通过 ext_authz 调用 authz-adapter,由它内部调用 user-rpc.ValidateToken 做会话态二次校验。
当前链路包含:
authz-adapter服务(实现 EnvoyCheckRequest/CheckResponse)- Envoy
ext_authzfilter 与authz_adapter_cluster jwt_authn注入x-auth-user-id、x-auth-is-adminauthz-adapter透传x-auth-user-id、x-auth-role-type- 失败码与错误体规范(401/403)
4) 与你现有 ValidateTokenLogic 的一致性提醒
当前 app/users/rpc/internal/logic/validateTokenLogic.go 中:
JwtManager.Valid()负责验证 JWT 字符串本身- 当前逻辑还会校验 JWT payload 中的
UserId与请求传入的userId一致,再查询数据库回填RoleType
这意味着当前 ext_authz -> user-rpc.ValidateToken 链已经具备 token 有效性和 userId 一致性校验。
ext_authz 适配方案
如果你希望 Envoy 在鉴权阶段调用 user-rpc.ValidateToken,请看完整落地文档:
该文档包含:
- Envoy 标准
Authorization.Check最小实现骨架 - 调用
user-rpc.ValidateToken的适配逻辑示例 - 可直接嵌入现有网关的
ext_authzfilter + cluster 配置片段
前端接入示例(邮箱验证码)
以下示例基于当前 K8s 网关配置:
- 登录:
POST /api/v1/auth/login(公共放行) - 发送验证码:
POST /api/v1/email/verification-code/send(公共放行,无需登录) - CSRF 头:
xsrf-token(请求头) - CSRF Cookie:
__Host-XSRF-TOKEN(可读) - CSRF Guard Cookie:
__Host-XSRF-GUARD(HttpOnly) - JWT Cookie:
JToken(HttpOnly,前端不可读,但会随请求自动携带)
注意:当前 Envoy 给 CSRF Cookie 设置了
Secure+SameSite=Strict。前端必须走 HTTPS 且同站点 才能稳定工作。
接入流程
- 先发一个安全方法请求(GET),让 Envoy 下发 XSRF 双 Cookie。
- 注册场景可直接调用发送验证码接口,仅需携带
xsrf-token。 - 登录场景可先调用登录接口拿
JToken,后续访问受保护接口时自动携带。
前端示例(TypeScript + fetch)
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/v1/auth/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/v1/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下,SecureCookie 不会生效;建议本地也走 HTTPS(例如反向代理或证书)。
快速开始
前置条件
- K8s 集群正在运行(已验证 ✅)
- Envoy Gateway Pod 处于 Running 状态
- 所有后端服务已部署
当前网关状态
# 查看 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
访问网关
# 通过 kubectl 端口转发(本地测试)
kubectl port-forward -n juwan svc/envoy-gateway 8080:80 &
# 测试
curl http://localhost:8080/healthz
添加新服务
场景:添加 Product 服务
1. 创建服务的 K8s 部署清单
编辑或创建 deploy/k8s/service/product/product-api.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 部分添加:
# ... 在现有路由下方添加:
- match:
prefix: /api/products
route:
cluster: product_api_cluster
timeout: 30s
3. 在 Envoy 网关中添加上游集群
编辑 deploy/k8s/envoy-gateway.yaml,在 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
4. 部署到集群
# 部署 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 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 部分:
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 部分:
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):
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:
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:
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
JwtSecret string `json:"jwtSecret"`
}
在 K8s Deployment 中设置环境变量:
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:
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:
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: 修改用户信息(只能修改自己)
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 启动失败
# 查看日志
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 认证失败
# 验证 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: 后端服务无法访问
# 查看 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 问题:
# 在 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 后的完整更新步骤:
# 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 了解完整的项目架构和工作流!