🔒️ 增强 ping 历史 API 鉴权
This commit is contained in:
		
							parent
							
								
									99ac12c9fd
								
							
						
					
					
						commit
						8dd509aa08
					
				@ -6,6 +6,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
@ -16,18 +17,28 @@ type apiV1 struct {
 | 
			
		||||
 | 
			
		||||
func (v *apiV1) serve() {
 | 
			
		||||
	r := v.r.Group("")
 | 
			
		||||
	// API
 | 
			
		||||
	// 强制认证的 API
 | 
			
		||||
	r.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   false,
 | 
			
		||||
		AllowAPI: true,
 | 
			
		||||
		Msg:      "访问此接口需要认证",
 | 
			
		||||
		Btn:      "点此登录",
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		AllowAPI:   true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要认证",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	r.GET("/server/list", v.serverList)
 | 
			
		||||
	r.GET("/server/details", v.serverDetails)
 | 
			
		||||
	// 不强制认证的 API
 | 
			
		||||
	mr := v.r.Group("monitor")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		MemberOnly:           false,
 | 
			
		||||
		IsPage:               false,
 | 
			
		||||
		ValidateViewPassword: true,
 | 
			
		||||
		AllowAPI:             true,
 | 
			
		||||
		Msg:                  "访问此接口需要认证",
 | 
			
		||||
		Btn:                  "点此登录",
 | 
			
		||||
		Redirect:             "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	mr.GET("/:id", v.monitorHistoriesById)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -84,5 +95,15 @@ func (v *apiV1) monitorHistoriesById(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, isMember := c.Get(model.CtxKeyAuthorizedUser)
 | 
			
		||||
	_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
 | 
			
		||||
	authorized := isMember || isViewPasswordVerfied
 | 
			
		||||
 | 
			
		||||
	if server.HideForGuest && !authorized {
 | 
			
		||||
		c.AbortWithStatusJSON(403, gin.H{"code": 403, "message": "需要认证"})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.JSON(200, singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": server.ID}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -43,11 +43,12 @@ type commonPage struct {
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) serve() {
 | 
			
		||||
	cr := cp.r.Group("")
 | 
			
		||||
	cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
 | 
			
		||||
	cr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		ValidateViewPassword: true,
 | 
			
		||||
	}))
 | 
			
		||||
	cr.Use(mygin.PreferredTheme)
 | 
			
		||||
	cr.GET("/terminal/:id", cp.terminal)
 | 
			
		||||
	cr.POST("/view-password", cp.issueViewPassword)
 | 
			
		||||
	cr.Use(cp.checkViewPassword) // 前端查看密码鉴权
 | 
			
		||||
	cr.GET("/", cp.home)
 | 
			
		||||
	cr.GET("/service", cp.service)
 | 
			
		||||
	// TODO: 界面直接跳转使用该接口
 | 
			
		||||
@ -86,31 +87,6 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
 | 
			
		||||
	c.Redirect(http.StatusFound, c.Request.Referer())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *commonPage) checkViewPassword(c *gin.Context) {
 | 
			
		||||
	if singleton.Conf.Site.ViewPassword == "" {
 | 
			
		||||
		c.Next()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if _, authorized := c.Get(model.CtxKeyAuthorizedUser); authorized {
 | 
			
		||||
		c.Next()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证查看密码
 | 
			
		||||
	viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
 | 
			
		||||
	if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
 | 
			
		||||
		c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/viewpassword"), mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
			"Title":      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
 | 
			
		||||
			"CustomCode": singleton.Conf.Site.CustomCode,
 | 
			
		||||
		}))
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.Set(model.CtxKeyViewPasswordVerified, true)
 | 
			
		||||
	c.Next()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *commonPage) service(c *gin.Context) {
 | 
			
		||||
	res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
 | 
			
		||||
		singleton.AlertsLock.RLock()
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,11 @@ type guestPage struct {
 | 
			
		||||
func (gp *guestPage) serve() {
 | 
			
		||||
	gr := gp.r.Group("")
 | 
			
		||||
	gr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Guest:    true,
 | 
			
		||||
		IsPage:   true,
 | 
			
		||||
		Msg:      "您已登录",
 | 
			
		||||
		Btn:      "返回首页",
 | 
			
		||||
		Redirect: "/",
 | 
			
		||||
		GuestOnly: true,
 | 
			
		||||
		IsPage:    true,
 | 
			
		||||
		Msg:       "您已登录",
 | 
			
		||||
		Btn:       "返回首页",
 | 
			
		||||
		Redirect:  "/",
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	gr.GET("/login", gp.login)
 | 
			
		||||
 | 
			
		||||
@ -28,11 +28,11 @@ type memberAPI struct {
 | 
			
		||||
func (ma *memberAPI) serve() {
 | 
			
		||||
	mr := ma.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   false,
 | 
			
		||||
		Msg:      "访问此接口需要登录",
 | 
			
		||||
		Btn:      "点此登录",
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要登录",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	mr.GET("/search-server", ma.searchServer)
 | 
			
		||||
@ -444,10 +444,12 @@ func (ma *memberAPI) addOrEditMonitor(c *gin.Context) {
 | 
			
		||||
				err = singleton.DB.Save(&m).Error
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if m.Cover == 0 {
 | 
			
		||||
			err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
 | 
			
		||||
		} else {
 | 
			
		||||
			err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if m.Cover == 0 {
 | 
			
		||||
				err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
 | 
			
		||||
			} else {
 | 
			
		||||
				err = singleton.DB.Unscoped().Delete(&model.MonitorHistory{}, "monitor_id = ? and server_id not in (?)", m.ID, strings.Split(m.SkipServersRaw[1:len(m.SkipServersRaw)-1], ",")).Error
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if err == nil {
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,11 @@ type memberPage struct {
 | 
			
		||||
func (mp *memberPage) serve() {
 | 
			
		||||
	mr := mp.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		Member:   true,
 | 
			
		||||
		IsPage:   true,
 | 
			
		||||
		Msg:      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
		Btn:      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     true,
 | 
			
		||||
		Msg:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
		Btn:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	mr.GET("/server", mp.server)
 | 
			
		||||
	mr.GET("/monitor", mp.monitor)
 | 
			
		||||
 | 
			
		||||
@ -6,25 +6,28 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/nicksnyder/go-i18n/v2/i18n"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthorizeOption struct {
 | 
			
		||||
	Guest    bool
 | 
			
		||||
	Member   bool
 | 
			
		||||
	IsPage   bool
 | 
			
		||||
	AllowAPI bool
 | 
			
		||||
	Msg      string
 | 
			
		||||
	Redirect string
 | 
			
		||||
	Btn      string
 | 
			
		||||
	GuestOnly            bool
 | 
			
		||||
	MemberOnly           bool
 | 
			
		||||
	ValidateViewPassword bool
 | 
			
		||||
	IsPage               bool
 | 
			
		||||
	AllowAPI             bool
 | 
			
		||||
	Msg                  string
 | 
			
		||||
	Redirect             string
 | 
			
		||||
	Btn                  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		var code = http.StatusForbidden
 | 
			
		||||
		if opt.Guest {
 | 
			
		||||
		if opt.GuestOnly {
 | 
			
		||||
			code = http.StatusBadRequest
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -67,15 +70,32 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 已登录且只能游客访问
 | 
			
		||||
		if isLogin && opt.Guest {
 | 
			
		||||
		if isLogin && opt.GuestOnly {
 | 
			
		||||
			ShowErrorPage(c, commonErr, opt.IsPage)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 未登录且需要登录
 | 
			
		||||
		if !isLogin && opt.Member {
 | 
			
		||||
		if !isLogin && opt.MemberOnly {
 | 
			
		||||
			ShowErrorPage(c, commonErr, opt.IsPage)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 验证查看密码
 | 
			
		||||
		if opt.ValidateViewPassword && singleton.Conf.Site.ViewPassword != "" {
 | 
			
		||||
			viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
 | 
			
		||||
			if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
 | 
			
		||||
				c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
 | 
			
		||||
					"Title":      singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
 | 
			
		||||
					"CustomCode": singleton.Conf.Site.CustomCode,
 | 
			
		||||
				}))
 | 
			
		||||
				c.Abort()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			c.Set(model.CtxKeyViewPasswordVerified, true)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user