From 990394bf463bef9f60a8b60f3f2ceaac47889058 Mon Sep 17 00:00:00 2001 From: Akkia Date: Wed, 18 May 2022 10:10:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Token=E7=94=9F=E6=88=90=EF=BD=9C?= =?UTF-8?q?=E5=AD=98=E5=82=A8=EF=BD=9C=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/dashboard/controller/api_v1.go | 78 +++++++++++++++++ cmd/dashboard/controller/member_api.go | 114 ++++++++++++++++--------- model/api_token.go | 7 +- pkg/mygin/auth.go | 23 ++--- service/singleton/api.go | 20 +++++ service/singleton/singleton.go | 1 + 6 files changed, 188 insertions(+), 55 deletions(-) create mode 100644 cmd/dashboard/controller/api_v1.go diff --git a/cmd/dashboard/controller/api_v1.go b/cmd/dashboard/controller/api_v1.go new file mode 100644 index 0000000..203b556 --- /dev/null +++ b/cmd/dashboard/controller/api_v1.go @@ -0,0 +1,78 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "github.com/naiba/nezha/pkg/mygin" + "github.com/naiba/nezha/service/singleton" + "strconv" + "strings" +) + +type apiV1 struct { + r gin.IRouter +} + +func (v *apiV1) serve() { + r := v.r.Group("") + // API + r.Use(mygin.Authorize(mygin.AuthorizeOption{ + Member: true, + IsPage: false, + AllowAPI: true, + Msg: "访问此接口需要认证", + Btn: "点此登录", + Redirect: "/login", + })) + r.GET("/server/list", v.serverList) + r.GET("/server/details", v.serverDetails) + +} + +// serverList 获取服务器列表 不传入Query参数则获取全部 +// header: Authorization: Token +// query: tag (服务器分组) +func (v *apiV1) serverList(c *gin.Context) { + token, _ := c.Cookie("Authorization") + tag := c.Query("tag") + serverAPI := &singleton.ServerAPI{ + Token: token, + Tag: tag, + } + if tag != "" { + c.JSON(200, serverAPI.GetListByTag()) + return + } + c.JSON(200, serverAPI.GetAllList()) +} + +// serverDetails 获取服务器信息 不传入Query参数则获取全部 +// header: Authorization: Token +// query: idList (服务器ID,逗号分隔,优先级高于tag查询) +// query: tag (服务器分组) +func (v *apiV1) serverDetails(c *gin.Context) { + token, _ := c.Cookie("Authorization") + var idList []uint64 + idListStr := strings.Split(c.Query("id"), ",") + if c.Query("id") != "" { + idList = make([]uint64, len(idListStr)) + for i, v := range idListStr { + id, _ := strconv.ParseUint(v, 10, 64) + idList[i] = id + } + } + tag := c.Query("tag") + serverAPI := &singleton.ServerAPI{ + Token: token, + IDList: idList, + Tag: tag, + } + if tag != "" { + c.JSON(200, serverAPI.GetStatusByTag()) + return + } + if len(idList) != 0 { + c.JSON(200, serverAPI.GetStatusByIDList()) + return + } + c.JSON(200, serverAPI.GetAllStatus()) +} diff --git a/cmd/dashboard/controller/member_api.go b/cmd/dashboard/controller/member_api.go index 334160f..9adc40f 100644 --- a/cmd/dashboard/controller/member_api.go +++ b/cmd/dashboard/controller/member_api.go @@ -43,59 +43,93 @@ func (ma *memberAPI) serve() { mr.POST("/setting", ma.updateSetting) mr.DELETE("/:model/:id", ma.delete) mr.POST("/logout", ma.logout) + mr.GET("/token", ma.getToken) + mr.POST("/token", ma.issueNewToken) + mr.DELETE("/token/:token", ma.deleteToken) // API - mr.GET("/server/list", ma.serverList) - mr.GET("/server/details", ma.serverDetails) + v1 := ma.r.Group("v1") + { + apiv1 := &apiV1{v1} + apiv1.serve() + } } -// serverList 获取服务器列表 不传入Query参数则获取全部 -// header: Authorization: Token -// query: tag (服务器分组) -func (ma *memberAPI) serverList(c *gin.Context) { - token, _ := c.Cookie("Authorization") - tag := c.Query("tag") - serverAPI := &singleton.ServerAPI{ - Token: token, - Tag: tag, - } - if tag != "" { - c.JSON(200, serverAPI.GetListByTag()) - return - } - c.JSON(200, serverAPI.GetAllList()) +type apiResult struct { + Token string `json:"token"` } -// serverDetails 获取服务器信息 不传入Query参数则获取全部 -// header: Authorization: Token -// query: idList (服务器ID,逗号分隔,优先级高于tag查询) -// query: tag (服务器分组) -func (ma *memberAPI) serverDetails(c *gin.Context) { - token, _ := c.Cookie("Authorization") - var idList []uint64 - idListStr := strings.Split(c.Query("id"), ",") - if c.Query("id") != "" { - idList = make([]uint64, len(idListStr)) - for i, v := range idListStr { - id, _ := strconv.ParseUint(v, 10, 64) - idList[i] = id +// getToken 获取 Token +func (ma *memberAPI) getToken(c *gin.Context) { + u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) + tokenList := singleton.UserIDToApiTokenList[u.ID] + res := make([]*apiResult, len(tokenList)) + for i, token := range tokenList { + res[i] = &apiResult{ + Token: token, } } - tag := c.Query("tag") - serverAPI := &singleton.ServerAPI{ - Token: token, - IDList: idList, - Tag: tag, + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "result": res, + }) +} + +// issueNewToken 生成新的 token +func (ma *memberAPI) issueNewToken(c *gin.Context) { + u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) + token := &model.ApiToken{ + UserID: u.ID, + Token: utils.MD5(fmt.Sprintf("%d%d%s", time.Now().UnixNano(), u.ID, u.Login)), } - if tag != "" { - c.JSON(200, serverAPI.GetStatusByTag()) + singleton.DB.Create(token) + singleton.ApiTokenList[token.Token] = token + singleton.UserIDToApiTokenList[u.ID] = append(singleton.UserIDToApiTokenList[u.ID], token.Token) + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + Message: "success", + Result: map[string]string{ + "token": token.Token, + }, + }) +} + +// deleteToken 删除 token +func (ma *memberAPI) deleteToken(c *gin.Context) { + token := c.Param("token") + if token == "" { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: "token 不能为空", + }) return } - if len(idList) != 0 { - c.JSON(200, serverAPI.GetStatusByIDList()) + if _, ok := singleton.ApiTokenList[token]; !ok { + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusBadRequest, + Message: "token 不存在", + }) return } - c.JSON(200, serverAPI.GetAllStatus()) + // 在数据库中删除该Token + singleton.DB.Unscoped().Delete(&model.ApiToken{}, "token = ?", token) + // 在UserIDToApiTokenList中删除该Token + for i, t := range singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID] { + if t == token { + singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID] = append(singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID][:i], singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID][i+1:]...) + break + } + } + if len(singleton.UserIDToApiTokenList[singleton.ApiTokenList[token].UserID]) == 0 { + delete(singleton.UserIDToApiTokenList, singleton.ApiTokenList[token].UserID) + } + // 在ApiTokenList中删除该Token + delete(singleton.ApiTokenList, token) + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + Message: "success", + }) } func (ma *memberAPI) delete(c *gin.Context) { diff --git a/model/api_token.go b/model/api_token.go index b07a571..db10e26 100644 --- a/model/api_token.go +++ b/model/api_token.go @@ -1,10 +1,7 @@ package model -import "time" - type ApiToken struct { Common - UserId uint64 `json:"user_id"` - Token string `json:"token"` - TokenExpired time.Time `json:"token_expired"` + UserID uint64 `json:"user_id"` + Token string `json:"token"` } diff --git a/pkg/mygin/auth.go b/pkg/mygin/auth.go index 9dcdfa1..dd56d92 100644 --- a/pkg/mygin/auth.go +++ b/pkg/mygin/auth.go @@ -15,6 +15,7 @@ type AuthorizeOption struct { Guest bool Member bool IsPage bool + AllowAPI bool Msg string Redirect string Btn string @@ -50,18 +51,20 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) { } // API鉴权 - apiToken := c.GetHeader("Authorization") - if apiToken != "" { - var t model.ApiToken - // TODO: 需要有缓存机制 减少数据库查询次数 - if err := singleton.DB.Where("token = ?", apiToken).First(&t).Error; err == nil { - isLogin = t.TokenExpired.After(time.Now()) - } - if isLogin { - c.Set(model.CtxKeyAuthorizedUser, &t) + if opt.AllowAPI { + apiToken := c.GetHeader("Authorization") + if apiToken != "" { + var u model.User + if _, ok := singleton.ApiTokenList[apiToken]; ok { + err := singleton.DB.First(&u).Where("id = ?", singleton.ApiTokenList[apiToken].UserID).Error + isLogin = err == nil + } + if isLogin { + c.Set(model.CtxKeyAuthorizedUser, &u) + c.Set("isAPI", true) + } } } - // 已登录且只能游客访问 if isLogin && opt.Guest { ShowErrorPage(c, commonErr, opt.IsPage) diff --git a/service/singleton/api.go b/service/singleton/api.go index 6d02c94..9cfbb4e 100644 --- a/service/singleton/api.go +++ b/service/singleton/api.go @@ -5,6 +5,11 @@ import ( "github.com/naiba/nezha/pkg/utils" ) +var ( + ApiTokenList = make(map[string]*model.ApiToken) + UserIDToApiTokenList = make(map[uint64][]string) +) + type ServerAPI struct { Token string // 传入Token 后期可能会需要用于scope判定 IDList []uint64 @@ -45,6 +50,21 @@ type ServerInfoResponse struct { Result []*CommonServerInfo `json:"result"` } +func InitAPI() { + ApiTokenList = make(map[string]*model.ApiToken) + UserIDToApiTokenList = make(map[uint64][]string) +} + +func LoadAPI() { + InitAPI() + var tokenList []*model.ApiToken + DB.Find(&tokenList) + for _, token := range tokenList { + ApiTokenList[token.Token] = token + UserIDToApiTokenList[token.UserID] = append(UserIDToApiTokenList[token.UserID], token.Token) + } +} + // GetStatusByIDList 获取传入IDList的服务器状态信息 func (s *ServerAPI) GetStatusByIDList() *ServerStatusResponse { res := &ServerStatusResponse{} diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index f94cd4f..d8eec66 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -38,6 +38,7 @@ func LoadSingleton() { LoadNotifications() // 加载通知服务 LoadServers() // 加载服务器列表 LoadCronTasks() // 加载定时任务 + LoadAPI() } // InitConfigFromPath 从给出的文件路径中加载配置