add: user auth accomplished

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