5348966633
WT 目前沿用 JToken 的 JWT 校验;撤销一致性留到后续 WT 专用网关设计。
1033 lines
28 KiB
Markdown
1033 lines
28 KiB
Markdown
# 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 # ← 本文件
|
||
```
|
||
|
||
---
|
||
|
||
希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝
|