From cdb1f049f3d82f2eb054b2677b9dd6e95e31392d Mon Sep 17 00:00:00 2001 From: GoEdgeLab Date: Fri, 10 May 2024 14:28:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0Ticket=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/db/models/login_ticket_dao.go | 121 ++++++++++++++++++ internal/db/models/login_ticket_dao_test.go | 6 + internal/db/models/login_ticket_model.go | 35 +++++ internal/db/models/login_ticket_model_ext.go | 1 + internal/nodes/api_node_services.go | 5 + internal/rpc/services/service_login_ticket.go | 71 ++++++++++ internal/setup/sql.json | 47 +++++++ 7 files changed, 286 insertions(+) create mode 100644 internal/db/models/login_ticket_dao.go create mode 100644 internal/db/models/login_ticket_dao_test.go create mode 100644 internal/db/models/login_ticket_model.go create mode 100644 internal/db/models/login_ticket_model_ext.go create mode 100644 internal/rpc/services/service_login_ticket.go diff --git a/internal/db/models/login_ticket_dao.go b/internal/db/models/login_ticket_dao.go new file mode 100644 index 00000000..a55b1e30 --- /dev/null +++ b/internal/db/models/login_ticket_dao.go @@ -0,0 +1,121 @@ +package models + +import ( + "errors" + teaconst "github.com/TeaOSLab/EdgeAPI/internal/const" + "github.com/TeaOSLab/EdgeAPI/internal/goman" + "github.com/TeaOSLab/EdgeAPI/internal/remotelogs" + "github.com/TeaOSLab/EdgeCommon/pkg/iputils" + _ "github.com/go-sql-driver/mysql" + "github.com/iwind/TeaGo/Tea" + "github.com/iwind/TeaGo/dbs" + "github.com/iwind/TeaGo/rands" + "github.com/iwind/TeaGo/types" + stringutil "github.com/iwind/TeaGo/utils/string" + "math/rand" + "time" +) + +func init() { + if !teaconst.IsMain { + return + } + + // 清理过期的票据 + var ticker = time.NewTicker(time.Duration(rands.Int(36, 48)) * time.Hour) + goman.New(func() { + for range ticker.C { + err := SharedLoginTicketDAO.CleanExpiredTickets(nil) + if err != nil { + remotelogs.Error("LoginTicketDAO", "clean expired tickets failed: "+err.Error()) + } + } + }) +} + +type LoginTicketDAO dbs.DAO + +func NewLoginTicketDAO() *LoginTicketDAO { + return dbs.NewDAO(&LoginTicketDAO{ + DAOObject: dbs.DAOObject{ + DB: Tea.Env, + Table: "edgeLoginTickets", + Model: new(LoginTicket), + PkName: "id", + }, + }).(*LoginTicketDAO) +} + +var SharedLoginTicketDAO *LoginTicketDAO + +func init() { + dbs.OnReady(func() { + SharedLoginTicketDAO = NewLoginTicketDAO() + }) +} + +// CreateLoginTicket 创建票据 +func (this *LoginTicketDAO) CreateLoginTicket(tx *dbs.Tx, adminId int64, userId int64, ip string) (ticketValue string, err error) { + if adminId <= 0 && userId <= 0 { + err = errors.New("either 'adminId' or 'userId' must be greater than 0") + return + } + + if len(ip) > 0 && !iputils.IsValid(ip) { + err = errors.New("invalid ip: '" + ip + "'") + return + } + + ticketValue = stringutil.Md5(types.String(adminId) + "@" + types.String(userId) + types.String(time.Now().UnixNano()) + "@" + types.String(rand.Int63()) + "@" + ip) + + var op = NewLoginTicketOperator() + op.AdminId = adminId + op.UserId = userId + op.ExpiresAt = time.Now().Unix() + 600 /* 10 minutes */ + op.Ip = ip + op.Value = ticketValue + err = this.Save(tx, op) + if err != nil { + return + } + + return ticketValue, nil +} + +// FindLoginTicketWithValue 查找票据 +func (this *LoginTicketDAO) FindLoginTicketWithValue(tx *dbs.Tx, value string) (*LoginTicket, error) { + if len(value) == 0 { + return nil, nil + } + + if len(value) != 32 { + return nil, nil + } + + one, err := this.Query(tx). + Attr("value", value). + Gt("expiresAt", time.Now().Unix()). + Find() + if one == nil || err != nil { + return nil, err + } + + var ticket = one.(*LoginTicket) + + // delete the ticket + err = this.Query(tx). + Pk(ticket.Id). + DeleteQuickly() + if err != nil { + return nil, err + } + + return ticket, nil +} + +// CleanExpiredTickets 清理过期的票据 +func (this *LoginTicketDAO) CleanExpiredTickets(tx *dbs.Tx) error { + return this.Query(tx). + Lt("expiresAt", time.Now().Unix()). + DeleteQuickly() +} diff --git a/internal/db/models/login_ticket_dao_test.go b/internal/db/models/login_ticket_dao_test.go new file mode 100644 index 00000000..6595eab6 --- /dev/null +++ b/internal/db/models/login_ticket_dao_test.go @@ -0,0 +1,6 @@ +package models_test + +import ( + _ "github.com/go-sql-driver/mysql" + _ "github.com/iwind/TeaGo/bootstrap" +) diff --git a/internal/db/models/login_ticket_model.go b/internal/db/models/login_ticket_model.go new file mode 100644 index 00000000..9f4402a5 --- /dev/null +++ b/internal/db/models/login_ticket_model.go @@ -0,0 +1,35 @@ +package models + +import "github.com/iwind/TeaGo/dbs" + +const ( + LoginTicketField_Id dbs.FieldName = "id" // ID + LoginTicketField_ExpiresAt dbs.FieldName = "expiresAt" // 过期时间 + LoginTicketField_Value dbs.FieldName = "value" // 票据值 + LoginTicketField_AdminId dbs.FieldName = "adminId" // 管理员ID + LoginTicketField_UserId dbs.FieldName = "userId" // 用户ID + LoginTicketField_Ip dbs.FieldName = "ip" // 用户IP +) + +// LoginTicket 登录票据 +type LoginTicket struct { + Id uint64 `field:"id"` // ID + ExpiresAt uint64 `field:"expiresAt"` // 过期时间 + Value string `field:"value"` // 票据值 + AdminId uint32 `field:"adminId"` // 管理员ID + UserId uint32 `field:"userId"` // 用户ID + Ip string `field:"ip"` // 用户IP +} + +type LoginTicketOperator struct { + Id any // ID + ExpiresAt any // 过期时间 + Value any // 票据值 + AdminId any // 管理员ID + UserId any // 用户ID + Ip any // 用户IP +} + +func NewLoginTicketOperator() *LoginTicketOperator { + return &LoginTicketOperator{} +} diff --git a/internal/db/models/login_ticket_model_ext.go b/internal/db/models/login_ticket_model_ext.go new file mode 100644 index 00000000..2640e7f9 --- /dev/null +++ b/internal/db/models/login_ticket_model_ext.go @@ -0,0 +1 @@ +package models diff --git a/internal/nodes/api_node_services.go b/internal/nodes/api_node_services.go index ef49ea32..eb79446c 100644 --- a/internal/nodes/api_node_services.go +++ b/internal/nodes/api_node_services.go @@ -334,6 +334,11 @@ func (this *APINode) registerServices(server *grpc.Server) { pb.RegisterLoginSessionServiceServer(server, instance) this.rest(instance) } + { + var instance = this.serviceInstance(&services.LoginTicketService{}).(*services.LoginTicketService) + pb.RegisterLoginTicketServiceServer(server, instance) + this.rest(instance) + } { var instance = this.serviceInstance(&services.UserAccessKeyService{}).(*services.UserAccessKeyService) pb.RegisterUserAccessKeyServiceServer(server, instance) diff --git a/internal/rpc/services/service_login_ticket.go b/internal/rpc/services/service_login_ticket.go new file mode 100644 index 00000000..0b20ea44 --- /dev/null +++ b/internal/rpc/services/service_login_ticket.go @@ -0,0 +1,71 @@ +// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package services + +import ( + "context" + "errors" + "github.com/TeaOSLab/EdgeAPI/internal/db/models" + "github.com/TeaOSLab/EdgeCommon/pkg/iputils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" +) + +// LoginTicketService 登录票据相关服务 +type LoginTicketService struct { + BaseService +} + +// CreateLoginTicket 创建票据 +func (this *LoginTicketService) CreateLoginTicket(ctx context.Context, req *pb.CreateLoginTicketRequest) (*pb.CreateLoginTicketResponse, error) { + _, err := this.ValidateAdmin(ctx) + if err != nil { + return nil, err + } + + if req.AdminId <= 0 && req.UserId <= 0 { + return nil, errors.New("either 'adminId' or 'userId' must be greater than 0") + } + + if len(req.Ip) > 0 && !iputils.IsValid(req.Ip) { + return nil, errors.New("invalid ip: '" + req.Ip + "'") + } + + var tx = this.NullTx() + value, err := models.SharedLoginTicketDAO.CreateLoginTicket(tx, req.AdminId, req.UserId, req.Ip) + if err != nil { + return nil, err + } + return &pb.CreateLoginTicketResponse{Value: value}, nil +} + +// FindLoginTicketWithValue 查找票据 +// 查找成功后,会自动删除票据信息,所以票据信息只能查询一次 +func (this *LoginTicketService) FindLoginTicketWithValue(ctx context.Context, req *pb.FindLoginTicketWithValueRequest) (*pb.FindLoginTicketWithValueResponse, error) { + _, _, err := this.ValidateAdminAndUser(ctx, false) + if err != nil { + return nil, err + } + + var tx = this.NullTx() + ticket, err := models.SharedLoginTicketDAO.FindLoginTicketWithValue(tx, req.Value) + if err != nil { + return nil, err + } + + if ticket == nil { + return &pb.FindLoginTicketWithValueResponse{ + LoginTicket: nil, + }, nil + } + + return &pb.FindLoginTicketWithValueResponse{ + LoginTicket: &pb.LoginTicket{ + Id: int64(ticket.Id), + ExpiresAt: int64(ticket.ExpiresAt), + Value: ticket.Value, + AdminId: int64(ticket.AdminId), + UserId: int64(ticket.UserId), + Ip: ticket.Ip, + }, + }, nil +} diff --git a/internal/setup/sql.json b/internal/setup/sql.json index 9601dee1..a86d5acf 100644 --- a/internal/setup/sql.json +++ b/internal/setup/sql.json @@ -115193,6 +115193,53 @@ ], "records": [] }, + { + "name": "edgeLoginTickets", + "engine": "InnoDB", + "charset": "utf8mb4_general_ci", + "definition": "CREATE TABLE `edgeLoginTickets` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',\n `expiresAt` bigint(11) unsigned DEFAULT '0' COMMENT '过期时间',\n `value` varchar(32) DEFAULT NULL COMMENT '票据值',\n `adminId` int(11) unsigned DEFAULT '0' COMMENT '管理员ID',\n `userId` int(11) unsigned DEFAULT '0' COMMENT '用户ID',\n `ip` varchar(64) DEFAULT NULL COMMENT '用户IP',\n PRIMARY KEY (`id`),\n KEY `value` (`value`),\n KEY `expiresAt` (`expiresAt`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录票据'", + "fields": [ + { + "name": "id", + "definition": "bigint(20) unsigned auto_increment COMMENT 'ID'" + }, + { + "name": "expiresAt", + "definition": "bigint(11) unsigned DEFAULT '0' COMMENT '过期时间'" + }, + { + "name": "value", + "definition": "varchar(32) COMMENT '票据值'" + }, + { + "name": "adminId", + "definition": "int(11) unsigned DEFAULT '0' COMMENT '管理员ID'" + }, + { + "name": "userId", + "definition": "int(11) unsigned DEFAULT '0' COMMENT '用户ID'" + }, + { + "name": "ip", + "definition": "varchar(64) COMMENT '用户IP'" + } + ], + "indexes": [ + { + "name": "PRIMARY", + "definition": "UNIQUE KEY `PRIMARY` (`id`) USING BTREE" + }, + { + "name": "value", + "definition": "KEY `value` (`value`) USING BTREE" + }, + { + "name": "expiresAt", + "definition": "KEY `expiresAt` (`expiresAt`) USING BTREE" + } + ], + "records": [] + }, { "name": "edgeLogins", "engine": "InnoDB",