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>