Browse Source

fix: Captcha some problem when users login with same ip

lejianwen 8 months ago
parent
commit
83baa140f5
5 changed files with 52 additions and 58 deletions
  1. 3 5
      http/controller/admin/login.go
  2. 5 4
      http/request/admin/login.go
  3. 8 8
      utils/captcha.go
  4. 16 25
      utils/login_limiter.go
  5. 20 16
      utils/login_limiter_test.go

+ 3 - 5
http/controller/admin/login.go

@@ -57,7 +57,7 @@ func (ct *Login) Login(c *gin.Context) {
57 57
 
58 58
 	// 检查是否需要验证码
59 59
 	if needCaptcha {
60
-		if f.Captcha == "" || !loginLimiter.VerifyCaptcha(clientIp, f.Captcha) {
60
+		if f.CaptchaId == "" || f.Captcha == "" || !loginLimiter.VerifyCaptcha(f.CaptchaId, f.Captcha) {
61 61
 			response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError"))
62 62
 			return
63 63
 		}
@@ -68,8 +68,6 @@ func (ct *Login) Login(c *gin.Context) {
68 68
 	if u.Id == 0 {
69 69
 		global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
70 70
 		loginLimiter.RecordFailedAttempt(clientIp)
71
-		// 移除验证码,重新生成
72
-		loginLimiter.RemoveCaptcha(clientIp)
73 71
 		if _, needCaptcha = loginLimiter.CheckSecurityStatus(clientIp); needCaptcha {
74 72
 			response.Fail(c, 110, response.TranslateMsg(c, "UsernameOrPasswordError"))
75 73
 		} else {
@@ -80,7 +78,6 @@ func (ct *Login) Login(c *gin.Context) {
80 78
 
81 79
 	if !service.AllService.UserService.CheckUserEnable(u) {
82 80
 		if needCaptcha {
83
-			loginLimiter.RemoveCaptcha(clientIp)
84 81
 			response.Fail(c, 110, response.TranslateMsg(c, "UserDisabled"))
85 82
 			return
86 83
 		}
@@ -113,7 +110,7 @@ func (ct *Login) Captcha(c *gin.Context) {
113 110
 		response.Fail(c, 101, response.TranslateMsg(c, "NoCaptchaRequired"))
114 111
 		return
115 112
 	}
116
-	err, captcha := loginLimiter.RequireCaptcha(clientIp)
113
+	err, captcha := loginLimiter.RequireCaptcha()
117 114
 	if err != nil {
118 115
 		response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError")+err.Error())
119 116
 		return
@@ -125,6 +122,7 @@ func (ct *Login) Captcha(c *gin.Context) {
125 122
 	}
126 123
 	response.Success(c, gin.H{
127 124
 		"captcha": gin.H{
125
+			"id":  captcha.Id,
128 126
 			"b64": b64,
129 127
 		},
130 128
 	})

+ 5 - 4
http/request/admin/login.go

@@ -1,10 +1,11 @@
1 1
 package admin
2 2
 
3 3
 type Login struct {
4
-	Username string `json:"username" validate:"required" label:"用户名"`
5
-	Password string `json:"password,omitempty" validate:"required" label:"密码"`
6
-	Platform string `json:"platform" label:"平台"`
7
-	Captcha  string `json:"captcha,omitempty" label:"验证码"`
4
+	Username  string `json:"username" validate:"required" label:"用户名"`
5
+	Password  string `json:"password,omitempty" validate:"required" label:"密码"`
6
+	Platform  string `json:"platform" label:"平台"`
7
+	Captcha   string `json:"captcha,omitempty" label:"验证码"`
8
+	CaptchaId string `json:"captcha_id,omitempty"`
8 9
 }
9 10
 
10 11
 type LoginLogQuery struct {

+ 8 - 8
utils/captcha.go

@@ -5,15 +5,15 @@ import (
5 5
 	"time"
6 6
 )
7 7
 
8
-var capdString = base64Captcha.NewDriverString(50, 150, 5, 10, 4, "123456789abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
8
+var capdString = base64Captcha.NewDriverString(50, 150, 0, 5, 4, "123456789abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
9 9
 
10
-var capdMath = base64Captcha.NewDriverMath(50, 150, 5, 10, nil, nil, nil)
10
+var capdMath = base64Captcha.NewDriverMath(50, 150, 3, 10, nil, nil, nil)
11 11
 
12 12
 type B64StringCaptchaProvider struct{}
13 13
 
14
-func (p B64StringCaptchaProvider) Generate(ip string) (string, string, error) {
15
-	_, content, answer := capdString.GenerateIdQuestionAnswer()
16
-	return content, answer, nil
14
+func (p B64StringCaptchaProvider) Generate() (string, string, string, error) {
15
+	id, content, answer := capdString.GenerateIdQuestionAnswer()
16
+	return id, content, answer, nil
17 17
 }
18 18
 
19 19
 func (p B64StringCaptchaProvider) Expiration() time.Duration {
@@ -30,9 +30,9 @@ func (p B64StringCaptchaProvider) Draw(content string) (string, error) {
30 30
 
31 31
 type B64MathCaptchaProvider struct{}
32 32
 
33
-func (p B64MathCaptchaProvider) Generate(ip string) (string, string, error) {
34
-	_, content, answer := capdMath.GenerateIdQuestionAnswer()
35
-	return content, answer, nil
33
+func (p B64MathCaptchaProvider) Generate() (string, string, string, error) {
34
+	id, content, answer := capdMath.GenerateIdQuestionAnswer()
35
+	return id, content, answer, nil
36 36
 }
37 37
 
38 38
 func (p B64MathCaptchaProvider) Expiration() time.Duration {

+ 16 - 25
utils/login_limiter.go

@@ -16,7 +16,7 @@ type SecurityPolicy struct {
16 16
 
17 17
 // 验证码提供者接口
18 18
 type CaptchaProvider interface {
19
-	Generate(ip string) (string, string, error)
19
+	Generate() (id string, content string, answer string, err error)
20 20
 	//Validate(ip, code string) bool
21 21
 	Expiration() time.Duration           // 验证码过期时间, 应该小于 AttemptsWindow
22 22
 	Draw(content string) (string, error) // 绘制验证码
@@ -24,6 +24,7 @@ type CaptchaProvider interface {
24 24
 
25 25
 // 验证码元数据
26 26
 type CaptchaMeta struct {
27
+	Id        string
27 28
 	Content   string
28 29
 	Answer    string
29 30
 	ExpiresAt time.Time
@@ -117,7 +118,7 @@ func (ll *LoginLimiter) RecordFailedAttempt(ip string) {
117 118
 }
118 119
 
119 120
 // 生成验证码
120
-func (ll *LoginLimiter) RequireCaptcha(ip string) (error, CaptchaMeta) {
121
+func (ll *LoginLimiter) RequireCaptcha() (error, CaptchaMeta) {
121 122
 	ll.mu.Lock()
122 123
 	defer ll.mu.Unlock()
123 124
 
@@ -125,23 +126,24 @@ func (ll *LoginLimiter) RequireCaptcha(ip string) (error, CaptchaMeta) {
125 126
 		return errors.New("no captcha provider available"), CaptchaMeta{}
126 127
 	}
127 128
 
128
-	content, answer, err := ll.provider.Generate(ip)
129
+	id, content, answer, err := ll.provider.Generate()
129 130
 	if err != nil {
130 131
 		return err, CaptchaMeta{}
131 132
 	}
132 133
 
133 134
 	// 存储验证码
134
-	ll.captchas[ip] = CaptchaMeta{
135
+	ll.captchas[id] = CaptchaMeta{
136
+		Id:        id,
135 137
 		Content:   content,
136 138
 		Answer:    answer,
137 139
 		ExpiresAt: time.Now().Add(ll.provider.Expiration()),
138 140
 	}
139 141
 
140
-	return nil, ll.captchas[ip]
142
+	return nil, ll.captchas[id]
141 143
 }
142 144
 
143 145
 // 验证验证码
144
-func (ll *LoginLimiter) VerifyCaptcha(ip, answer string) bool {
146
+func (ll *LoginLimiter) VerifyCaptcha(id, answer string) bool {
145 147
 	ll.mu.Lock()
146 148
 	defer ll.mu.Unlock()
147 149
 
@@ -151,20 +153,20 @@ func (ll *LoginLimiter) VerifyCaptcha(ip, answer string) bool {
151 153
 	}
152 154
 
153 155
 	// 获取并验证验证码
154
-	captcha, exists := ll.captchas[ip]
156
+	captcha, exists := ll.captchas[id]
155 157
 	if !exists {
156 158
 		return false
157 159
 	}
158 160
 
159 161
 	// 清理过期验证码
160 162
 	if time.Now().After(captcha.ExpiresAt) {
161
-		delete(ll.captchas, ip)
163
+		delete(ll.captchas, id)
162 164
 		return false
163 165
 	}
164 166
 
165 167
 	// 验证并清理状态
166 168
 	if answer == captcha.Answer {
167
-		delete(ll.captchas, ip)
169
+		delete(ll.captchas, id)
168 170
 		return true
169 171
 	}
170 172
 
@@ -176,16 +178,6 @@ func (ll *LoginLimiter) DrawCaptcha(content string) (err error, str string) {
176 178
 	return
177 179
 }
178 180
 
179
-func (ll *LoginLimiter) RemoveCaptcha(ip string) {
180
-	ll.mu.Lock()
181
-	defer ll.mu.Unlock()
182
-
183
-	_, exists := ll.captchas[ip]
184
-	if exists {
185
-		delete(ll.captchas, ip)
186
-	}
187
-}
188
-
189 181
 // 清除记录窗口
190 182
 func (ll *LoginLimiter) RemoveAttempts(ip string) {
191 183
 	ll.mu.Lock()
@@ -212,7 +204,6 @@ func (ll *LoginLimiter) CheckSecurityStatus(ip string) (banned bool, captchaRequ
212 204
 
213 205
 	// 清理过期数据
214 206
 	ll.pruneAttempts(ip, time.Now().Add(-ll.policy.AttemptsWindow))
215
-	ll.pruneCaptchas(ip)
216 207
 
217 208
 	// 检查验证码要求
218 209
 	captchaRequired = len(ll.attempts[ip]) >= ll.policy.CaptchaThreshold
@@ -272,10 +263,10 @@ func (ll *LoginLimiter) pruneAttempts(ip string, cutoff time.Time) []time.Time {
272 263
 	return valid
273 264
 }
274 265
 
275
-func (ll *LoginLimiter) pruneCaptchas(ip string) {
276
-	if captcha, exists := ll.captchas[ip]; exists {
266
+func (ll *LoginLimiter) pruneCaptchas(id string) {
267
+	if captcha, exists := ll.captchas[id]; exists {
277 268
 		if time.Now().After(captcha.ExpiresAt) {
278
-			delete(ll.captchas, ip)
269
+			delete(ll.captchas, id)
279 270
 		}
280 271
 	}
281 272
 }
@@ -299,7 +290,7 @@ func (ll *LoginLimiter) cleanupExpired() {
299 290
 	}
300 291
 
301 292
 	// 清理验证码
302
-	for ip := range ll.captchas {
303
-		ll.pruneCaptchas(ip)
293
+	for id := range ll.captchas {
294
+		ll.pruneCaptchas(id)
304 295
 	}
305 296
 }

+ 20 - 16
utils/login_limiter_test.go

@@ -2,18 +2,18 @@ package utils
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"github.com/google/uuid"
5 6
 	"testing"
6 7
 	"time"
7 8
 )
8 9
 
9 10
 type MockCaptchaProvider struct{}
10 11
 
11
-func (p *MockCaptchaProvider) Generate(ip string) (string, string, error) {
12
-	return "CONTENT", "MOCK", nil
13
-}
14
-
15
-func (p *MockCaptchaProvider) Validate(ip, code string) bool {
16
-	return code == "MOCK"
12
+func (p *MockCaptchaProvider) Generate() (string, string, string, error) {
13
+	id := uuid.New().String()
14
+	content := uuid.New().String()
15
+	answer := uuid.New().String()
16
+	return id, content, answer, nil
17 17
 }
18 18
 
19 19
 func (p *MockCaptchaProvider) Expiration() time.Duration {
@@ -74,17 +74,22 @@ func TestCaptchaFlow(t *testing.T) {
74 74
 	}
75 75
 
76 76
 	// 生成验证码
77
-	err, capc := limiter.RequireCaptcha(ip)
77
+	err, capc := limiter.RequireCaptcha()
78 78
 	if err != nil {
79 79
 		t.Fatalf("生成验证码失败: %v", err)
80 80
 	}
81 81
 	fmt.Printf("验证码内容: %#v\n", capc)
82 82
 
83 83
 	// 验证成功
84
-	if !limiter.VerifyCaptcha(ip, capc.Answer) {
84
+	if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
85 85
 		t.Error("验证码应该验证成功")
86 86
 	}
87 87
 
88
+	// 验证已删除
89
+	if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
90
+		t.Error("验证码应该已删除")
91
+	}
92
+
88 93
 	limiter.RemoveAttempts(ip)
89 94
 	// 验证后状态
90 95
 	if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
@@ -104,14 +109,14 @@ func TestCaptchaMustFlow(t *testing.T) {
104 109
 	}
105 110
 
106 111
 	// 生成验证码
107
-	err, capc := limiter.RequireCaptcha(ip)
112
+	err, capc := limiter.RequireCaptcha()
108 113
 	if err != nil {
109 114
 		t.Fatalf("生成验证码失败: %v", err)
110 115
 	}
111 116
 	fmt.Printf("验证码内容: %#v\n", capc)
112 117
 
113 118
 	// 验证成功
114
-	if !limiter.VerifyCaptcha(ip, capc.Answer) {
119
+	if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
115 120
 		t.Error("验证码应该验证成功")
116 121
 	}
117 122
 
@@ -136,7 +141,7 @@ func TestAttemptTimeout(t *testing.T) {
136 141
 	}
137 142
 
138 143
 	// 生成验证码
139
-	err, _ := limiter.RequireCaptcha(ip)
144
+	err, _ := limiter.RequireCaptcha()
140 145
 	if err != nil {
141 146
 		t.Fatalf("生成验证码失败: %v", err)
142 147
 	}
@@ -167,7 +172,7 @@ func TestCaptchaTimeout(t *testing.T) {
167 172
 	}
168 173
 
169 174
 	// 生成验证码
170
-	err, _ := limiter.RequireCaptcha(ip)
175
+	err, capc := limiter.RequireCaptcha()
171 176
 	if err != nil {
172 177
 		t.Fatalf("生成验证码失败: %v", err)
173 178
 	}
@@ -175,9 +180,8 @@ func TestCaptchaTimeout(t *testing.T) {
175 180
 	// 等待超过 CaptchaValidPeriod
176 181
 	time.Sleep(3 * time.Second)
177 182
 
178
-	code := "MOCK"
179 183
 	// 验证成功
180
-	if limiter.VerifyCaptcha(ip, code) {
184
+	if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
181 185
 		t.Error("验证码应该已过期")
182 186
 	}
183 187
 
@@ -261,7 +265,7 @@ func TestB64CaptchaFlow(t *testing.T) {
261 265
 	}
262 266
 
263 267
 	// 生成验证码
264
-	err, capc := limiter.RequireCaptcha(ip)
268
+	err, capc := limiter.RequireCaptcha()
265 269
 	if err != nil {
266 270
 		t.Fatalf("生成验证码失败: %v", err)
267 271
 	}
@@ -275,7 +279,7 @@ func TestB64CaptchaFlow(t *testing.T) {
275 279
 	fmt.Printf("验证码内容: %#v\n", b64)
276 280
 
277 281
 	// 验证成功
278
-	if !limiter.VerifyCaptcha(ip, capc.Answer) {
282
+	if !limiter.VerifyCaptcha(capc.Id, capc.Answer) {
279 283
 		t.Error("验证码应该验证成功")
280 284
 	}
281 285
 	limiter.RemoveAttempts(ip)