diff --git a/captcha/aliyun.go b/captcha/aliyun.go new file mode 100644 index 000000000..59a8a33cb --- /dev/null +++ b/captcha/aliyun.go @@ -0,0 +1,105 @@ +// Copyright 2022 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package captcha + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/casdoor/casdoor/util" +) + +const AliyunCaptchaVerifyUrl = "http://afs.aliyuncs.com" + +type AliyunCaptchaProvider struct { +} + +func NewAliyunCaptchaProvider() *AliyunCaptchaProvider { + captcha := &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, clientSecret string) (bool, error) { + pathData, err := url.ParseQuery(token) + 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"} + + 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))) + if err != nil { + return false, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + + type captchaResponse struct { + Code int `json:"Code"` + Msg string `json:"Msg"` + } + captchaResp := &captchaResponse{} + + err = json.Unmarshal(body, captchaResp) + if err != nil { + return false, err + } + + if captchaResp.Code != 100 { + return false, errors.New(captchaResp.Msg) + } + + return true, nil +} diff --git a/captcha/provider.go b/captcha/provider.go index 769522632..01a54e20a 100644 --- a/captcha/provider.go +++ b/captcha/provider.go @@ -25,6 +25,8 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider { return NewReCaptchaProvider() } else if captchaType == "hCaptcha" { return NewHCaptchaProvider() + } else if captchaType == "Aliyun Captcha" { + return NewAliyunCaptchaProvider() } return nil } diff --git a/controllers/account.go b/controllers/account.go index a8ece7155..271f6c53e 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -76,13 +76,16 @@ type Response struct { } type Captcha struct { - Type string `json:"type"` - AppKey string `json:"appKey"` - Scene string `json:"scene"` - CaptchaId string `json:"captchaId"` - CaptchaImage []byte `json:"captchaImage"` - ClientId string `json:"clientId"` - ClientSecret string `json:"clientSecret"` + Type string `json:"type"` + AppKey string `json:"appKey"` + Scene string `json:"scene"` + CaptchaId string `json:"captchaId"` + CaptchaImage []byte `json:"captchaImage"` + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + ClientId2 string `json:"clientId2"` + ClientSecret2 string `json:"clientSecret2"` + SubType string `json:"subType"` } // Signup @@ -313,7 +316,14 @@ func (c *ApiController) GetCaptcha() { c.ResponseOk(Captcha{Type: captchaProvider.Type, CaptchaId: id, CaptchaImage: img}) return } else if captchaProvider.Type != "" { - c.ResponseOk(Captcha{Type: captchaProvider.Type, ClientId: captchaProvider.ClientId, ClientSecret: captchaProvider.ClientSecret}) + c.ResponseOk(Captcha{ + Type: captchaProvider.Type, + SubType: captchaProvider.SubType, + ClientId: captchaProvider.ClientId, + ClientSecret: captchaProvider.ClientSecret, + ClientId2: captchaProvider.ClientId2, + ClientSecret2: captchaProvider.ClientSecret2, + }) return } } diff --git a/util/crypto.go b/util/crypto.go new file mode 100644 index 000000000..658adb93d --- /dev/null +++ b/util/crypto.go @@ -0,0 +1,30 @@ +// Copyright 2022 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" +) + +func GetHmacSha1(keyStr, value string) string { + key := []byte(keyStr) + mac := hmac.New(sha1.New, key) + mac.Write([]byte(value)) + res := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + + return res +} diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 1ccec8959..0bbf7c485 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -81,7 +81,11 @@ class ProviderEditPage extends React.Component { return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip")); } case "Captcha": - return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip")); + if (this.state.provider.type === "Aliyun Captcha") { + return Setting.getLabel(i18next.t("provider:Access key"), i18next.t("provider:Access key - Tooltip")); + } else { + return Setting.getLabel(i18next.t("provider:Site key"), i18next.t("provider:Site key - Tooltip")); + } default: return Setting.getLabel(i18next.t("provider:Client ID"), i18next.t("provider:Client ID - Tooltip")); } @@ -100,7 +104,11 @@ class ProviderEditPage extends React.Component { return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); } case "Captcha": - return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); + if (this.state.provider.type === "Aliyun Captcha") { + return Setting.getLabel(i18next.t("provider:Secret access key"), i18next.t("provider:SecretAccessKey - Tooltip")); + } else { + return Setting.getLabel(i18next.t("provider:Secret key"), i18next.t("provider:Secret key - Tooltip")); + } default: return Setting.getLabel(i18next.t("provider:Client secret"), i18next.t("provider:Client secret - Tooltip")); } @@ -242,7 +250,7 @@ class ProviderEditPage extends React.Component { { - this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" ? null : ( + this.state.provider.type !== "WeCom" && this.state.provider.type !== "Infoflow" && this.state.provider.type !== "Aliyun Captcha" ? null : ( @@ -378,11 +386,13 @@ class ProviderEditPage extends React.Component { ) } { - this.state.provider.type !== "WeChat" ? null : ( + this.state.provider.type !== "WeChat" && this.state.provider.type !== "Aliyun Captcha" ? null : ( - {Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))} + {this.state.provider.type === "Aliyun Captcha" + ? Setting.getLabel(i18next.t("provider:Scene"), i18next.t("provider:Scene - Tooltip")) + : Setting.getLabel(i18next.t("provider:Client ID 2"), i18next.t("provider:Client ID 2 - Tooltip"))} { @@ -392,7 +402,9 @@ class ProviderEditPage extends React.Component { - {Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))} + {this.state.provider.type === "Aliyun Captcha" + ? Setting.getLabel(i18next.t("provider:App key"), i18next.t("provider:App key - Tooltip")) + : Setting.getLabel(i18next.t("provider:Client secret 2"), i18next.t("provider:Client secret 2 - Tooltip"))} { @@ -686,10 +698,13 @@ class ProviderEditPage extends React.Component { providerName={this.state.providerName} clientSecret={this.state.provider.clientSecret} captchaType={this.state.provider.type} + subType={this.state.provider.subType} owner={this.state.provider.owner} clientId={this.state.provider.clientId} name={this.state.provider.name} providerUrl={this.state.provider.providerUrl} + clientId2={this.state.provider.clientId2} + clientSecret2={this.state.provider.clientSecret2} /> diff --git a/web/src/Setting.js b/web/src/Setting.js index 24ae1a101..3525ad096 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -118,6 +118,10 @@ export const OtherProviderInfo = { "hCaptcha": { logo: `${StaticBaseUrl}/img/social_hcaptcha.png`, url: "https://www.hcaptcha.com", + }, + "Aliyun Captcha": { + logo: `${StaticBaseUrl}/img/social_aliyun.png`, + url: "https://help.aliyun.com/product/28308.html", } } }; @@ -614,6 +618,7 @@ export function getProviderTypeOptions(category) { {id: 'Default', name: 'Default'}, {id: 'reCAPTCHA', name: 'reCAPTCHA'}, {id: 'hCaptcha', name: 'hCaptcha'}, + {id: 'Aliyun Captcha', name: 'Aliyun Captcha'}, ]); } else { return []; @@ -628,6 +633,11 @@ export function getProviderSubTypeOptions(type) { {id: 'Third-party', name: 'Third-party'}, ] ); + } else if (type === "Aliyun Captcha") { + return [ + {id: 'nc', name: 'Sliding Validation'}, + {id: 'ic', name: 'Intelligent Validation'}, + ]; } else { return []; } diff --git a/web/src/common/CaptchaPreview.js b/web/src/common/CaptchaPreview.js index afc94dd3e..84ec4be71 100644 --- a/web/src/common/CaptchaPreview.js +++ b/web/src/common/CaptchaPreview.js @@ -20,18 +20,27 @@ import * as ProviderBackend from "../backend/ProviderBackend"; import { SafetyOutlined } from "@ant-design/icons"; import { CaptchaWidget } from "./CaptchaWidget"; -export const CaptchaPreview = ({ provider, providerName, clientSecret, captchaType, owner, clientId, name, providerUrl }) => { +export const CaptchaPreview = ({ + provider, + providerName, + clientSecret, + captchaType, + subType, + owner, + clientId, + name, + providerUrl, + clientId2, + clientSecret2, +}) => { const [visible, setVisible] = React.useState(false); const [captchaImg, setCaptchaImg] = React.useState(""); const [captchaToken, setCaptchaToken] = React.useState(""); const [secret, setSecret] = React.useState(clientSecret); + const [secret2, setSecret2] = React.useState(clientSecret2); const handleOk = () => { - UserBackend.verifyCaptcha( - captchaType, - captchaToken, - secret - ).then(() => { + UserBackend.verifyCaptcha(captchaType, captchaToken, secret).then(() => { setCaptchaToken(""); setVisible(false); }); @@ -48,9 +57,10 @@ export const CaptchaPreview = ({ provider, providerName, clientSecret, captchaTy setCaptchaImg(res.captchaImage); } else { setSecret(res.clientSecret); + setSecret2(res.clientSecret2); } }); - } + }; const clickPreview = () => { setVisible(true); @@ -100,24 +110,50 @@ export const CaptchaPreview = ({ provider, providerName, clientSecret, captchaTy setCaptchaToken(token); }; - const renderCheck = () => { if (captchaType === "Default") { return renderDefaultCaptcha(); } else { return ( - + + + + + ); } }; + const getButtonDisabled = () => { + if (captchaType !== "Default") { + if (!clientId || !clientSecret) { + return true; + } + if (captchaType === "Aliyun Captcha") { + if (!subType || !clientId2 || !clientSecret2) { + return true; + } + } + } + return false; + }; + return ( - { +export const CaptchaWidget = ({ captchaType, subType, siteKey, clientSecret, onChange, clientId2, clientSecret2 }) => { const loadScript = (src) => { var tag = document.createElement("script"); tag.async = false; @@ -53,11 +53,34 @@ export const CaptchaWidget = ({ captchaType, siteKey, onChange }) => { } }, 300); break; + case "Aliyun Captcha": + const AWSCTimer = setInterval(() => { + if (!window.AWSC) { + loadScript("https://g.alicdn.com/AWSC/AWSC/awsc.js"); + } + + if (window.AWSC) { + 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`); + }, + }); + }); + } + clearInterval(AWSCTimer); + } + }, 300); + break; default: break; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [captchaType, siteKey]); + }, [captchaType, subType, siteKey, clientSecret, clientId2, clientSecret2]); return
; }; diff --git a/web/src/common/CountDownInput.js b/web/src/common/CountDownInput.js index 22ef207c2..ae42e87d4 100644 --- a/web/src/common/CountDownInput.js +++ b/web/src/common/CountDownInput.js @@ -14,7 +14,6 @@ import {Button, Col, Input, Modal, Row} from "antd"; import React from "react"; -import * as Setting from "../Setting"; import i18next from "i18next"; import * as UserBackend from "../backend/UserBackend"; import {SafetyOutlined} from "@ant-design/icons"; @@ -34,6 +33,9 @@ export const CountDownInput = (props) => { const [buttonLoading, setButtonLoading] = React.useState(false); const [buttonDisabled, setButtonDisabled] = React.useState(true); const [clientId, setClientId] = React.useState(""); + const [subType, setSubType] = React.useState(""); + const [clientId2, setClientId2] = React.useState(""); + const [clientSecret2, setClientSecret2] = React.useState(""); const handleCountDown = (leftTime = 60) => { let leftTimeSecond = leftTime @@ -79,13 +81,14 @@ export const CountDownInput = (props) => { setCaptchaImg(res.captchaImage); setCheckType("Default"); setVisible(true); - } else if (res.type === "reCAPTCHA" || res.type === "hCaptcha") { + } else { setCheckType(res.type); setClientId(res.clientId); setCheckId(res.clientSecret); setVisible(true); - } else { - Setting.showMessage("error", i18next.t("signup:Unknown Check Type")); + setSubType(res.subType); + setClientId2(res.clientId2); + setClientSecret2(res.clientSecret2); } }) } @@ -123,8 +126,12 @@ export const CountDownInput = (props) => { return ( ); }