feat: upgrade Alibaba cloud captcha provider from v1 to v2 (#3879)

This commit is contained in:
DacongDA
2025-06-12 23:02:36 +08:00
committed by GitHub
parent 0c08ae5365
commit 8cc22dec91
13 changed files with 142 additions and 104 deletions

View File

@ -15,32 +15,51 @@
package captcha package captcha
import ( import (
"encoding/json" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"errors" openapiutil "github.com/alibabacloud-go/openapi-util/service"
"fmt" teaUtil "github.com/alibabacloud-go/tea-utils/v2/service"
"io" "github.com/alibabacloud-go/tea/tea"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/casdoor/casdoor/util"
) )
const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com" const AliyunCaptchaVerifyUrl = "captcha.cn-shanghai.aliyuncs.com"
type captchaSuccessResponse struct { type VerifyCaptchaRequest struct {
Code int `json:"Code"` CaptchaVerifyParam *string `json:"CaptchaVerifyParam,omitempty" xml:"CaptchaVerifyParam,omitempty"`
Msg string `json:"Msg"` SceneId *string `json:"SceneId,omitempty" xml:"SceneId,omitempty"`
} }
type captchaFailResponse struct { type VerifyCaptchaResponseBodyResult struct {
Code string `json:"Code"` VerifyResult *bool `json:"VerifyResult,omitempty" xml:"VerifyResult,omitempty"`
Message string `json:"Message"`
} }
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{} type AliyunCaptchaProvider struct{}
func NewAliyunCaptchaProvider() *AliyunCaptchaProvider { func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
@ -48,68 +67,69 @@ func NewAliyunCaptchaProvider() *AliyunCaptchaProvider {
return captcha return captcha
} }
func contentEscape(str string) string { func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error) {
str = strings.Replace(str, " ", "%20", -1) config := &openapi.Config{}
str = url.QueryEscape(str)
return str
}
func (captcha *AliyunCaptchaProvider) VerifyCaptcha(token, clientSecret string) (bool, error) { config.Endpoint = tea.String(AliyunCaptchaVerifyUrl)
pathData, err := url.ParseQuery(token) 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 { if err != nil {
return false, err return false, err
} }
pathData["Action"] = []string{"AuthenticateSig"} request := VerifyCaptchaRequest{CaptchaVerifyParam: tea.String(token), SceneId: tea.String(clientId2)}
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"}
var keys []string err = teaUtil.ValidateModel(&request)
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)))
if err != nil { if err != nil {
return false, err return false, err
} }
defer resp.Body.Close() runtime := &teaUtil.RuntimeOptions{}
body, err := io.ReadAll(resp.Body)
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 { if err != nil {
return false, err return false, err
} }
return handleCaptchaResponse(body) err = tea.Convert(resBody, &res)
}
func handleCaptchaResponse(body []byte) (bool, error) {
captchaResp := &captchaSuccessResponse{}
err := json.Unmarshal(body, captchaResp)
if err != nil { if err != nil {
captchaFailResp := &captchaFailResponse{} return false, err
err = json.Unmarshal(body, captchaFailResp)
if err != nil {
return false, err
}
return false, errors.New(captchaFailResp.Message)
} }
return true, nil if res.Body.Result.VerifyResult != nil && *res.Body.Result.VerifyResult {
return true, nil
}
return false, nil
} }

View File

@ -23,6 +23,6 @@ func NewDefaultCaptchaProvider() *DefaultCaptchaProvider {
return captcha 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 return object.VerifyCaptcha(clientSecret, token), nil
} }

View File

