# Juwan 后端项目完整使用指南 ## 项目概述 ``` Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用以下架构: ┌─────────────────────────────────────────────────────────────────────┐ │ Envoy Gateway (负载均衡、认证) │ │ 端口: 80 (HTTP) │ └──────────────┬──────────────────────────────────────────────────────┘ │ ┌───────┴──────────┐ │ │ ┌───▼────────┐ ┌───▼────────┐ │ User API │ │ Order API │ │ (8888) │ │ (8888) │ └───┬────────┘ └────────────┘ │ ┌───▼────────────────────┐ │ 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 # ← 本文件 ``` --- 希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