28 KiB
28 KiB
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:
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:
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: 生成代码
# 生成 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:
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:
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 中添加:
# 在 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: 部署到集群
# 应用 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:
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(进一步限制)
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:
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:
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 集群:
# ❌❌❌ 不要这样做 ❌❌❌
# 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
# 生成 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:
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:
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:
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 可处理分级访问:
// 在 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 层处理:
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 的自定义拒绝响应:
# 在 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:
HTTP/1.1 401 Unauthorized
content-type: text/html
Jwt verification fails.
在 API 层添加自定义错误处理
如果希望自定义错误响应,可以在每个受保护的 handler 中添加检查:
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: 注册用户(无需认证)
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(无需认证)
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)
# ❌ 无 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: 查看他人信息(部分数据)
# 用户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- 添加路由规则
- 添加上游集群
- 验证健康检查地址
- 测试验证
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: 新服务无法访问
# 检查 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 配置错误
# 查看 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 认证失败
# 检查 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 # ← 本文件
希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