Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: captcha #796

Merged
merged 1 commit into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions middleware/captcha.go
Original file line number Diff line number Diff line change
@@ -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()
topjohncian marked this conversation as resolved.
Show resolved Hide resolved
}
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)
topjohncian marked this conversation as resolved.
Show resolved Hide resolved
}
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)
topjohncian marked this conversation as resolved.
Show resolved Hide resolved
}
if *response.Response.CaptchaCode != int64(1) {
c.JSON(200, serializer.ParamErr("验证失败,请刷新网页后再次验证", nil))
c.Abort()
}
break
}
}
c.Next()
}
}
HFO4 marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion models/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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"},
Expand Down
58 changes: 30 additions & 28 deletions pkg/serializer/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion routers/controllers/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

// 如果已登录,则同时返回用户信息和标签
Expand Down
5 changes: 3 additions & 2 deletions routers/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// 邮件激活
Expand Down
59 changes: 4 additions & 55 deletions service/user/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 密码重设服务
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
Expand Down
37 changes: 5 additions & 32 deletions service/user/register.go
Original file line number Diff line number Diff line change
@@ -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"])
Expand Down