mirror of
				https://github.com/TeaOSLab/EdgeAdmin.git
				synced 2025-11-04 05:00:25 +08:00 
			
		
		
		
	登录页尝试使用csrf校验
This commit is contained in:
		
							
								
								
									
										3
									
								
								internal/configs/secret.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								internal/configs/secret.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package configs
 | 
			
		||||
 | 
			
		||||
var Secret = ""
 | 
			
		||||
							
								
								
									
										58
									
								
								internal/csrf/token_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/csrf/token_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								internal/csrf/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/csrf/utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								internal/web/actions/actionutils/csrf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								internal/web/actions/actionutils/csrf.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								internal/web/actions/default/csrf/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/web/actions/default/csrf/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								internal/web/actions/default/csrf/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/web/actions/default/csrf/token.go
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
			
		||||
}
 | 
			
		||||
@@ -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).
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								web/public/js/components/common/csrf-token.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								web/public/js/components/common/csrf-token.js
									
									
									
									
									
										Normal file
									
								
							@@ -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: `<input type="hidden" name="csrfToken" :value="token" ref="token"/>`
 | 
			
		||||
})
 | 
			
		||||
@@ -10,6 +10,7 @@
 | 
			
		||||
	<script type="text/javascript" src="/js/md5.min.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/js/utils.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
 | 
			
		||||
	<script type="text/javascript" src="/ui/components.js?v=1.0.0"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div>
 | 
			
		||||
@@ -17,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
	<div class="form-box">
 | 
			
		||||
		<form class="ui form" data-tea-action="$" data-tea-before="submitBefore" data-tea-done="submitDone" data-tea-success="submitSuccess" autocomplete="off">
 | 
			
		||||
			<csrf-token></csrf-token>
 | 
			
		||||
			<input type="hidden" name="password" v-model="passwordMd5"/>
 | 
			
		||||
			<input type="hidden" name="token" v-model="token"/>
 | 
			
		||||
			<div class="ui segment stacked">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user