feat: Token生成|存储|验证

This commit is contained in:
Akkia 2022-05-18 10:10:35 +08:00
parent 5d356a30e2
commit 990394bf46
No known key found for this signature in database
GPG Key ID: DABE9A4AB2DD7EF3
6 changed files with 188 additions and 55 deletions

View File

@ -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())
}

View File

@ -43,59 +43,93 @@ func (ma *memberAPI) serve() {
mr.POST("/setting", ma.updateSetting) mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete) mr.DELETE("/:model/:id", ma.delete)
mr.POST("/logout", ma.logout) mr.POST("/logout", ma.logout)
mr.GET("/token", ma.getToken)
mr.POST("/token", ma.issueNewToken)
mr.DELETE("/token/:token", ma.deleteToken)
// API // API
mr.GET("/server/list", ma.serverList) v1 := ma.r.Group("v1")
mr.GET("/server/details", ma.serverDetails) {
apiv1 := &apiV1{v1}
apiv1.serve()
}
} }
// serverList 获取服务器列表 不传入Query参数则获取全部 type apiResult struct {
// header: Authorization: Token Token string `json:"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())
} }
// serverDetails 获取服务器信息 不传入Query参数则获取全部 // getToken 获取 Token
// header: Authorization: Token func (ma *memberAPI) getToken(c *gin.Context) {
// query: idList (服务器ID逗号分隔优先级高于tag查询) u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
// query: tag (服务器分组) tokenList := singleton.UserIDToApiTokenList[u.ID]
func (ma *memberAPI) serverDetails(c *gin.Context) { res := make([]*apiResult, len(tokenList))
token, _ := c.Cookie("Authorization") for i, token := range tokenList {
var idList []uint64 res[i] = &apiResult{
idListStr := strings.Split(c.Query("id"), ",") Token: token,
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") c.JSON(http.StatusOK, gin.H{
serverAPI := &singleton.ServerAPI{ "code": 0,
Token: token, "message": "success",
IDList: idList, "result": res,
Tag: tag, })
}
// 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 != "" { singleton.DB.Create(token)
c.JSON(200, serverAPI.GetStatusByTag()) 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 return
} }
if len(idList) != 0 { if _, ok := singleton.ApiTokenList[token]; !ok {
c.JSON(200, serverAPI.GetStatusByIDList()) c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: "token 不存在",
})
return 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) { func (ma *memberAPI) delete(c *gin.Context) {

View File

@ -1,10 +1,7 @@
package model package model
import "time"
type ApiToken struct { type ApiToken struct {
Common Common
UserId uint64 `json:"user_id"` UserID uint64 `json:"user_id"`
Token string `json:"token"` Token string `json:"token"`
TokenExpired time.Time `json:"token_expired"`
} }

View File

@ -15,6 +15,7 @@ type AuthorizeOption struct {
Guest bool Guest bool
Member bool Member bool
IsPage bool IsPage bool
AllowAPI bool
Msg string Msg string
Redirect string Redirect string
Btn string Btn string
@ -50,18 +51,20 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
} }
// API鉴权 // API鉴权
apiToken := c.GetHeader("Authorization") if opt.AllowAPI {
if apiToken != "" { apiToken := c.GetHeader("Authorization")
var t model.ApiToken if apiToken != "" {
// TODO: 需要有缓存机制 减少数据库查询次数 var u model.User
if err := singleton.DB.Where("token = ?", apiToken).First(&t).Error; err == nil { if _, ok := singleton.ApiTokenList[apiToken]; ok {
isLogin = t.TokenExpired.After(time.Now()) err := singleton.DB.First(&u).Where("id = ?", singleton.ApiTokenList[apiToken].UserID).Error
} isLogin = err == nil
if isLogin { }
c.Set(model.CtxKeyAuthorizedUser, &t) if isLogin {
c.Set(model.CtxKeyAuthorizedUser, &u)
c.Set("isAPI", true)
}
} }
} }
// 已登录且只能游客访问 // 已登录且只能游客访问
if isLogin && opt.Guest { if isLogin && opt.Guest {
ShowErrorPage(c, commonErr, opt.IsPage) ShowErrorPage(c, commonErr, opt.IsPage)

View File

@ -5,6 +5,11 @@ import (
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
) )
var (
ApiTokenList = make(map[string]*model.ApiToken)
UserIDToApiTokenList = make(map[uint64][]string)
)
type ServerAPI struct { type ServerAPI struct {
Token string // 传入Token 后期可能会需要用于scope判定 Token string // 传入Token 后期可能会需要用于scope判定
IDList []uint64 IDList []uint64
@ -45,6 +50,21 @@ type ServerInfoResponse struct {
Result []*CommonServerInfo `json:"result"` 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的服务器状态信息 // GetStatusByIDList 获取传入IDList的服务器状态信息
func (s *ServerAPI) GetStatusByIDList() *ServerStatusResponse { func (s *ServerAPI) GetStatusByIDList() *ServerStatusResponse {
res := &ServerStatusResponse{} res := &ServerStatusResponse{}

View File

@ -38,6 +38,7 @@ func LoadSingleton() {
LoadNotifications() // 加载通知服务 LoadNotifications() // 加载通知服务
LoadServers() // 加载服务器列表 LoadServers() // 加载服务器列表
LoadCronTasks() // 加载定时任务 LoadCronTasks() // 加载定时任务
LoadAPI()
} }
// InitConfigFromPath 从给出的文件路径中加载配置 // InitConfigFromPath 从给出的文件路径中加载配置