mirror of
https://github.com/lejianwen/rustdesk-api.git
synced 2025-09-15 14:28:34 +08:00
add captcha #82
This commit is contained in:
parent
d60fdff179
commit
821b0a6faf
3
go.mod
3
go.mod
@ -43,6 +43,7 @@ require (
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
@ -58,6 +59,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.4.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mojocn/base64Captcha v1.3.6 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
@ -69,6 +71,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
|
||||
@ -11,11 +11,126 @@ import (
|
||||
"Gwen/service"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
}
|
||||
|
||||
// Captcha 验证码结构
|
||||
type Captcha struct {
|
||||
Id string `json:"id"` // 验证码 ID
|
||||
B64 string `json:"b64"` // base64 验证码
|
||||
Code string `json:"-"` // 验证码内容
|
||||
ExpiresAt time.Time `json:"-"` // 过期时间
|
||||
}
|
||||
type LoginLimiter struct {
|
||||
mu sync.RWMutex
|
||||
failCount map[string]int // 记录每个 IP 的失败次数
|
||||
timestamp map[string]time.Time // 记录每个 IP 的最后失败时间
|
||||
captchas map[string]Captcha // 每个 IP 的验证码
|
||||
threshold int // 失败阈值
|
||||
expiry time.Duration // 失败记录过期时间
|
||||
}
|
||||
|
||||
func NewLoginLimiter(threshold int, expiry time.Duration) *LoginLimiter {
|
||||
return &LoginLimiter{
|
||||
failCount: make(map[string]int),
|
||||
timestamp: make(map[string]time.Time),
|
||||
captchas: make(map[string]Captcha),
|
||||
threshold: threshold,
|
||||
expiry: expiry,
|
||||
}
|
||||
}
|
||||
|
||||
// RecordFailure 记录登录失败
|
||||
func (l *LoginLimiter) RecordFailure(ip string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// 如果该 IP 的记录已经过期,重置计数
|
||||
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) > l.expiry {
|
||||
l.failCount[ip] = 0
|
||||
}
|
||||
|
||||
// 更新失败次数和时间戳
|
||||
l.failCount[ip]++
|
||||
l.timestamp[ip] = time.Now()
|
||||
}
|
||||
|
||||
// NeedsCaptcha 检查是否需要验证码
|
||||
func (l *LoginLimiter) NeedsCaptcha(ip string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
// 检查记录是否存在且未过期
|
||||
if lastTime, exists := l.timestamp[ip]; exists && time.Since(lastTime) <= l.expiry {
|
||||
return l.failCount[ip] >= l.threshold
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateCaptcha 为指定 IP 生成验证码
|
||||
func (l *LoginLimiter) GenerateCaptcha(ip string) Captcha {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
capd := base64Captcha.NewDriverString(50, 150, 5, 10, 4, "1234567890abcdefghijklmnopqrstuvwxyz", nil, nil, nil)
|
||||
b64cap := base64Captcha.NewCaptcha(capd, base64Captcha.DefaultMemStore)
|
||||
id, b64s, answer, err := b64cap.Generate()
|
||||
if err != nil {
|
||||
global.Logger.Error("Generate captcha failed: " + err.Error())
|
||||
return Captcha{}
|
||||
}
|
||||
// 保存验证码到对应 IP
|
||||
l.captchas[ip] = Captcha{
|
||||
Id: id,
|
||||
B64: b64s,
|
||||
Code: answer,
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
return l.captchas[ip]
|
||||
}
|
||||
|
||||
// VerifyCaptcha 验证指定 IP 的验证码
|
||||
func (l *LoginLimiter) VerifyCaptcha(ip, code string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
|
||||
// 检查验证码是否存在且未过期
|
||||
if captcha, exists := l.captchas[ip]; exists && time.Now().Before(captcha.ExpiresAt) {
|
||||
return captcha.Code == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CleanupExpired 清理过期的记录
|
||||
func (l *LoginLimiter) CleanupExpired() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for ip, lastTime := range l.timestamp {
|
||||
if now.Sub(lastTime) > l.expiry {
|
||||
delete(l.failCount, ip)
|
||||
delete(l.timestamp, ip)
|
||||
delete(l.captchas, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (l *LoginLimiter) RemoveRecord(ip string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
delete(l.failCount, ip)
|
||||
delete(l.timestamp, ip)
|
||||
delete(l.captchas, ip)
|
||||
}
|
||||
|
||||
var loginLimiter = NewLoginLimiter(3, 5*time.Minute)
|
||||
|
||||
// Login 登录
|
||||
// @Tags 登录
|
||||
// @Summary 登录
|
||||
@ -30,22 +145,37 @@ type Login struct {
|
||||
func (ct *Login) Login(c *gin.Context) {
|
||||
f := &admin.Login{}
|
||||
err := c.ShouldBindJSON(f)
|
||||
clientIp := c.ClientIP()
|
||||
if err != nil {
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
errList := global.Validator.ValidStruct(c, f)
|
||||
if len(errList) > 0 {
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), clientIp))
|
||||
response.Fail(c, 101, errList[0])
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要验证码
|
||||
if loginLimiter.NeedsCaptcha(clientIp) {
|
||||
if f.Captcha == "" {
|
||||
response.Fail(c, 110, response.TranslateMsg(c, "CaptchaRequired"))
|
||||
return
|
||||
}
|
||||
if !loginLimiter.VerifyCaptcha(clientIp, f.Captcha) {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "CaptchaError"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
|
||||
|
||||
if u.Id == 0 {
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), c.ClientIP()))
|
||||
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
|
||||
loginLimiter.RecordFailure(clientIp)
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
|
||||
return
|
||||
}
|
||||
@ -54,13 +184,30 @@ func (ct *Login) Login(c *gin.Context) {
|
||||
UserId: u.Id,
|
||||
Client: model.LoginLogClientWebAdmin,
|
||||
Uuid: "", //must be empty
|
||||
Ip: c.ClientIP(),
|
||||
Ip: clientIp,
|
||||
Type: model.LoginLogTypeAccount,
|
||||
Platform: f.Platform,
|
||||
})
|
||||
|
||||
// 成功后清除记录
|
||||
loginLimiter.RemoveRecord(clientIp)
|
||||
|
||||
// 清理过期记录
|
||||
go loginLimiter.CleanupExpired()
|
||||
|
||||
responseLoginSuccess(c, u, ut.Token)
|
||||
}
|
||||
func (ct *Login) Captcha(c *gin.Context) {
|
||||
clientIp := c.ClientIP()
|
||||
if !loginLimiter.NeedsCaptcha(clientIp) {
|
||||
response.Fail(c, 101, response.TranslateMsg(c, "NoCaptchaRequired"))
|
||||
return
|
||||
}
|
||||
captcha := loginLimiter.GenerateCaptcha(clientIp)
|
||||
response.Success(c, gin.H{
|
||||
"captcha": captcha,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 登出
|
||||
// @Tags 登录
|
||||
@ -90,10 +237,12 @@ func (ct *Login) Logout(c *gin.Context) {
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /admin/login-options [post]
|
||||
func (ct *Login) LoginOptions(c *gin.Context) {
|
||||
ip := c.ClientIP()
|
||||
ops := service.AllService.OauthService.GetOauthProviders()
|
||||
response.Success(c, gin.H{
|
||||
"ops": ops,
|
||||
"register": global.Config.App.Register,
|
||||
"ops": ops,
|
||||
"register": global.Config.App.Register,
|
||||
"need_captcha": loginLimiter.NeedsCaptcha(ip),
|
||||
})
|
||||
}
|
||||
|
||||
@ -154,11 +303,10 @@ func (ct *Login) OidcAuthQuery(c *gin.Context) {
|
||||
responseLoginSuccess(c, u, ut.Token)
|
||||
}
|
||||
|
||||
|
||||
func responseLoginSuccess(c *gin.Context, u *model.User, token string) {
|
||||
lp := &adResp.LoginPayload{}
|
||||
lp.FromUser(u)
|
||||
lp.Token = token
|
||||
lp.RouteNames = service.AllService.UserService.RouteNames(u)
|
||||
response.Success(c, lp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ type Login struct {
|
||||
Username string `json:"username" validate:"required" label:"用户名"`
|
||||
Password string `json:"password,omitempty" validate:"required" label:"密码"`
|
||||
Platform string `json:"platform" label:"平台"`
|
||||
Captcha string `json:"captcha,omitempty" label:"验证码"`
|
||||
}
|
||||
|
||||
type LoginLogQuery struct {
|
||||
|
||||
@ -49,6 +49,7 @@ func Init(g *gin.Engine) {
|
||||
func LoginBind(rg *gin.RouterGroup) {
|
||||
cont := &admin.Login{}
|
||||
rg.POST("/login", cont.Login)
|
||||
rg.GET("/captcha", cont.Captcha)
|
||||
rg.POST("/logout", cont.Logout)
|
||||
rg.GET("/login-options", cont.LoginOptions)
|
||||
rg.POST("/oidc/auth", cont.OidcAuth)
|
||||
|
||||
@ -123,3 +123,13 @@ other = "Share Group"
|
||||
description = "Register closed."
|
||||
one = "Register closed."
|
||||
other = "Register closed."
|
||||
|
||||
[CaptchaRequired]
|
||||
description = "Captcha required."
|
||||
one = "Captcha required."
|
||||
other = "Captcha required."
|
||||
|
||||
[CaptchaError]
|
||||
description = "Captcha error."
|
||||
one = "Captcha error."
|
||||
other = "Captcha error."
|
||||
|
||||
@ -132,3 +132,13 @@ other = "Grupo compartido"
|
||||
description = "Register closed."
|
||||
one = "Registro cerrado."
|
||||
other = "Registro cerrado."
|
||||
|
||||
[CaptchaRequired]
|
||||
description = "Captcha required."
|
||||
one = "Captcha requerido."
|
||||
other = "Captcha requerido."
|
||||
|
||||
[CaptchaError]
|
||||
description = "Captcha error."
|
||||
one = "Error de captcha."
|
||||
other = "Error de captcha."
|
||||
@ -125,4 +125,14 @@ other = "공유 그룹"
|
||||
[RegisterClosed]
|
||||
description = "Register closed."
|
||||
one = "가입이 종료되었습니다."
|
||||
other = "가입이 종료되었습니다."
|
||||
other = "가입이 종료되었습니다."
|
||||
|
||||
[CaptchaRequired]
|
||||
description = "Captcha required."
|
||||
one = "Captcha가 필요합니다."
|
||||
other = "Captcha가 필요합니다."
|
||||
|
||||
[CaptchaError]
|
||||
description = "Captcha error."
|
||||
one = "Captcha 오류."
|
||||
other = "Captcha 오류."
|
||||
@ -131,4 +131,14 @@ other = "Общая группа"
|
||||
[RegisterClosed]
|
||||
description = "Register closed."
|
||||
one = "Регистрация закрыта."
|
||||
other = "Регистрация закрыта."
|
||||
other = "Регистрация закрыта."
|
||||
|
||||
[CaptchaRequired]
|
||||
description = "Captcha required."
|
||||
one = "Требуется капча."
|
||||
other = "Требуется капча."
|
||||
|
||||
[CaptchaError]
|
||||
description = "Captcha error."
|
||||
one = "Ошибка капчи."
|
||||
other = "Ошибка капчи."
|
||||
@ -124,4 +124,14 @@ other = "共享组"
|
||||
[RegisterClosed]
|
||||
description = "Register closed."
|
||||
one = "注册已关闭。"
|
||||
other = "注册已关闭。"
|
||||
other = "注册已关闭。"
|
||||
|
||||
[CaptchaRequired]
|
||||
description = "Captcha required."
|
||||
one = "需要验证码。"
|
||||
other = "需要验证码。"
|
||||
|
||||
[CaptchaError]
|
||||
description = "Captcha error."
|
||||
one = "验证码错误。"
|
||||
other = "验证码错误。"
|
||||
Loading…
Reference in New Issue
Block a user