From 60b6f40f9f51fdb0e169a73f166b29941b2822c7 Mon Sep 17 00:00:00 2001 From: wwweww <2646787260@qq.com> Date: Thu, 26 Feb 2026 02:17:07 +0800 Subject: [PATCH] add: user auth accomplished --- app/email/api/email.go | 2 +- app/email/api/etc/email-api.yaml | 30 +- .../logic/email/sendVerificationCodeLogic.go | 4 +- app/email/mq/email.go | 2 +- app/email/mq/etc/email.yaml | 2 +- app/email/mq/internal/consumer/consumer.go | 1 - app/snowflake/rpc/etc/snowflake.yaml | 6 +- app/users/api/etc/user-api.yaml | 3 + app/users/api/internal/config/config.go | 1 + app/users/api/internal/contextx/contextx.go | 30 ++ .../api/internal/handler/user/loginHandler.go | 22 +- .../internal/handler/user/registerHandler.go | 70 +++- .../internal/logic/user/getUserInfoLogic.go | 20 +- .../api/internal/logic/user/loginLogic.go | 24 +- .../api/internal/logic/user/logoutLogic.go | 13 +- .../api/internal/logic/user/registerLogic.go | 54 ++- app/users/api/internal/svc/serviceContext.go | 16 +- app/users/api/internal/types/types.go | 4 +- app/users/api/users.go | 34 ++ app/users/rpc/internal/logic/loginLogic.go | 30 +- app/users/rpc/internal/logic/logoutLogic.go | 34 ++ app/users/rpc/internal/logic/registerLogic.go | 92 +++++ .../rpc/internal/logic/validateTokenLogic.go | 20 +- .../models/{usersmodel.go => usersModel.go} | 0 .../{usersmodel_gen.go => usersModel_gen.go} | 38 +- .../rpc/internal/server/usercenterServer.go | 10 + app/users/rpc/internal/utils/jwks.go | 90 ---- app/users/rpc/internal/utils/jwt.go | 78 +++- app/users/rpc/pb.go | 1 + app/users/rpc/pb/users.pb.go | 360 +++++++++++++--- app/users/rpc/pb/users_grpc.pb.go | 100 ++++- app/users/rpc/usercenter/usercenter.go | 16 + cnpg_for_specific_namespace.yaml | 0 common/utils/responses.go | 13 + deploy/envoy/ENVOY_CONFIG_GUIDE.md | 320 --------------- deploy/envoy/QUICK_REFERENCE.md | 371 ----------------- deploy/envoy/deploy.sh | 331 --------------- deploy/envoy/envoy.yaml | 385 ------------------ deploy/envoy/generate-jwks.sh | 48 --- deploy/envoy/setup-jwt-auth.sh | 55 --- deploy/k8s/base/snowflake-workid.yaml | 33 -- deploy/k8s/envoy-gateway.yaml | 262 ------------ deploy/k8s/envoy/envoy.yaml | 294 +++++++++++++ deploy/k8s/service/email/email-api.yaml | 15 +- deploy/k8s/service/email/email-mq.yaml | 15 +- deploy/k8s/service/snowflake/snowflake.yaml | 6 +- deploy/k8s/service/user/user-api.yaml | 98 ++--- deploy/k8s/service/user/user-rpc.yaml | 292 ++++++------- desc/api/users.api | 6 +- desc/rpc/users.proto | 70 +++- desc/sql/users.sql | 16 +- docs/README.md | 63 +-- go.mod | 1 + go.sum | 3 + 54 files changed, 1601 insertions(+), 2303 deletions(-) create mode 100644 app/users/api/internal/contextx/contextx.go create mode 100644 app/users/api/users.go create mode 100644 app/users/rpc/internal/logic/logoutLogic.go create mode 100644 app/users/rpc/internal/logic/registerLogic.go rename app/users/rpc/internal/models/{usersmodel.go => usersModel.go} (100%) rename app/users/rpc/internal/models/{usersmodel_gen.go => usersModel_gen.go} (81%) delete mode 100644 app/users/rpc/internal/utils/jwks.go create mode 100644 cnpg_for_specific_namespace.yaml create mode 100644 common/utils/responses.go delete mode 100644 deploy/envoy/ENVOY_CONFIG_GUIDE.md delete mode 100644 deploy/envoy/QUICK_REFERENCE.md delete mode 100644 deploy/envoy/deploy.sh delete mode 100644 deploy/envoy/envoy.yaml delete mode 100644 deploy/envoy/generate-jwks.sh delete mode 100644 deploy/envoy/setup-jwt-auth.sh delete mode 100644 deploy/k8s/base/snowflake-workid.yaml delete mode 100644 deploy/k8s/envoy-gateway.yaml create mode 100644 deploy/k8s/envoy/envoy.yaml diff --git a/app/email/api/email.go b/app/email/api/email.go index a4d6ebe..df6cc60 100644 --- a/app/email/api/email.go +++ b/app/email/api/email.go @@ -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() diff --git a/app/email/api/etc/email-api.yaml b/app/email/api/etc/email-api.yaml index 6378883..b57ce1a 100644 --- a/app/email/api/etc/email-api.yaml +++ b/app/email/api/etc/email-api.yaml @@ -2,18 +2,24 @@ 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 - Pass: "${REDIS_PASSWORD}" - User: "default" - - Host: "${REDIS_S_HOST}" - Type: node - Pass: "${REDIS_PASSWORD}" - User: "default" + - Host: "${REDIS_M_HOST}" + Type: node + Pass: "${REDIS_PASSWORD}" + User: "default" + - Host: "${REDIS_S_HOST}" + Type: node + Pass: "${REDIS_PASSWORD}" + User: "default" Kmq: - Name: email-api - Brokers: - - "${KAFKA_BROKER}" - Topic: "email-task" + Name: email-api + Brokers: + - "${KAFKA_BROKER}" + Group: "email-api-group" + Topic: "email-task" diff --git a/app/email/api/internal/logic/email/sendVerificationCodeLogic.go b/app/email/api/internal/logic/email/sendVerificationCodeLogic.go index cdde681..f5464dd 100644 --- a/app/email/api/internal/logic/email/sendVerificationCodeLogic.go +++ b/app/email/api/internal/logic/email/sendVerificationCodeLogic.go @@ -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 } diff --git a/app/email/mq/email.go b/app/email/mq/email.go index c9fe118..0b4f6ee 100644 --- a/app/email/mq/email.go +++ b/app/email/mq/email.go @@ -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) } diff --git a/app/email/mq/etc/email.yaml b/app/email/mq/etc/email.yaml index 6eb5915..754d152 100644 --- a/app/email/mq/etc/email.yaml +++ b/app/email/mq/etc/email.yaml @@ -2,7 +2,7 @@ Name: email-mq Prometheus: Host: 0.0.0.0 - Port: 4003 + Port: 4001 Path: /metrics Kmq: diff --git a/app/email/mq/internal/consumer/consumer.go b/app/email/mq/internal/consumer/consumer.go index d84065b..ec08c72 100644 --- a/app/email/mq/internal/consumer/consumer.go +++ b/app/email/mq/internal/consumer/consumer.go @@ -10,7 +10,6 @@ import ( ) func Mqs(c config.Config) []service.Service { - //svcContext := NewServiceContext ctx := context.Background() svcCtx := svc.NewServiceContext(c) diff --git a/app/snowflake/rpc/etc/snowflake.yaml b/app/snowflake/rpc/etc/snowflake.yaml index 92d0492..5ba165e 100644 --- a/app/snowflake/rpc/etc/snowflake.yaml +++ b/app/snowflake/rpc/etc/snowflake.yaml @@ -1,10 +1,6 @@ Name: snowflake.rpc ListenOn: 0.0.0.0:8080 -#Etcd: -# Hosts: -# - 127.0.0.1:2379 -# Key: snowflake.rpc Snowflake: DatacenterId: 1 - WorkerId: 0 \ No newline at end of file + WorkerId: 0 diff --git a/app/users/api/etc/user-api.yaml b/app/users/api/etc/user-api.yaml index 3db86be..93d74f3 100644 --- a/app/users/api/etc/user-api.yaml +++ b/app/users/api/etc/user-api.yaml @@ -9,3 +9,6 @@ Prometheus: UsercenterRpcConf: Target: k8s://juwan/user-rpc-svc:9001 + +SnowflakeRpcConf: + Target: k8s://juwan/snowflake-svc:8080 diff --git a/app/users/api/internal/config/config.go b/app/users/api/internal/config/config.go index 33dfe0b..d864b93 100644 --- a/app/users/api/internal/config/config.go +++ b/app/users/api/internal/config/config.go @@ -11,4 +11,5 @@ import ( type Config struct { rest.RestConf UsercenterRpcConf zrpc.RpcClientConf + SnowflakeRpcConf zrpc.RpcClientConf } diff --git a/app/users/api/internal/contextx/contextx.go b/app/users/api/internal/contextx/contextx.go new file mode 100644 index 0000000..50a1de9 --- /dev/null +++ b/app/users/api/internal/contextx/contextx.go @@ -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 +} diff --git a/app/users/api/internal/handler/user/loginHandler.go b/app/users/api/internal/handler/user/loginHandler.go index d9bd694..6bdd0b2 100644 --- a/app/users/api/internal/handler/user/loginHandler.go +++ b/app/users/api/internal/handler/user/loginHandler.go @@ -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 { diff --git a/app/users/api/internal/handler/user/registerHandler.go b/app/users/api/internal/handler/user/registerHandler.go index 7601653..9cb375a 100644 --- a/app/users/api/internal/handler/user/registerHandler.go +++ b/app/users/api/internal/handler/user/registerHandler.go @@ -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 +} diff --git a/app/users/api/internal/logic/user/getUserInfoLogic.go b/app/users/api/internal/logic/user/getUserInfoLogic.go index 9422789..dda2297 100644 --- a/app/users/api/internal/logic/user/getUserInfoLogic.go +++ b/app/users/api/internal/logic/user/getUserInfoLogic.go @@ -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 + 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") + } - return + //req.UserId + return user, nil } diff --git a/app/users/api/internal/logic/user/loginLogic.go b/app/users/api/internal/logic/user/loginLogic.go index 2a79ab3..baa6522 100644 --- a/app/users/api/internal/logic/user/loginLogic.go +++ b/app/users/api/internal/logic/user/loginLogic.go @@ -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) { + 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") + } - return &types.LoginResp{}, nil + 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 } diff --git a/app/users/api/internal/logic/user/logoutLogic.go b/app/users/api/internal/logic/user/logoutLogic.go index 6a5f334..b740558 100644 --- a/app/users/api/internal/logic/user/logoutLogic.go +++ b/app/users/api/internal/logic/user/logoutLogic.go @@ -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 + if req.UserId <= 0 { + return nil, errors.New("invalid userId") + } - return + _, 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 } diff --git a/app/users/api/internal/logic/user/registerLogic.go b/app/users/api/internal/logic/user/registerLogic.go index 544f183..26520c8 100644 --- a/app/users/api/internal/logic/user/registerLogic.go +++ b/app/users/api/internal/logic/user/registerLogic.go @@ -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(), - Username: req.Username, - Passwd: hashedPassword, - Phone: req.Phone, - State: true, + 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.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 } diff --git a/app/users/api/internal/svc/serviceContext.go b/app/users/api/internal/svc/serviceContext.go index 03afc77..b83e976 100644 --- a/app/users/api/internal/svc/serviceContext.go +++ b/app/users/api/internal/svc/serviceContext.go @@ -4,24 +4,28 @@ 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" ) type ServiceContext struct { - Config config.Config - Logger rest.Middleware - UserRpc usercenter.Usercenter + Config config.Config + Logger rest.Middleware + UserRpc usercenter.Usercenter + SnowflakeRpc snowflake.SnowflakeServiceClient } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ - Config: c, - Logger: middleware.NewLoggerMiddleware().Handle, - UserRpc: usercenter.NewUsercenter(zrpc.MustNewClient(c.UsercenterRpcConf)), + Config: c, + Logger: middleware.NewLoggerMiddleware().Handle, + UserRpc: usercenter.NewUsercenter(zrpc.MustNewClient(c.UsercenterRpcConf)), + SnowflakeRpc: snowflakex.NewClient(c.SnowflakeRpcConf), } } diff --git a/app/users/api/internal/types/types.go b/app/users/api/internal/types/types.go index cd9bb8b..6ddf4dc 100644 --- a/app/users/api/internal/types/types.go +++ b/app/users/api/internal/types/types.go @@ -26,7 +26,8 @@ type LoginResp struct { } type LogoutReq struct { - UserId int64 `path:"userId" binding:"required,gt=0"` + 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 { diff --git a/app/users/api/users.go b/app/users/api/users.go new file mode 100644 index 0000000..b023a60 --- /dev/null +++ b/app/users/api/users.go @@ -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() +} diff --git a/app/users/rpc/internal/logic/loginLogic.go b/app/users/rpc/internal/logic/loginLogic.go index 879143b..9ed7bf2 100644 --- a/app/users/rpc/internal/logic/loginLogic.go +++ b/app/users/rpc/internal/logic/loginLogic.go @@ -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 + 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") + } - return &pb.LoginResp{}, nil + 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 } diff --git a/app/users/rpc/internal/logic/logoutLogic.go b/app/users/rpc/internal/logic/logoutLogic.go new file mode 100644 index 0000000..3d1e42f --- /dev/null +++ b/app/users/rpc/internal/logic/logoutLogic.go @@ -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 +} diff --git a/app/users/rpc/internal/logic/registerLogic.go b/app/users/rpc/internal/logic/registerLogic.go new file mode 100644 index 0000000..f86fd5a --- /dev/null +++ b/app/users/rpc/internal/logic/registerLogic.go @@ -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 +} diff --git a/app/users/rpc/internal/logic/validateTokenLogic.go b/app/users/rpc/internal/logic/validateTokenLogic.go index f388342..fe4fdf2 100644 --- a/app/users/rpc/internal/logic/validateTokenLogic.go +++ b/app/users/rpc/internal/logic/validateTokenLogic.go @@ -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 + 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{}, nil + return &pb.ValidateTokenResp{ + Valid: true, + Message: "OK", + UserId: in.UserId, + RoleType: users.RoleType, + }, nil } diff --git a/app/users/rpc/internal/models/usersmodel.go b/app/users/rpc/internal/models/usersModel.go similarity index 100% rename from app/users/rpc/internal/models/usersmodel.go rename to app/users/rpc/internal/models/usersModel.go diff --git a/app/users/rpc/internal/models/usersmodel_gen.go b/app/users/rpc/internal/models/usersModel_gen.go similarity index 81% rename from app/users/rpc/internal/models/usersmodel_gen.go rename to app/users/rpc/internal/models/usersModel_gen.go index ee854d2..9919251 100644 --- a/app/users/rpc/internal/models/usersmodel_gen.go +++ b/app/users/rpc/internal/models/usersModel_gen.go @@ -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 } diff --git a/app/users/rpc/internal/server/usercenterServer.go b/app/users/rpc/internal/server/usercenterServer.go index 98fc8a1..7c523b1 100644 --- a/app/users/rpc/internal/server/usercenterServer.go +++ b/app/users/rpc/internal/server/usercenterServer.go @@ -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) +} diff --git a/app/users/rpc/internal/utils/jwks.go b/app/users/rpc/internal/utils/jwks.go deleted file mode 100644 index e23ddc1..0000000 --- a/app/users/rpc/internal/utils/jwks.go +++ /dev/null @@ -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 -} diff --git a/app/users/rpc/internal/utils/jwt.go b/app/users/rpc/internal/utils/jwt.go index 578f3ad..e6815df 100644 --- a/app/users/rpc/internal/utils/jwt.go +++ b/app/users/rpc/internal/utils/jwt.go @@ -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 +} diff --git a/app/users/rpc/pb.go b/app/users/rpc/pb.go index c0c69c3..a448628 100644 --- a/app/users/rpc/pb.go +++ b/app/users/rpc/pb.go @@ -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" diff --git a/app/users/rpc/pb/users.pb.go b/app/users/rpc/pb/users.pb.go index a1296f8..97b33b5 100644 --- a/app/users/rpc/pb/users.pb.go +++ b/app/users/rpc/pb/users.pb.go @@ -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 + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // JWT token + 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, }, diff --git a/app/users/rpc/pb/users_grpc.pb.go b/app/users/rpc/pb/users_grpc.pb.go index 83321a7..5cd7f95 100644 --- a/app/users/rpc/pb/users_grpc.pb.go +++ b/app/users/rpc/pb/users_grpc.pb.go @@ -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", diff --git a/app/users/rpc/usercenter/usercenter.go b/app/users/rpc/usercenter/usercenter.go index 96f7ca0..75d8de5 100644 --- a/app/users/rpc/usercenter/usercenter.go +++ b/app/users/rpc/usercenter/usercenter.go @@ -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...) +} diff --git a/cnpg_for_specific_namespace.yaml b/cnpg_for_specific_namespace.yaml new file mode 100644 index 0000000..e69de29 diff --git a/common/utils/responses.go b/common/utils/responses.go new file mode 100644 index 0000000..b0b881e --- /dev/null +++ b/common/utils/responses.go @@ -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(), + } +} diff --git a/deploy/envoy/ENVOY_CONFIG_GUIDE.md b/deploy/envoy/ENVOY_CONFIG_GUIDE.md deleted file mode 100644 index 71c4059..0000000 --- a/deploy/envoy/ENVOY_CONFIG_GUIDE.md +++ /dev/null @@ -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 -- \ - 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 -- \ - 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) diff --git a/deploy/envoy/QUICK_REFERENCE.md b/deploy/envoy/QUICK_REFERENCE.md deleted file mode 100644 index ec43fc8..0000000 --- a/deploy/envoy/QUICK_REFERENCE.md +++ /dev/null @@ -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 -- \ - curl http://user-rpc-svc:9001/.well-known/jwks.json - ``` - -- [ ] 后端服务健康 - ```bash - kubectl exec -it -n juwan -- \ - 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) diff --git a/deploy/envoy/deploy.sh b/deploy/envoy/deploy.sh deleted file mode 100644 index 289eba5..0000000 --- a/deploy/envoy/deploy.sh +++ /dev/null @@ -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:///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 "$@" diff --git a/deploy/envoy/envoy.yaml b/deploy/envoy/envoy.yaml deleted file mode 100644 index cf4492c..0000000 --- a/deploy/envoy/envoy.yaml +++ /dev/null @@ -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 diff --git a/deploy/envoy/generate-jwks.sh b/deploy/envoy/generate-jwks.sh deleted file mode 100644 index e80840a..0000000 --- a/deploy/envoy/generate-jwks.sh +++ /dev/null @@ -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" < 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=; Path=/; SameSite=Strict - -To add Secure or HttpOnly, update the string in envoy_on_response. +- `csrf_token=; Path=/; SameSite=Strict` +- `csrf_guard=; 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`. diff --git a/go.mod b/go.mod index 4da5096..486d038 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 59e1ea9..b15815e 100644 --- a/go.sum +++ b/go.sum @@ -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=