add: user auth accomplished
This commit is contained in:
@@ -21,7 +21,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
defer server.Stop()
|
||||
|
||||
@@ -2,6 +2,11 @@ Name: email-api
|
||||
Host: 0.0.0.0
|
||||
Port: 8888
|
||||
|
||||
Prometheus:
|
||||
Host: 0.0.0.0
|
||||
Port: 4001
|
||||
Path: /metrics
|
||||
|
||||
CacheConf:
|
||||
- Host: "${REDIS_M_HOST}"
|
||||
Type: node
|
||||
@@ -16,4 +21,5 @@ Kmq:
|
||||
Name: email-api
|
||||
Brokers:
|
||||
- "${KAFKA_BROKER}"
|
||||
Group: "email-api-group"
|
||||
Topic: "email-task"
|
||||
|
||||
@@ -44,11 +44,11 @@ func (l *SendVerificationCodeLogic) SendVerificationCode(req *types.SendVerifica
|
||||
code := utils.GenCode()
|
||||
requestID := uuid.NewString()
|
||||
|
||||
redisKey := fmt.Sprintf("%s:%s:%s", req.Email, code, req.Email)
|
||||
redisKey := fmt.Sprintf("vcode:%s:%s:%s", requestID, req.Scene, req.Email)
|
||||
if exists, getErr := l.svcCtx.RedisCluster.Get(l.ctx, redisKey).Result(); getErr == nil && exists != "" {
|
||||
return nil, fmt.Errorf("verification code already sent, please wait before requesting a new one")
|
||||
}
|
||||
if setErr := l.svcCtx.RedisCluster.Set(l.ctx, redisKey, req.Scene, 60*time.Second).Err(); setErr != nil {
|
||||
if setErr := l.svcCtx.RedisCluster.Set(l.ctx, redisKey, code, 60*time.Second).Err(); setErr != nil {
|
||||
return nil, setErr
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||
if err := c.SetUp(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ Name: email-mq
|
||||
|
||||
Prometheus:
|
||||
Host: 0.0.0.0
|
||||
Port: 4003
|
||||
Port: 4001
|
||||
Path: /metrics
|
||||
|
||||
Kmq:
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func Mqs(c config.Config) []service.Service {
|
||||
//svcContext := NewServiceContext
|
||||
ctx := context.Background()
|
||||
svcCtx := svc.NewServiceContext(c)
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
Name: snowflake.rpc
|
||||
ListenOn: 0.0.0.0:8080
|
||||
#Etcd:
|
||||
# Hosts:
|
||||
# - 127.0.0.1:2379
|
||||
# Key: snowflake.rpc
|
||||
|
||||
Snowflake:
|
||||
DatacenterId: 1
|
||||
|
||||
@@ -9,3 +9,6 @@ Prometheus:
|
||||
|
||||
UsercenterRpcConf:
|
||||
Target: k8s://juwan/user-rpc-svc:9001
|
||||
|
||||
SnowflakeRpcConf:
|
||||
Target: k8s://juwan/snowflake-svc:8080
|
||||
|
||||
@@ -11,4 +11,5 @@ import (
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
UsercenterRpcConf zrpc.RpcClientConf
|
||||
SnowflakeRpcConf zrpc.RpcClientConf
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func WithRequestId(c context.Context, requestId string) context.Context {
|
||||
return context.WithValue(c, "request_id", requestId)
|
||||
}
|
||||
|
||||
func RequestIdFrom(c context.Context) (string, error) {
|
||||
requestID, ok := c.Value("request_id").(string)
|
||||
if !ok {
|
||||
return "", errors.New("request_id not found in context")
|
||||
}
|
||||
return requestID, nil
|
||||
}
|
||||
|
||||
func WithToken(c context.Context, token string) context.Context {
|
||||
return context.WithValue(c, "token", token)
|
||||
}
|
||||
|
||||
func TokenFrom(c context.Context) (string, error) {
|
||||
token, ok := c.Value("token").(string)
|
||||
if !ok {
|
||||
return "", errors.New("token not found in context")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"juwan-backend/app/users/api/internal/logic/user"
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
// 用户登录接口
|
||||
@@ -23,6 +23,22 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
l := user.NewLoginLogic(r.Context(), svcCtx)
|
||||
resp, err := l.Login(&req)
|
||||
token := resp.Token
|
||||
resp.Token = ""
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "JToken",
|
||||
Value: token,
|
||||
Quoted: false,
|
||||
Path: "/",
|
||||
Domain: "",
|
||||
RawExpires: "",
|
||||
MaxAge: 691200,
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Partitioned: false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
|
||||
@@ -4,29 +4,93 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"juwan-backend/app/users/api/internal/contextx"
|
||||
"juwan-backend/common/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"juwan-backend/app/users/api/internal/logic/user"
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
// 用户注册接口
|
||||
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := normalizeRegisterBody(r); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req types.RegisterReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewRegisterLogic(r.Context(), svcCtx)
|
||||
requestId := r.Header.Get("X-Request-ID")
|
||||
//regCtx := context.WithValue(r.Context(), "request_id", requestId)
|
||||
regCtx := contextx.WithRequestId(r.Context(), requestId)
|
||||
if requestId == "" {
|
||||
httpx.ErrorCtx(r.Context(), w, errors.New("bad request"))
|
||||
}
|
||||
|
||||
l := user.NewRegisterLogic(regCtx, svcCtx)
|
||||
resp, err := l.Register(&req)
|
||||
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
httpx.OkJsonCtx(r.Context(), w, utils.NewErrorResp(400, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRegisterBody(r *http.Request) error {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if len(body) == 0 {
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return nil
|
||||
}
|
||||
|
||||
vcode, exists := payload["vcode"]
|
||||
if exists {
|
||||
switch value := vcode.(type) {
|
||||
case string:
|
||||
parsed, convErr := strconv.Atoi(value)
|
||||
if convErr != nil {
|
||||
return fmt.Errorf("invalid vcode format")
|
||||
}
|
||||
payload["vcode"] = parsed
|
||||
case float64:
|
||||
payload["vcode"] = int(value)
|
||||
}
|
||||
}
|
||||
|
||||
normalized, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Body = io.NopCloser(bytes.NewReader(normalized))
|
||||
r.ContentLength = int64(len(normalized))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"juwan-backend/app/users/rpc/usercenter"
|
||||
"juwan-backend/common/converter"
|
||||
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
@@ -27,8 +30,21 @@ func NewGetUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUs
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (resp *types.UserInfo, err error) {
|
||||
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (resp types.UserInfo, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return
|
||||
pbUser, err := l.svcCtx.UserRpc.GetUsersById(l.ctx, &usercenter.GetUsersByIdReq{
|
||||
Id: req.UserId,
|
||||
})
|
||||
if err != nil {
|
||||
return types.UserInfo{}, errors.New("failed to get user info by userid")
|
||||
}
|
||||
user := types.UserInfo{}
|
||||
err = converter.StructToStruct(&pbUser.Users, &user)
|
||||
if err != nil {
|
||||
logx.Errorf("struct to user info failed, err:%v.", err)
|
||||
return types.UserInfo{}, errors.New("failed to get user info by userid")
|
||||
}
|
||||
|
||||
//req.UserId
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"errors"
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
"juwan-backend/app/users/rpc/usercenter"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -28,6 +30,24 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic
|
||||
}
|
||||
|
||||
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
|
||||
|
||||
return &types.LoginResp{}, nil
|
||||
if len(req.Username) < 3 || len(req.Password) > 20 || len(req.Password) < 8 || len(req.Password) > 20 {
|
||||
return nil, errors.New("the information is illegal")
|
||||
}
|
||||
|
||||
res, err := l.svcCtx.UserRpc.Login(l.ctx, &usercenter.LoginReq{
|
||||
Username: req.Username,
|
||||
Passwd: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
logx.Errorf("rpc login err: %v", err)
|
||||
return nil, errors.New("login fail")
|
||||
}
|
||||
|
||||
return &types.LoginResp{
|
||||
UserId: res.Id,
|
||||
Username: res.Username,
|
||||
Email: res.Email,
|
||||
Token: res.Token,
|
||||
Expires: int64((7 * 24 * time.Hour).Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
"juwan-backend/app/users/rpc/usercenter"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -28,7 +30,14 @@ func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogi
|
||||
}
|
||||
|
||||
func (l *LogoutLogic) Logout(req *types.LogoutReq) (resp *types.LogoutResp, err error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return
|
||||
if req.UserId <= 0 {
|
||||
return nil, errors.New("invalid userId")
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.UserRpc.Logout(l.ctx, &usercenter.LogoutReq{UserId: req.UserId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.LogoutResp{Message: "logout success"}, nil
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"juwan-backend/app/users/api/internal/contextx"
|
||||
"regexp"
|
||||
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
"juwan-backend/app/users/api/internal/types"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
"juwan-backend/app/users/rpc/usercenter"
|
||||
"juwan-backend/common/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
@@ -31,40 +33,54 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register
|
||||
}
|
||||
}
|
||||
|
||||
var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_]+$")
|
||||
|
||||
func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) {
|
||||
// 检查用户是否已存在
|
||||
existingUser, err := l.svcCtx.UserRpc.GetUserByUsername(l.ctx, &pb.GetUserByUsernameReq{
|
||||
Username: req.Username,
|
||||
})
|
||||
if len(req.Username) < 3 {
|
||||
return nil, errors.New("username must be at least 3 characters long")
|
||||
}
|
||||
if len(req.Username) > 20 {
|
||||
return nil, errors.New("username must be at most 20 characters long")
|
||||
}
|
||||
if !usernameRegex.MatchString(req.Username) {
|
||||
return nil, errors.New("username can only contain letters, numbers, and underscores")
|
||||
}
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
// 生成用户ID
|
||||
userId, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.New("generate user ID failed")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := utils.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return nil, errors.New("hash password failed")
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
_res, err := l.svcCtx.UserRpc.AddUsers(l.ctx, &pb.AddUsersReq{
|
||||
UserId: userId.String(),
|
||||
requestId, err := contextx.RequestIdFrom(l.ctx)
|
||||
if err != nil {
|
||||
logx.Errorf("contextx.RequestIdFrom failed: %v", errjA)
|
||||
return nil, errors.New("contextx.RequestIdFrom failed")
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.UserRpc.Register(l.ctx, &usercenter.RegisterReq{
|
||||
Username: req.Username,
|
||||
Passwd: hashedPassword,
|
||||
Phone: req.Phone,
|
||||
State: true,
|
||||
Phone: req.Username,
|
||||
Vcode: req.Vcode,
|
||||
Email: req.Email,
|
||||
RequestId: requestId,
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("AddUsers failed: %v", err)
|
||||
return nil, errors.New("add user failed")
|
||||
logx.Error("failed to register user: ", err)
|
||||
return nil, errors.New("failed to register user")
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
return &types.RegisterResp{}, nil
|
||||
return &types.RegisterResp{
|
||||
UserId: 0,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Message: "register success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"juwan-backend/app/snowflake/rpc/snowflake"
|
||||
"juwan-backend/app/users/api/internal/config"
|
||||
"juwan-backend/app/users/api/internal/middleware"
|
||||
"juwan-backend/app/users/rpc/usercenter"
|
||||
"juwan-backend/common/snowflakex"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
@@ -16,6 +18,7 @@ type ServiceContext struct {
|
||||
Config config.Config
|
||||
Logger rest.Middleware
|
||||
UserRpc usercenter.Usercenter
|
||||
SnowflakeRpc snowflake.SnowflakeServiceClient
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
@@ -23,5 +26,6 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
Config: c,
|
||||
Logger: middleware.NewLoggerMiddleware().Handle,
|
||||
UserRpc: usercenter.NewUsercenter(zrpc.MustNewClient(c.UsercenterRpcConf)),
|
||||
SnowflakeRpc: snowflakex.NewClient(c.SnowflakeRpcConf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type LoginResp struct {
|
||||
|
||||
type LogoutReq struct {
|
||||
UserId int64 `path:"userId" binding:"required,gt=0"`
|
||||
Token string `header:"Authorization" binding:"required"`
|
||||
}
|
||||
|
||||
type LogoutResp struct {
|
||||
@@ -38,6 +39,7 @@ type RegisterReq struct {
|
||||
Password string `json:"password" binding:"required,min=6,max=128"`
|
||||
Email string `json:"email,omitempty" binding:"omitempty,email"`
|
||||
Phone string `json:"phone,omitempty" binding:"omitempty,len=11"`
|
||||
Vcode int32 `json:"vcode"`
|
||||
}
|
||||
|
||||
type RegisterResp struct {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.9.2
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"juwan-backend/app/users/api/internal/config"
|
||||
"juwan-backend/app/users/api/internal/handler"
|
||||
"juwan-backend/app/users/api/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/user-api.yaml", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
defer server.Stop()
|
||||
|
||||
ctx := svc.NewServiceContext(c)
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"errors"
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
utils2 "juwan-backend/app/users/rpc/internal/utils"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
"juwan-backend/common/utils"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -24,7 +26,29 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic
|
||||
}
|
||||
|
||||
func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.LoginResp{}, nil
|
||||
user, err := l.svcCtx.UsersModelRO.FindOneByUsername(l.ctx, in.Username)
|
||||
if err != nil {
|
||||
logx.WithContext(l.ctx).Errorf("LoginLogic.Login error:%v", err)
|
||||
return nil, err
|
||||
}
|
||||
if !utils.VerifyPassword(user.Passwd, in.Passwd) {
|
||||
logx.WithContext(l.ctx).Errorf("User %s Login failed", user.Username)
|
||||
return nil, errors.New("incorrect password")
|
||||
}
|
||||
|
||||
token, err := l.svcCtx.JwtManager.New(l.ctx, &utils2.TokenPayload{
|
||||
UserId: user.UserId,
|
||||
IsAdmin: false,
|
||||
})
|
||||
if err != nil {
|
||||
logx.Errorf("LoginLogic.Login gen jwt for user %v error:%v", user.UserId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.LoginResp{
|
||||
Token: token,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Id: user.UserId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type LogoutLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogic {
|
||||
return &LogoutLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogoutLogic) Logout(in *pb.LogoutReq) (*pb.LogoutResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
err := l.svcCtx.JwtManager.Logout(l.ctx, in.UserId)
|
||||
if err != nil {
|
||||
logx.WithContext(l.ctx).Errorf("Logout failed: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return &pb.LogoutResp{}, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"juwan-backend/app/snowflake/rpc/snowflake"
|
||||
"juwan-backend/app/users/rpc/internal/models"
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type RegisterLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
|
||||
return &RegisterLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewRandomNickname() string {
|
||||
bytes := make([]byte, 5)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "NewUser"
|
||||
}
|
||||
nickname := strings.Builder{}
|
||||
nickname.WriteString("user_")
|
||||
nickname.WriteString(hex.EncodeToString(bytes))
|
||||
return nickname.String()
|
||||
}
|
||||
|
||||
func (l *RegisterLogic) Register(in *pb.RegisterReq) (*pb.RegisterResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
if in.Phone == "" || in.Username == "" || in.Passwd == "" {
|
||||
logx.Error("invalid input")
|
||||
return nil, errors.New("invalid input")
|
||||
}
|
||||
|
||||
redisKey := fmt.Sprintf("vcode:%s:%s:%s", in.RequestId, "register", in.Email)
|
||||
vcode, err := l.svcCtx.RedisCluster.Get(l.ctx, redisKey).Result()
|
||||
logx.Infof("vcode:%s, err:%v", vcode, err)
|
||||
if err != nil {
|
||||
logx.Error("invalid verification code")
|
||||
return nil, errors.New("invalid verification code")
|
||||
}
|
||||
|
||||
code, err := strconv.ParseInt(vcode, 10, 32)
|
||||
if err != nil || int32(code) != in.Vcode {
|
||||
logx.Error("invalid verification code")
|
||||
return nil, errors.New("invalid verification code")
|
||||
}
|
||||
|
||||
resp, err := l.svcCtx.Snowflake.NextId(l.ctx, &snowflake.NextIdReq{})
|
||||
if err != nil {
|
||||
return nil, errors.New("generate user ID failed")
|
||||
}
|
||||
|
||||
user := models.Users{
|
||||
UserId: resp.Id,
|
||||
Username: in.Username,
|
||||
Nickname: mustNewRandomNickname(),
|
||||
Passwd: in.Passwd,
|
||||
Phone: in.Phone,
|
||||
Email: in.Email,
|
||||
RoleType: 0,
|
||||
IsVerified: false,
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.UsersModelRW.Insert(l.ctx, &user)
|
||||
if err != nil {
|
||||
logx.Error("failed to create user: ", err)
|
||||
return nil, errors.New("failed to create user")
|
||||
}
|
||||
|
||||
return &pb.RegisterResp{
|
||||
Res: "user registered successfully",
|
||||
}, nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
var USER_TOKEN_TEMP = "jwt:%v"
|
||||
|
||||
type ValidateTokenLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
@@ -24,7 +27,20 @@ func NewValidateTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Val
|
||||
}
|
||||
|
||||
func (l *ValidateTokenLogic) ValidateToken(in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.ValidateTokenResp{}, nil
|
||||
redisKey := fmt.Sprintf(USER_TOKEN_TEMP, in.UserId)
|
||||
_, err := l.svcCtx.JwtManager.Valid(l.ctx, redisKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := l.svcCtx.UsersModelRO.FindOne(l.ctx, in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.ValidateTokenResp{
|
||||
Valid: true,
|
||||
Message: "OK",
|
||||
UserId: in.UserId,
|
||||
RoleType: users.RoleType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+32
-6
@@ -25,6 +25,7 @@ var (
|
||||
usersRowsWithPlaceHolder = builder.PostgreSqlJoin(stringx.Remove(usersFieldNames, "user_id", "create_at", "create_time", "created_at", "update_at", "update_time", "updated_at"))
|
||||
|
||||
cachePublicUsersUserIdPrefix = "cache:public:users:userId:"
|
||||
cachePublicUsersEmailPrefix = "cache:public:users:email:"
|
||||
cachePublicUsersPhonePrefix = "cache:public:users:phone:"
|
||||
cachePublicUsersUsernamePrefix = "cache:public:users:username:"
|
||||
)
|
||||
@@ -33,6 +34,7 @@ type (
|
||||
usersModel interface {
|
||||
Insert(ctx context.Context, data *Users) (sql.Result, error)
|
||||
FindOne(ctx context.Context, userId int64) (*Users, error)
|
||||
FindOneByEmail(ctx context.Context, email string) (*Users, error)
|
||||
FindOneByPhone(ctx context.Context, phone string) (*Users, error)
|
||||
FindOneByUsername(ctx context.Context, username string) (*Users, error)
|
||||
Update(ctx context.Context, data *Users) error
|
||||
@@ -56,6 +58,7 @@ type (
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at"`
|
||||
Email string `db:"email"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -72,13 +75,14 @@ func (m *defaultUsersModel) Delete(ctx context.Context, userId int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
publicUsersEmailKey := fmt.Sprintf("%s%v", cachePublicUsersEmailPrefix, data.Email)
|
||||
publicUsersPhoneKey := fmt.Sprintf("%s%v", cachePublicUsersPhonePrefix, data.Phone)
|
||||
publicUsersUserIdKey := fmt.Sprintf("%s%v", cachePublicUsersUserIdPrefix, userId)
|
||||
publicUsersUsernameKey := fmt.Sprintf("%s%v", cachePublicUsersUsernamePrefix, data.Username)
|
||||
_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("delete from %s where user_id = $1", m.table)
|
||||
return conn.ExecCtx(ctx, query, userId)
|
||||
}, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
}, publicUsersEmailKey, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -99,6 +103,26 @@ func (m *defaultUsersModel) FindOne(ctx context.Context, userId int64) (*Users,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUsersModel) FindOneByEmail(ctx context.Context, email string) (*Users, error) {
|
||||
publicUsersEmailKey := fmt.Sprintf("%s%v", cachePublicUsersEmailPrefix, email)
|
||||
var resp Users
|
||||
err := m.QueryRowIndexCtx(ctx, &resp, publicUsersEmailKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) {
|
||||
query := fmt.Sprintf("select %s from %s where email = $1 limit 1", usersRows, m.table)
|
||||
if err := conn.QueryRowCtx(ctx, &resp, query, email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.UserId, nil
|
||||
}, m.queryPrimary)
|
||||
switch err {
|
||||
case nil:
|
||||
return &resp, nil
|
||||
case sqlc.ErrNotFound:
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultUsersModel) FindOneByPhone(ctx context.Context, phone string) (*Users, error) {
|
||||
publicUsersPhoneKey := fmt.Sprintf("%s%v", cachePublicUsersPhonePrefix, phone)
|
||||
var resp Users
|
||||
@@ -140,13 +164,14 @@ func (m *defaultUsersModel) FindOneByUsername(ctx context.Context, username stri
|
||||
}
|
||||
|
||||
func (m *defaultUsersModel) Insert(ctx context.Context, data *Users) (sql.Result, error) {
|
||||
publicUsersEmailKey := fmt.Sprintf("%s%v", cachePublicUsersEmailPrefix, data.Email)
|
||||
publicUsersPhoneKey := fmt.Sprintf("%s%v", cachePublicUsersPhonePrefix, data.Phone)
|
||||
publicUsersUserIdKey := fmt.Sprintf("%s%v", cachePublicUsersUserIdPrefix, data.UserId)
|
||||
publicUsersUsernameKey := fmt.Sprintf("%s%v", cachePublicUsersUsernamePrefix, data.Username)
|
||||
ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("insert into %s (%s) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)", m.table, usersRowsExpectAutoSet)
|
||||
return conn.ExecCtx(ctx, query, data.UserId, data.Username, data.Passwd, data.Nickname, data.Phone, data.RoleType, data.IsVerified, data.State, data.DeletedAt)
|
||||
}, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
query := fmt.Sprintf("insert into %s (%s) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", m.table, usersRowsExpectAutoSet)
|
||||
return conn.ExecCtx(ctx, query, data.UserId, data.Username, data.Passwd, data.Nickname, data.Phone, data.RoleType, data.IsVerified, data.State, data.DeletedAt, data.Email)
|
||||
}, publicUsersEmailKey, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
return ret, err
|
||||
}
|
||||
|
||||
@@ -156,13 +181,14 @@ func (m *defaultUsersModel) Update(ctx context.Context, newData *Users) error {
|
||||
return err
|
||||
}
|
||||
|
||||
publicUsersEmailKey := fmt.Sprintf("%s%v", cachePublicUsersEmailPrefix, data.Email)
|
||||
publicUsersPhoneKey := fmt.Sprintf("%s%v", cachePublicUsersPhonePrefix, data.Phone)
|
||||
publicUsersUserIdKey := fmt.Sprintf("%s%v", cachePublicUsersUserIdPrefix, data.UserId)
|
||||
publicUsersUsernameKey := fmt.Sprintf("%s%v", cachePublicUsersUsernamePrefix, data.Username)
|
||||
_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
|
||||
query := fmt.Sprintf("update %s set %s where user_id = $1", m.table, usersRowsWithPlaceHolder)
|
||||
return conn.ExecCtx(ctx, query, newData.UserId, newData.Username, newData.Passwd, newData.Nickname, newData.Phone, newData.RoleType, newData.IsVerified, newData.State, newData.DeletedAt)
|
||||
}, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
return conn.ExecCtx(ctx, query, newData.UserId, newData.Username, newData.Passwd, newData.Nickname, newData.Phone, newData.RoleType, newData.IsVerified, newData.State, newData.DeletedAt, newData.Email)
|
||||
}, publicUsersEmailKey, publicUsersPhoneKey, publicUsersUserIdKey, publicUsersUsernameKey)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ func (s *UsercenterServer) Login(ctx context.Context, in *pb.LoginReq) (*pb.Logi
|
||||
return l.Login(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) Register(ctx context.Context, in *pb.RegisterReq) (*pb.RegisterResp, error) {
|
||||
l := logic.NewRegisterLogic(ctx, s.svcCtx)
|
||||
return l.Register(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) ValidateToken(ctx context.Context, in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||
l := logic.NewValidateTokenLogic(ctx, s.svcCtx)
|
||||
return l.ValidateToken(in)
|
||||
@@ -68,3 +73,8 @@ func (s *UsercenterServer) CheckPermission(ctx context.Context, in *pb.CheckPerm
|
||||
l := logic.NewCheckPermissionLogic(ctx, s.svcCtx)
|
||||
return l.CheckPermission(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) Logout(ctx context.Context, in *pb.LogoutReq) (*pb.LogoutResp, error) {
|
||||
l := logic.NewLogoutLogic(ctx, s.svcCtx)
|
||||
return l.Logout(in)
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// JWKS (JSON Web Key Set) 结构
|
||||
type JWKSKey struct {
|
||||
Kty string `json:"kty"`
|
||||
Use string `json:"use"`
|
||||
Kid string `json:"kid"`
|
||||
N string `json:"n,omitempty"`
|
||||
E string `json:"e,omitempty"`
|
||||
K string `json:"k,omitempty"` // 对称密钥
|
||||
Alg string `json:"alg"`
|
||||
}
|
||||
|
||||
type JWKS struct {
|
||||
Keys []JWKSKey `json:"keys"`
|
||||
}
|
||||
|
||||
// GenerateJWKSFromSecret 从密钥生成 JWKS(用于对称加密 HS256)
|
||||
func GenerateJWKSFromSecret(secretKey string, keyID string) *JWKS {
|
||||
// 对于 HS256,将密钥进行 base64 编码
|
||||
encodedSecret := base64.RawURLEncoding.EncodeToString([]byte(secretKey))
|
||||
|
||||
return &JWKS{
|
||||
Keys: []JWKSKey{
|
||||
{
|
||||
Kty: "oct",
|
||||
Use: "sig",
|
||||
Kid: keyID,
|
||||
K: encodedSecret,
|
||||
Alg: "HS256",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJWKSEndpoint 生成可以被 Envoy 使用的 JWKS JSON
|
||||
// 此端点应在 user-rpc 中暴露,URL 为 /.well-known/jwks.json
|
||||
func GenerateJWKSEndpoint(secretKey string, keyID string) (string, error) {
|
||||
if secretKey == "" {
|
||||
return "", fmt.Errorf("secret key cannot be empty")
|
||||
}
|
||||
|
||||
jwks := GenerateJWKSFromSecret(secretKey, keyID)
|
||||
|
||||
jsonData, err := json.MarshalIndent(jwks, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// TokenPayload 令牌负载
|
||||
type TokenMetadata struct {
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
Subject string // userId
|
||||
Issuer string
|
||||
Audience string
|
||||
}
|
||||
|
||||
// ExtractTokenMetadata 从 token 中提取元数据(不验证签名)
|
||||
func ExtractTokenMetadata(tokenString string) (*TokenMetadata, error) {
|
||||
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims type")
|
||||
}
|
||||
|
||||
return &TokenMetadata{
|
||||
IssuedAt: claims.IssuedAt.Time,
|
||||
ExpiresAt: claims.ExpiresAt.Time,
|
||||
Subject: claims.UserId,
|
||||
Issuer: claims.Issuer,
|
||||
Audience: "", // 如果需要,可以增加到 Claims 中
|
||||
}, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type TokenPayload struct {
|
||||
UserId string
|
||||
UserId int64
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ var (
|
||||
errInvalidToken = errors.New("invalid token claims")
|
||||
errTokenNotInCache = errors.New("token not found in cache")
|
||||
errNoRedisClient = errors.New("redis client not configured")
|
||||
errInvalidUserID = errors.New("invalid user id")
|
||||
// errExpiredToken = errors.New("token expired")
|
||||
)
|
||||
|
||||
@@ -74,8 +76,7 @@ func (m *JwtManager) New(ctx context.Context, payload *TokenPayload) (string, er
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 存储 token 到 Redis,TTL 为 30 天
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
userKey := tokenCachePrefixUser + strconv.FormatInt(claims.UserId, 10)
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
|
||||
tokenData, _ := json.Marshal(payload)
|
||||
@@ -105,12 +106,12 @@ func (m *JwtManager) Valid(ctx context.Context, tokenString string) (*TokenPaylo
|
||||
// 检查 token 是否在 Redis 中
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||
if err != nil && err != redis.Nil {
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payload TokenPayload
|
||||
if err == redis.Nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, errTokenNotInCache
|
||||
}
|
||||
|
||||
@@ -125,6 +126,20 @@ func (m *JwtManager) Valid(ctx context.Context, tokenString string) (*TokenPaylo
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
if _, renewErr := m.Renew(ctx, tokenString); renewErr != nil {
|
||||
return nil, renewErr
|
||||
}
|
||||
|
||||
if token != nil {
|
||||
if claims, ok := token.Claims.(*Claims); ok {
|
||||
return &claims.TokenPayload, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -146,7 +161,7 @@ func (m *JwtManager) Renew(ctx context.Context, tokenString string) (string, err
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return "", errTokenNotInCache
|
||||
}
|
||||
return "", err
|
||||
@@ -159,15 +174,15 @@ func (m *JwtManager) Renew(ctx context.Context, tokenString string) (string, err
|
||||
}
|
||||
|
||||
// 删除旧 token 记录
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
userKey := tokenCachePrefixUser + strconv.FormatInt(payload.UserId, 10)
|
||||
m.redisCluster.Del(ctx, tokenKey, userKey)
|
||||
|
||||
// 生成新 token
|
||||
return m.New(ctx, &payload)
|
||||
}
|
||||
|
||||
// extract payload from token without validating expiration (used for auto-renewal)
|
||||
func (m *JwtManager) Extract(ctx context.Context, tokenString string) (*TokenPayload, error) {
|
||||
// Extract payload from token without validating expiration (used for auto-renewal)
|
||||
func (m *JwtManager) Extract(_ context.Context, tokenString string) (*TokenPayload, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(m.secretKey), nil
|
||||
})
|
||||
@@ -184,7 +199,7 @@ func (m *JwtManager) Extract(ctx context.Context, tokenString string) (*TokenPay
|
||||
return &claims.TokenPayload, nil
|
||||
}
|
||||
|
||||
// check if token exists in Redis (i.e. is valid and not revoked)
|
||||
// Exists check if token exists in Redis (i.e. is valid and not revoked)
|
||||
func (m *JwtManager) Exists(ctx context.Context, tokenString string) (bool, error) {
|
||||
if m.redisCluster == nil {
|
||||
return false, errNoRedisClient
|
||||
@@ -199,12 +214,12 @@ func (m *JwtManager) Exists(ctx context.Context, tokenString string) (bool, erro
|
||||
return exists > 0, nil
|
||||
}
|
||||
|
||||
// extract payload from JWT claims
|
||||
// ClaimsToPayload extract payload from JWT claims
|
||||
func (m *JwtManager) ClaimsToPayload(claims *Claims) *TokenPayload {
|
||||
return &claims.TokenPayload
|
||||
}
|
||||
|
||||
// revoke token by deleting both user -> token and token -> payload keys from Redis
|
||||
// Revoke revoke token by deleting both user -> token and token -> payload keys from Redis
|
||||
func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
|
||||
if m.redisCluster == nil {
|
||||
return errNoRedisClient
|
||||
@@ -215,7 +230,7 @@ func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
userKey := tokenCachePrefixUser + strconv.FormatInt(payload.UserId, 10)
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
|
||||
pipe := m.redisCluster.Pipeline()
|
||||
@@ -225,19 +240,48 @@ func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *JwtManager) GetUserToken(ctx context.Context, userID string) (string, error) {
|
||||
func (m *JwtManager) GetUserToken(ctx context.Context, userID int64) (string, error) {
|
||||
if m.redisCluster == nil {
|
||||
return "", errNoRedisClient
|
||||
}
|
||||
//userID, err := strconv.FormatInt(userID, 10)
|
||||
id := strconv.FormatInt(userID, 10)
|
||||
|
||||
userKey := tokenCachePrefixUser + userID
|
||||
userKey := tokenCachePrefixUser + id
|
||||
token, err := m.redisCluster.Get(ctx, userKey).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("user %s has no token", userID)
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return "", fmt.Errorf("user %v has no token", userID)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Logout 按用户登出:删除 user->token 和 token->payload 两类缓存数据
|
||||
func (m *JwtManager) Logout(ctx context.Context, userID int64) error {
|
||||
if m.redisCluster == nil {
|
||||
return errNoRedisClient
|
||||
}
|
||||
|
||||
if userID <= 0 {
|
||||
return errInvalidUserID
|
||||
}
|
||||
|
||||
userKey := tokenCachePrefixUser + strconv.FormatInt(userID, 10)
|
||||
tokenString, err := m.redisCluster.Get(ctx, userKey).Result()
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return err
|
||||
}
|
||||
|
||||
pipe := m.redisCluster.Pipeline()
|
||||
pipe.Del(ctx, userKey)
|
||||
if !errors.Is(err, redis.Nil) && tokenString != "" {
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
pipe.Del(ctx, tokenKey)
|
||||
}
|
||||
|
||||
_, execErr := pipe.Exec(ctx)
|
||||
return execErr
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/core/service"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
|
||||
+308
-50
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc v6.32.0
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.29.6
|
||||
// source: users.proto
|
||||
|
||||
package pb
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
// --------------------------------users--------------------------------
|
||||
type Users struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
UserId int64 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` //username
|
||||
Passwd string `protobuf:"bytes,3,opt,name=passwd,proto3" json:"passwd,omitempty"` //passwd
|
||||
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` //nickname
|
||||
@@ -69,11 +69,11 @@ func (*Users) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Users) GetUserId() string {
|
||||
func (x *Users) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Users) GetUsername() string {
|
||||
@@ -148,7 +148,7 @@ func (x *Users) GetDeletedAt() int64 {
|
||||
|
||||
type AddUsersReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
UserId int64 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` //username
|
||||
Passwd string `protobuf:"bytes,3,opt,name=passwd,proto3" json:"passwd,omitempty"` //passwd
|
||||
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` //nickname
|
||||
@@ -193,11 +193,11 @@ func (*AddUsersReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AddUsersReq) GetUserId() string {
|
||||
func (x *AddUsersReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *AddUsersReq) GetUsername() string {
|
||||
@@ -308,7 +308,7 @@ func (*AddUsersResp) Descriptor() ([]byte, []int) {
|
||||
|
||||
type UpdateUsersReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
UserId int64 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` //username
|
||||
Passwd string `protobuf:"bytes,3,opt,name=passwd,proto3" json:"passwd,omitempty"` //passwd
|
||||
Nickname string `protobuf:"bytes,4,opt,name=nickname,proto3" json:"nickname,omitempty"` //nickname
|
||||
@@ -353,11 +353,11 @@ func (*UpdateUsersReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *UpdateUsersReq) GetUserId() string {
|
||||
func (x *UpdateUsersReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *UpdateUsersReq) GetUsername() string {
|
||||
@@ -638,7 +638,7 @@ type SearchUsersReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Page int64 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` //page
|
||||
Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` //limit
|
||||
UserId string `protobuf:"bytes,3,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
UserId int64 `protobuf:"varint,3,opt,name=userId,proto3" json:"userId,omitempty"` //userId
|
||||
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` //username
|
||||
Passwd string `protobuf:"bytes,5,opt,name=passwd,proto3" json:"passwd,omitempty"` //passwd
|
||||
Nickname string `protobuf:"bytes,6,opt,name=nickname,proto3" json:"nickname,omitempty"` //nickname
|
||||
@@ -697,11 +697,11 @@ func (x *SearchUsersReq) GetLimit() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SearchUsersReq) GetUserId() string {
|
||||
func (x *SearchUsersReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SearchUsersReq) GetUsername() string {
|
||||
@@ -961,6 +961,9 @@ func (x *LoginReq) GetPasswd() string {
|
||||
type LoginResp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
|
||||
Id int64 `protobuf:"varint,4,opt,name=id,proto3" json:"id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1002,10 +1005,31 @@ func (x *LoginResp) GetToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *LoginResp) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *LoginResp) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *LoginResp) GetId() int64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ValidateTokenReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // JWT token
|
||||
UserId string `protobuf:"bytes,2,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
UserId int64 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1047,18 +1071,18 @@ func (x *ValidateTokenReq) GetToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ValidateTokenReq) GetUserId() string {
|
||||
func (x *ValidateTokenReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
type ValidateTokenResp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` // token 是否有效(不在黑名单中)
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 验证失败原因
|
||||
UserId string `protobuf:"bytes,3,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
UserId int64 `protobuf:"varint,3,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
RoleType int64 `protobuf:"varint,4,opt,name=roleType,proto3" json:"roleType,omitempty"` // 用户角色
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -1108,11 +1132,11 @@ func (x *ValidateTokenResp) GetMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ValidateTokenResp) GetUserId() string {
|
||||
func (x *ValidateTokenResp) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ValidateTokenResp) GetRoleType() int64 {
|
||||
@@ -1124,7 +1148,7 @@ func (x *ValidateTokenResp) GetRoleType() int64 {
|
||||
|
||||
type CheckPermissionReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId string `protobuf:"bytes,1,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
UserId int64 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"` // 用户ID
|
||||
Resource string `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` // 资源 ID
|
||||
Action string `protobuf:"bytes,3,opt,name=action,proto3" json:"action,omitempty"` // 操作类型: read/write/delete
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@@ -1161,11 +1185,11 @@ func (*CheckPermissionReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *CheckPermissionReq) GetUserId() string {
|
||||
func (x *CheckPermissionReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CheckPermissionReq) GetResource() string {
|
||||
@@ -1234,13 +1258,221 @@ func (x *CheckPermissionResp) GetMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type RegisterReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Passwd string `protobuf:"bytes,2,opt,name=passwd,proto3" json:"passwd,omitempty"`
|
||||
Phone string `protobuf:"bytes,3,opt,name=phone,proto3" json:"phone,omitempty"`
|
||||
Vcode int32 `protobuf:"varint,4,opt,name=vcode,proto3" json:"vcode,omitempty"`
|
||||
Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"`
|
||||
RequestId string `protobuf:"bytes,6,opt,name=requestId,proto3" json:"requestId,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterReq) Reset() {
|
||||
*x = RegisterReq{}
|
||||
mi := &file_users_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RegisterReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RegisterReq) ProtoMessage() {}
|
||||
|
||||
func (x *RegisterReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RegisterReq.ProtoReflect.Descriptor instead.
|
||||
func (*RegisterReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetPasswd() string {
|
||||
if x != nil {
|
||||
return x.Passwd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetPhone() string {
|
||||
if x != nil {
|
||||
return x.Phone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetVcode() int32 {
|
||||
if x != nil {
|
||||
return x.Vcode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterReq) GetRequestId() string {
|
||||
if x != nil {
|
||||
return x.RequestId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RegisterResp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Res string `protobuf:"bytes,1,opt,name=res,proto3" json:"res,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RegisterResp) Reset() {
|
||||
*x = RegisterResp{}
|
||||
mi := &file_users_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RegisterResp) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RegisterResp) ProtoMessage() {}
|
||||
|
||||
func (x *RegisterResp) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RegisterResp.ProtoReflect.Descriptor instead.
|
||||
func (*RegisterResp) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *RegisterResp) GetRes() string {
|
||||
if x != nil {
|
||||
return x.Res
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type LogoutReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId int64 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogoutReq) Reset() {
|
||||
*x = LogoutReq{}
|
||||
mi := &file_users_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*LogoutReq) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_proto_msgTypes[21]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use LogoutReq.ProtoReflect.Descriptor instead.
|
||||
func (*LogoutReq) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *LogoutReq) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type LogoutResp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LogoutResp) Reset() {
|
||||
*x = LogoutResp{}
|
||||
mi := &file_users_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutResp) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*LogoutResp) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutResp) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_users_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use LogoutResp.ProtoReflect.Descriptor instead.
|
||||
func (*LogoutResp) Descriptor() ([]byte, []int) {
|
||||
return file_users_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
var File_users_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_users_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vusers.proto\x12\x02pb\"\xb1\x02\n" +
|
||||
"\x05Users\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\busername\x18\x02 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x03 \x01(\tR\x06passwd\x12\x1a\n" +
|
||||
"\bnickname\x18\x04 \x01(\tR\bnickname\x12\x14\n" +
|
||||
@@ -1255,7 +1487,7 @@ const file_users_proto_rawDesc = "" +
|
||||
" \x01(\x03R\tupdatedAt\x12\x1c\n" +
|
||||
"\tdeletedAt\x18\v \x01(\x03R\tdeletedAt\"\xb7\x02\n" +
|
||||
"\vAddUsersReq\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\busername\x18\x02 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x03 \x01(\tR\x06passwd\x12\x1a\n" +
|
||||
"\bnickname\x18\x04 \x01(\tR\bnickname\x12\x14\n" +
|
||||
@@ -1271,7 +1503,7 @@ const file_users_proto_rawDesc = "" +
|
||||
"\tdeletedAt\x18\v \x01(\x03R\tdeletedAt\"\x0e\n" +
|
||||
"\fAddUsersResp\"\xba\x02\n" +
|
||||
"\x0eUpdateUsersReq\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\busername\x18\x02 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x03 \x01(\tR\x06passwd\x12\x1a\n" +
|
||||
"\bnickname\x18\x04 \x01(\tR\bnickname\x12\x14\n" +
|
||||
@@ -1296,7 +1528,7 @@ const file_users_proto_rawDesc = "" +
|
||||
"\x0eSearchUsersReq\x12\x12\n" +
|
||||
"\x04page\x18\x01 \x01(\x03R\x04page\x12\x14\n" +
|
||||
"\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x16\n" +
|
||||
"\x06userId\x18\x03 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x03 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\busername\x18\x04 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x05 \x01(\tR\x06passwd\x12\x1a\n" +
|
||||
"\bnickname\x18\x06 \x01(\tR\bnickname\x12\x14\n" +
|
||||
@@ -1318,24 +1550,40 @@ const file_users_proto_rawDesc = "" +
|
||||
"\x05users\x18\x01 \x01(\v2\t.pb.UsersR\x05users\">\n" +
|
||||
"\bLoginReq\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x02 \x01(\tR\x06passwd\"!\n" +
|
||||
"\x06passwd\x18\x02 \x01(\tR\x06passwd\"c\n" +
|
||||
"\tLoginResp\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\"@\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n" +
|
||||
"\busername\x18\x02 \x01(\tR\busername\x12\x14\n" +
|
||||
"\x05email\x18\x03 \x01(\tR\x05email\x12\x0e\n" +
|
||||
"\x02id\x18\x04 \x01(\x03R\x02id\"@\n" +
|
||||
"\x10ValidateTokenReq\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\x12\x16\n" +
|
||||
"\x06userId\x18\x02 \x01(\tR\x06userId\"w\n" +
|
||||
"\x06userId\x18\x02 \x01(\x03R\x06userId\"w\n" +
|
||||
"\x11ValidateTokenResp\x12\x14\n" +
|
||||
"\x05valid\x18\x01 \x01(\bR\x05valid\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\x16\n" +
|
||||
"\x06userId\x18\x03 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x03 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\broleType\x18\x04 \x01(\x03R\broleType\"`\n" +
|
||||
"\x12CheckPermissionReq\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\tR\x06userId\x12\x1a\n" +
|
||||
"\x06userId\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" +
|
||||
"\bresource\x18\x02 \x01(\tR\bresource\x12\x16\n" +
|
||||
"\x06action\x18\x03 \x01(\tR\x06action\"I\n" +
|
||||
"\x13CheckPermissionResp\x12\x18\n" +
|
||||
"\aallowed\x18\x01 \x01(\bR\aallowed\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage2\x87\x04\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\"\xa1\x01\n" +
|
||||
"\vRegisterReq\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x16\n" +
|
||||
"\x06passwd\x18\x02 \x01(\tR\x06passwd\x12\x14\n" +
|
||||
"\x05phone\x18\x03 \x01(\tR\x05phone\x12\x14\n" +
|
||||
"\x05vcode\x18\x04 \x01(\x05R\x05vcode\x12\x14\n" +
|
||||
"\x05email\x18\x05 \x01(\tR\x05email\x12\x1c\n" +
|
||||
"\trequestId\x18\x06 \x01(\tR\trequestId\" \n" +
|
||||
"\fRegisterResp\x12\x10\n" +
|
||||
"\x03res\x18\x01 \x01(\tR\x03res\"#\n" +
|
||||
"\tLogoutReq\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\x03R\x06userId\"\f\n" +
|
||||
"\n" +
|
||||
"LogoutResp2\xdf\x04\n" +
|
||||
"\n" +
|
||||
"usercenter\x12-\n" +
|
||||
"\bAddUsers\x12\x0f.pb.AddUsersReq\x1a\x10.pb.AddUsersResp\x126\n" +
|
||||
@@ -1344,9 +1592,11 @@ const file_users_proto_rawDesc = "" +
|
||||
"\fGetUsersById\x12\x13.pb.GetUsersByIdReq\x1a\x14.pb.GetUsersByIdResp\x12H\n" +
|
||||
"\x11GetUserByUsername\x12\x18.pb.GetUserByUsernameReq\x1a\x19.pb.GetUserByUsernameResp\x126\n" +
|
||||
"\vSearchUsers\x12\x12.pb.SearchUsersReq\x1a\x13.pb.SearchUsersResp\x12$\n" +
|
||||
"\x05Login\x12\f.pb.LoginReq\x1a\r.pb.LoginResp\x12<\n" +
|
||||
"\x05Login\x12\f.pb.LoginReq\x1a\r.pb.LoginResp\x12-\n" +
|
||||
"\bRegister\x12\x0f.pb.RegisterReq\x1a\x10.pb.RegisterResp\x12<\n" +
|
||||
"\rValidateToken\x12\x14.pb.ValidateTokenReq\x1a\x15.pb.ValidateTokenResp\x12B\n" +
|
||||
"\x0fCheckPermission\x12\x16.pb.CheckPermissionReq\x1a\x17.pb.CheckPermissionRespB\x06Z\x04./pbb\x06proto3"
|
||||
"\x0fCheckPermission\x12\x16.pb.CheckPermissionReq\x1a\x17.pb.CheckPermissionResp\x12'\n" +
|
||||
"\x06Logout\x12\r.pb.LogoutReq\x1a\x0e.pb.LogoutRespB\x06Z\x04./pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_users_proto_rawDescOnce sync.Once
|
||||
@@ -1360,7 +1610,7 @@ func file_users_proto_rawDescGZIP() []byte {
|
||||
return file_users_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||
var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
|
||||
var file_users_proto_goTypes = []any{
|
||||
(*Users)(nil), // 0: pb.Users
|
||||
(*AddUsersReq)(nil), // 1: pb.AddUsersReq
|
||||
@@ -1381,6 +1631,10 @@ var file_users_proto_goTypes = []any{
|
||||
(*ValidateTokenResp)(nil), // 16: pb.ValidateTokenResp
|
||||
(*CheckPermissionReq)(nil), // 17: pb.CheckPermissionReq
|
||||
(*CheckPermissionResp)(nil), // 18: pb.CheckPermissionResp
|
||||
(*RegisterReq)(nil), // 19: pb.RegisterReq
|
||||
(*RegisterResp)(nil), // 20: pb.RegisterResp
|
||||
(*LogoutReq)(nil), // 21: pb.LogoutReq
|
||||
(*LogoutResp)(nil), // 22: pb.LogoutResp
|
||||
}
|
||||
var file_users_proto_depIdxs = []int32{
|
||||
0, // 0: pb.GetUsersByIdResp.users:type_name -> pb.Users
|
||||
@@ -1393,19 +1647,23 @@ var file_users_proto_depIdxs = []int32{
|
||||
11, // 7: pb.usercenter.GetUserByUsername:input_type -> pb.GetUserByUsernameReq
|
||||
9, // 8: pb.usercenter.SearchUsers:input_type -> pb.SearchUsersReq
|
||||
13, // 9: pb.usercenter.Login:input_type -> pb.LoginReq
|
||||
15, // 10: pb.usercenter.ValidateToken:input_type -> pb.ValidateTokenReq
|
||||
17, // 11: pb.usercenter.CheckPermission:input_type -> pb.CheckPermissionReq
|
||||
2, // 12: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp
|
||||
4, // 13: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp
|
||||
6, // 14: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp
|
||||
8, // 15: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp
|
||||
12, // 16: pb.usercenter.GetUserByUsername:output_type -> pb.GetUserByUsernameResp
|
||||
10, // 17: pb.usercenter.SearchUsers:output_type -> pb.SearchUsersResp
|
||||
14, // 18: pb.usercenter.Login:output_type -> pb.LoginResp
|
||||
16, // 19: pb.usercenter.ValidateToken:output_type -> pb.ValidateTokenResp
|
||||
18, // 20: pb.usercenter.CheckPermission:output_type -> pb.CheckPermissionResp
|
||||
12, // [12:21] is the sub-list for method output_type
|
||||
3, // [3:12] is the sub-list for method input_type
|
||||
19, // 10: pb.usercenter.Register:input_type -> pb.RegisterReq
|
||||
15, // 11: pb.usercenter.ValidateToken:input_type -> pb.ValidateTokenReq
|
||||
17, // 12: pb.usercenter.CheckPermission:input_type -> pb.CheckPermissionReq
|
||||
21, // 13: pb.usercenter.Logout:input_type -> pb.LogoutReq
|
||||
2, // 14: pb.usercenter.AddUsers:output_type -> pb.AddUsersResp
|
||||
4, // 15: pb.usercenter.UpdateUsers:output_type -> pb.UpdateUsersResp
|
||||
6, // 16: pb.usercenter.DelUsers:output_type -> pb.DelUsersResp
|
||||
8, // 17: pb.usercenter.GetUsersById:output_type -> pb.GetUsersByIdResp
|
||||
12, // 18: pb.usercenter.GetUserByUsername:output_type -> pb.GetUserByUsernameResp
|
||||
10, // 19: pb.usercenter.SearchUsers:output_type -> pb.SearchUsersResp
|
||||
14, // 20: pb.usercenter.Login:output_type -> pb.LoginResp
|
||||
20, // 21: pb.usercenter.Register:output_type -> pb.RegisterResp
|
||||
16, // 22: pb.usercenter.ValidateToken:output_type -> pb.ValidateTokenResp
|
||||
18, // 23: pb.usercenter.CheckPermission:output_type -> pb.CheckPermissionResp
|
||||
22, // 24: pb.usercenter.Logout:output_type -> pb.LogoutResp
|
||||
14, // [14:25] is the sub-list for method output_type
|
||||
3, // [3:14] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
@@ -1422,7 +1680,7 @@ func file_users_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_users_proto_rawDesc), len(file_users_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 19,
|
||||
NumMessages: 23,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.32.0
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc v5.29.6
|
||||
// source: users.proto
|
||||
|
||||
package pb
|
||||
@@ -26,8 +26,10 @@ const (
|
||||
Usercenter_GetUserByUsername_FullMethodName = "/pb.usercenter/GetUserByUsername"
|
||||
Usercenter_SearchUsers_FullMethodName = "/pb.usercenter/SearchUsers"
|
||||
Usercenter_Login_FullMethodName = "/pb.usercenter/Login"
|
||||
Usercenter_Register_FullMethodName = "/pb.usercenter/Register"
|
||||
Usercenter_ValidateToken_FullMethodName = "/pb.usercenter/ValidateToken"
|
||||
Usercenter_CheckPermission_FullMethodName = "/pb.usercenter/CheckPermission"
|
||||
Usercenter_Logout_FullMethodName = "/pb.usercenter/Logout"
|
||||
)
|
||||
|
||||
// UsercenterClient is the client API for Usercenter service.
|
||||
@@ -42,8 +44,10 @@ type UsercenterClient interface {
|
||||
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
||||
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
||||
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
|
||||
Register(ctx context.Context, in *RegisterReq, opts ...grpc.CallOption) (*RegisterResp, error)
|
||||
ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error)
|
||||
CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error)
|
||||
Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error)
|
||||
}
|
||||
|
||||
type usercenterClient struct {
|
||||
@@ -124,6 +128,16 @@ func (c *usercenterClient) Login(ctx context.Context, in *LoginReq, opts ...grpc
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *usercenterClient) Register(ctx context.Context, in *RegisterReq, opts ...grpc.CallOption) (*RegisterResp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RegisterResp)
|
||||
err := c.cc.Invoke(ctx, Usercenter_Register_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *usercenterClient) ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ValidateTokenResp)
|
||||
@@ -144,6 +158,16 @@ func (c *usercenterClient) CheckPermission(ctx context.Context, in *CheckPermiss
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *usercenterClient) Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(LogoutResp)
|
||||
err := c.cc.Invoke(ctx, Usercenter_Logout_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UsercenterServer is the server API for Usercenter service.
|
||||
// All implementations must embed UnimplementedUsercenterServer
|
||||
// for forward compatibility.
|
||||
@@ -156,8 +180,10 @@ type UsercenterServer interface {
|
||||
GetUserByUsername(context.Context, *GetUserByUsernameReq) (*GetUserByUsernameResp, error)
|
||||
SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error)
|
||||
Login(context.Context, *LoginReq) (*LoginResp, error)
|
||||
Register(context.Context, *RegisterReq) (*RegisterResp, error)
|
||||
ValidateToken(context.Context, *ValidateTokenReq) (*ValidateTokenResp, error)
|
||||
CheckPermission(context.Context, *CheckPermissionReq) (*CheckPermissionResp, error)
|
||||
Logout(context.Context, *LogoutReq) (*LogoutResp, error)
|
||||
mustEmbedUnimplementedUsercenterServer()
|
||||
}
|
||||
|
||||
@@ -169,31 +195,37 @@ type UsercenterServer interface {
|
||||
type UnimplementedUsercenterServer struct{}
|
||||
|
||||
func (UnimplementedUsercenterServer) AddUsers(context.Context, *AddUsersReq) (*AddUsersResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AddUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) UpdateUsers(context.Context, *UpdateUsersReq) (*UpdateUsersResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) DelUsers(context.Context, *DelUsersReq) (*DelUsersResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DelUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DelUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) GetUsersById(context.Context, *GetUsersByIdReq) (*GetUsersByIdResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUsersById not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUsersById not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) GetUserByUsername(context.Context, *GetUserByUsernameReq) (*GetUserByUsernameResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserByUsername not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserByUsername not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) SearchUsers(context.Context, *SearchUsersReq) (*SearchUsersResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SearchUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SearchUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) Login(context.Context, *LoginReq) (*LoginResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Login not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) Register(context.Context, *RegisterReq) (*RegisterResp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Register not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) ValidateToken(context.Context, *ValidateTokenReq) (*ValidateTokenResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ValidateToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ValidateToken not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) CheckPermission(context.Context, *CheckPermissionReq) (*CheckPermissionResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckPermission not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckPermission not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) Logout(context.Context, *LogoutReq) (*LogoutResp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
|
||||
}
|
||||
func (UnimplementedUsercenterServer) mustEmbedUnimplementedUsercenterServer() {}
|
||||
func (UnimplementedUsercenterServer) testEmbeddedByValue() {}
|
||||
@@ -206,7 +238,7 @@ type UnsafeUsercenterServer interface {
|
||||
}
|
||||
|
||||
func RegisterUsercenterServer(s grpc.ServiceRegistrar, srv UsercenterServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUsercenterServer was
|
||||
// If the following call panics, it indicates UnimplementedUsercenterServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@@ -342,6 +374,24 @@ func _Usercenter_Login_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Usercenter_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RegisterReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UsercenterServer).Register(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Usercenter_Register_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UsercenterServer).Register(ctx, req.(*RegisterReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Usercenter_ValidateToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ValidateTokenReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -378,6 +428,24 @@ func _Usercenter_CheckPermission_Handler(srv interface{}, ctx context.Context, d
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Usercenter_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(LogoutReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UsercenterServer).Logout(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Usercenter_Logout_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UsercenterServer).Logout(ctx, req.(*LogoutReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Usercenter_ServiceDesc is the grpc.ServiceDesc for Usercenter service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -413,6 +481,10 @@ var Usercenter_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Login",
|
||||
Handler: _Usercenter_Login_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Register",
|
||||
Handler: _Usercenter_Register_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ValidateToken",
|
||||
Handler: _Usercenter_ValidateToken_Handler,
|
||||
@@ -421,6 +493,10 @@ var Usercenter_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CheckPermission",
|
||||
Handler: _Usercenter_CheckPermission_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Logout",
|
||||
Handler: _Usercenter_Logout_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "users.proto",
|
||||
|
||||
@@ -26,6 +26,10 @@ type (
|
||||
GetUsersByIdResp = pb.GetUsersByIdResp
|
||||
LoginReq = pb.LoginReq
|
||||
LoginResp = pb.LoginResp
|
||||
LogoutReq = pb.LogoutReq
|
||||
LogoutResp = pb.LogoutResp
|
||||
RegisterReq = pb.RegisterReq
|
||||
RegisterResp = pb.RegisterResp
|
||||
SearchUsersReq = pb.SearchUsersReq
|
||||
SearchUsersResp = pb.SearchUsersResp
|
||||
UpdateUsersReq = pb.UpdateUsersReq
|
||||
@@ -43,8 +47,10 @@ type (
|
||||
GetUserByUsername(ctx context.Context, in *GetUserByUsernameReq, opts ...grpc.CallOption) (*GetUserByUsernameResp, error)
|
||||
SearchUsers(ctx context.Context, in *SearchUsersReq, opts ...grpc.CallOption) (*SearchUsersResp, error)
|
||||
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginResp, error)
|
||||
Register(ctx context.Context, in *RegisterReq, opts ...grpc.CallOption) (*RegisterResp, error)
|
||||
ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error)
|
||||
CheckPermission(ctx context.Context, in *CheckPermissionReq, opts ...grpc.CallOption) (*CheckPermissionResp, error)
|
||||
Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error)
|
||||
}
|
||||
|
||||
defaultUsercenter struct {
|
||||
@@ -94,6 +100,11 @@ func (m *defaultUsercenter) Login(ctx context.Context, in *LoginReq, opts ...grp
|
||||
return client.Login(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (m *defaultUsercenter) Register(ctx context.Context, in *RegisterReq, opts ...grpc.CallOption) (*RegisterResp, error) {
|
||||
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||
return client.Register(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (m *defaultUsercenter) ValidateToken(ctx context.Context, in *ValidateTokenReq, opts ...grpc.CallOption) (*ValidateTokenResp, error) {
|
||||
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||
return client.ValidateToken(ctx, in, opts...)
|
||||
@@ -103,3 +114,8 @@ func (m *defaultUsercenter) CheckPermission(ctx context.Context, in *CheckPermis
|
||||
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||
return client.CheckPermission(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (m *defaultUsercenter) Logout(ctx context.Context, in *LogoutReq, opts ...grpc.CallOption) (*LogoutResp, error) {
|
||||
client := pb.NewUsercenterClient(m.cli.Conn())
|
||||
return client.Logout(ctx, in, opts...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewErrorResp(code int, msg error) *ErrorResponse {
|
||||
return &ErrorResponse{
|
||||
Code: code,
|
||||
Msg: msg.Error(),
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
# Envoy Gateway 配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
Envoy Gateway 作为 API 统一入口,提供以下功能:
|
||||
- **JWT 身份验证**:所有 API 请求(除登录/注册)都需要有效的 JWT token
|
||||
- **CSRF 防护**:防止跨站点请求伪造攻击
|
||||
- **速率限制**:防止 DDoS 攻击
|
||||
- **TLS 加密**:所有通信都加密
|
||||
- **负载均衡**:分担后端服务的流量
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Client │
|
||||
└──────┬──────┘
|
||||
│ HTTP/HTTPS (Port 80/443)
|
||||
│
|
||||
┌──────▼────────────────┐
|
||||
│ Envoy Gateway │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ JWT Validator │ │ ◄─── JWT Verification (offline)
|
||||
│ │ CSRF Filter │ │
|
||||
│ │ Rate Limiter │ │
|
||||
│ │ Router │ │
|
||||
│ └────────────────┘ │
|
||||
└────────┬─────────────┘
|
||||
│ gRPC/HTTP
|
||||
┌────┴────┬──────────┐
|
||||
│ │ │
|
||||
┌───▼──┐ ┌──▼────┐ ┌──▼─────┐
|
||||
│User │ │Order │ │User │
|
||||
│API │ │API │ │RPC │
|
||||
└──────┘ └───────┘ └────────┘
|
||||
```
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 生成 TLS 证书
|
||||
|
||||
```bash
|
||||
# 为 Envoy 生成自签名证书(生产环境应使用正式证书)
|
||||
kubectl create secret tls envoy-tls \
|
||||
--cert=path/to/tls.crt \
|
||||
--key=path/to/tls.key \
|
||||
-n juwan
|
||||
|
||||
# 或生成自签名证书(仅用于测试)
|
||||
openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
|
||||
-days 365 -nodes -subj "/CN=api.juwan.local"
|
||||
|
||||
kubectl create secret tls envoy-tls \
|
||||
--cert=tls.crt \
|
||||
--key=tls.key \
|
||||
-n juwan
|
||||
```
|
||||
|
||||
### 2. 部署 Envoy Gateway
|
||||
|
||||
```bash
|
||||
# 应用 Envoy 配置
|
||||
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||
|
||||
# 查看部署状态
|
||||
kubectl get pods -n juwan -l app=envoy-gateway
|
||||
kubectl get svc -n juwan envoy-gateway
|
||||
```
|
||||
|
||||
### 3. 配置 JWKS 端点
|
||||
|
||||
在 user-rpc 中暴露 JWKS 端点,供 Envoy 验证 JWT:
|
||||
|
||||
#### 在 `app/users/rpc` 中添加 HTTP 路由(go-zero)
|
||||
|
||||
编辑 `app/users/rpc/internal/handler/` 或在 `main.go` 中:
|
||||
|
||||
```go
|
||||
// 在 rpc server 启动时,添加 HTTP 端点用于暴露 JWKS
|
||||
import (
|
||||
"net/http"
|
||||
"juwan-backend/app/users/rpc/internal/utils"
|
||||
)
|
||||
|
||||
// 在 main 函数中
|
||||
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = "your-default-secret-key"
|
||||
}
|
||||
|
||||
jwksJSON, err := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate JWKS", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(jwksJSON))
|
||||
})
|
||||
|
||||
// 在单独的 goroutine 中启动 HTTP 服务器
|
||||
go func() {
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}()
|
||||
```
|
||||
|
||||
**或使用 Echo 框架(更推荐)**:
|
||||
|
||||
```go
|
||||
// 在 main.go 中
|
||||
import "github.com/labstack/echo/v4"
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/.well-known/jwks.json", func(c echo.Context) error {
|
||||
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||
jwksJSON, _ := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||
return c.JSONBlob(http.StatusOK, []byte(jwksJSON))
|
||||
})
|
||||
|
||||
go func() {
|
||||
e.Start(":8080")
|
||||
}()
|
||||
```
|
||||
|
||||
### 4. 更新环境变量
|
||||
|
||||
在 K8s Secret 中配置 JWT_SECRET_KEY:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: jwt-secret
|
||||
namespace: juwan
|
||||
type: Opaque
|
||||
data:
|
||||
JWT_SECRET_KEY: "$(echo -n 'your-secret-key-change-this' | base64)"
|
||||
```
|
||||
|
||||
### 5. 验证 JWKS 端点
|
||||
|
||||
```bash
|
||||
# 端口转发
|
||||
kubectl port-forward -n juwan svc/user-rpc-svc 9001:9001
|
||||
|
||||
# 验证 JWKS 端点可访问
|
||||
curl http://localhost:9001/.well-known/jwks.json
|
||||
```
|
||||
|
||||
## JWT 验证流程
|
||||
|
||||
### 1. 登录获取 Token
|
||||
|
||||
```bash
|
||||
curl -X POST http://api.juwan.local/api/v1/users/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}'
|
||||
|
||||
# 响应:
|
||||
# {
|
||||
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
# "expires": 1708780800
|
||||
# }
|
||||
```
|
||||
|
||||
### 2. 使用 Token 访问受保护资源
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://api.juwan.local/api/v1/users/123
|
||||
|
||||
# Envoy 验证步骤:
|
||||
# 1. 从 Authorization header 提取 token
|
||||
# 2. 从 JWKS 端点获取公钥(缓存 5 分钟)
|
||||
# 3. 验证 token 签名
|
||||
# 4. 检查 token 过期时间
|
||||
# 5. 将验证后的用户信息添加到请求头(X-USER-ID)
|
||||
# 6. 转发请求到 user-api
|
||||
```
|
||||
|
||||
## CSRF 防护
|
||||
|
||||
### 配置说明
|
||||
|
||||
Envoy 的 CSRF 过滤器检查:
|
||||
- 只对 POST/PUT/DELETE/PATCH 请求进行检查
|
||||
- 检查 `Origin` 和 `Referer` header
|
||||
- 验证请求来自已知域名
|
||||
|
||||
### 跨域请求配置
|
||||
|
||||
```yaml
|
||||
# Envoy 配置中允许的来源
|
||||
additional_origins:
|
||||
- exact: "https://admin.juwan.local"
|
||||
- exact: "https://web.juwan.local"
|
||||
```
|
||||
|
||||
## Token 黑名单检查(可选)
|
||||
|
||||
如果需要验证 token 未被撤销,可启用额外的 RPC 验证:
|
||||
|
||||
```yaml
|
||||
# envoy-gateway.yaml 中取消注释
|
||||
- name: envoy.filters.http.ext_authz
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
|
||||
grpc_service:
|
||||
envoy_grpc:
|
||||
cluster_name: user_rpc_cluster
|
||||
failure_mode_allow: false
|
||||
```
|
||||
|
||||
然后在 user-rpc 中实现 ValidateToken RPC:
|
||||
|
||||
```protobuf
|
||||
rpc ValidateToken(ValidateTokenReq) returns(ValidateTokenResp);
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. JWT 验证失败
|
||||
|
||||
```bash
|
||||
# 查看 Envoy 日志
|
||||
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||
|
||||
# 验证 JWKS 端点是否可访问
|
||||
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||
curl http://user-rpc-svc:9001/.well-known/jwks.json
|
||||
```
|
||||
|
||||
### 2. 无法连接到后端服务
|
||||
|
||||
```bash
|
||||
# 验证服务发现
|
||||
kubectl get endpoints -n juwan
|
||||
|
||||
# 验证网络策略
|
||||
kubectl get networkpolicy -n juwan
|
||||
|
||||
# 测试连接
|
||||
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||
curl http://user-api-svc:8888/health
|
||||
```
|
||||
|
||||
### 3. CSRF 错误
|
||||
|
||||
- 确保设置了 `Origin` 和 `Referer` header
|
||||
- 检查 `additional_origins` 配置是否包含你的域名
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. JWKS 缓存
|
||||
|
||||
```yaml
|
||||
cache_ttl:
|
||||
seconds: 300 # 缓存 5 分钟,减少 RPC 调用
|
||||
```
|
||||
|
||||
### 2. 连接池
|
||||
|
||||
```yaml
|
||||
http2_protocol_options: {} # 启用 HTTP/2 多路复用
|
||||
```
|
||||
|
||||
### 3. 速率限制调整
|
||||
|
||||
根据实际流量调整令牌桶参数:
|
||||
|
||||
```yaml
|
||||
token_bucket:
|
||||
max_tokens: 10000 # 最大令牌数
|
||||
tokens_per_fill: 10000 # 每次填充的令牌数
|
||||
fill_interval: 1s # 填充间隔
|
||||
```
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 访问日志
|
||||
|
||||
```bash
|
||||
# 查看访问日志
|
||||
kubectl logs -n juwan -l app=envoy-gateway --follow
|
||||
|
||||
# 格式包含:
|
||||
# - 请求时间、方法、路径
|
||||
# - 响应状态码、字节数
|
||||
# - 上游服务信息
|
||||
```
|
||||
|
||||
### Prometheus 指标
|
||||
|
||||
Envoy 在 `:9901/stats` 暴露 Prometheus 指标:
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n juwan svc/envoy-gateway 9901:9901
|
||||
curl localhost:9901/stats | grep jwt
|
||||
```
|
||||
|
||||
## 生产环境检查清单
|
||||
|
||||
- [ ] 使用正式 TLS 证书(不是自签名)
|
||||
- [ ] 配置正确的 JWT_SECRET_KEY(强密码)
|
||||
- [ ] 启用 HTTPS(关闭 HTTP)
|
||||
- [ ] 配置网络策略限制访问
|
||||
- [ ] 启用访问日志和监控
|
||||
- [ ] 设置合理的速率限制
|
||||
- [ ] 测试 token 过期和刷新流程
|
||||
- [ ] 配置告警规则
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [Envoy JWT Authentication](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto)
|
||||
- [Envoy CSRF Protection](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/csrf/v3/csrf.proto)
|
||||
- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
|
||||
@@ -1,371 +0,0 @@
|
||||
# Envoy 配置完整清单
|
||||
|
||||
## 📋 配置文件清单
|
||||
|
||||
### 1. Proto 更新
|
||||
- **文件**: [desc/rpc/users.proto](../../desc/rpc/users.proto)
|
||||
- **更改**: 添加了两个 RPC 方法
|
||||
- `ValidateToken()`: 验证 token 是否有效(检查黑名单)
|
||||
- `CheckPermission()`: 检查用户权限
|
||||
|
||||
### 2. Envoy 部署
|
||||
- **配置文件**: [envoy.yaml](./envoy.yaml)
|
||||
- HTTP 监听器(端口 8080)
|
||||
- HTTPS 监听器(端口 8443)
|
||||
- JWT 验证过滤器
|
||||
- CSRF 防护过滤器
|
||||
- 速率限制(DDoS 防护)
|
||||
- 路由配置
|
||||
|
||||
- **K8s 部署**: [../k8s/envoy-gateway.yaml](../k8s/envoy-gateway.yaml)
|
||||
- 2 个副本
|
||||
- 负载均衡器服务
|
||||
- Service Account 和 RBAC
|
||||
- Network Policy
|
||||
- ConfigMap 用于配置管理
|
||||
|
||||
### 3. 工具代码
|
||||
- **JWKS 生成**: [app/users/rpc/internal/utils/jwks.go](../../app/users/rpc/internal/utils/jwks.go)
|
||||
- `GenerateJWKSFromSecret()`: 从 JWT 密钥生成 JWKS
|
||||
- `GenerateJWKSEndpoint()`: 生成 JSON 输出供 Envoy 使用
|
||||
- `ExtractTokenMetadata()`: 提取 token 元数据
|
||||
|
||||
### 4. Dockerfile
|
||||
- **文件**: [Dockerfile](./Dockerfile)
|
||||
- **用途**: 构建 Envoy 容器镜像
|
||||
|
||||
### 5. 脚本
|
||||
- **文件**: [generate-jwks.sh](./generate-jwks.sh)
|
||||
- **用途**: 快速生成 JWKS JSON 文件
|
||||
|
||||
### 6. 文档
|
||||
- **文件**: [ENVOY_CONFIG_GUIDE.md](./ENVOY_CONFIG_GUIDE.md)
|
||||
- **内容**: 详细的配置和部署指南
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速部署步骤
|
||||
|
||||
### 步骤 1: 生成 TLS 证书
|
||||
|
||||
```bash
|
||||
# 测试环境:生成自签名证书
|
||||
openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt \
|
||||
-days 365 -nodes -subj "/CN=api.juwan.local"
|
||||
|
||||
# 创建 K8s Secret
|
||||
kubectl create secret tls envoy-tls \
|
||||
--cert=tls.crt \
|
||||
--key=tls.key \
|
||||
-n juwan
|
||||
```
|
||||
|
||||
### 步骤 2: 部署 Envoy Gateway
|
||||
|
||||
```bash
|
||||
# 应用部署文件
|
||||
kubectl apply -f deploy/k8s/envoy-gateway.yaml
|
||||
|
||||
# 验证部署
|
||||
kubectl get pods -n juwan -l app=envoy-gateway
|
||||
kubectl get svc -n juwan envoy-gateway
|
||||
|
||||
# 等待 LoadBalancer 获取外部 IP
|
||||
kubectl get svc -n juwan envoy-gateway -w
|
||||
```
|
||||
|
||||
### 步骤 3: 在 User RPC 中暴露 JWKS 端点
|
||||
|
||||
编辑 `app/users/rpc/rpcserver.go` 或 `main.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"http"
|
||||
"os"
|
||||
"juwan-backend/app/users/rpc/internal/utils"
|
||||
)
|
||||
|
||||
// 在启动 RPC server 前,添加 HTTP 端点
|
||||
func setupJWKSEndpoint() {
|
||||
secretKey := os.Getenv("JWT_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = "your-default-secret-key"
|
||||
}
|
||||
|
||||
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
jwksJSON, err := utils.GenerateJWKSEndpoint(secretKey, "default-key-id")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate JWKS", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=300") // 缓存 5 分钟
|
||||
w.Write([]byte(jwksJSON))
|
||||
})
|
||||
|
||||
// 在独立的 goroutine 中启动 HTTP 服务器
|
||||
go func() {
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupJWKSEndpoint()
|
||||
|
||||
// ... 其他 RPC 启动代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 4: 更新 User RPC 配置
|
||||
|
||||
编辑 `app/users/rpc/etc/pb.yaml`:
|
||||
|
||||
```yaml
|
||||
Name: pb.rpc
|
||||
ListenOn: 0.0.0.0:9001
|
||||
|
||||
Prometheus:
|
||||
Host: 0.0.0.0
|
||||
Port: 4001
|
||||
Path: /metrics
|
||||
|
||||
# ... 其他配置 ...
|
||||
|
||||
Jwt:
|
||||
SecretKey: "${JWT_SECRET_KEY:your-secret-jwt-key-change-this-in-production}"
|
||||
Issuer: "juwan-user-rpc"
|
||||
```
|
||||
|
||||
### 步骤 5: 构建并推送容器镜像
|
||||
|
||||
```bash
|
||||
# 构建 User API 镜像
|
||||
docker build -t your-registry/user-api:latest ./app/users/api/
|
||||
docker push your-registry/user-api:latest
|
||||
|
||||
# 构建 User RPC 镜像
|
||||
docker build -t your-registry/user-rpc:latest ./app/users/rpc/
|
||||
docker push your-registry/user-rpc:latest
|
||||
|
||||
# 构建 Envoy 镜像
|
||||
docker build -f deploy/envoy/Dockerfile -t your-registry/envoy-gateway:latest .
|
||||
docker push your-registry/envoy-gateway:latest
|
||||
```
|
||||
|
||||
### 步骤 6: 更新 K8s 部署
|
||||
|
||||
更新 `deploy/k8s/service/user/user-api.yaml` 和 `user-rpc.yaml`,确保使用正确的镜像和环境变量。
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试流程
|
||||
|
||||
### 1. 登录获取 Token
|
||||
|
||||
```bash
|
||||
# 获取 Envoy 外部 IP
|
||||
ENVOY_IP=$(kubectl get svc -n juwan envoy-gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
|
||||
# 登录
|
||||
curl -k -X POST "https://$ENVOY_IP:443/api/v1/users/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"password": "password123"
|
||||
}' | jq .
|
||||
|
||||
# 示例响应:
|
||||
# {
|
||||
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
# "expires": 1708780800
|
||||
# }
|
||||
```
|
||||
|
||||
### 2. 使用 Token 访问受保护资源
|
||||
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
curl -k -X GET "https://$ENVOY_IP:443/api/v1/users/123" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq .
|
||||
```
|
||||
|
||||
### 3. 验证 CSRF 防护
|
||||
|
||||
```bash
|
||||
# POST 请求必须有正确的 Origin/Referer
|
||||
curl -k -X POST "https://$ENVOY_IP:443/api/v1/users/logout" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Origin: https://api.juwan.local" \
|
||||
-H "Referer: https://api.juwan.local/" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"userId": 123}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 验证检查清单
|
||||
|
||||
- [ ] Envoy 容器运行正常
|
||||
```bash
|
||||
kubectl logs -n juwan -l app=envoy-gateway
|
||||
```
|
||||
|
||||
- [ ] JWKS 端点可访问
|
||||
```bash
|
||||
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||
curl http://user-rpc-svc:9001/.well-known/jwks.json
|
||||
```
|
||||
|
||||
- [ ] 后端服务健康
|
||||
```bash
|
||||
kubectl exec -it -n juwan <envoy-pod> -- \
|
||||
curl http://user-api-svc:8888/health
|
||||
```
|
||||
|
||||
- [ ] JWT 验证工作
|
||||
```bash
|
||||
# 不带 token 访问受保护资源应返回 401
|
||||
curl -k https://api.juwan.local/api/v1/users/123
|
||||
```
|
||||
|
||||
- [ ] CSRF 防护生效
|
||||
```bash
|
||||
# 缺少 Origin header 的 POST 应被拒绝
|
||||
curl -k -X POST https://api.juwan.local/api/v1/users/logout \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置调整
|
||||
|
||||
### 修改 JWT 密钥
|
||||
|
||||
```bash
|
||||
# 1. 更新 K8s Secret
|
||||
kubectl patch secret jwt-secret -n juwan \
|
||||
-p '{"data":{"JWT_SECRET_KEY":"'$(echo -n 'new-secret-key' | base64)'"}}'
|
||||
|
||||
# 2. 重启 User RPC 和 Envoy
|
||||
kubectl rollout restart deployment/user-rpc-svc -n juwan
|
||||
kubectl rollout restart deployment/envoy-gateway -n juwan
|
||||
```
|
||||
|
||||
### 调整 Envoy 速率限制
|
||||
|
||||
编辑 ConfigMap:
|
||||
```bash
|
||||
kubectl edit cm envoy-config -n juwan
|
||||
```
|
||||
|
||||
修改 `token_bucket` 参数:
|
||||
```yaml
|
||||
token_bucket:
|
||||
max_tokens: 5000 # 降低限制
|
||||
tokens_per_fill: 5000
|
||||
fill_interval: 1s
|
||||
```
|
||||
|
||||
### 添加信任的 CSRF 来源
|
||||
|
||||
编辑 ConfigMap:
|
||||
```yaml
|
||||
additional_origins:
|
||||
- exact: "https://admin.juwan.local"
|
||||
- exact: "https://web.juwan.local"
|
||||
- prefix: "https://app.juwan.local" # 支持前缀匹配
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 监控和日志
|
||||
|
||||
### 查看 Envoy 统计
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n juwan svc/envoy-gateway 9901:9901
|
||||
curl localhost:9901/stats | grep -E "(jwt_authn|csrf|http_ratelimit)"
|
||||
```
|
||||
|
||||
### 实时日志
|
||||
|
||||
```bash
|
||||
kubectl logs -n juwan -l app=envoy-gateway -f
|
||||
|
||||
# 查看特定日志行
|
||||
kubectl logs -n juwan -l app=envoy-gateway | grep "401\|403"
|
||||
```
|
||||
|
||||
### 监控指标(集成 Prometheus)
|
||||
|
||||
```yaml
|
||||
# prometheus-scrape-config.yaml
|
||||
- job_name: 'envoy-gateway'
|
||||
static_configs:
|
||||
- targets: ['envoy-gateway.juwan.svc.cluster.local:9901']
|
||||
metrics_path: '/stats/prometheus'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文件位置
|
||||
|
||||
```
|
||||
deploy/
|
||||
├── envoy/
|
||||
│ ├── envoy.yaml ← Envoy 核心配置
|
||||
│ ├── ENVOY_CONFIG_GUIDE.md ← 详细指南
|
||||
│ ├── generate-jwks.sh ← JWKS 生成脚本
|
||||
│ ├── Dockerfile ← Envoy 镜像
|
||||
│ └── QUICK_REFERENCE.md ← 本文件
|
||||
├── k8s/
|
||||
│ ├── envoy-gateway.yaml ← K8s 部署清单
|
||||
│ └── secrets/jwt-secret.yaml ← JWT 密钥配置
|
||||
└── script/
|
||||
└── init-secrets.sh ← 初始化脚本
|
||||
|
||||
app/users/
|
||||
├── rpc/
|
||||
│ ├── internal/utils/
|
||||
│ │ ├── jwks.go ← JWKS 生成工具
|
||||
│ │ └── jwt.go ← JWT 管理器
|
||||
│ └── etc/pb.yaml ← RPC 配置
|
||||
└── api/
|
||||
└── etc/user-api.yaml ← API 配置
|
||||
|
||||
desc/
|
||||
└── rpc/users.proto ← Proto 定义(已更新)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤔 常见问题
|
||||
|
||||
1. **Envoy 无法连接到后端服务**
|
||||
- 检查 K8s Service DNS: `user-api-svc.juwan.svc.cluster.local`
|
||||
- 验证 NetworkPolicy 允许流量
|
||||
|
||||
2. **JWT 验证失败**
|
||||
- 确保 JWT_SECRET_KEY 一致
|
||||
- 检查 JWKS 端点是否可访问
|
||||
- 查看 Envoy 日志: `grep "jwt_authn" envoy.log`
|
||||
|
||||
3. **CSRF 防护过于严格**
|
||||
- 在 `additional_origins` 中添加允许的来源
|
||||
- 对于单页应用,确保发送 `Origin` header
|
||||
|
||||
4. **速率限制阻止正常流量**
|
||||
- 增加 `max_tokens` 和 `tokens_per_fill`
|
||||
- 针对特定客户端配置不同的限制
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- 查看 [Envoy 官方文档](https://www.envoyproxy.io/docs)
|
||||
- 查看 [JWT 规范](https://tools.ietf.org/html/rfc7519)
|
||||
- 检查 [CSRF 防护最佳实践](https://owasp.org/www-community/attacks/csrf)
|
||||
@@ -1,331 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Envoy 快速部署脚本
|
||||
# 用途:自动化部署 Envoy Gateway 到 Kubernetes
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
NAMESPACE="${NAMESPACE:-juwan}"
|
||||
RELEASE_NAME="${RELEASE_NAME:-envoy-gateway}"
|
||||
TIMEOUT="${TIMEOUT:-300s}"
|
||||
CONTEXT="${CONTEXT:-}"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
log_info "检查依赖..."
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
if ! command -v kubectl &> /dev/null; then
|
||||
missing_deps+=("kubectl")
|
||||
fi
|
||||
|
||||
if ! command -v openssl &> /dev/null; then
|
||||
missing_deps+=("openssl")
|
||||
fi
|
||||
|
||||
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||
log_error "缺少以下依赖: ${missing_deps[*]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "所有依赖已安装"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 生成 TLS 证书
|
||||
generate_tls_cert() {
|
||||
log_info "生成 TLS 证书..."
|
||||
|
||||
local cert_dir="certs"
|
||||
local key_file="$cert_dir/tls.key"
|
||||
local cert_file="$cert_dir/tls.crt"
|
||||
|
||||
# 创建 certs 目录
|
||||
mkdir -p "$cert_dir"
|
||||
|
||||
# 检查是否已存在证书
|
||||
if [ -f "$cert_file" ] && [ -f "$key_file" ]; then
|
||||
log_warn "证书已存在: $cert_file, $key_file"
|
||||
read -p "是否要重新生成? (y/n) " -t 10 -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_success "使用现有证书"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 生成自签名证书(仅用于测试)
|
||||
openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout "$key_file" \
|
||||
-out "$cert_file" \
|
||||
-days 365 -nodes \
|
||||
-subj "/CN=api.juwan.local" \
|
||||
-addext "subjectAltName=DNS:api.juwan.local,DNS:*.juwan.local" \
|
||||
> /dev/null 2>&1
|
||||
|
||||
log_success "TLS 证书已生成"
|
||||
log_warn "警告: 这是自签名证书,仅用于测试环境"
|
||||
log_warn "生产环境应使用正式的 CA 签发证书"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 创建命名空间
|
||||
create_namespace() {
|
||||
log_info "创建 Kubernetes 命名空间..."
|
||||
|
||||
if kubectl get namespace "$NAMESPACE" &> /dev/null; then
|
||||
log_warn "命名空间已存在: $NAMESPACE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
kubectl create namespace "$NAMESPACE"
|
||||
log_success "命名空间已创建: $NAMESPACE"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 创建 TLS Secret
|
||||
create_tls_secret() {
|
||||
log_info "创建 TLS Secret..."
|
||||
|
||||
local cert_dir="certs"
|
||||
local key_file="$cert_dir/tls.key"
|
||||
local cert_file="$cert_dir/tls.crt"
|
||||
|
||||
# 检查证书文件
|
||||
if [ ! -f "$cert_file" ] || [ ! -f "$key_file" ]; then
|
||||
log_error "证书文件不存在"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查 Secret 是否已存在
|
||||
if kubectl get secret envoy-tls -n "$NAMESPACE" &> /dev/null; then
|
||||
log_warn "Secret 已存在,删除后重建"
|
||||
kubectl delete secret envoy-tls -n "$NAMESPACE"
|
||||
fi
|
||||
|
||||
# 创建 Secret
|
||||
kubectl create secret tls envoy-tls \
|
||||
-n "$NAMESPACE" \
|
||||
--cert="$cert_file" \
|
||||
--key="$key_file"
|
||||
|
||||
log_success "TLS Secret 已创建: envoy-tls"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 部署 Envoy Gateway
|
||||
deploy_envoy() {
|
||||
log_info "部署 Envoy Gateway..."
|
||||
|
||||
local manifest_file="deploy/k8s/envoy-gateway.yaml"
|
||||
|
||||
if [ ! -f "$manifest_file" ]; then
|
||||
log_error "找不到部署清单: $manifest_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 应用部署
|
||||
if [ -n "$CONTEXT" ]; then
|
||||
kubectl apply -f "$manifest_file" --context="$CONTEXT"
|
||||
else
|
||||
kubectl apply -f "$manifest_file"
|
||||
fi
|
||||
|
||||
log_success "Envoy Gateway 部署清单已应用"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 等待部署完成
|
||||
wait_deployment() {
|
||||
log_info "等待部署完成(超时: $TIMEOUT)..."
|
||||
|
||||
kubectl rollout status deployment/envoy-gateway \
|
||||
-n "$NAMESPACE" \
|
||||
--timeout="$TIMEOUT" || {
|
||||
log_error "部署超时"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_success "部署已完成"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 验证部署
|
||||
verify_deployment() {
|
||||
log_info "验证部署..."
|
||||
|
||||
# 检查 Pod
|
||||
local pod_count=$(kubectl get pods -n "$NAMESPACE" \
|
||||
-l app=envoy-gateway \
|
||||
-o jsonpath='{.items | length}')
|
||||
|
||||
if [ "$pod_count" -eq 0 ]; then
|
||||
log_error "未找到 Envoy 容器"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "找到 $pod_count 个 Envoy 容器"
|
||||
|
||||
# 检查 Service
|
||||
local svc_status=$(kubectl get svc -n "$NAMESPACE" |
|
||||
grep envoy-gateway || echo "")
|
||||
|
||||
if [ -z "$svc_status" ]; then
|
||||
log_error "未找到 Service"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Service 已创建"
|
||||
|
||||
# 显示 LoadBalancer IP
|
||||
log_info "等待 LoadBalancer IP..."
|
||||
local lb_ip=""
|
||||
for i in {1..30}; do
|
||||
lb_ip=$(kubectl get svc envoy-gateway -n "$NAMESPACE" \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$lb_ip" ] && [ "$lb_ip" != "null" ]; then
|
||||
log_success "LoadBalancer IP: $lb_ip"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $i -eq 30 ]; then
|
||||
log_warn "未获得 LoadBalancer IP(可能在内网环境或使用 NodePort)"
|
||||
kubectl get svc -n "$NAMESPACE" envoy-gateway
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
show_summary() {
|
||||
log_info "部署摘要"
|
||||
echo ""
|
||||
echo " Namespace: $NAMESPACE"
|
||||
echo " Release: $RELEASE_NAME"
|
||||
echo ""
|
||||
echo " Pods:"
|
||||
kubectl get pods -n "$NAMESPACE" -l app=envoy-gateway \
|
||||
-o custom-columns=NAME:.metadata.name,STATUS:.status.phase,IP:.status.podIP \
|
||||
| sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo " Service:"
|
||||
kubectl get svc -n "$NAMESPACE" envoy-gateway \
|
||||
-o custom-columns=NAME:.metadata.name,TYPE:.spec.type,IP:.spec.clusterIP,EXTERNAL_IP:.status.loadBalancer.ingress[0].ip \
|
||||
| sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo " 后续步骤:"
|
||||
echo " 1. 在 User RPC 中暴露 JWKS 端点 (/.well-known/jwks.json)"
|
||||
echo " 2. 配置 JWT_SECRET_KEY 环境变量"
|
||||
echo " 3. 测试 JWT 验证: curl -k https://<ENVOY_IP>/api/v1/users/login"
|
||||
echo ""
|
||||
echo " 文档:"
|
||||
echo " - 配置指南: deploy/envoy/ENVOY_CONFIG_GUIDE.md"
|
||||
echo " - 快速参考: deploy/envoy/QUICK_REFERENCE.md"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 清理部署
|
||||
cleanup() {
|
||||
log_warn "清理 Envoy Gateway..."
|
||||
|
||||
kubectl delete -f deploy/k8s/envoy-gateway.yaml -n "$NAMESPACE" || true
|
||||
kubectl delete secret envoy-tls -n "$NAMESPACE" || true
|
||||
|
||||
log_success "清理完成"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ Envoy Gateway 快速部署脚本 ║${NC}"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# 解析命令行参数
|
||||
local cmd="${1:-deploy}"
|
||||
|
||||
case "$cmd" in
|
||||
deploy)
|
||||
check_dependencies || exit 1
|
||||
generate_tls_cert || exit 1
|
||||
create_namespace || exit 1
|
||||
create_tls_secret || exit 1
|
||||
deploy_envoy || exit 1
|
||||
wait_deployment || exit 1
|
||||
verify_deployment || exit 1
|
||||
show_summary
|
||||
log_success "Envoy Gateway 已成功部署!"
|
||||
;;
|
||||
cleanup)
|
||||
cleanup
|
||||
;;
|
||||
status)
|
||||
log_info "部署状态"
|
||||
kubectl get all -n "$NAMESPACE" -l app=envoy-gateway
|
||||
;;
|
||||
logs)
|
||||
log_info "Envoy 日志"
|
||||
kubectl logs -n "$NAMESPACE" -l app=envoy-gateway -f
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 <命令>"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " deploy 部署 Envoy Gateway(默认)"
|
||||
echo " cleanup 移除部署"
|
||||
echo " status 查看部署状态"
|
||||
echo " logs 查看 Envoy 日志"
|
||||
echo ""
|
||||
echo "环境变量:"
|
||||
echo " NAMESPACE K8s 命名空间(默认: juwan)"
|
||||
echo " RELEASE_NAME 发布名称(默认: envoy-gateway)"
|
||||
echo " TIMEOUT 部署超时(默认: 300s)"
|
||||
echo " CONTEXT K8s 上下文(可选)"
|
||||
echo ""
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,385 +0,0 @@
|
||||
static_resources:
|
||||
listeners:
|
||||
# HTTP 监听器(重定向到 HTTPS)
|
||||
- name: listener_http
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_number: 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:
|
||||
# CSRF 防护过滤器
|
||||
- name: envoy.filters.http.local_ratelimit
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
|
||||
stat_prefix: http_local_rate_limiter
|
||||
token_bucket:
|
||||
max_tokens: 1000
|
||||
tokens_per_fill: 1000
|
||||
fill_interval: 1s
|
||||
filter_enabled:
|
||||
runtime_key: local_rate_limit_enabled
|
||||
default_value:
|
||||
numerator: 100
|
||||
denominator: HUNDRED
|
||||
filter_enforced:
|
||||
runtime_key: local_rate_limit_enforced
|
||||
default_value:
|
||||
numerator: 100
|
||||
denominator: HUNDRED
|
||||
|
||||
# 路由过滤器
|
||||
- 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:
|
||||
# 登录端点 - 不需要 JWT
|
||||
- match:
|
||||
path: /api/v1/users/login
|
||||
headers:
|
||||
- name: ":method"
|
||||
string_match:
|
||||
exact: "POST"
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
|
||||
# 注册端点 - 不需要 JWT
|
||||
- match:
|
||||
path: /api/v1/users/register
|
||||
headers:
|
||||
- name: ":method"
|
||||
string_match:
|
||||
exact: "POST"
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
|
||||
# 其他所有用户 API 端点 - 需要 JWT
|
||||
- match:
|
||||
prefix: /api/v1/users
|
||||
headers:
|
||||
- name: ":method"
|
||||
string_match:
|
||||
exact: "GET"
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: "x-verified-user"
|
||||
value: "%REQ(X-USER-ID)%"
|
||||
|
||||
# 订单 API - 需要 JWT
|
||||
- match:
|
||||
prefix: /api/v1/orders
|
||||
route:
|
||||
cluster: order_api_cluster
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: "x-verified-user"
|
||||
value: "%REQ(X-USER-ID)%"
|
||||
|
||||
# 健康检查端点
|
||||
- match:
|
||||
path: /health
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 10s
|
||||
|
||||
# 默认路由
|
||||
- match:
|
||||
prefix: /
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
direct_response:
|
||||
status: 404
|
||||
body:
|
||||
inline_string: "Not Found"
|
||||
|
||||
# HTTPS 监听器(需要配置 TLS 证书)
|
||||
- name: listener_https
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_number: 8443
|
||||
filter_chains:
|
||||
- transport_socket:
|
||||
name: envoy.transport_sockets.tls
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
|
||||
common_tls_context:
|
||||
tls_certificates:
|
||||
- certificate_chain:
|
||||
filename: /etc/envoy/certs/tls.crt
|
||||
private_key:
|
||||
filename: /etc/envoy/certs/tls.key
|
||||
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_https
|
||||
access_log:
|
||||
- name: envoy.access_loggers.file
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
|
||||
path: /var/log/envoy/access.log
|
||||
format: |
|
||||
[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
|
||||
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT%
|
||||
"%DURATION%" "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"
|
||||
"%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"
|
||||
|
||||
http_filters:
|
||||
# JWT 验证过滤器
|
||||
- name: envoy.filters.http.jwt_authn
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
|
||||
providers:
|
||||
jwt_provider:
|
||||
issuer: "juwan-user-rpc"
|
||||
audiences: "api.juwan.local"
|
||||
# 本地验证(离线模式)- 需要在 ConfigMap 中配置公钥
|
||||
local_jwks:
|
||||
inline_string: |
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "oct",
|
||||
"k": "YOUR-BASE64-ENCODED-SECRET-KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
# 也可以使用远程 JWKS(更推荐)
|
||||
# remote_jwks:
|
||||
# http_uri:
|
||||
# uri: "http://user-rpc-svc:9001/.well-known/jwks.json"
|
||||
# cluster: user_rpc_cluster
|
||||
# timeout: 5s
|
||||
# cache_ttl:
|
||||
# seconds: 300
|
||||
# payload_in_metadata: "JWT_PAYLOAD"
|
||||
rules:
|
||||
# 不需要验证的路由
|
||||
- match:
|
||||
prefix: /api/v1/users/login
|
||||
allow_missing_or_failed: true
|
||||
|
||||
- match:
|
||||
prefix: /api/v1/users/register
|
||||
allow_missing_or_failed: true
|
||||
|
||||
- match:
|
||||
path: /health
|
||||
allow_missing_or_failed: true
|
||||
|
||||
# 所有其他路由都需要有效的 JWT
|
||||
- match:
|
||||
prefix: /
|
||||
requires:
|
||||
provider_name: jwt_provider
|
||||
|
||||
# CSRF 防护过滤器
|
||||
- name: envoy.filters.http.csrf
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.csrf.v3.CsrfPolicy
|
||||
filter_enabled:
|
||||
default_value:
|
||||
numerator: 100
|
||||
denominator: HUNDRED
|
||||
runtime_key: csrf_filter_enabled
|
||||
shadow_enabled:
|
||||
default_value:
|
||||
numerator: 0
|
||||
denominator: HUNDRED
|
||||
runtime_key: csrf_filter_shadow_enabled
|
||||
additional_origins:
|
||||
- exact: "https://admin.juwan.local"
|
||||
ignore_method_matches:
|
||||
- google_re2:
|
||||
regex: "^(GET|HEAD|OPTIONS|TRACE)$"
|
||||
|
||||
# 代理验证过滤器(可选 - 调用 RPC 验证 token 黑名单)
|
||||
# - name: envoy.filters.http.ext_authz
|
||||
# typed_config:
|
||||
# "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
|
||||
# grpc_service:
|
||||
# envoy_grpc:
|
||||
# cluster_name: user_rpc_cluster
|
||||
# failure_mode_allow: false
|
||||
# with_request_body:
|
||||
# max_request_bytes: 8192
|
||||
# allow_partial_message: false
|
||||
|
||||
# 本地速率限制(DDOS 防护)
|
||||
- name: envoy.filters.http.local_ratelimit
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
|
||||
stat_prefix: https_local_rate_limiter
|
||||
token_bucket:
|
||||
max_tokens: 10000
|
||||
tokens_per_fill: 10000
|
||||
fill_interval: 1s
|
||||
filter_enabled:
|
||||
runtime_key: local_rate_limit_enabled
|
||||
default_value:
|
||||
numerator: 100
|
||||
denominator: HUNDRED
|
||||
|
||||
# 路由过滤器
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
|
||||
route_config:
|
||||
name: https_route
|
||||
virtual_hosts:
|
||||
- name: backend
|
||||
domains: ["*"]
|
||||
routes:
|
||||
# 登录和注册不需要 JWT
|
||||
- match:
|
||||
path: /api/v1/users/login
|
||||
headers:
|
||||
- name: ":method"
|
||||
string_match:
|
||||
exact: "POST"
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
|
||||
- match:
|
||||
path: /api/v1/users/register
|
||||
headers:
|
||||
- name: ":method"
|
||||
string_match:
|
||||
exact: "POST"
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
|
||||
# 用户 API(带 JWT 验证)
|
||||
- match:
|
||||
prefix: /api/v1/users
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: "x-verified-user"
|
||||
value: "%REQ(X-USER-ID)%"
|
||||
|
||||
# 订单 API(带 JWT 验证)
|
||||
- match:
|
||||
prefix: /api/v1/orders
|
||||
route:
|
||||
cluster: order_api_cluster
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: "x-verified-user"
|
||||
value: "%REQ(X-USER-ID)%"
|
||||
|
||||
# 健康检查
|
||||
- match:
|
||||
path: /health
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 10s
|
||||
|
||||
# 默认路由
|
||||
- match:
|
||||
prefix: /
|
||||
direct_response:
|
||||
status: 404
|
||||
body:
|
||||
inline_string: "Not Found"
|
||||
|
||||
clusters:
|
||||
# User API 集群
|
||||
- name: user_api_cluster
|
||||
connect_timeout: 10s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: user_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: user-api-svc
|
||||
port_number: 8888
|
||||
health_checks:
|
||||
- timeout: 5s
|
||||
interval: 10s
|
||||
unhealthy_threshold: 2
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
expected_statuses:
|
||||
- start: 200
|
||||
end: 299
|
||||
|
||||
# Order API 集群
|
||||
- name: order_api_cluster
|
||||
connect_timeout: 10s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: order_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: order-api-svc
|
||||
port_number: 8889
|
||||
health_checks:
|
||||
- timeout: 5s
|
||||
interval: 10s
|
||||
unhealthy_threshold: 2
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
expected_statuses:
|
||||
- start: 200
|
||||
end: 299
|
||||
|
||||
# User RPC 集群(用于 ext_authz 调用)
|
||||
- name: user_rpc_cluster
|
||||
connect_timeout: 10s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: user_rpc_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: user-rpc-svc
|
||||
port_number: 9001
|
||||
http2_protocol_options: {}
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_number: 9901
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 生成 JWKS JSON 文件的脚本
|
||||
# 用于 Envoy JWT 验证
|
||||
|
||||
set -e
|
||||
|
||||
# 参数
|
||||
JWT_SECRET_KEY="${1:-your-secret-key-change-this-in-production}"
|
||||
OUTPUT_FILE="${2:-jwks.json}"
|
||||
KEY_ID="${3:-default-key-id}"
|
||||
|
||||
echo "生成 JWKS JSON..."
|
||||
echo "- Secret Key: ${JWT_SECRET_KEY:0:10}..."
|
||||
echo "- Key ID: $KEY_ID"
|
||||
echo "- Output: $OUTPUT_FILE"
|
||||
|
||||
# 对密钥进行 base64 编码(URL-safe 无填充)
|
||||
ENCODED_KEY=$(echo -n "$JWT_SECRET_KEY" | base64 | tr '+/' '-_' | sed 's/=//g')
|
||||
|
||||
# 生成 JWKS JSON
|
||||
cat > "$OUTPUT_FILE" <<EOF
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "oct",
|
||||
"use": "sig",
|
||||
"kid": "$KEY_ID",
|
||||
"k": "$ENCODED_KEY",
|
||||
"alg": "HS256"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✓ JWKS 文件已生成: $OUTPUT_FILE"
|
||||
echo ""
|
||||
echo "内容预览:"
|
||||
cat "$OUTPUT_FILE"
|
||||
echo ""
|
||||
echo ""
|
||||
echo "配置说明:"
|
||||
echo "1. 在 user-rpc 的 .well-known/jwks.json 端点暴露此文件"
|
||||
echo "2. 在 Envoy 中配置远程 JWKS URI:"
|
||||
echo " remote_jwks:"
|
||||
echo " http_uri:"
|
||||
echo " uri: http://user-rpc-svc:9001/.well-known/jwks.json"
|
||||
echo ""
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JWT 和认证配置完整设置脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔐 Juwan JWT 认证配置脚本"
|
||||
echo "===================================="
|
||||
|
||||
NAMESPACE="juwan"
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
JWKS_KEY_ID="juwan-key-2026"
|
||||
|
||||
echo "✅ 生成 JWT 密钥..."
|
||||
echo " Secret: $JWT_SECRET"
|
||||
|
||||
# Step 1: 创建 JWT Secret
|
||||
echo ""
|
||||
echo "📝 创建 K8s Secret..."
|
||||
kubectl create secret generic jwt-secret \
|
||||
--from-literal=key=$JWT_SECRET \
|
||||
-n $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Step 2: 生成 JWKS JSON(包含公钥)
|
||||
# 注意:对于 HMAC 算法,JWKS 包含密钥本身
|
||||
JWKS_JSON=$(cat <<EOF
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "oct",
|
||||
"kid": "$JWKS_KEY_ID",
|
||||
"k": "$(echo -n $JWT_SECRET | base64 -w 0)",
|
||||
"alg": "HS256",
|
||||
"use": "sig"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "📝 创建 JWKS ConfigMap..."
|
||||
kubectl create configmap jwks-config \
|
||||
--from-literal=jwks.json="$JWKS_JSON" \
|
||||
-n $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo ""
|
||||
echo "✅ JWT 认证配置完成!"
|
||||
echo ""
|
||||
echo "后续步骤:"
|
||||
echo "1. 更新 Envoy ConfigMap,挂载 JWKS 文件"
|
||||
echo "2. 在各 API 服务中配置 JWT_SECRET 环境变量"
|
||||
echo "3. 登录端点使用此密钥签名 Token"
|
||||
echo ""
|
||||
echo "JWT 密钥已保存到 K8s Secret: jwt-secret"
|
||||
echo "JWKS 已保存到 K8s ConfigMap: jwks-config"
|
||||
@@ -1,33 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: snowflake-sve
|
||||
namespace: juwan
|
||||
spec:
|
||||
ClusterIP: None
|
||||
selector:
|
||||
app: snowflake
|
||||
ports:
|
||||
- port: 9000
|
||||
targetPort: 9000
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: snowflake
|
||||
namespace: juwan
|
||||
spec:
|
||||
serviceName: snowflake-svc
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: snowflake
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: snowflake
|
||||
spec:
|
||||
containers:
|
||||
- name: snowflake
|
||||
image:
|
||||
@@ -1,262 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: juwan
|
||||
|
||||
---
|
||||
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
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
http_filters:
|
||||
- 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/v1/users
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
- match:
|
||||
prefix: /api/v1/orders
|
||||
route:
|
||||
cluster: order_api_cluster
|
||||
timeout: 30s
|
||||
- match:
|
||||
prefix: /health
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 10s
|
||||
- match:
|
||||
prefix: /
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
|
||||
clusters:
|
||||
- name: user_api_cluster
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: user_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: user-api-svc.juwan.svc.cluster.local
|
||||
port_value: 8888
|
||||
health_checks:
|
||||
- timeout: 3s
|
||||
interval: 10s
|
||||
unhealthy_threshold: 2
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
|
||||
- name: order_api_cluster
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: order_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: order-api-svc.juwan.svc.cluster.local
|
||||
port_value: 8889
|
||||
health_checks:
|
||||
- timeout: 3s
|
||||
interval: 10s
|
||||
unhealthy_threshold: 2
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
|
||||
admin:
|
||||
access_log_path: /tmp/admin_access.log
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
- name: admin
|
||||
port: 9901
|
||||
targetPort: 9901
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: envoy-gateway
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: envoy-gateway
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: envoy-gateway
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: envoy-gateway
|
||||
spec:
|
||||
serviceAccountName: envoy-gateway
|
||||
containers:
|
||||
- name: envoy
|
||||
image: envoyproxy/envoy:v1.27-latest
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
- name: admin
|
||||
containerPort: 9901
|
||||
volumeMounts:
|
||||
- name: envoy-config
|
||||
mountPath: /etc/envoy
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /stats
|
||||
port: 9901
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /stats
|
||||
port: 9901
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
volumes:
|
||||
- name: envoy-config
|
||||
configMap:
|
||||
name: envoy-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["endpoints"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["discovery.k8s.io"]
|
||||
resources: ["endpointslices"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: envoy-gateway
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: envoy-gateway-network-policy
|
||||
namespace: juwan
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: envoy-gateway
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: user-api
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: order-api
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8888
|
||||
- protocol: TCP
|
||||
port: 8889
|
||||
@@ -0,0 +1,294 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: envoy-config
|
||||
namespace: juwan
|
||||
data:
|
||||
envoy.yaml: |
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: ingress_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
|
||||
codec_type: AUTO
|
||||
generate_request_id: true
|
||||
use_remote_address: true
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: juwan_services
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
path: /healthz
|
||||
direct_response:
|
||||
status: 200
|
||||
body:
|
||||
inline_string: ok
|
||||
- match:
|
||||
prefix: /api/email
|
||||
route:
|
||||
cluster: email_api_cluster
|
||||
timeout: 30s
|
||||
- match:
|
||||
prefix: /api/users
|
||||
route:
|
||||
cluster: user_api_cluster
|
||||
timeout: 30s
|
||||
- match:
|
||||
prefix: /
|
||||
direct_response:
|
||||
status: 404
|
||||
body:
|
||||
inline_string: "gateway route not found"
|
||||
http_filters:
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local TOKEN_COOKIE = "csrf_token"
|
||||
local GUARD_COOKIE = "csrf_guard"
|
||||
local TOKEN_HEADER = "x-csrf-token"
|
||||
local GUARD_HEADER = "x-csrf-guard"
|
||||
|
||||
local seeded = false
|
||||
|
||||
local function seed_random()
|
||||
if seeded then
|
||||
return
|
||||
end
|
||||
seeded = true
|
||||
math.randomseed(os.time())
|
||||
end
|
||||
|
||||
local function split_cookie(header)
|
||||
local out = {}
|
||||
if not header then
|
||||
return out
|
||||
end
|
||||
for pair in string.gmatch(header, "([^;]+)") do
|
||||
local key, value = string.match(pair, "^%s*([^=]+)=?(.*)$")
|
||||
if key ~= nil and value ~= nil then
|
||||
out[string.lower(key)] = value
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function is_safe_method(method)
|
||||
return method == "GET" or method == "HEAD" or method == "OPTIONS"
|
||||
end
|
||||
|
||||
local function build_token(request_id)
|
||||
seed_random()
|
||||
local rnd = tostring(math.random(100000, 999999))
|
||||
local rid = request_id or "rid"
|
||||
return tostring(os.time()) .. "-" .. rid .. "-" .. rnd
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
local method = headers:get(":method")
|
||||
|
||||
local cookie_header = headers:get("cookie")
|
||||
local cookies = split_cookie(cookie_header)
|
||||
local csrf_token_cookie = cookies[TOKEN_COOKIE]
|
||||
local csrf_guard_cookie = cookies[GUARD_COOKIE]
|
||||
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", csrf_token_cookie == nil or csrf_token_cookie == "")
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", csrf_guard_cookie == nil or csrf_guard_cookie == "")
|
||||
|
||||
if csrf_token_cookie == nil or csrf_token_cookie == "" then
|
||||
csrf_token_cookie = build_token(headers:get("x-request-id"))
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", csrf_token_cookie)
|
||||
else
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", csrf_token_cookie)
|
||||
end
|
||||
|
||||
if csrf_guard_cookie == nil or csrf_guard_cookie == "" then
|
||||
csrf_guard_cookie = build_token(headers:get("x-request-id"))
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", csrf_guard_cookie)
|
||||
else
|
||||
request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", csrf_guard_cookie)
|
||||
end
|
||||
|
||||
if is_safe_method(method) then
|
||||
return
|
||||
end
|
||||
|
||||
local csrf_token_header = headers:get(TOKEN_HEADER)
|
||||
local csrf_guard_header = headers:get(GUARD_HEADER)
|
||||
|
||||
if csrf_token_header == nil or csrf_guard_header == nil then
|
||||
request_handle:respond(
|
||||
{[":status"] = "403", ["content-type"] = "application/json"},
|
||||
'{"code":403,"message":"missing csrf headers"}'
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if csrf_token_cookie == nil or csrf_guard_cookie == nil then
|
||||
request_handle:respond(
|
||||
{[":status"] = "403", ["content-type"] = "application/json"},
|
||||
'{"code":403,"message":"missing csrf cookies"}'
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if csrf_token_header ~= csrf_token_cookie or csrf_guard_header ~= csrf_guard_cookie then
|
||||
request_handle:respond(
|
||||
{[":status"] = "403", ["content-type"] = "application/json"},
|
||||
'{"code":403,"message":"csrf token mismatch"}'
|
||||
)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function envoy_on_response(response_handle)
|
||||
local metadata = response_handle:streamInfo():dynamicMetadata():get("csrf")
|
||||
if metadata == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local token_value = metadata["token_value"]
|
||||
local guard_value = metadata["guard_value"]
|
||||
|
||||
if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then
|
||||
response_handle:headers():add(
|
||||
"set-cookie",
|
||||
TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; SameSite=Strict"
|
||||
)
|
||||
end
|
||||
|
||||
if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then
|
||||
response_handle:headers():add(
|
||||
"set-cookie",
|
||||
GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; SameSite=Strict"
|
||||
)
|
||||
end
|
||||
end
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
|
||||
clusters:
|
||||
- name: user_api_cluster
|
||||
connect_timeout: 2s
|
||||
type: STRICT_DNS
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: user_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: user-api-svc.juwan.svc.cluster.local
|
||||
port_value: 8888
|
||||
- name: email_api_cluster
|
||||
connect_timeout: 2s
|
||||
type: STRICT_DNS
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: email_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: email-api-svc.juwan.svc.cluster.local
|
||||
port_value: 8888
|
||||
|
||||
admin:
|
||||
access_log_path: /tmp/admin_access.log
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: envoy-gateway
|
||||
spec:
|
||||
replicas: 2
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
app: envoy-gateway
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: envoy-gateway
|
||||
spec:
|
||||
containers:
|
||||
- name: envoy
|
||||
image: envoyproxy/envoy:v1.31-latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ["/usr/local/bin/envoy"]
|
||||
args:
|
||||
- "-c"
|
||||
- "/etc/envoy/envoy.yaml"
|
||||
- "--log-level"
|
||||
- "info"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
- containerPort: 9901
|
||||
name: admin
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
volumeMounts:
|
||||
- name: envoy-config
|
||||
mountPath: /etc/envoy
|
||||
volumes:
|
||||
- name: envoy-config
|
||||
configMap:
|
||||
name: envoy-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: envoy-gateway
|
||||
namespace: juwan
|
||||
spec:
|
||||
selector:
|
||||
app: envoy-gateway
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
- name: admin
|
||||
port: 9901
|
||||
targetPort: 9901
|
||||
type: ClusterIP
|
||||
@@ -19,12 +19,13 @@ spec:
|
||||
serviceAccountName: find-endpoints
|
||||
containers:
|
||||
- name: email-api
|
||||
image: email
|
||||
image: 103.236.53.208:4418/library/email-api@sha256:fe5c66f5bcb1a39652620df42351de3e48227920a34be3110a45eb13db327020
|
||||
ports:
|
||||
- containerPort: 8888
|
||||
- containerPort: 4001
|
||||
env:
|
||||
- name: KAFKA_BROKER
|
||||
value: "my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092"
|
||||
value: "my-cluster-kafka-bootstrap.kafka:9092"
|
||||
- name: REDIS_M_HOST
|
||||
value: "user-redis-master.juwan:6379"
|
||||
- name: REDIS_S_HOST
|
||||
@@ -65,10 +66,18 @@ kind: Service
|
||||
metadata:
|
||||
name: email-api-svc
|
||||
namespace: juwan
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "4001"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
ports:
|
||||
- port: 8888
|
||||
- name: http
|
||||
port: 8888
|
||||
targetPort: 8888
|
||||
- name: metrics
|
||||
port: 4001
|
||||
targetPort: 4001
|
||||
selector:
|
||||
app: email-api
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ spec:
|
||||
serviceAccountName: find-endpoints
|
||||
containers:
|
||||
- name: email-consumer
|
||||
image: 103.236.53.208:4418/library/email-consumer@sha256:6fe8a3a57310a5e79feecc4bf38ac2c5b8c58a7f200f104f7bf4707b9db5fc13
|
||||
image: 103.236.53.208:4418/library/email-mq@sha256:a9f76e8f4a17d1c00cefc429962037550e17feebb5cf38b28d360c91c8ba3e68
|
||||
ports:
|
||||
- containerPort: 4001
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
@@ -46,10 +48,17 @@ kind: Service
|
||||
metadata:
|
||||
name: email-consumer-svc
|
||||
namespace: juwan
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "4001"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
# - port: 8080
|
||||
# targetPort: 8080
|
||||
- name: metrics
|
||||
port: 4001
|
||||
targetPort: 4001
|
||||
selector:
|
||||
app: email-consumer
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: snowflake
|
||||
namespace: juwan
|
||||
@@ -71,7 +71,7 @@ metadata:
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
kind: StatefulSet
|
||||
name: snowflake
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
@@ -94,7 +94,7 @@ metadata:
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
kind: StatefulSet
|
||||
name: snowflake
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: user-api
|
||||
spec:
|
||||
replicas: 3
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -19,7 +19,7 @@ spec:
|
||||
serviceAccountName: find-endpoints
|
||||
containers:
|
||||
- name: user-api
|
||||
image: user-api:v1
|
||||
image: 103.236.53.208:4418/library/user-api@sha256:a152f5fd13fc865ae3d9aeaa54eacad6bcaa0cb4f0ccb770fbb746be95360991
|
||||
ports:
|
||||
- containerPort: 8888
|
||||
readinessProbe:
|
||||
@@ -61,50 +61,50 @@ spec:
|
||||
selector:
|
||||
app: user-api
|
||||
|
||||
---
|
||||
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: user-api-hpa-c
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: user-api-hpa-c
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: user-api
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
|
||||
---
|
||||
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: user-api-hpa-m
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: user-api-hpa-m
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: user-api
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
#---
|
||||
#
|
||||
#apiVersion: autoscaling/v2
|
||||
#kind: HorizontalPodAutoscaler
|
||||
#metadata:
|
||||
# name: user-api-hpa-c
|
||||
# namespace: juwan
|
||||
# labels:
|
||||
# app: user-api-hpa-c
|
||||
#spec:
|
||||
# scaleTargetRef:
|
||||
# apiVersion: apps/v1
|
||||
# kind: Deployment
|
||||
# name: user-api
|
||||
# minReplicas: 3
|
||||
# maxReplicas: 10
|
||||
# metrics:
|
||||
# - type: Resource
|
||||
# resource:
|
||||
# name: cpu
|
||||
# target:
|
||||
# type: Utilization
|
||||
# averageUtilization: 80
|
||||
#
|
||||
#---
|
||||
#
|
||||
#apiVersion: autoscaling/v2
|
||||
#kind: HorizontalPodAutoscaler
|
||||
#metadata:
|
||||
# name: user-api-hpa-m
|
||||
# namespace: juwan
|
||||
# labels:
|
||||
# app: user-api-hpa-m
|
||||
#spec:
|
||||
# scaleTargetRef:
|
||||
# apiVersion: apps/v1
|
||||
# kind: Deployment
|
||||
# name: user-api
|
||||
# minReplicas: 3
|
||||
# maxReplicas: 10
|
||||
# metrics:
|
||||
# - type: Resource
|
||||
# resource:
|
||||
# name: memory
|
||||
# target:
|
||||
# type: Utilization
|
||||
# averageUtilization: 80
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
labels:
|
||||
app: user-rpc
|
||||
spec:
|
||||
replicas: 3
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 5
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -29,7 +29,7 @@ spec:
|
||||
]
|
||||
containers:
|
||||
- name: user-rpc
|
||||
image: 103.236.53.208:4418/library/user-rpc@sha256:57746256905acb5757153aef536ebfd19338b7f935f01ba1f538fbfd0a12f6f5
|
||||
image: 103.236.53.208:4418/library/user-rpc@sha256:3d1d3cc02188a9b1a29a308a4867638b25b6e480e5a6bdaeb938f262f53969b7
|
||||
ports:
|
||||
- containerPort: 9001
|
||||
- containerPort: 4001
|
||||
@@ -114,143 +114,151 @@ spec:
|
||||
selector:
|
||||
app: user-rpc
|
||||
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: user-rpc-hpa-c
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: user-rpc-hpa-c
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: user-rpc
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: user-rpc-hpa-m
|
||||
namespace: juwan
|
||||
labels:
|
||||
app: user-rpc-hpa-m
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: user-rpc
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
---
|
||||
# Redis 主从复制
|
||||
apiVersion: redis.redis.opstreelabs.in/v1beta2
|
||||
kind: RedisReplication
|
||||
metadata:
|
||||
name: user-redis
|
||||
namespace: juwan
|
||||
spec:
|
||||
clusterSize: 3
|
||||
kubernetesConfig:
|
||||
image: quay.io/opstree/redis:v7.0.12
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
redisSecret:
|
||||
name: user-redis
|
||||
key: password
|
||||
|
||||
redisExporter:
|
||||
enabled: true
|
||||
image: quay.io/opstree/redis-exporter:latest
|
||||
imagePullPolicy: Always
|
||||
podSecurityContext:
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
storage:
|
||||
volumeClaimTemplate:
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
||||
---
|
||||
# Sentinel 监控
|
||||
apiVersion: redis.redis.opstreelabs.in/v1beta2
|
||||
kind: RedisSentinel
|
||||
metadata:
|
||||
name: user-redis-sentinel
|
||||
namespace: juwan
|
||||
spec:
|
||||
clusterSize: 3
|
||||
kubernetesConfig:
|
||||
image: quay.io/opstree/redis-sentinel:v7.0.12
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
podSecurityContext:
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
redisSentinelConfig:
|
||||
redisReplicationName: user-redis
|
||||
masterGroupName: mymaster
|
||||
redisPort: "6379"
|
||||
quorum: "2"
|
||||
downAfterMilliseconds: "5000"
|
||||
failoverTimeout: "10000"
|
||||
parallelSyncs: "1"
|
||||
|
||||
---
|
||||
# PostgreSQL 集群
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
namespace: juwan
|
||||
name: user-db
|
||||
spec:
|
||||
instances: 3
|
||||
backup:
|
||||
barmanObjectStore:
|
||||
destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/
|
||||
endpointURL: https://cn-nb1.rains3.com
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: rc-creds
|
||||
key: SOucqRaJr4OyfcIu
|
||||
secretAccessKey:
|
||||
name: rc-creds
|
||||
key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz
|
||||
wal:
|
||||
compression: gzip
|
||||
storage:
|
||||
size: 1Gi
|
||||
monitoring:
|
||||
enablePodMonitor: true
|
||||
#---
|
||||
#apiVersion: autoscaling/v2
|
||||
#kind: HorizontalPodAutoscaler
|
||||
#metadata:
|
||||
# name: user-rpc-hpa-c
|
||||
# namespace: juwan
|
||||
# labels:
|
||||
# app: user-rpc-hpa-c
|
||||
#spec:
|
||||
# scaleTargetRef:
|
||||
# apiVersion: apps/v1
|
||||
# kind: Deployment
|
||||
# name: user-rpc
|
||||
# minReplicas: 3
|
||||
# maxReplicas: 10
|
||||
# metrics:
|
||||
# - type: Resource
|
||||
# resource:
|
||||
# name: cpu
|
||||
# target:
|
||||
# type: Utilization
|
||||
# averageUtilization: 80
|
||||
#
|
||||
#---
|
||||
#apiVersion: autoscaling/v2
|
||||
#kind: HorizontalPodAutoscaler
|
||||
#metadata:
|
||||
# name: user-rpc-hpa-m
|
||||
# namespace: juwan
|
||||
# labels:
|
||||
# app: user-rpc-hpa-m
|
||||
#spec:
|
||||
# scaleTargetRef:
|
||||
# apiVersion: apps/v1
|
||||
# kind: Deployment
|
||||
# name: user-rpc
|
||||
# minReplicas: 3
|
||||
# maxReplicas: 10
|
||||
# metrics:
|
||||
# - type: Resource
|
||||
# resource:
|
||||
# name: memory
|
||||
# target:
|
||||
# type: Utilization
|
||||
# averageUtilization: 80
|
||||
#---
|
||||
## Redis 主从复制
|
||||
#apiVersion: redis.redis.opstreelabs.in/v1beta2
|
||||
#kind: RedisReplication
|
||||
#metadata:
|
||||
# name: user-redis
|
||||
# namespace: juwan
|
||||
#spec:
|
||||
# clusterSize: 3
|
||||
# kubernetesConfig:
|
||||
# image: quay.io/opstree/redis:v7.0.12
|
||||
# imagePullPolicy: IfNotPresent
|
||||
# resources:
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 500m
|
||||
# memory: 512Mi
|
||||
# redisSecret:
|
||||
# name: user-redis
|
||||
# key: password
|
||||
#
|
||||
# redisExporter:
|
||||
# enabled: true
|
||||
# image: quay.io/opstree/redis-exporter:latest
|
||||
# imagePullPolicy: Always
|
||||
# podSecurityContext:
|
||||
# runAsUser: 1000
|
||||
# fsGroup: 1000
|
||||
# storage:
|
||||
# volumeClaimTemplate:
|
||||
# spec:
|
||||
# accessModes: ["ReadWriteOnce"]
|
||||
# resources:
|
||||
# requests:
|
||||
# storage: 1Gi
|
||||
#
|
||||
#---
|
||||
## Sentinel 监控
|
||||
#apiVersion: redis.redis.opstreelabs.in/v1beta2
|
||||
#kind: RedisSentinel
|
||||
#metadata:
|
||||
# name: user-redis-sentinel
|
||||
# namespace: juwan
|
||||
#spec:
|
||||
# clusterSize: 3
|
||||
# kubernetesConfig:
|
||||
# image: quay.io/opstree/redis-sentinel:v7.0.12
|
||||
# imagePullPolicy: IfNotPresent
|
||||
# resources:
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# limits:
|
||||
# cpu: 500m
|
||||
# memory: 512Mi
|
||||
# podSecurityContext:
|
||||
# runAsUser: 1000
|
||||
# fsGroup: 1000
|
||||
# redisSentinelConfig:
|
||||
# redisReplicationName: user-redis
|
||||
# masterGroupName: mymaster
|
||||
# redisPort: "6379"
|
||||
# quorum: "2"
|
||||
# downAfterMilliseconds: "5000"
|
||||
# failoverTimeout: "10000"
|
||||
# parallelSyncs: "1"
|
||||
#
|
||||
#---
|
||||
## PostgreSQL 集群
|
||||
#apiVersion: postgresql.cnpg.io/v1
|
||||
#kind: Cluster
|
||||
#metadata:
|
||||
# namespace: juwan
|
||||
# name: user-db
|
||||
#spec:
|
||||
# instances: 3
|
||||
# primaryUpdateStrategy: unsupervised
|
||||
# bootstrap:
|
||||
# initdb:
|
||||
# database: app
|
||||
# owner: app
|
||||
# # 只在 PVC 为空时初始化
|
||||
# postInitSQL:
|
||||
# - CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
# backup:
|
||||
# barmanObjectStore:
|
||||
# destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/
|
||||
# endpointURL: https://cn-nb1.rains3.com
|
||||
# s3Credentials:
|
||||
# accessKeyId:
|
||||
# name: rc-creds
|
||||
# key: SOucqRaJr4OyfcIu
|
||||
# secretAccessKey:
|
||||
# name: rc-creds
|
||||
# key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz
|
||||
# wal:
|
||||
# compression: gzip
|
||||
# storage:
|
||||
# size: 1Gi
|
||||
# monitoring:
|
||||
# enablePodMonitor: true
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ type (
|
||||
Password string `json:"password" binding:"required,min=6,max=128"`
|
||||
Email string `json:"email,omitempty" binding:"omitempty,email"`
|
||||
Phone string `json:"phone,omitempty" binding:"omitempty,len=11"`
|
||||
Vcode int32 `json:"vcode"`
|
||||
}
|
||||
RegisterResp {
|
||||
UserId int64 `json:"userId"`
|
||||
@@ -63,6 +64,7 @@ type (
|
||||
}
|
||||
LogoutReq {
|
||||
UserId int64 `path:"userId" binding:"required,gt=0"`
|
||||
Token string `header:"Authorization" binding:"required"`
|
||||
}
|
||||
LogoutResp {
|
||||
Message string `json:"message"`
|
||||
@@ -116,7 +118,7 @@ service user-api {
|
||||
|
||||
@doc (
|
||||
summary: "用户登出"
|
||||
description: "清除用户的登录会话,使用户令牌失效"
|
||||
description: "需要携带 Authorization 令牌,清除用户会话并使令牌失效"
|
||||
)
|
||||
@handler Logout
|
||||
post /:userId/logout (LogoutReq) returns (LogoutResp)
|
||||
|
||||
+33
-7
@@ -10,7 +10,7 @@ package pb;
|
||||
|
||||
//--------------------------------users--------------------------------
|
||||
message Users {
|
||||
string userId = 1; //userId
|
||||
int64 userId = 1; //userId
|
||||
string username = 2; //username
|
||||
string passwd = 3; //passwd
|
||||
string nickname = 4; //nickname
|
||||
@@ -24,7 +24,7 @@ message Users {
|
||||
}
|
||||
|
||||
message AddUsersReq {
|
||||
string userId = 1; //userId
|
||||
int64 userId = 1; //userId
|
||||
string username = 2; //username
|
||||
string passwd = 3; //passwd
|
||||
string nickname = 4; //nickname
|
||||
@@ -41,7 +41,7 @@ message AddUsersResp {
|
||||
}
|
||||
|
||||
message UpdateUsersReq {
|
||||
string userId = 1; //userId
|
||||
int64 userId = 1; //userId
|
||||
string username = 2; //username
|
||||
string passwd = 3; //passwd
|
||||
string nickname = 4; //nickname
|
||||
@@ -75,7 +75,7 @@ message GetUsersByIdResp {
|
||||
message SearchUsersReq {
|
||||
int64 page = 1; //page
|
||||
int64 limit = 2; //limit
|
||||
string userId = 3; //userId
|
||||
int64 userId = 3; //userId
|
||||
string username = 4; //username
|
||||
string passwd = 5; //passwd
|
||||
string nickname = 6; //nickname
|
||||
@@ -107,22 +107,25 @@ message LoginReq {
|
||||
|
||||
message LoginResp {
|
||||
string token = 1;
|
||||
string username = 2;
|
||||
string email = 3;
|
||||
int64 id = 4;
|
||||
}
|
||||
|
||||
message ValidateTokenReq {
|
||||
string token = 1; // JWT token
|
||||
string userId = 2; // 用户ID
|
||||
int64 userId = 2; // 用户ID
|
||||
}
|
||||
|
||||
message ValidateTokenResp {
|
||||
bool valid = 1; // token 是否有效(不在黑名单中)
|
||||
string message = 2; // 验证失败原因
|
||||
string userId = 3; // 用户ID
|
||||
int64 userId = 3; // 用户ID
|
||||
int64 roleType = 4; // 用户角色
|
||||
}
|
||||
|
||||
message CheckPermissionReq {
|
||||
string userId = 1; // 用户ID
|
||||
int64 userId = 1; // 用户ID
|
||||
string resource = 2; // 资源 ID
|
||||
string action = 3; // 操作类型: read/write/delete
|
||||
}
|
||||
@@ -132,6 +135,27 @@ message CheckPermissionResp {
|
||||
string message = 2; // 拒绝原因
|
||||
}
|
||||
|
||||
message RegisterReq {
|
||||
string username = 1;
|
||||
string passwd = 2;
|
||||
string phone = 3;
|
||||
int32 vcode = 4;
|
||||
string email = 5;
|
||||
string requestId = 6;
|
||||
}
|
||||
|
||||
message RegisterResp {
|
||||
string res = 1;
|
||||
}
|
||||
|
||||
message LogoutReq {
|
||||
int64 userId = 1;
|
||||
}
|
||||
|
||||
message LogoutResp {
|
||||
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Rpc Func
|
||||
// ------------------------------------
|
||||
@@ -146,6 +170,8 @@ service usercenter {
|
||||
rpc GetUserByUsername(GetUserByUsernameReq) returns (GetUserByUsernameResp);
|
||||
rpc SearchUsers(SearchUsersReq) returns (SearchUsersResp);
|
||||
rpc Login(LoginReq) returns (LoginResp);
|
||||
rpc Register(RegisterReq) returns (RegisterResp);
|
||||
rpc ValidateToken(ValidateTokenReq) returns (ValidateTokenResp);
|
||||
rpc CheckPermission(CheckPermissionReq) returns (CheckPermissionResp);
|
||||
rpc Logout(LogoutReq) returns (LogoutResp);
|
||||
}
|
||||
+9
-7
@@ -1,15 +1,17 @@
|
||||
create extension if not exists "uuid-ossp";
|
||||
create extension if not exists "pg_trgm";
|
||||
|
||||
-- create
|
||||
-- extension if not exists "uuid-ossp";
|
||||
create
|
||||
extension if not exists "pg_trgm";
|
||||
|
||||
create table users
|
||||
(
|
||||
user_id uuid primary key default uuid_generate_v4(),
|
||||
user_id BIGINT primary key not null,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
passwd VARCHAR(255) NOT NULL,
|
||||
nickname VARCHAR(50) NOT NULL,
|
||||
phone VARCHAR(20) UNIQUE NOT NULL,
|
||||
role_type SMALLINT NOT NULL, -- 1:玩家, 2:打手, 3:店长
|
||||
nickname VARCHAR(50) NOT NULL DEFAULT ('user_' || substr(md5(random()::text), 1, 10)),
|
||||
phone VARCHAR(20) UNIQUE NOT NULL default '',
|
||||
email varchar(50) unique not null default '',
|
||||
role_type SMALLINT NOT NULL default 1, -- 1:玩家, 2:打手, 3:店长
|
||||
is_verified BOOLEAN DEFAULT false,
|
||||
state BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
+38
-25
@@ -1,16 +1,18 @@
|
||||
# Envoy Gateway Configuration
|
||||
|
||||
This document explains how the Envoy gateway is configured and how to modify it.
|
||||
This document explains how the Envoy unified ingress gateway is configured and how to modify it.
|
||||
|
||||
## Files
|
||||
|
||||
- envoy.yaml: ConfigMap + Deployment + Service for Envoy
|
||||
- deploy/k8s/envoy/envoy.yaml: ConfigMap + Deployment + Service for Envoy
|
||||
|
||||
## Current Behavior
|
||||
|
||||
- Envoy listens on port 8080 in the Pod and exposes port 80 via a ClusterIP Service.
|
||||
- All HTTP traffic is routed to user-api only.
|
||||
- gRPC is not exposed by this gateway.
|
||||
- Route `/api/users` to `user-api-svc:8888`.
|
||||
- Route `/api/email` to `email-api-svc:8888`.
|
||||
- Route `/healthz` returns `200 ok` directly from gateway.
|
||||
- Unknown routes return `404` from gateway.
|
||||
|
||||
## Routing
|
||||
|
||||
@@ -20,27 +22,30 @@ static_resources -> listeners -> http_connection_manager -> route_config -> virt
|
||||
|
||||
The current routing rules are:
|
||||
|
||||
- All requests (prefix: "/") -> cluster: user-api
|
||||
- `prefix: /api/users` -> `cluster: user_api_cluster`
|
||||
- `prefix: /api/email` -> `cluster: email_api_cluster`
|
||||
- `path: /healthz` -> direct response `200`
|
||||
- `prefix: /` -> direct response `404`
|
||||
|
||||
To add a new HTTP service, add a new route above the default route and define a new cluster.
|
||||
|
||||
Example: route /order to order-api-svc:8899
|
||||
Example: route `/api/order` to `order-api-svc:8899`
|
||||
|
||||
1) Add a route match:
|
||||
|
||||
- match:
|
||||
prefix: "/order"
|
||||
prefix: "/api/order"
|
||||
route:
|
||||
cluster: order-api
|
||||
cluster: order_api_cluster
|
||||
|
||||
2) Add a cluster:
|
||||
1) Add a cluster:
|
||||
|
||||
- name: order-api
|
||||
- name: order_api_cluster
|
||||
connect_timeout: 2s
|
||||
type: STRICT_DNS
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: order-api
|
||||
cluster_name: order_api_cluster
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
@@ -49,22 +54,29 @@ Example: route /order to order-api-svc:8899
|
||||
address: order-api-svc.juwan.svc.cluster.local
|
||||
port_value: 8899
|
||||
|
||||
## CSRF Protection
|
||||
## CSRF Protection (Double Cookie)
|
||||
|
||||
Envoy uses a Lua filter for CSRF validation:
|
||||
Envoy uses a Lua filter for double-cookie CSRF validation:
|
||||
|
||||
- Safe methods (GET/HEAD/OPTIONS):
|
||||
- If csrf_token cookie is missing, Envoy generates one and sets it in the response.
|
||||
- If missing, Envoy auto-issues two cookies:
|
||||
- `csrf_token`
|
||||
- `csrf_guard`
|
||||
- Unsafe methods (POST/PUT/PATCH/DELETE, etc):
|
||||
- Requires BOTH:
|
||||
- header: X-CSRF-Token
|
||||
- cookie: csrf_token
|
||||
- Values must match, otherwise Envoy returns 403.
|
||||
- Requires BOTH headers:
|
||||
- `X-CSRF-Token`
|
||||
- `X-CSRF-Guard`
|
||||
- Requires BOTH cookies:
|
||||
- `csrf_token`
|
||||
- `csrf_guard`
|
||||
- Header values must exactly match cookie values, otherwise Envoy returns `403`.
|
||||
|
||||
If you want a different cookie name or header name, update these in the Lua code:
|
||||
If you want different cookie or header names, update these constants in Lua:
|
||||
|
||||
- Header: x-csrf-token
|
||||
- Cookie: csrf_token
|
||||
- `TOKEN_COOKIE`
|
||||
- `GUARD_COOKIE`
|
||||
- `TOKEN_HEADER`
|
||||
- `GUARD_HEADER`
|
||||
|
||||
To relax or tighten rules, edit the functions:
|
||||
|
||||
@@ -75,9 +87,8 @@ To relax or tighten rules, edit the functions:
|
||||
|
||||
Current Set-Cookie:
|
||||
|
||||
csrf_token=<value>; Path=/; SameSite=Strict
|
||||
|
||||
To add Secure or HttpOnly, update the string in envoy_on_response.
|
||||
- `csrf_token=<value>; Path=/; SameSite=Strict`
|
||||
- `csrf_guard=<value>; Path=/; SameSite=Strict`
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -90,6 +101,8 @@ kubectl apply -f deploy/k8s/envoy/envoy.yaml
|
||||
- Change listening port:
|
||||
- Update listener port_value and Service targetPort/port.
|
||||
- Change service namespace:
|
||||
- Update cluster DNS addresses (e.g. service.ns.svc.cluster.local).
|
||||
- Update cluster DNS addresses (e.g. `service.ns.svc.cluster.local`).
|
||||
- Add more services:
|
||||
- Add route + add cluster, as shown above.
|
||||
- Update CSRF policy:
|
||||
- Edit Lua validation logic in `envoy.filters.http.lua`.
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.25.1
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
github.com/zeromicro/go-zero v1.10.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
|
||||
@@ -92,6 +92,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
||||
Reference in New Issue
Block a user