mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-11 18:30: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
|
package nodes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||||
"github.com/TeaOSLab/EdgeAdmin/internal/errors"
|
"github.com/TeaOSLab/EdgeAdmin/internal/errors"
|
||||||
"github.com/iwind/TeaGo"
|
"github.com/iwind/TeaGo"
|
||||||
"github.com/iwind/TeaGo/Tea"
|
"github.com/iwind/TeaGo/Tea"
|
||||||
@@ -25,6 +26,7 @@ func NewAdminNode() *AdminNode {
|
|||||||
func (this *AdminNode) Run() {
|
func (this *AdminNode) Run() {
|
||||||
// 启动管理界面
|
// 启动管理界面
|
||||||
secret := this.genSecret()
|
secret := this.genSecret()
|
||||||
|
configs.Secret = secret
|
||||||
|
|
||||||
// 检查server配置
|
// 检查server配置
|
||||||
err := this.checkServer()
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IndexAction actions.Action
|
type IndexAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
// 首页(登录页)
|
// 首页(登录页)
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ func (this *IndexAction) RunPost(params struct {
|
|||||||
Remember bool
|
Remember bool
|
||||||
Must *actions.Must
|
Must *actions.Must
|
||||||
Auth *helpers.UserShouldAuth
|
Auth *helpers.UserShouldAuth
|
||||||
|
CSRF *actionutils.CSRF
|
||||||
}) {
|
}) {
|
||||||
params.Must.
|
params.Must.
|
||||||
Field("username", params.Username).
|
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"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/cluster"
|
_ "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/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/dashboard"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
|
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/db"
|
||||||
_ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/dns"
|
_ "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/md5.min.js"></script>
|
||||||
<script type="text/javascript" src="/js/utils.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="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/ui/components.js?v=1.0.0"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
<div class="form-box">
|
<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">
|
<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="password" v-model="passwordMd5"/>
|
||||||
<input type="hidden" name="token" v-model="token"/>
|
<input type="hidden" name="token" v-model="token"/>
|
||||||
<div class="ui segment stacked">
|
<div class="ui segment stacked">
|
||||||
|
|||||||
Reference in New Issue
Block a user