Browse Source

fix: Captcha some problem when users login with same ip

lejianwen 8 months ago
parent
commit
83baa140f5

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

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

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

@@ -1,10 +1,11 @@
1
 package admin
1
 package admin
2
 
2
 
3
 type Login struct {
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
 type LoginLogQuery struct {
11
 type LoginLogQuery struct {

+ 8 - 8
utils/captcha.go

@@ -5,15 +5,15 @@ import (
5
 	"time"
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
 type B64StringCaptchaProvider struct{}
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
 func (p B64StringCaptchaProvider) Expiration() time.Duration {
19
 func (p B64StringCaptchaProvider) Expiration() time.Duration {
@@ -30,9 +30,9 @@ func (p B64StringCaptchaProvider) Draw(content string) (string, error) {
30
 
30
 
31
 type B64MathCaptchaProvider struct{}
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
 func (p B64MathCaptchaProvider) Expiration() time.Duration {
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
 type CaptchaProvider interface {
18
 type CaptchaProvider interface {
19
-	Generate(ip string) (string, string, error)
19
+	Generate() (id string, content string, answer string, err error)
20
 	//Validate(ip, code string) bool
20
 	//Validate(ip, code string) bool
21
 	Expiration() time.Duration           // 验证码过期时间, 应该小于 AttemptsWindow
21
 	Expiration() time.Duration           // 验证码过期时间, 应该小于 AttemptsWindow
22
 	Draw(content string) (string, error) // 绘制验证码
22
 	Draw(content string) (string, error) // 绘制验证码
@@ -24,6 +24,7 @@ type CaptchaProvider interface {
24
 
24
 
25
 // 验证码元数据
25
 // 验证码元数据
26
 type CaptchaMeta struct {
26
 type CaptchaMeta struct {
27
+	Id        string
27
 	Content   string
28
 	Content   string
28
 	Answer    string
29
 	Answer    string
29
 	ExpiresAt time.Time
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
 	ll.mu.Lock()
122
 	ll.mu.Lock()
122
 	defer ll.mu.Unlock()
123
 	defer ll.mu.Unlock()
123
 
124
 
@@ -125,23 +126,24 @@ func (ll *LoginLimiter) RequireCaptcha(ip string) (error, CaptchaMeta) {
125
 		return errors.New("no captcha provider available"), CaptchaMeta{}
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
 	if err != nil {
130
 	if err != nil {
130
 		return err, CaptchaMeta{}
131
 		return err, CaptchaMeta{}
131
 	}
132
 	}
132
 
133
 
133
 	// 存储验证码
134
 	// 存储验证码
134
-	ll.captchas[ip] = CaptchaMeta{
135
+	ll.captchas[id] = CaptchaMeta{
136
+		Id:        id,
135
 		Content:   content,
137
 		Content:   content,
136
 		Answer:    answer,
138
 		Answer:    answer,
137
 		ExpiresAt: time.Now().Add(ll.provider.Expiration()),
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
 	ll.mu.Lock()
147
 	ll.mu.Lock()
146
 	defer ll.mu.Unlock()
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
 	if !exists {
157
 	if !exists {
156
 		return false
158
 		return false
157
 	}
159
 	}
158
 
160
 
159
 	// 清理过期验证码
161
 	// 清理过期验证码
160
 	if time.Now().After(captcha.ExpiresAt) {
162
 	if time.Now().After(captcha.ExpiresAt) {
161
-		delete(ll.captchas, ip)
163
+		delete(ll.captchas, id)
162
 		return false
164
 		return false
163
 	}
165
 	}
164
 
166
 
165
 	// 验证并清理状态
167
 	// 验证并清理状态
166
 	if answer == captcha.Answer {
168
 	if answer == captcha.Answer {
167
-		delete(ll.captchas, ip)
169
+		delete(ll.captchas, id)
168
 		return true
170
 		return true
169
 	}
171
 	}
170
 
172
 
@@ -176,16 +178,6 @@ func (ll *LoginLimiter) DrawCaptcha(content string) (err error, str string) {
176
 	return
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
 func (ll *LoginLimiter) RemoveAttempts(ip string) {
182
 func (ll *LoginLimiter) RemoveAttempts(ip string) {
191
 	ll.mu.Lock()
183
 	ll.mu.Lock()
@@ -212,7 +204,6 @@ func (ll *LoginLimiter) CheckSecurityStatus(ip string) (banned bool, captchaRequ
212
 
204
 
213
 	// 清理过期数据
205
 	// 清理过期数据
214
 	ll.pruneAttempts(ip, time.Now().Add(-ll.policy.AttemptsWindow))
206
 	ll.pruneAttempts(ip, time.Now().Add(-ll.policy.AttemptsWindow))
215
-	ll.pruneCaptchas(ip)
216
 
207
 
217
 	// 检查验证码要求
208
 	// 检查验证码要求
218
 	captchaRequired = len(ll.attempts[ip]) >= ll.policy.CaptchaThreshold
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
 	return valid
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
 		if time.Now().After(captcha.ExpiresAt) {
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
 import (
3
 import (
4
 	"fmt"
4
 	"fmt"
5
+	"github.com/google/uuid"
5
 	"testing"
6
 	"testing"
6
 	"time"
7
 	"time"
7
 )
8
 )
8
 
9
 
9
 type MockCaptchaProvider struct{}
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
 func (p *MockCaptchaProvider) Expiration() time.Duration {
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
 	if err != nil {
78
 	if err != nil {
79
 		t.Fatalf("生成验证码失败: %v", err)
79
 		t.Fatalf("生成验证码失败: %v", err)
80
 	}
80
 	}
81
 	fmt.Printf("验证码内容: %#v\n", capc)
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
 		t.Error("验证码应该验证成功")
85
 		t.Error("验证码应该验证成功")
86
 	}
86
 	}
87
 
87
 
88
+	// 验证已删除
89
+	if limiter.VerifyCaptcha(capc.Id, capc.Answer) {
90
+		t.Error("验证码应该已删除")
91
+	}
92
+
88
 	limiter.RemoveAttempts(ip)
93
 	limiter.RemoveAttempts(ip)
89
 	// 验证后状态
94
 	// 验证后状态
90
 	if banned, need := limiter.CheckSecurityStatus(ip); banned || need {
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
 	if err != nil {
113
 	if err != nil {
109
 		t.Fatalf("生成验证码失败: %v", err)
114
 		t.Fatalf("生成验证码失败: %v", err)
110
 	}
115
 	}
111
 	fmt.Printf("验证码内容: %#v\n", capc)
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
 		t.Error("验证码应该验证成功")
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
 	if err != nil {
145
 	if err != nil {
141
 		t.Fatalf("生成验证码失败: %v", err)
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
 	if err != nil {
176
 	if err != nil {
172
 		t.Fatalf("生成验证码失败: %v", err)
177
 		t.Fatalf("生成验证码失败: %v", err)
173
 	}
178
 	}
@@ -175,9 +180,8 @@ func TestCaptchaTimeout(t *testing.T) {
175
 	// 等待超过 CaptchaValidPeriod
180
 	// 等待超过 CaptchaValidPeriod
176
 	time.Sleep(3 * time.Second)
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
 		t.Error("验证码应该已过期")
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
 	if err != nil {
269
 	if err != nil {
266
 		t.Fatalf("生成验证码失败: %v", err)
270
 		t.Fatalf("生成验证码失败: %v", err)
267
 	}
271
 	}
@@ -275,7 +279,7 @@ func TestB64CaptchaFlow(t *testing.T) {
275
 	fmt.Printf("验证码内容: %#v\n", b64)
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
 		t.Error("验证码应该验证成功")
283
 		t.Error("验证码应该验证成功")
280
 	}
284
 	}
281
 	limiter.RemoveAttempts(ip)
285
 	limiter.RemoveAttempts(ip)