@ -35,7 +35,7 @@ func NewGEETESTCaptchaProvider() *GEETESTCaptchaProvider {
return captcha 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) pathData, err := url.ParseQuery(token)
if err != nil { if err != nil {
return false, err return false, err

View File

@ -32,7 +32,7 @@ func NewHCaptchaProvider() *HCaptchaProvider {
return captcha 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{ reqData := url.Values{
"secret": {clientSecret}, "secret": {clientSecret},
"response": {token}, "response": {token},

View File

@ -17,7 +17,7 @@ package captcha
import "fmt" import "fmt"
type CaptchaProvider interface { type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error) VerifyCaptcha(token, clientId, clientSecret, clientId2 string) (bool, error)
} }
func GetCaptchaProvider(captchaType string) CaptchaProvider { func GetCaptchaProvider(captchaType string) CaptchaProvider {
@ -43,11 +43,11 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
return nil return nil
} }
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) { func VerifyCaptchaByCaptchaType(captchaType, token, clientId, clientSecret, clientId2 string) (bool, error) {
provider := GetCaptchaProvider(captchaType) provider := GetCaptchaProvider(captchaType)
if provider == nil { if provider == nil {
return false, fmt.Errorf("invalid captcha provider: %s", captchaType) return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
} }
return provider.VerifyCaptcha(token, clientSecret) return provider.VerifyCaptcha(token, clientId, clientSecret, clientId2)
} }

View File

@ -32,7 +32,7 @@ func NewReCaptchaProvider() *ReCaptchaProvider {
return captcha 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{ reqData := url.Values{
"secret": {clientSecret}, "secret": {clientSecret},
"response": {token}, "response": {token},

View File

@ -32,7 +32,7 @@ func NewCloudflareTurnstileProvider() *CloudflareTurnstileProvider {
return captcha 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{ reqData := url.Values{
"secret": {clientSecret}, "secret": {clientSecret},
"response": {token}, "response": {token},

View File

@ -571,7 +571,7 @@ func (c *ApiController) Login() {
} }
var isHuman bool 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 { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -160,7 +160,7 @@ func (c *ApiController) SendVerificationCode() {
if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil { if captchaProvider := captcha.GetCaptchaProvider(vform.CaptchaType); captchaProvider == nil {
c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType) c.ResponseError(c.T("general:don't support captchaProvider: ") + vform.CaptchaType)
return 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()) c.ResponseError(err.Error())
return return
} else if !isHuman { } else if !isHuman {
@ -349,7 +349,7 @@ func (c *ApiController) VerifyCaptcha() {
return return
} }
isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, vform.ClientSecret) isValid, err := provider.VerifyCaptcha(vform.CaptchaToken, captchaProvider.ClientId, vform.ClientSecret, captchaProvider.ClientId2)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

2
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
github.com/alibabacloud-go/facebody-20191230/v5 v5.1.2 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 v1.3.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/aws/aws-sdk-go v1.45.5 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/darabonba-number v1.0.4 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // 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/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect

View File

@ -371,11 +371,6 @@ class ProviderEditPage extends React.Component {
{id: "Third-party", name: i18next.t("provider:Third-party")}, {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 { } else {
return []; return [];
} }
@ -674,7 +669,7 @@ class ProviderEditPage extends React.Component {
</Col> </Col>
</Row> </Row>
{ {
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 : (
<React.Fragment> <React.Fragment>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={2}> <Col style={{marginTop: "5px"}} span={2}>

View File

@ -13,6 +13,8 @@
// limitations under the License. // limitations under the License.
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {Button} from "antd";
import i18next from "i18next";
export const CaptchaWidget = (props) => { export const CaptchaWidget = (props) => {
const {captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2, onChange} = props; const {captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2, onChange} = props;
@ -85,23 +87,34 @@ export const CaptchaWidget = (props) => {
break; break;
} }
case "Aliyun Captcha": { case "Aliyun Captcha": {
window.AliyunCaptchaConfig = {
region: "cn",
prefix: clientSecret2,
};
const AWSCTimer = setInterval(() => { const AWSCTimer = setInterval(() => {
if (!window.AWSC) { if (!window.initAliyunCaptcha) {
loadScript("https://g.alicdn.com/AWSC/AWSC/awsc.js"); loadScript("https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js");
} }
if (window.AWSC) { if (window.initAliyunCaptcha) {
if (clientSecret2 && clientSecret2 !== "***") { if (clientSecret2 && clientSecret2 !== "***") {
window.AWSC.use(subType, function(state, module) { window.initAliyunCaptcha({
module.init({ SceneId: clientId2,
appkey: clientSecret2, mode: "embed",
scene: clientId2, element: "#captcha",
renderTo: "captcha", button: "#captcha-button",
success: function(data) { captchaVerifyCallback: (data) => {
onChange(`SessionId=${data.sessionId}&AccessKeyId=${siteKey}&Scene=${clientId2}&AppKey=${clientSecret2}&Token=${data.token}&Sig=${data.sig}&RemoteIp=192.168.0.1`); onChange(data.toString());
}, },
}); slideStyle: {
width: 320,
height: 40,
},
language: "cn",
immediate: true,
}); });
} }
clearInterval(AWSCTimer); clearInterval(AWSCTimer);
} }
@ -154,5 +167,9 @@ export const CaptchaWidget = (props) => {
} }
}, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]); }, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]);
return <div id="captcha" />; return <div id="captcha">
{
captchaType === "Aliyun Captcha" && window.initAliyunCaptcha ? <Button id="captcha-button">{i18next.t("general:Verifications")}</Button> : null
}
</div>;
}; };

View File

@ -44,6 +44,12 @@ export const CaptchaModal = (props) => {
} }
}, [visible]); }, [visible]);
useEffect(() => {
if (captchaToken !== "" && captchaType !== "Default") {
handleOk();
}
}, [captchaToken]);
const handleOk = () => { const handleOk = () => {
onOk?.(captchaType, captchaToken, clientSecret); onOk?.(captchaType, captchaToken, clientSecret);
}; };
@ -138,19 +144,18 @@ export const CaptchaModal = (props) => {
if (!regex.test(captchaToken)) { if (!regex.test(captchaToken)) {
isOkDisabled = true; isOkDisabled = true;
} }
} else if (captchaToken === "") { return [
isOkDisabled = true; null,
<Button key="ok" disabled={isOkDisabled} type="primary" onClick={handleOk}>{i18next.t("general:OK")}</Button>,
];
} }
return [ return null;
<Button key="cancel" onClick={handleCancel}>{i18next.t("general:Cancel")}</Button>,
<Button key="ok" disabled={isOkDisabled} type="primary" onClick={handleOk}>{i18next.t("general:OK")}</Button>,
];
}; };
return ( return (
<Modal <Modal
closable={false} closable={true}
maskClosable={false} maskClosable={false}
destroyOnClose={true} destroyOnClose={true}
title={i18next.t("general:Captcha")} title={i18next.t("general:Captcha")}
@ -160,6 +165,7 @@ export const CaptchaModal = (props) => {
width={350} width={350}
footer={renderFooter()} footer={renderFooter()}
onCancel={handleCancel} onCancel={handleCancel}
afterClose={handleCancel}
onOk={handleOk} onOk={handleOk}
> >
<div style={{marginTop: "20px", marginBottom: "50px"}}> <div style={{marginTop: "20px", marginBottom: "50px"}}>