diff --git a/middleware/captcha.go b/middleware/captcha.go new file mode 100644 index 0000000..f22c60c --- /dev/null +++ b/middleware/captcha.go @@ -0,0 +1,109 @@ +package middleware + +import ( + "bytes" + "encoding/json" + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/recaptcha" + "github.com/cloudreve/Cloudreve/v3/pkg/serializer" + "github.com/cloudreve/Cloudreve/v3/pkg/util" + "github.com/gin-gonic/gin" + "github.com/mojocn/base64Captcha" + captcha "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha/v20190722" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + "io" + "io/ioutil" + "strconv" + "time" +) + +type req struct { + CaptchaCode string `json:"captchaCode"` + Ticket string `json:"ticket"` + Randstr string `json:"randstr"` +} + +// CaptchaRequired 验证请求签名 +func CaptchaRequired(configName string) gin.HandlerFunc { + return func(c *gin.Context) { + // 相关设定 + options := model.GetSettingByNames(configName, + "captcha_type", + "captcha_ReCaptchaSecret", + "captcha_TCaptcha_SecretId", + "captcha_TCaptcha_SecretKey", + "captcha_TCaptcha_CaptchaAppId", + "captcha_TCaptcha_AppSecretKey") + // 检查验证码 + isCaptchaRequired := model.IsTrueVal(options[configName]) + + if isCaptchaRequired { + var service req + bodyCopy := new(bytes.Buffer) + _, err := io.Copy(bodyCopy, c.Request.Body) + if err != nil { + c.JSON(200, serializer.ParamErr("验证码错误", err)) + c.Abort() + } + bodyData := bodyCopy.Bytes() + err = json.Unmarshal(bodyData, &service) + if err != nil { + c.JSON(200, serializer.ParamErr("验证码错误", err)) + c.Abort() + } + c.Request.Body = ioutil.NopCloser(bytes.NewReader(bodyData)) + switch options["captcha_type"] { + case "normal": + captchaID := util.GetSession(c, "captchaID") + util.DeleteSession(c, "captchaID") + if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) { + c.JSON(200, serializer.ParamErr("验证码错误", nil)) + c.Abort() + } + break + case "recaptcha": + reCAPTCHA, err := recaptcha.NewReCAPTCHA(options["captcha_ReCaptchaSecret"], recaptcha.V2, 10*time.Second) + if err != nil { + util.Log().Warning("reCAPTCHA 验证错误, %s", err) + } + err = reCAPTCHA.Verify(service.CaptchaCode) + if err != nil { + util.Log().Warning("reCAPTCHA 验证错误, %s", err) + c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil)) + c.Abort() + } + break + case "tcaptcha": + credential := common.NewCredential( + options["captcha_TCaptcha_SecretId"], + options["captcha_TCaptcha_SecretKey"], + ) + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "captcha.tencentcloudapi.com" + client, _ := captcha.NewClient(credential, "", cpf) + + request := captcha.NewDescribeCaptchaResultRequest() + + request.CaptchaType = common.Uint64Ptr(9) + appid, _ := strconv.Atoi(options["captcha_TCaptcha_CaptchaAppId"]) + request.CaptchaAppId = common.Uint64Ptr(uint64(appid)) + request.AppSecretKey = common.StringPtr(options["captcha_TCaptcha_AppSecretKey"]) + request.Ticket = common.StringPtr(service.Ticket) + request.Randstr = common.StringPtr(service.Randstr) + request.UserIp = common.StringPtr(c.ClientIP()) + + response, err := client.DescribeCaptchaResult(request) + if err != nil { + util.Log().Warning("TCaptcha 验证错误, %s", err) + } + if *response.Response.CaptchaCode != int64(1) { + c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil)) + c.Abort() + } + break + } + } + c.Next() + } +} diff --git a/models/migration.go b/models/migration.go index 4522c8e..42fb14d 100644 --- a/models/migration.go +++ b/models/migration.go @@ -149,6 +149,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "share_view_method", Value: "list", Type: "view"}, {Name: "cron_garbage_collect", Value: "@hourly", Type: "cron"}, {Name: "authn_enabled", Value: "0", Type: "authn"}, + {Name: "captcha_type", Value: "normal", Type: "captcha"}, {Name: "captcha_height", Value: "60", Type: "captcha"}, {Name: "captcha_width", Value: "240", Type: "captcha"}, {Name: "captcha_mode", Value: "3", Type: "captcha"}, @@ -160,9 +161,12 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "captcha_IsShowSlimeLine", Value: "1", Type: "captcha"}, {Name: "captcha_IsShowSineLine", Value: "0", Type: "captcha"}, {Name: "captcha_CaptchaLen", Value: "6", Type: "captcha"}, - {Name: "captcha_IsUseReCaptcha", Value: "0", Type: "captcha"}, {Name: "captcha_ReCaptchaKey", Value: "defaultKey", Type: "captcha"}, {Name: "captcha_ReCaptchaSecret", Value: "defaultSecret", Type: "captcha"}, + {Name: "captcha_TCaptcha_CaptchaAppId", Value: "", Type: "captcha"}, + {Name: "captcha_TCaptcha_AppSecretKey", Value: "", Type: "captcha"}, + {Name: "captcha_TCaptcha_SecretId", Value: "", Type: "captcha"}, + {Name: "captcha_TCaptcha_SecretKey", Value: "", Type: "captcha"}, {Name: "thumb_width", Value: "400", Type: "thumb"}, {Name: "thumb_height", Value: "300", Type: "thumb"}, {Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"}, diff --git a/pkg/serializer/setting.go b/pkg/serializer/setting.go index c5b310b..8c238da 100644 --- a/pkg/serializer/setting.go +++ b/pkg/serializer/setting.go @@ -4,20 +4,21 @@ import model "github.com/cloudreve/Cloudreve/v3/models" // SiteConfig 站点全局设置序列 type SiteConfig struct { - SiteName string `json:"title"` - SiteICPId string `json:"siteICPId"` - LoginCaptcha bool `json:"loginCaptcha"` - RegCaptcha bool `json:"regCaptcha"` - ForgetCaptcha bool `json:"forgetCaptcha"` - EmailActive bool `json:"emailActive"` - Themes string `json:"themes"` - DefaultTheme string `json:"defaultTheme"` - HomepageViewMethod string `json:"home_view_method"` - ShareViewMethod string `json:"share_view_method"` - Authn bool `json:"authn"` - User User `json:"user"` - UseReCaptcha bool `json:"captcha_IsUseReCaptcha"` - ReCaptchaKey string `json:"captcha_ReCaptchaKey"` + SiteName string `json:"title"` + SiteICPId string `json:"siteICPId"` + LoginCaptcha bool `json:"loginCaptcha"` + RegCaptcha bool `json:"regCaptcha"` + ForgetCaptcha bool `json:"forgetCaptcha"` + EmailActive bool `json:"emailActive"` + Themes string `json:"themes"` + DefaultTheme string `json:"defaultTheme"` + HomepageViewMethod string `json:"home_view_method"` + ShareViewMethod string `json:"share_view_method"` + Authn bool `json:"authn"` + User User `json:"user"` + ReCaptchaKey string `json:"captcha_ReCaptchaKey"` + CaptchaType string `json:"captcha_type"` + TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"` } type task struct { @@ -64,20 +65,21 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response { } res := Response{ Data: SiteConfig{ - SiteName: checkSettingValue(settings, "siteName"), - SiteICPId: checkSettingValue(settings, "siteICPId"), - LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")), - RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")), - ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")), - EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")), - Themes: checkSettingValue(settings, "themes"), - DefaultTheme: checkSettingValue(settings, "defaultTheme"), - HomepageViewMethod: checkSettingValue(settings, "home_view_method"), - ShareViewMethod: checkSettingValue(settings, "share_view_method"), - Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")), - User: userRes, - UseReCaptcha: model.IsTrueVal(checkSettingValue(settings, "captcha_IsUseReCaptcha")), - ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"), + SiteName: checkSettingValue(settings, "siteName"), + SiteICPId: checkSettingValue(settings, "siteICPId"), + LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")), + RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")), + ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")), + EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")), + Themes: checkSettingValue(settings, "themes"), + DefaultTheme: checkSettingValue(settings, "defaultTheme"), + HomepageViewMethod: checkSettingValue(settings, "home_view_method"), + ShareViewMethod: checkSettingValue(settings, "share_view_method"), + Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")), + User: userRes, + ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"), + CaptchaType: checkSettingValue(settings, "captcha_type"), + TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"), }} return res } diff --git a/routers/controllers/site.go b/routers/controllers/site.go index 8de9707..056a982 100644 --- a/routers/controllers/site.go +++ b/routers/controllers/site.go @@ -24,8 +24,9 @@ func SiteConfig(c *gin.Context) { "home_view_method", "share_view_method", "authn_enabled", - "captcha_IsUseReCaptcha", "captcha_ReCaptchaKey", + "captcha_type", + "captcha_TCaptcha_CaptchaAppId", ) // 如果已登录,则同时返回用户信息和标签 diff --git a/routers/router.go b/routers/router.go index 7b0f36e..3ae63e9 100644 --- a/routers/router.go +++ b/routers/router.go @@ -116,16 +116,17 @@ func InitMasterRouter() *gin.Engine { user := v3.Group("user") { // 用户登录 - user.POST("session", controllers.UserLogin) + user.POST("session", middleware.CaptchaRequired("login_captcha"), controllers.UserLogin) // 用户注册 user.POST("", middleware.IsFunctionEnabled("register_enabled"), + middleware.CaptchaRequired("reg_captcha"), controllers.UserRegister, ) // 用二步验证户登录 user.POST("2fa", controllers.User2FALogin) // 发送密码重设邮件 - user.POST("reset", controllers.UserSendReset) + user.POST("reset", middleware.CaptchaRequired("forget_captcha"), controllers.UserSendReset) // 通过邮件里的链接重设密码 user.PATCH("reset", controllers.UserReset) // 邮件激活 diff --git a/service/user/login.go b/service/user/login.go index 4689bc4..0e5c921 100644 --- a/service/user/login.go +++ b/service/user/login.go @@ -2,33 +2,27 @@ package user import ( "fmt" - "net/url" - "time" - model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/cache" "github.com/cloudreve/Cloudreve/v3/pkg/email" "github.com/cloudreve/Cloudreve/v3/pkg/hashid" - "github.com/cloudreve/Cloudreve/v3/pkg/recaptcha" "github.com/cloudreve/Cloudreve/v3/pkg/serializer" "github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/gin-gonic/gin" - "github.com/mojocn/base64Captcha" "github.com/pquerna/otp/totp" + "net/url" ) // UserLoginService 管理用户登录的服务 type UserLoginService struct { //TODO 细致调整验证规则 - UserName string `form:"userName" json:"userName" binding:"required,email"` - Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"` - CaptchaCode string `form:"captchaCode" json:"captchaCode"` + UserName string `form:"userName" json:"userName" binding:"required,email"` + Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"` } // UserResetEmailService 发送密码重设邮件服务 type UserResetEmailService struct { - UserName string `form:"userName" json:"userName" binding:"required,email"` - CaptchaCode string `form:"captchaCode" json:"captchaCode"` + UserName string `form:"userName" json:"userName" binding:"required,email"` } // UserResetService 密码重设服务 @@ -69,28 +63,6 @@ func (service *UserResetService) Reset(c *gin.Context) serializer.Response { // Reset 发送密码重设邮件 func (service *UserResetEmailService) Reset(c *gin.Context) serializer.Response { - // 检查验证码 - isCaptchaRequired := model.IsTrueVal(model.GetSettingByName("forget_captcha")) - useRecaptcha := model.IsTrueVal(model.GetSettingByName("captcha_IsUseReCaptcha")) - recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret") - if isCaptchaRequired && !useRecaptcha { - captchaID := util.GetSession(c, "captchaID") - util.DeleteSession(c, "captchaID") - if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) { - return serializer.ParamErr("验证码错误", nil) - } - } else if isCaptchaRequired && useRecaptcha { - captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second) - if err != nil { - util.Log().Error(err.Error()) - } - err = captcha.Verify(service.CaptchaCode) - if err != nil { - util.Log().Error(err.Error()) - return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil) - } - } - // 查找用户 if user, err := model.GetUserByEmail(service.UserName); err == nil { @@ -151,30 +123,7 @@ func (service *Enable2FA) Login(c *gin.Context) serializer.Response { // Login 用户登录函数 func (service *UserLoginService) Login(c *gin.Context) serializer.Response { - isCaptchaRequired := model.GetSettingByName("login_captcha") - useRecaptcha := model.GetSettingByName("captcha_IsUseReCaptcha") - recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret") expectedUser, err := model.GetUserByEmail(service.UserName) - - if (model.IsTrueVal(isCaptchaRequired)) && !(model.IsTrueVal(useRecaptcha)) { - // TODO 验证码校验 - captchaID := util.GetSession(c, "captchaID") - util.DeleteSession(c, "captchaID") - if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) { - return serializer.ParamErr("验证码错误", nil) - } - } else if (model.IsTrueVal(isCaptchaRequired)) && (model.IsTrueVal(useRecaptcha)) { - captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second) - if err != nil { - util.Log().Error(err.Error()) - } - err = captcha.Verify(service.CaptchaCode) - if err != nil { - util.Log().Error(err.Error()) - return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil) - } - } - // 一系列校验 if err != nil { return serializer.Err(serializer.CodeCredentialInvalid, "用户邮箱或密码错误", err) diff --git a/service/user/register.go b/service/user/register.go index 94c5eda..3daa9af 100644 --- a/service/user/register.go +++ b/service/user/register.go @@ -1,54 +1,27 @@ package user import ( - "net/url" - "strings" - "time" - model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/auth" "github.com/cloudreve/Cloudreve/v3/pkg/email" "github.com/cloudreve/Cloudreve/v3/pkg/hashid" - "github.com/cloudreve/Cloudreve/v3/pkg/recaptcha" "github.com/cloudreve/Cloudreve/v3/pkg/serializer" - "github.com/cloudreve/Cloudreve/v3/pkg/util" "github.com/gin-gonic/gin" - "github.com/mojocn/base64Captcha" + "net/url" + "strings" ) // UserRegisterService 管理用户注册的服务 type UserRegisterService struct { //TODO 细致调整验证规则 - UserName string `form:"userName" json:"userName" binding:"required,email"` - Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"` - CaptchaCode string `form:"captchaCode" json:"captchaCode"` + UserName string `form:"userName" json:"userName" binding:"required,email"` + Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"` } // Register 新用户注册 func (service *UserRegisterService) Register(c *gin.Context) serializer.Response { // 相关设定 - options := model.GetSettingByNames("email_active", "reg_captcha") - // 检查验证码 - isCaptchaRequired := model.IsTrueVal(options["reg_captcha"]) - useRecaptcha := model.IsTrueVal(model.GetSettingByName("captcha_IsUseReCaptcha")) - recaptchaSecret := model.GetSettingByName("captcha_ReCaptchaSecret") - if isCaptchaRequired && !useRecaptcha { - captchaID := util.GetSession(c, "captchaID") - util.DeleteSession(c, "captchaID") - if captchaID == nil || !base64Captcha.VerifyCaptcha(captchaID.(string), service.CaptchaCode) { - return serializer.ParamErr("验证码错误", nil) - } - } else if isCaptchaRequired && useRecaptcha { - captcha, err := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second) - if err != nil { - util.Log().Error(err.Error()) - } - err = captcha.Verify(service.CaptchaCode) - if err != nil { - util.Log().Error(err.Error()) - return serializer.ParamErr("验证失败,请刷新网页后再次验证", nil) - } - } + options := model.GetSettingByNames("email_active") // 相关设定 isEmailRequired := model.IsTrueVal(options["email_active"])