diff --git a/internal/configs/secret.go b/internal/configs/secret.go new file mode 100644 index 00000000..5a889ad9 --- /dev/null +++ b/internal/configs/secret.go @@ -0,0 +1,3 @@ +package configs + +var Secret = "" diff --git a/internal/csrf/token_manager.go b/internal/csrf/token_manager.go new file mode 100644 index 00000000..39151a41 --- /dev/null +++ b/internal/csrf/token_manager.go @@ -0,0 +1,58 @@ +package csrf + +import ( + "sync" + "time" +) + +var sharedTokenManager = NewTokenManager() + +func init() { + go func() { + ticker := time.NewTicker(1 * time.Hour) + for range ticker.C { + sharedTokenManager.Clean() + } + }() +} + +type TokenManager struct { + tokenMap map[string]int64 // token => timestamp + + locker sync.Mutex +} + +func NewTokenManager() *TokenManager { + return &TokenManager{ + tokenMap: map[string]int64{}, + } +} + +func (this *TokenManager) Put(token string) { + this.locker.Lock() + this.tokenMap[token] = time.Now().Unix() + this.locker.Unlock() +} + +func (this *TokenManager) Exists(token string) bool { + this.locker.Lock() + _, ok := this.tokenMap[token] + this.locker.Unlock() + return ok +} + +func (this *TokenManager) Delete(token string) { + this.locker.Lock() + delete(this.tokenMap, token) + this.locker.Unlock() +} + +func (this *TokenManager) Clean() { + this.locker.Lock() + for token, timestamp := range this.tokenMap { + if time.Now().Unix()-timestamp > 3600 { // 删除一个小时前的 + delete(this.tokenMap, token) + } + } + this.locker.Unlock() +} diff --git a/internal/csrf/utils.go b/internal/csrf/utils.go new file mode 100644 index 00000000..316111fc --- /dev/null +++ b/internal/csrf/utils.go @@ -0,0 +1,66 @@ +package csrf + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "github.com/TeaOSLab/EdgeAdmin/internal/configs" + "github.com/iwind/TeaGo/types" + "strconv" + "time" +) + +// 生成Token +func Generate() string { + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + + h := sha256.New() + h.Write([]byte(configs.Secret)) + h.Write([]byte(timestamp)) + s := h.Sum(nil) + token := base64.StdEncoding.EncodeToString([]byte(timestamp + fmt.Sprintf("%x", s))) + sharedTokenManager.Put(token) + return token +} + +// 校验Token +func Validate(token string) (b bool) { + if len(token) == 0 { + return + } + + if !sharedTokenManager.Exists(token) { + return + } + defer func() { + sharedTokenManager.Delete(token) + }() + + data, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return + } + + hashString := string(data) + if len(hashString) < 10+32 { + return + } + + timestampString := hashString[:10] + hashString = hashString[10:] + + h := sha256.New() + h.Write([]byte(configs.Secret)) + h.Write([]byte(timestampString)) + hashData := h.Sum(nil) + if hashString != fmt.Sprintf("%x", hashData) { + return + } + + timestamp := types.Int64(timestampString) + if timestamp < time.Now().Unix()-1800 { // 有效期半个小时 + return + } + + return true +} diff --git a/internal/nodes/admin_node.go b/internal/nodes/admin_node.go index 91189cc4..749e7e55 100644 --- a/internal/nodes/admin_node.go +++ b/internal/nodes/admin_node.go @@ -1,6 +1,7 @@ package nodes import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configs" "github.com/TeaOSLab/EdgeAdmin/internal/errors" "github.com/iwind/TeaGo" "github.com/iwind/TeaGo/Tea" @@ -25,6 +26,7 @@ func NewAdminNode() *AdminNode { func (this *AdminNode) Run() { // 启动管理界面 secret := this.genSecret() + configs.Secret = secret // 检查server配置 err := this.checkServer() diff --git a/internal/web/actions/actionutils/csrf.go b/internal/web/actions/actionutils/csrf.go new file mode 100644 index 00000000..33934517 --- /dev/null +++ b/internal/web/actions/actionutils/csrf.go @@ -0,0 +1,22 @@ +package actionutils + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/csrf" + "github.com/iwind/TeaGo/actions" + "net/http" +) + +type CSRF struct { +} + +func (this *CSRF) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) { + action := actionPtr.Object() + token := action.ParamString("csrfToken") + if !csrf.Validate(token) { + action.ResponseWriter.WriteHeader(http.StatusForbidden) + action.WriteString("表单已失效,请刷新页面后重试(001)") + return + } + + return true +} diff --git a/internal/web/actions/default/csrf/init.go b/internal/web/actions/default/csrf/init.go new file mode 100644 index 00000000..feb330b2 --- /dev/null +++ b/internal/web/actions/default/csrf/init.go @@ -0,0 +1,12 @@ +package csrf + +import "github.com/iwind/TeaGo" + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Prefix("/csrf"). + Get("/token", new(TokenAction)). + EndAll() + }) +} diff --git a/internal/web/actions/default/csrf/token.go b/internal/web/actions/default/csrf/token.go new file mode 100644 index 00000000..02fa1343 --- /dev/null +++ b/internal/web/actions/default/csrf/token.go @@ -0,0 +1,39 @@ +package csrf + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/csrf" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "sync" + "time" +) + +var lastTimestamp = int64(0) +var locker sync.Mutex + +type TokenAction struct { + actionutils.ParentAction +} + +func (this *TokenAction) Init() { + this.Nav("", "", "") +} + +func (this *TokenAction) RunGet(params struct { + Auth *helpers.UserShouldAuth +}) { + locker.Lock() + defer locker.Unlock() + + defer func() { + lastTimestamp = time.Now().Unix() + }() + + // 没有登录,则限制请求速度 + if params.Auth.AdminId() <= 0 && lastTimestamp > 0 && time.Now().Unix()-lastTimestamp <= 1 { + this.Fail("请求速度过快,请稍后刷新后重试") + } + + this.Data["token"] = csrf.Generate() + this.Success() +} diff --git a/internal/web/actions/default/index/index.go b/internal/web/actions/default/index/index.go index b5c8c3d5..4e816c4c 100644 --- a/internal/web/actions/default/index/index.go +++ b/internal/web/actions/default/index/index.go @@ -15,7 +15,9 @@ import ( "time" ) -type IndexAction actions.Action +type IndexAction struct { + actionutils.ParentAction +} // 首页(登录页) @@ -56,6 +58,7 @@ func (this *IndexAction) RunPost(params struct { Remember bool Must *actions.Must Auth *helpers.UserShouldAuth + CSRF *actionutils.CSRF }) { params.Must. Field("username", params.Username). diff --git a/internal/web/import.go b/internal/web/import.go index 5586f927..36262d8e 100644 --- a/internal/web/import.go +++ b/internal/web/import.go @@ -7,6 +7,7 @@ import ( _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster/settings" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/csrf" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dashboard" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns" diff --git a/web/public/js/components/common/csrf-token.js b/web/public/js/components/common/csrf-token.js new file mode 100644 index 00000000..53c5d302 --- /dev/null +++ b/web/public/js/components/common/csrf-token.js @@ -0,0 +1,27 @@ +Vue.component("csrf-token", { + created: function () { + this.refreshToken() + }, + mounted: function () { + let that = this + this.$refs.token.form.addEventListener("submit", function () { + that.refreshToken() + }) + }, + data: function () { + return { + token: "" + } + }, + methods: { + refreshToken: function () { + let that = this + Tea.action("/csrf/token") + .get() + .success(function (resp) { + that.token = resp.data.token + }) + } + }, + template: `` +}) diff --git a/web/views/@default/index/index.html b/web/views/@default/index/index.html index 33804c8d..3cfca4bb 100644 --- a/web/views/@default/index/index.html +++ b/web/views/@default/index/index.html @@ -10,6 +10,7 @@ +
@@ -17,6 +18,7 @@
+