From 8cc22dec9163415bbfd8ac5a42aaa7353910cbe4 Mon Sep 17 00:00:00 2001 From: DacongDA Date: Thu, 12 Jun 2025 23:02:36 +0800 Subject: [PATCH] feat: upgrade Alibaba cloud captcha provider from v1 to v2 (#3879) --- captcha/aliyun.go | 152 +++++++++++++++------------ captcha/default.go | 2 +- captcha/geetest.go | 2 +- captcha/hcaptcha.go | 2 +- captcha/provider.go | 6 +- captcha/recaptcha.go | 2 +- captcha/turnstile.go | 2 +- controllers/auth.go | 2 +- controllers/verification.go | 4 +- go.mod | 2 +- web/src/ProviderEditPage.js | 7 +- web/src/common/CaptchaWidget.js | 43 +++++--- web/src/common/modal/CaptchaModal.js | 20 ++-- 13 files changed, 142 insertions(+), 104 deletions(-) diff --git a/captcha/aliyun.go b/captcha/aliyun.go index e275c395..bad42533 100644 --- a/captcha/aliyun.go +++ b/captcha/aliyun.go @@ -15,32 +15,51 @@ package captcha import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "github.com/casdoor/casdoor/util" + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + openapiutil "github.com/alibabacloud-go/openapi-util/service" + teaUtil "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" ) -const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com" +const AliyunCaptchaVerifyUrl = "captcha.cn-shanghai.aliyuncs.com" -type captchaSuccessResponse struct { - Code int `json:"Code"` - Msg string `json:"Msg"` +type VerifyCaptchaRequest struct { + CaptchaVerifyParam *string `json:"CaptchaVerifyParam,omitempty" xml:"CaptchaVerifyParam,omitempty"` + SceneId *string `json:"SceneId,omitempty" xml:"SceneId,omitempty"` } -type captchaFailResponse struct { - Code string `json:"Code"` - Message string `json:"Message"` +type VerifyCaptchaResponseBodyResult struct { + VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"` } +type VerifyCaptchaResponseBody struct { + Code *string `json:"Code,omitempty" xml:"Code,omitempty"` + Message *string `json:"Message,omitempty" xml:"Message,omitempty"` + // Id of the request + RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"` + Result *VerifyCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"` + Success *bool `json:"Success,omitempty" xml:"Success,omitempty"` +} + +type VerifyIntelligentCaptchaResponseBodyResult struct { + VerifyCode *string `json:"VerifyCode,omitempty" xml:"VerifyCode,omitempty"` + VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"` +} + +type VerifyIntelligentCaptchaResponseBody struct { + Code *string `json:"Code,omitempty" xml:"Code,omitempty"` + Message *string `json:"Message,omitempty" xml:"Message,omitempty"` + // Id of the request + RequestId *string `json:"RequestId,omitempty" xml:"RequestId,omitempty"` + Result *VerifyIntelligentCaptchaResponseBodyResult `json:"Result,omitempty" xml:"Result,omitempty" type:"Struct"` + Success *bool `json:"Success,omitempty" xml:"Success,omitempty"` +} + +type VerifyIntelligentCaptchaResponse struct { + Headers map[string]*string `json:"headers,omitempty" xml:"headers,omitempty" require:"true"` + StatusCode *int32 `json:"statusCode,omitempty" xml:"statusCode,omitempty" require:"true"` + Body *VerifyIntelligentCaptchaResponseBody `json:"body,omitempty" xml:"body,omitempty" require:"true"` +} type AliyunCaptchaProvider struct{} func NewAliyunCaptchaProvider() *AliyunCaptchaProvider { @@ -48,68 +67,69 @@ func NewAliyunCaptchaProvider() *AliyunCaptchaProvider { return captcha } -func contentEscape(str string) string { - str = strings.Replace(str, " ", "%20", -1) - str = url.QueryEscape(str) - return str -} +func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { + config := &openapi.Config{} -func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { - pathData, err := url.ParseQuery(token) + config.Endpoint = tea.String(AliyunCaptchaVerifyUrl) + config.ConnectTimeout = tea.Int(5000) + config.ReadTimeout = tea.Int(5000) + config.AccessKeyId = tea.String(clientId) + config.AccessKeySecret = tea.String(clientSecret) + + client := new(openapi.Client) + err := client.Init(config) if err != nil { return false, err } - pathData["Action"] = []string{"AuthenticateSig"} - pathData["Format"] = []string{"json"} - pathData["SignatureMethod"] = []string{"HMAC-SHA1"} - pathData["SignatureNonce"] = []string{strconv.FormatInt(time.Now().UnixNano(), 10)} - pathData["SignatureVersion"] = []string{"1.0"} - pathData["Timestamp"] = []string{time.Now().UTC().Format("2006-01-02T15:04:05Z")} - pathData["Version"] = []string{"2018-01-12"} + request := VerifyCaptchaRequest{CaptchaVerifyParam: tea.String(token), SceneId: tea.String(clientId2)} - var keys []string - for k := range pathData { - keys = append(keys, k) - } - sort.Strings(keys) - - sortQuery := "" - for _, k := range keys { - sortQuery += k + "=" + contentEscape(pathData[k][0]) + "&" - } - sortQuery = strings.TrimSuffix(sortQuery, "&") - - stringToSign := fmt.Sprintf("GET&%s&%s", url.QueryEscape("/"), url.QueryEscape(sortQuery)) - - signature := util.GetHmacSha1(clientSecret+"&", stringToSign) - - resp, err := http.Get(fmt.Sprintf("%s?%s&Signature=%s", AliyunCaptchaVerifyUrl, sortQuery, url.QueryEscape(signature))) + err = teaUtil.ValidateModel(&request) if err != nil { return false, err } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) + runtime := &teaUtil.RuntimeOptions{} + + body := map[string]interface{}{} + if !tea.BoolValue(teaUtil.IsUnset(request.CaptchaVerifyParam)) { + body["CaptchaVerifyParam"] = request.CaptchaVerifyParam + } + + if !tea.BoolValue(teaUtil.IsUnset(request.SceneId)) { + body["SceneId"] = request.SceneId + } + + req := &openapi.OpenApiRequest{ + Body: openapiutil.ParseToMap(body), + } + params := &openapi.Params{ + Action: tea.String("VerifyIntelligentCaptcha"), + Version: tea.String("2023-03-05"), + Protocol: tea.String("HTTPS"), + Pathname: tea.String("/"), + Method: tea.String("POST"), + AuthType: tea.String("AK"), + Style: tea.String("RPC"), + ReqBodyType: tea.String("formData"), + BodyType: tea.String("json"), + } + + res := &VerifyIntelligentCaptchaResponse{} + + resBody, err := client.CallApi(params, req, runtime) if err != nil { return false, err } - return handleCaptchaResponse(body) -} - -func handleCaptchaResponse(body []byte) (bool, error) { - captchaResp := &captchaSuccessResponse{} - err := json.Unmarshal(body, captchaResp) + err = tea.Convert(resBody, &res) if err != nil { - captchaFailResp := &captchaFailResponse{} - err = json.Unmarshal(body, captchaFailResp) - if err != nil { - return false, err - } - - return false, errors.New(captchaFailResp.Message) + return false, err } - return true, nil + if res.Body.Result.VerifyResult != nil && *res.Body.Result.VerifyResult { + return true, nil + } + + return false, nil } diff --git a/captcha/default.go b/captcha/default.go index d3c2037c..ae879b3e 100644 --- a/captcha/default.go +++ b/captcha/default.go @@ -23,6 +23,6 @@ func NewDefaultCaptchaProvider() *DefaultCaptchaProvider { return captcha } -func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { +func (captcha *DefaultCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { return object.VerifyCaptcha(clientSecret, token), nil } diff --git a/captcha/geetest.go b/captcha/geetest.go index bd48c8e4..132f328d 100644 --- a/captcha/geetest.go +++ b/captcha/geetest.go @@ -35,7 +35,7 @@ func NewGEETESTCaptchaProvider() *GEETESTCaptchaProvider { return captcha } -func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { +func (captcha *GEETESTCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { pathData, err := url.ParseQuery(token) if err != nil { return false, err diff --git a/captcha/hcaptcha.go b/captcha/hcaptcha.go index f7f5198b..ebb0cc9b 100644 --- a/captcha/hcaptcha.go +++ b/captcha/hcaptcha.go @@ -32,7 +32,7 @@ func NewHCaptchaProvider() *HCaptchaProvider { return captcha } -func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { +func (captcha *HCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { reqData := url.Values{ "secret": {clientSecret}, "response": {token}, diff --git a/captcha/provider.go b/captcha/provider.go index d7901cd0..1ad2c982 100644 --- a/captcha/provider.go +++ b/captcha/provider.go @@ -17,7 +17,7 @@ package captcha import "fmt" type CaptchaProvider interface { - VerifyCaptcha(token, clientSecret string) (bool, error) + VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) } func GetCaptchaProvider(captchaType string) CaptchaProvider { @@ -43,11 +43,11 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider { return nil } -func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) { +func VerifyCaptchaByCaptchaType(captchaType, token, clientId, clientSecret, clientId2 string) (bool, error) { provider := GetCaptchaProvider(captchaType) if provider == nil { return false, fmt.Errorf("invalid captcha provider: %s", captchaType) } - return provider.VerifyCaptcha(token, clientSecret) + return provider.VerifyCaptcha(token, clientId, clientSecret, clientId2) } diff --git a/captcha/recaptcha.go b/captcha/recaptcha.go index 84d32e3c..3b90a38b 100644 --- a/captcha/recaptcha.go +++ b/captcha/recaptcha.go @@ -32,7 +32,7 @@ func NewReCaptchaProvider() *ReCaptchaProvider { return captcha } -func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { +func (captcha *ReCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { reqData := url.Values{ "secret": {clientSecret}, "response": {token}, diff --git a/captcha/turnstile.go b/captcha/turnstile.go index f143ea00..55f9cf8d 100644 --- a/captcha/turnstile.go +++ b/captcha/turnstile.go @@ -32,7 +32,7 @@ func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider { return captcha } -func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { +func (captcha *CloudflareTurnstileProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) { reqData := url.Values{ "secret": {clientSecret}, "response": {token}, diff --git a/controllers/auth.go b/controllers/auth.go index 5d4674c7..07877448 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -571,7 +571,7 @@ func (c *ApiController) Login() { } var isHuman bool - isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, authForm.ClientSecret) + isHuman, err = captcha.VerifyCaptchaByCaptchaType(authForm.CaptchaType, authForm.CaptchaToken, captchaProvider.ClientId, authForm.ClientSecret, captchaProvider.ClientId2) if err != nil { c.ResponseError(err.Error()) return diff --git a/controllers/verification.go b/controllers/verification.go index 01454112..d5247890 100644 --- a/controllers/verification.go +++ b/controllers/verification.go @@ -160,7 +160,7 @@ func (c *ApiController) SendVerificationCode() { if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil { c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType) return - } else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret); err != nil { + } else if isHuman, err := captchaProvider.VerifyCaptcha(vform.CaptchaToken, provider.ClientId, vform.ClientSecret, provider.ClientId2); err != nil { c.ResponseError(err.Error()) return } else if !isHuman { @@ -349,7 +349,7 @@ func (c *ApiController) VerifyCaptcha() { return } - isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret) + isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2) if err != nil { c.ResponseError(err.Error()) return diff --git a/go.mod b/go.mod index ee1c2411..c33a002c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4 github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2 + github.com/alibabacloud-go/openapi-util v0.1.0 github.com/alibabacloud-go/tea v1.3.2 github.com/alibabacloud-go/tea-utils/v2 v2.0.7 github.com/aws/aws-sdk-go v1.45.5 @@ -90,7 +91,6 @@ require ( github.com/alibabacloud-go/darabonba-number v1.0.4 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect - github.com/alibabacloud-go/openapi-util v0.1.0 // indirect github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 5a320a45..6e9762a7 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -371,11 +371,6 @@ class ProviderEditPage extends React.Component { {id: "Third-party", name: i18next.t("provider:Third-party")}, ] ); - } else if (type === "Aliyun Captcha") { - return [ - {id: "nc", name: i18next.t("provider:Sliding Validation")}, - {id: "ic", name: i18next.t("provider:Intelligent Validation")}, - ]; } else { return []; } @@ -674,7 +669,7 @@ class ProviderEditPage extends React.Component { { - this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "Aliyun Captcha" ? null : ( + this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" ? null : ( diff --git a/web/src/common/CaptchaWidget.js b/web/src/common/CaptchaWidget.js index 412c0769..10c90bb6 100644 --- a/web/src/common/CaptchaWidget.js +++ b/web/src/common/CaptchaWidget.js @@ -13,6 +13,8 @@ // limitations under the License. import React, {useEffect} from "react"; +import {Button} from "antd"; +import i18next from "i18next"; export const CaptchaWidget = (props) => { const {captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2, onChange} = props; @@ -85,23 +87,34 @@ export const CaptchaWidget = (props) => { break; } case "Aliyun Captcha": { + window.AliyunCaptchaConfig = { + region: "cn", + prefix: clientSecret2, + }; + const AWSCTimer = setInterval(() => { - if (!window.AWSC) { - loadScript("https://g.alicdn.com/AWSC/AWSC/awsc.js"); + if (!window.initAliyunCaptcha) { + loadScript("https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"); } - if (window.AWSC) { + if (window.initAliyunCaptcha) { if (clientSecret2 && clientSecret2 !== "***") { - window.AWSC.use(subType, function(state, module) { - module.init({ - appkey: clientSecret2, - scene: clientId2, - renderTo: "captcha", - success: function(data) { - onChange(`SessionId=${data.sessionId}&AccessKeyId=${siteKey}&Scene=${clientId2}&AppKey=${clientSecret2}&Token=${data.token}&Sig=${data.sig}&RemoteIp=192.168.0.1`); - }, - }); + window.initAliyunCaptcha({ + SceneId: clientId2, + mode: "embed", + element: "#captcha", + button: "#captcha-button", + captchaVerifyCallback: (data) => { + onChange(data.toString()); + }, + slideStyle: { + width: 320, + height: 40, + }, + language: "cn", + immediate: true, }); + } clearInterval(AWSCTimer); } @@ -154,5 +167,9 @@ export const CaptchaWidget = (props) => { } }, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]); - return
; + return
+ { + captchaType === "Aliyun Captcha" && window.initAliyunCaptcha ? : null + } +
; }; diff --git a/web/src/common/modal/CaptchaModal.js b/web/src/common/modal/CaptchaModal.js index 8771d191..5850611f 100644 --- a/web/src/common/modal/CaptchaModal.js +++ b/web/src/common/modal/CaptchaModal.js @@ -44,6 +44,12 @@ export const CaptchaModal = (props) => { } }, [visible]); + useEffect(() => { + if (captchaToken !== "" && captchaType !== "Default") { + handleOk(); + } + }, [captchaToken]); + const handleOk = () => { onOk?.(captchaType, captchaToken, clientSecret); }; @@ -138,19 +144,18 @@ export const CaptchaModal = (props) => { if (!regex.test(captchaToken)) { isOkDisabled = true; } - } else if (captchaToken === "") { - isOkDisabled = true; + return [ + null, + , + ]; } - return [ - , - , - ]; + return null; }; return ( { width={350} footer={renderFooter()} onCancel={handleCancel} + afterClose={handleCancel} onOk={handleOk} >