feat: allow captcha to be enabled when logging in (#1211)

* Fix bug in GetAcceptLanguage()

* feat: allow captcha to be enabled when logging in

* feat: when the login password is wrong, enable captcha

* feat: Restrict captcha from frontend

* fix: modify CaptchaModal component

* fix: modify the words of i18n

* Update data.json

Co-authored-by: Gucheng Wang <nomeguy@qq.com>
Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
wenxuan70
2022-10-28 13:38:14 +08:00
committed by GitHub
parent 7e5952c804
commit 25d56ee8d5
19 changed files with 373 additions and 114 deletions

View File

@ -14,6 +14,8 @@
package captcha package captcha
import "fmt"
type CaptchaProvider interface { type CaptchaProvider interface {
VerifyCaptcha(token, clientSecret string) (bool, error) VerifyCaptcha(token, clientSecret string) (bool, error)
} }
@ -32,3 +34,12 @@ func GetCaptchaProvider(captchaType string) CaptchaProvider {
} }
return nil return nil
} }
func VerifyCaptchaByCaptchaType(captchaType, token, clientSecret string) (bool, error) {
provider := GetCaptchaProvider(captchaType)
if provider == nil {
return false, fmt.Errorf("invalid captcha provider: %s", captchaType)
}
return provider.VerifyCaptcha(token, clientSecret)
}

View File

@ -64,6 +64,10 @@ type RequestForm struct {
RelayState string `json:"relayState"` RelayState string `json:"relayState"`
SamlRequest string `json:"samlRequest"` SamlRequest string `json:"samlRequest"`
SamlResponse string `json:"samlResponse"` SamlResponse string `json:"samlResponse"`
CaptchaType string `json:"captchaType"`
CaptchaToken string `json:"captchaToken"`
ClientSecret string `json:"clientSecret"`
} }
type Response struct { type Response struct {

View File

@ -23,6 +23,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/casdoor/casdoor/captcha"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/idp" "github.com/casdoor/casdoor/idp"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
@ -251,6 +253,25 @@ func (c *ApiController) Login() {
return return
} }
} else { } else {
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
if application == nil {
c.ResponseError(fmt.Sprintf("The application: %s does not exist", form.Application))
return
}
if object.CheckToEnableCaptcha(application) {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
return
}
if !isHuman {
c.ResponseError("Turing test failed.")
return
}
}
password := form.Password password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage()) user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage())
} }

View File

@ -340,3 +340,20 @@ func CheckUsername(username string, lang string) string {
return "" return ""
} }
func CheckToEnableCaptcha(application *Application) bool {
if len(application.Providers) == 0 {
return false
}
for _, providerItem := range application.Providers {
if providerItem.Provider == nil {
continue
}
if providerItem.Provider.Category == "Captcha" && providerItem.Provider.Type == "Default" {
return providerItem.Rule == "Always"
}
}
return false
}

View File

