From f8685490dc7b1f7870f99c03d7d83a288580641c Mon Sep 17 00:00:00 2001 From: wsczx <wsc@wsczx.com> Date: Sat, 26 Oct 2024 09:13:02 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E9=98=B2=E7=88=86=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=90=8E=E6=B2=A1=E6=9C=89=E9=87=8D=E7=BD=AE=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E7=9A=84Bug=202.=E5=A2=9E=E5=8A=A0otp=E9=98=B2=E7=88=86=203.?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0otp=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E=204.?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dbdata/db.go | 3 +- server/handler/antiBruteForce.go | 72 +++++++------- server/handler/link_auth.go | 5 +- server/handler/link_auth_otp.go | 3 + server/handler/server.go | 2 +- web/src/pages/user/List.vue | 166 ++++++++----------------------- 6 files changed, 86 insertions(+), 165 deletions(-) diff --git a/server/dbdata/db.go b/server/dbdata/db.go index f33ed37..388476b 100644 --- a/server/dbdata/db.go +++ b/server/dbdata/db.go @@ -203,7 +203,8 @@ const accountMail = `<p>您好:</p> <ul> <li>请使用OTP软件扫描动态码二维码</li> <li>然后使用anyconnect客户端进行登陆</li> - <li>登陆密码为 【PIN码+动态码】(中间没有+号)</li> + <li>登陆密码为 PIN 码</li> + <li>OTP密码为扫码后生成的动态码</li> </ul> </div> <p> diff --git a/server/handler/antiBruteForce.go b/server/handler/antiBruteForce.go index 248f8d2..da799f5 100644 --- a/server/handler/antiBruteForce.go +++ b/server/handler/antiBruteForce.go @@ -12,11 +12,7 @@ import ( "github.com/bjdgyc/anylink/base" ) -// 自定义 contextKey 类型,避免键冲突 -type contextKey string - -// 定义常量作为上下文的键 -const loginStatusKey contextKey = "login_status" +const loginStatusKey = "login_status" const defaultGlobalLockStateExpirationTime = 3600 func initAntiBruteForce() { @@ -53,6 +49,20 @@ func antiBruteForce(next http.Handler) http.Handler { } username := cr.Auth.Username + if r.URL.Path == "/otp-verification" { + sessionID, err := GetCookie(r, "auth-session-id") + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + + sessionData, err := SessStore.GetAuthSession(sessionID) + if err != nil { + http.Error(w, "Invalid session, please login again", http.StatusUnauthorized) + return + } + username = sessionData.ClientRequest.Auth.Username + } ip, _, err := net.SplitHostPort(r.RemoteAddr) // 提取纯 IP 地址,去掉端口号 if err != nil { http.Error(w, "Unable to parse IP address", http.StatusInternalServerError) @@ -109,13 +119,17 @@ func antiBruteForce(next http.Handler) http.Handler { // 调用下一个处理器 next.ServeHTTP(w, r) - // 从 context 中获取登录状态 - loginStatus, _ := r.Context().Value(loginStatusKey).(bool) + // 检查登录状态 + Status, _ := lockManager.loginStatus.Load(loginStatusKey) + loginStatus, _ := Status.(bool) // 更新用户登录状态 lockManager.updateGlobalIPLock(ip, now, loginStatus) lockManager.updateGlobalUserLock(username, now, loginStatus) lockManager.updateUserIPLock(username, ip, now, loginStatus) + + // 清除登录状态 + lockManager.loginStatus.Delete(loginStatusKey) }) } @@ -131,6 +145,7 @@ type IPWhitelists struct { type LockManager struct { mu sync.Mutex + loginStatus sync.Map // 登录状态 ipLocks map[string]*LockState // 全局IP锁定状态 userLocks map[string]*LockState // 全局用户锁定状态 ipUserLocks map[string]map[string]*LockState // 单用户IP锁定状态 @@ -140,6 +155,7 @@ type LockManager struct { } var lockManager = &LockManager{ + loginStatus: sync.Map{}, ipLocks: make(map[string]*LockState), userLocks: make(map[string]*LockState), ipUserLocks: make(map[string]map[string]*LockState), @@ -251,14 +267,7 @@ func (lm *LockManager) checkGlobalIPLock(ip string, now time.Time) bool { return false } - // 如果超过时间窗口,重置失败计数 - lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalIPBanResetTime) - - if !state.LockTime.IsZero() && now.Before(state.LockTime) { - return true - } - - return false + return lm.checkLockState(state, now, base.Cfg.GlobalIPBanResetTime) } // 检查全局用户锁定 @@ -274,14 +283,7 @@ func (lm *LockManager) checkGlobalUserLock(username string, now time.Time) bool if !exists { return false } - // 如果超过时间窗口,重置失败计数 - lm.resetLockStateIfExpired(state, now, base.Cfg.GlobalUserBanResetTime) - - if !state.LockTime.IsZero() && now.Before(state.LockTime) { - return true - } - - return false + return lm.checkLockState(state, now, base.Cfg.GlobalUserBanResetTime) } // 检查单个用户的 IP 锁定 @@ -303,14 +305,7 @@ func (lm *LockManager) checkUserIPLock(username, ip string, now time.Time) bool return false } - // 如果超过时间窗口,重置失败计数 - lm.resetLockStateIfExpired(state, now, base.Cfg.BanResetTime) - - if !state.LockTime.IsZero() && now.Before(state.LockTime) { - return true - } - - return false + return lm.checkLockState(state, now, base.Cfg.BanResetTime) } // 更新全局 IP 锁定状态 @@ -383,22 +378,27 @@ func (lm *LockManager) updateLockState(state *LockState, now time.Time, success state.LastAttempt = now } -// 超过窗口时间和锁定时间时重置锁定状态 -func (lm *LockManager) resetLockStateIfExpired(state *LockState, now time.Time, resetTime int) { +// 检查锁定状态 +func (lm *LockManager) checkLockState(state *LockState, now time.Time, resetTime int) bool { if state == nil || state.LastAttempt.IsZero() { - return + return false } // 如果超过锁定时间,重置锁定状态 if !state.LockTime.IsZero() && now.After(state.LockTime) { state.FailureCount = 0 state.LockTime = time.Time{} - return + return false } - // 如果超过窗口时间,重置失败计数 if now.Sub(state.LastAttempt) > time.Duration(resetTime)*time.Second { state.FailureCount = 0 state.LockTime = time.Time{} + return false } + // 如果锁定时间还在有效期内,继续锁定 + if !state.LockTime.IsZero() && now.Before(state.LockTime) { + return true + } + return false } diff --git a/server/handler/link_auth.go b/server/handler/link_auth.go index fa42c96..a5a9f3d 100644 --- a/server/handler/link_auth.go +++ b/server/handler/link_auth.go @@ -2,7 +2,6 @@ package handler import ( "bytes" - "context" "encoding/xml" "fmt" "io" @@ -95,7 +94,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { // TODO 用户密码校验 err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) if err != nil { - r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, false)) // 传递登录失败状态 + lockManager.loginStatus.Store(loginStatusKey, false) // 记录登录失败状态 base.Warn(err, r.RemoteAddr) ua.Info = err.Error() ua.Status = dbdata.UserAuthFail @@ -109,7 +108,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { tplRequest(tpl_request, w, data) return } - r = r.WithContext(context.WithValue(r.Context(), loginStatusKey, true)) // 传递登录成功状态 dbdata.UserActLogIns.Add(*ua, userAgent) v := &dbdata.User{} @@ -121,6 +119,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) { } // 用户otp验证 if !v.DisableOtp { + lockManager.loginStatus.Store(loginStatusKey, true) // 重置OTP验证计数 sessionID, err := GenerateSessionID() if err != nil { base.Error("Failed to generate session ID: ", err) diff --git a/server/handler/link_auth_otp.go b/server/handler/link_auth_otp.go index 91ef5b3..64b6362 100644 --- a/server/handler/link_auth_otp.go +++ b/server/handler/link_auth_otp.go @@ -110,6 +110,8 @@ func DeleteCookie(w http.ResponseWriter, name string) { http.SetCookie(w, cookie) } func CreateSession(w http.ResponseWriter, r *http.Request, authSession *AuthSession) { + lockManager.loginStatus.Store(loginStatusKey, true) // 更新登录成功状态 + cr := authSession.ClientRequest ua := authSession.UserActLog @@ -200,6 +202,7 @@ func LinkAuth_otp(w http.ResponseWriter, r *http.Request) { http.Error(w, "TooManyError, please login again", http.StatusBadRequest) return } + lockManager.loginStatus.Store(loginStatusKey, false) // 记录登录失败状态 base.Warn("OTP 动态码错误", username, r.RemoteAddr) ua.Info = "OTP 动态码错误" diff --git a/server/handler/server.go b/server/handler/server.go index bf0a140..c257aad 100644 --- a/server/handler/server.go +++ b/server/handler/server.go @@ -114,7 +114,7 @@ func initRoute() http.Handler { r.Handle("/", antiBruteForce(http.HandlerFunc(LinkAuth))).Methods(http.MethodPost) r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect) r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet) - r.HandleFunc("/otp-verification", LinkAuth_otp) + r.Handle("/otp-verification", antiBruteForce(http.HandlerFunc(LinkAuth_otp))).Methods(http.MethodPost) r.HandleFunc(fmt.Sprintf("/profile_%s.xml", base.Cfg.ProfileName), func(w http.ResponseWriter, r *http.Request) { b, _ := os.ReadFile(base.Cfg.Profile) w.Write(b) diff --git a/web/src/pages/user/List.vue b/web/src/pages/user/List.vue index 4e4476d..cf898ba 100644 --- a/web/src/pages/user/List.vue +++ b/web/src/pages/user/List.vue @@ -3,22 +3,13 @@ <el-card> <el-form :inline="true"> <el-form-item> - <el-button - size="small" - type="primary" - icon="el-icon-plus" - @click="handleEdit('')">添加 + <el-button size="small" type="primary" icon="el-icon-plus" @click="handleEdit('')">添加 </el-button> </el-form-item> <el-form-item> <el-dropdown size="small" placement="bottom"> - <el-upload - class="uploaduser" - action="uploaduser" - accept=".xlsx, .xls" - :http-request="upLoadUser" - :limit="1" - :show-file-list="false"> + <el-upload class="uploaduser" action="uploaduser" accept=".xlsx, .xls" :http-request="upLoadUser" :limit="1" + :show-file-list="false"> <el-button size="small" icon="el-icon-upload2" type="primary">批量添加</el-button> </el-upload> <el-dropdown-menu slot="dropdown"> @@ -32,79 +23,45 @@ </el-form-item> <el-form-item label="用户名或姓名或邮箱:"> <el-input size="small" v-model="searchData" placeholder="请输入内容" - @keydown.enter.native="searchEnterFun"></el-input> + @keydown.enter.native="searchEnterFun"></el-input> </el-form-item> <el-form-item> - <el-button - size="small" - type="primary" - icon="el-icon-search" - @click="handleSearch()">搜索 + <el-button size="small" type="primary" icon="el-icon-search" @click="handleSearch()">搜索 </el-button> - <el-button - size="small" - icon="el-icon-refresh" - @click="reset">重置搜索 + <el-button size="small" icon="el-icon-refresh" @click="reset">重置搜索 </el-button> </el-form-item> </el-form> - <el-table - ref="multipleTable" - :data="tableData" - border> + <el-table ref="multipleTable" :data="tableData" border> - <el-table-column - sortable="true" - prop="id" - label="ID" - width="60"> + <el-table-column sortable="true" prop="id" label="ID" width="60"> </el-table-column> - <el-table-column - prop="username" - label="用户名" - width="150"> + <el-table-column prop="username" label="用户名" width="150"> </el-table-column> - <el-table-column - prop="nickname" - label="姓名" - width="100"> + <el-table-column prop="nickname" label="姓名" width="100"> </el-table-column> - <el-table-column - prop="email" - label="邮箱"> + <el-table-column prop="email" label="邮箱"> </el-table-column> - <el-table-column - prop="otp_secret" - label="OTP密钥" - width="110"> + <el-table-column prop="otp_secret" label="OTP密钥" width="110"> <template slot-scope="scope"> - <el-button - v-if="!scope.row.disable_otp" - type="text" - icon="el-icon-view" - @click="getOtpImg(scope.row)"> + <el-button v-if="!scope.row.disable_otp" type="text" icon="el-icon-view" @click="getOtpImg(scope.row)"> {{ scope.row.otp_secret.substring(0, 6) }} </el-button> </template> </el-table-column> - <el-table-column - prop="groups" - label="用户组"> + <el-table-column prop="groups" label="用户组"> <template slot-scope="scope"> <el-row v-for="item in scope.row.groups" :key="item">{{ item }}</el-row> </template> </el-table-column> - <el-table-column - prop="status" - label="状态" - width="70"> + <el-table-column prop="status" label="状态" width="70"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === 1" type="success">可用</el-tag> <el-tag v-if="scope.row.status === 0" type="danger">停用</el-tag> @@ -112,20 +69,12 @@ </template> </el-table-column> - <el-table-column - prop="updated_at" - label="更新时间" - :formatter="tableDateFormat"> + <el-table-column prop="updated_at" label="更新时间" :formatter="tableDateFormat"> </el-table-column> - <el-table-column - label="操作" - width="210"> + <el-table-column label="操作" width="210"> <template slot-scope="scope"> - <el-button - size="mini" - type="primary" - @click="handleEdit(scope.row)">编辑 + <el-button size="mini" type="primary" @click="handleEdit(scope.row)">编辑 </el-button> <!-- <el-popconfirm @@ -139,14 +88,8 @@ </el-button> </el-popconfirm>--> - <el-popconfirm - class="m-left-10" - @confirm="handleDel(scope.row)" - title="确定要删除用户吗?"> - <el-button - slot="reference" - size="mini" - type="danger">删除 + <el-popconfirm class="m-left-10" @confirm="handleDel(scope.row)" title="确定要删除用户吗?"> + <el-button slot="reference" size="mini" type="danger">删除 </el-button> </el-popconfirm> @@ -156,34 +99,20 @@ <div class="sh-20"></div> - <el-pagination - background - layout="prev, pager, next" - :pager-count="11" - @current-change="pageChange" - :current-page="page" - :total="count"> + <el-pagination background layout="prev, pager, next" :pager-count="11" @current-change="pageChange" + :current-page="page" :total="count"> </el-pagination> </el-card> - <el-dialog - title="OTP密钥" - :visible.sync="otpImgData.visible" - width="350px" - center> + <el-dialog title="OTP密钥" :visible.sync="otpImgData.visible" width="350px" center> <div style="text-align: center">{{ otpImgData.username }} : {{ otpImgData.nickname }}</div> - <img :src="otpImgData.base64Img" alt="otp-img"/> + <img :src="otpImgData.base64Img" alt="otp-img" /> </el-dialog> <!--新增、修改弹出框--> - <el-dialog - :close-on-click-modal="false" - title="用户" - :visible="user_edit_dialog" - @close="disVisible" - width="650px" - center> + <el-dialog :close-on-click-modal="false" title="用户" :visible="user_edit_dialog" @close="disVisible" width="650px" + center> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm"> <el-form-item label="用户ID" prop="id"> @@ -204,21 +133,13 @@ </el-form-item> <el-form-item label="过期时间" prop="limittime"> - <el-date-picker - v-model="ruleForm.limittime" - type="date" - size="small" - align="center" - style="width:130px" - :picker-options="pickerOptions" - placeholder="选择日期"> + <el-date-picker v-model="ruleForm.limittime" type="date" size="small" align="center" style="width:130px" + :picker-options="pickerOptions" placeholder="选择日期"> </el-date-picker> </el-form-item> <el-form-item label="禁用OTP" prop="disable_otp"> - <el-switch - v-model="ruleForm.disable_otp" - active-text="开启OTP后,用户密码为【PIN码+OTP动态码】(中间没有+号)"> + <el-switch v-model="ruleForm.disable_otp" active-text="开启OTP后,用户密码为PIN码,OTP密码为扫码后生成的动态码"> </el-switch> </el-form-item> @@ -233,8 +154,7 @@ </el-form-item> <el-form-item label="发送邮件" prop="send_email"> - <el-switch - v-model="ruleForm.send_email"> + <el-switch v-model="ruleForm.send_email"> </el-switch> </el-form-item> @@ -286,7 +206,7 @@ export default { } }, searchData: '', - otpImgData: {visible: false, username: '', nickname: '', base64Img: ''}, + otpImgData: { visible: false, username: '', nickname: '', base64Img: '' }, ruleForm: { send_email: true, status: 1, @@ -294,30 +214,30 @@ export default { }, rules: { username: [ - {required: true, message: '请输入用户名', trigger: 'blur'}, - {max: 50, message: '长度小于 50 个字符', trigger: 'blur'} + { required: true, message: '请输入用户名', trigger: 'blur' }, + { max: 50, message: '长度小于 50 个字符', trigger: 'blur' } ], nickname: [ - {required: true, message: '请输入用户姓名', trigger: 'blur'} + { required: true, message: '请输入用户姓名', trigger: 'blur' } ], email: [ - {required: true, message: '请输入用户邮箱', trigger: 'blur'}, - {type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']} + { required: true, message: '请输入用户邮箱', trigger: 'blur' }, + { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] } ], password: [ - {min: 6, message: '长度大于 6 个字符', trigger: 'blur'} + { min: 6, message: '长度大于 6 个字符', trigger: 'blur' } ], pin_code: [ - {min: 6, message: 'PIN码大于 6 个字符', trigger: 'blur'} + { min: 6, message: 'PIN码大于 6 个字符', trigger: 'blur' } ], date1: [ - {type: 'date', required: true, message: '请选择日期', trigger: 'change'} + { type: 'date', required: true, message: '请选择日期', trigger: 'change' } ], groups: [ - {type: 'array', required: true, message: '请至少选择一个组', trigger: 'change'} + { type: 'array', required: true, message: '请至少选择一个组', trigger: 'change' } ], status: [ - {required: true} + { required: true } ], }, } @@ -473,6 +393,4 @@ export default { } </script> -<style scoped> - -</style> +<style scoped></style>