@ -143,7 +143,7 @@ func initBuiltInApplication() {
EnablePassword: true, EnablePassword: true,
EnableSignUp: true, EnableSignUp: true,
Providers: []*ProviderItem{ Providers: []*ProviderItem{
{Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Provider: nil}, {Name: "provider_captcha_default", CanSignUp: false, CanSignIn: false, CanUnlink: false, Prompted: false, AlertType: "None", Rule: "None", Provider: nil},
}, },
SignupItems: []*SignupItem{ SignupItems: []*SignupItem{
{Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"}, {Name: "ID", Visible: false, Required: true, Prompted: false, Rule: "Random"},

View File

@ -21,6 +21,7 @@ type ProviderItem struct {
CanUnlink bool `json:"canUnlink"` CanUnlink bool `json:"canUnlink"`
Prompted bool `json:"prompted"` Prompted bool `json:"prompted"`
AlertType string `json:"alertType"` AlertType string `json:"alertType"`
Rule string `json:"rule"`
Provider *Provider `json:"provider"` Provider *Provider `json:"provider"`
} }

View File

@ -39,7 +39,7 @@ class ProviderTable extends React.Component {
} }
addRow(table) { addRow(table) {
const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None"}; const row = {name: Setting.getNewRowNameForTable(table, "Please select a provider"), canSignUp: true, canSignIn: true, canUnlink: true, alertType: "None", rule: "None"};
if (table === undefined) { if (table === undefined) {
table = []; table = [];
} }
@ -193,6 +193,28 @@ class ProviderTable extends React.Component {
// ) // )
// } // }
// }, // },
{
title: i18next.t("application:Rule"),
dataIndex: "rule",
key: "rule",
width: "100px",
render: (text, record, index) => {
if (record.provider?.category !== "Captcha") {
return null;
}
return (
<Select virtual={false} style={{width: "100%"}}
value={text}
defaultValue="None"
onChange={value => {
this.updateField(table, index, "rule", value);
}} >
<Option key="None" value="None">{i18next.t("application:None")}</Option>
<Option key="Always" value="Always">{i18next.t("application:Always")}</Option>
</Select>
);
},
},
{ {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
key: "action", key: "action",

View File

@ -164,7 +164,7 @@ class SignupTable extends React.Component {
}, },
}, },
{ {
title: i18next.t("application:rule"), title: i18next.t("application:Rule"),
dataIndex: "rule", dataIndex: "rule",
key: "rule", key: "rule",
width: "155px", width: "155px",

View File

@ -29,6 +29,7 @@ import CustomGithubCorner from "../CustomGithubCorner";
import {CountDownInput} from "../common/CountDownInput"; import {CountDownInput} from "../common/CountDownInput";
import SelectLanguageBox from "../SelectLanguageBox"; import SelectLanguageBox from "../SelectLanguageBox";
import {withTranslation} from "react-i18next"; import {withTranslation} from "react-i18next";
import {CaptchaModal} from "../common/CaptchaModal";
const {TabPane} = Tabs; const {TabPane} = Tabs;
@ -48,6 +49,9 @@ class LoginPage extends React.Component {
validEmail: false, validEmail: false,
validPhone: false, validPhone: false,
loginMethod: "password", loginMethod: "password",
enableCaptchaModal: false,
openCaptchaModal: false,
verifyCaptcha: undefined,
}; };
if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) { if (this.state.type === "cas" && props.match?.params.casApplicationName !== undefined) {
@ -68,6 +72,18 @@ class LoginPage extends React.Component {
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.application && !prevState.application) {
const defaultCaptchaProviderItems = this.getDefaultCaptchaProviderItems(this.state.application);
if (!defaultCaptchaProviderItems) {
return;
}
this.setState({enableCaptchaModal: defaultCaptchaProviderItems.some(providerItem => providerItem.rule === "Always")});
}
}
getApplicationLogin() { getApplicationLogin() {
const oAuthParams = Util.getOAuthGetParameters(); const oAuthParams = Util.getOAuthGetParameters();
AuthBackend.getApplicationLogin(oAuthParams) AuthBackend.getApplicationLogin(oAuthParams)
@ -225,6 +241,23 @@ class LoginPage extends React.Component {
return; return;
} }
if (this.state.loginMethod === "password" && this.state.enableCaptchaModal) {
this.setState({
openCaptchaModal: true,
verifyCaptcha: (captchaType, captchaToken, secret) => {
values["captchaType"] = captchaType;
values["captchaToken"] = captchaToken;
values["clientSecret"] = secret;
this.login(values);
},
});
} else {
this.login(values);
}
}
login(values) {
// here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server // here we are supposed to determine whether Casdoor is working as an OAuth server or CAS server
if (this.state.type === "cas") { if (this.state.type === "cas") {
// CAS // CAS
@ -239,6 +272,8 @@ class LoginPage extends React.Component {
} }
Util.showMessage("success", msg); Util.showMessage("success", msg);
this.setState({openCaptchaModal: false});
if (casParams.service !== "") { if (casParams.service !== "") {
const st = res.data; const st = res.data;
const newUrl = new URL(casParams.service); const newUrl = new URL(casParams.service);
@ -246,6 +281,7 @@ class LoginPage extends React.Component {
window.location.href = newUrl.toString(); window.location.href = newUrl.toString();
} }
} else { } else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`); Util.showMessage("error", `Failed to log in: ${res.msg}`);
} }
}); });
@ -258,6 +294,7 @@ class LoginPage extends React.Component {
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const responseType = values["type"]; const responseType = values["type"];
if (responseType === "login") { if (responseType === "login") {
Util.showMessage("success", "Logged in successfully"); Util.showMessage("success", "Logged in successfully");
@ -275,6 +312,7 @@ class LoginPage extends React.Component {
Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`); Setting.goToLink(`${redirectUri}?SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
} }
} else { } else {
this.setState({openCaptchaModal: false});
Util.showMessage("error", `Failed to log in: ${res.msg}`); Util.showMessage("error", `Failed to log in: ${res.msg}`);
} }
}); });
@ -418,6 +456,9 @@ class LoginPage extends React.Component {
i18next.t("login:Sign In") i18next.t("login:Sign In")
} }
</Button> </Button>
{
this.renderCaptchaModal(application)
}
{ {
this.renderFooter(application) this.renderFooter(application)
} }
@ -460,6 +501,46 @@ class LoginPage extends React.Component {
} }
} }
getDefaultCaptchaProviderItems(application) {
const providers = application?.providers;
if (providers === undefined || providers === null) {
return null;
}
return providers.filter(providerItem => {
if (providerItem.provider === undefined || providerItem.provider === null) {
return false;
}
return providerItem.provider.category === "Captcha" && providerItem.provider.type === "Default";
});
}
renderCaptchaModal(application) {
if (!this.state.enableCaptchaModal) {
return null;
}
const provider = this.getDefaultCaptchaProviderItems(application)
.filter(providerItem => providerItem.rule === "Always")
.map(providerItem => providerItem.provider)[0];
return <CaptchaModal
owner={provider.owner}
name={provider.name}
captchaType={provider.type}
subType={provider.subType}
clientId={provider.clientId}
clientId2={provider.clientId2}
clientSecret={provider.clientSecret}
clientSecret2={provider.clientSecret2}
open={this.state.openCaptchaModal}
onOk={(captchaType, captchaToken, secret) => this.state.verifyCaptcha?.(captchaType, captchaToken, secret)}
canCancel={false}
/>;
}
renderFooter(application) { renderFooter(application) {
if (this.state.mode === "signup") { if (this.state.mode === "signup") {
return ( return (

View File

@ -0,0 +1,159 @@
// 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.
import {Button, Col, Input, Modal, Row} from "antd";
import i18next from "i18next";
import React, {useEffect} from "react";
import * as UserBackend from "../backend/UserBackend";
import {CaptchaWidget} from "./CaptchaWidget";
import {SafetyOutlined} from "@ant-design/icons";
export const CaptchaModal = ({
owner,
name,
captchaType,
subType,
clientId,
clientId2,
clientSecret,
clientSecret2,
open,
onOk,
onCancel,
canCancel,
}) => {
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);
useEffect(() => {
setVisible(() => {
if (open) {
getCaptchaFromBackend();
} else {
cleanUp();
}
return open;
});
}, [open]);
const handleOk = () => {
onOk?.(captchaType, captchaToken, secret);
};
const handleCancel = () => {
onCancel?.();
};
const cleanUp = () => {
setCaptchaToken("");
};
const getCaptchaFromBackend = () => {
UserBackend.getCaptcha(owner, name, true).then((res) => {
if (captchaType === "Default") {
setSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
} else {
setSecret(res.clientSecret);
setSecret2(res.clientSecret2);
}
});
};
const renderDefaultCaptcha = () => {
return (
<Col>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: 10,
}}
/>
<Row>
<Input
autoFocus
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</Col>
);
};
const onSubmit = (token) => {
setCaptchaToken(token);
};
const renderCheck = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={secret}
onChange={onSubmit}
clientId2={clientId2}
clientSecret2={secret2}
/>
</Row>
</Col>
);
}
};
const renderFooter = () => {
if (canCancel) {
return [
<Button key="cancel" onClick={handleCancel}>{i18next.t("user:Cancel")}</Button>,
<Button key="ok" type="primary" onClick={handleOk}>{i18next.t("user:OK")}</Button>,
];
} else {
return [
<Button key="ok" type="primary" onClick={handleOk}>{i18next.t("user:OK")}</Button>,
];
}
};
return (
<React.Fragment>
<Modal
closable={false}
maskClosable={false}
destroyOnClose={true}
title={i18next.t("general:Captcha")}
visible={visible}
width={348}
footer={renderFooter()}
>
{renderCheck()}
</Modal>
</React.Fragment>
);
};

View File

@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Button, Col, Input, Modal, Row} from "antd"; import {Button} from "antd";
import React from "react"; import React from "react";
import i18next from "i18next"; import i18next from "i18next";
import * as UserBackend from "../backend/UserBackend"; import {CaptchaModal} from "./CaptchaModal";
import * as ProviderBackend from "../backend/ProviderBackend"; import * as ProviderBackend from "../backend/ProviderBackend";
import {SafetyOutlined} from "@ant-design/icons"; import * as UserBackend from "../backend/UserBackend";
import {CaptchaWidget} from "./CaptchaWidget";
export const CaptchaPreview = ({ export const CaptchaPreview = ({
provider, provider,
@ -33,37 +32,9 @@ export const CaptchaPreview = ({
clientId2, clientId2,
clientSecret2, clientSecret2,
}) => { }) => {
const [visible, setVisible] = React.useState(false); const [open, setOpen] = 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(() => {
setCaptchaToken("");
setVisible(false);
});
};
const handleCancel = () => {
setVisible(false);
};
const getCaptchaFromBackend = () => {
UserBackend.getCaptcha(owner, name, true).then((res) => {
if (captchaType === "Default") {
setSecret(res.captchaId);
setCaptchaImg(res.captchaImage);
} else {
setSecret(res.clientSecret);
setSecret2(res.clientSecret2);
}
});
};
const clickPreview = () => { const clickPreview = () => {
setVisible(true);
provider.name = name; provider.name = name;
provider.clientId = clientId; provider.clientId = clientId;
provider.type = captchaType; provider.type = captchaType;
@ -71,64 +42,10 @@ export const CaptchaPreview = ({
if (clientSecret !== "***") { if (clientSecret !== "***") {
provider.clientSecret = clientSecret; provider.clientSecret = clientSecret;
ProviderBackend.updateProvider(owner, providerName, provider).then(() => { ProviderBackend.updateProvider(owner, providerName, provider).then(() => {
getCaptchaFromBackend(); setOpen(true);
}); });
} else { } else {
getCaptchaFromBackend(); setOpen(true);
}
};
const renderDefaultCaptcha = () => {
return (
<Col>
<Row
style={{
backgroundImage: `url('data:image/png;base64,${captchaImg}')`,
backgroundRepeat: "no-repeat",
height: "80px",
width: "200px",
borderRadius: "5px",
border: "1px solid #ccc",
marginBottom: 10,
}}
/>
<Row>
<Input
autoFocus
value={captchaToken}
prefix={<SafetyOutlined />}
placeholder={i18next.t("general:Captcha")}
onPressEnter={handleOk}
onChange={(e) => setCaptchaToken(e.target.value)}
/>
</Row>
</Col>
);
};
const onSubmit = (token) => {
setCaptchaToken(token);
};
const renderCheck = () => {
if (captchaType === "Default") {
return renderDefaultCaptcha();
} else {
return (
<Col>
<Row>
<CaptchaWidget
captchaType={captchaType}
subType={subType}
siteKey={clientId}
clientSecret={secret}
onChange={onSubmit}
clientId2={clientId2}
clientSecret2={secret2}
/>
</Row>
</Col>
);
} }
}; };
@ -146,6 +63,16 @@ export const CaptchaPreview = ({
return false; return false;
}; };
const onOk = (captchaType, captchaToken, secret) => {
UserBackend.verifyCaptcha(captchaType, captchaToken, secret).then(() => {
setOpen(false);
});
};
const onCancel = () => {
setOpen(false);
};
return ( return (
<React.Fragment> <React.Fragment>
<Button <Button
@ -156,20 +83,20 @@ export const CaptchaPreview = ({
> >
{i18next.t("general:Preview")} {i18next.t("general:Preview")}
</Button> </Button>
<Modal <CaptchaModal
closable={false} owner={owner}
maskClosable={false} name={name}
destroyOnClose={true} captchaType={captchaType}
title={i18next.t("general:Captcha")} subType={subType}
visible={visible} clientId={clientId}
okText={i18next.t("user:OK")} clientId2={clientId2}
cancelText={i18next.t("user:Cancel")} clientSecret={clientSecret}
onOk={handleOk} clientSecret2={clientSecret2}
onCancel={handleCancel} open={open}
width={348} onOk={onOk}
> onCancel={onCancel}
{renderCheck()} canCancel={true}
</Modal> />
</React.Fragment> </React.Fragment>
); );
}; };

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Token läuft ab - Tooltip", "Token expire - Tooltip": "Token läuft ab - Tooltip",
"Token format": "Token-Format", "Token format": "Token-Format",
"Token format - Tooltip": "Token-Format - Tooltip", "Token format - Tooltip": "Token-Format - Tooltip",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Bitgröße", "Bit size": "Bitgröße",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Token expire - Tooltip", "Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip", "Token format - Tooltip": "Token format - Tooltip",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Bit size", "Bit size": "Bit size",

View File

@ -45,7 +45,9 @@
"Token expire - Tooltip": "Expiración del Token - Tooltip", "Token expire - Tooltip": "Expiración del Token - Tooltip",
"Token format": "Formato del Token", "Token format": "Formato del Token",
"Token format - Tooltip": "Formato del Token - Tooltip", "Token format - Tooltip": "Formato del Token - Tooltip",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Tamaño del Bit", "Bit size": "Tamaño del Bit",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Expiration du jeton - Info-bulle", "Token expire - Tooltip": "Expiration du jeton - Info-bulle",
"Token format": "Format du jeton", "Token format": "Format du jeton",
"Token format - Tooltip": "Format du jeton - infobulle", "Token format - Tooltip": "Format du jeton - infobulle",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Taille du bit", "Bit size": "Taille du bit",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "トークンの有効期限 - ツールチップ", "Token expire - Tooltip": "トークンの有効期限 - ツールチップ",
"Token format": "トークンのフォーマット", "Token format": "トークンのフォーマット",
"Token format - Tooltip": "トークンフォーマット - ツールチップ", "Token format - Tooltip": "トークンフォーマット - ツールチップ",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "ビットサイズ", "Bit size": "ビットサイズ",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Token expire - Tooltip", "Token expire - Tooltip": "Token expire - Tooltip",
"Token format": "Token format", "Token format": "Token format",
"Token format - Tooltip": "Token format - Tooltip", "Token format - Tooltip": "Token format - Tooltip",
"rule": "rule" "Rule": "Rule",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Bit size", "Bit size": "Bit size",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Истек токен - Подсказка", "Token expire - Tooltip": "Истек токен - Подсказка",
"Token format": "Формат токена", "Token format": "Формат токена",
"Token format - Tooltip": "Формат токена - Подсказка", "Token format - Tooltip": "Формат токена - Подсказка",
"rule": "правило" "Rule": "правило",
"None": "None",
"Always": "Always"
}, },
"cert": { "cert": {
"Bit size": "Размер бита", "Bit size": "Размер бита",

View File

@ -68,7 +68,9 @@
"Token expire - Tooltip": "Access Token过期时间", "Token expire - Tooltip": "Access Token过期时间",
"Token format": "Access Token格式", "Token format": "Access Token格式",
"Token format - Tooltip": "Access Token格式", "Token format - Tooltip": "Access Token格式",
"rule": "规则" "Rule": "规则",
"None": "关闭",
"Always": "始终开启"
}, },
"cert": { "cert": {
"Bit size": "位大小", "Bit size": "位大小",