[英]How to implement Recaptcha on keycloak login page
我想在 keycloak 登錄頁面(如注冊頁面)中實現 recaptcha。 我用所需的工廠類擴展了 UsernamePasswordForm 類。 我什至還實施了動作所需的課程。 但我仍然看不到在提供程序選項卡中添加登錄。 我也修改了現有的 login.ftl,但沒有運氣。
下面是我嘗試過的。
我的身份驗證器類:
public class MyLoginAuthenticator extends UsernamePasswordForm {
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.cancelLogin();
return;
}
if (!validateForm(context, formData)) {
return;
}
context.success();
}
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
return validateUserAndPassword(context, formData);
}
@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
if (loginHint != null || rememberMeUsername != null) {
if (loginHint != null) {
formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
} else {
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
formData.add("rememberMe", "on");
}
}
Response challengeResponse = challenge(context, formData);
context.challenge(challengeResponse);
}
@Override
public boolean requiresUser() {
return false;
}
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
LoginFormsProvider forms = context.form();
if (formData.size() > 0) forms.setFormData(formData);
return forms.createLogin();
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
// never called
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// never called
}
@Override
public void close() {
}
}
我的工廠類:
public class LoginAuthenticatorFactory extends UsernamePasswordFormFactory {
public static final String G_RECAPTCHA_RESPONSE = "g-recaptcha-response";
public static final String RECAPTCHA_REFERENCE_CATEGORY = "login-recaptcha";
public static final String SITE_KEY = "site.key";
public static final String SITE_SECRET = "secret";
public static final String PROVIDER_ID = "auth-username-password-form-recaptcha";
public static final MyLoginAuthenticator SINGLETON = new MyLoginAuthenticator();
@Override
public String getDisplayType() {
System.out.println("Ranveer Singh getDisplayType ");
return "Login Recaptcha";
}
@Override
public String getReferenceCategory() {
return RECAPTCHA_REFERENCE_CATEGORY;
}
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public boolean isConfigurable() {
return true;
}
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
public void buildPage(FormContext context, LoginFormsProvider form) {
System.out.println("Ranveer Singh buildPage");
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
if (captchaConfig == null || captchaConfig.getConfig() == null || captchaConfig.getConfig().get(SITE_KEY) == null || captchaConfig.getConfig().get(SITE_SECRET) == null) {
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
return;
}
String siteKey = captchaConfig.getConfig().get(SITE_KEY);
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", siteKey);
form.addScript("https://www.google.com/recaptcha/api.js");
}
public void validate(ValidationContext context) {
System.out.println("Ranveer Singh validate");
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
boolean success = false;
context.getEvent().detail(Details.REGISTER_METHOD, "form");
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha)) {
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
String secret = captchaConfig.getConfig().get(SITE_SECRET);
success = validateRecaptcha(context, success, captcha, secret);
}
if (success) {
context.success();
} else {
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
context.error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
}
}
protected boolean validateRecaptcha(ValidationContext context, boolean success, String captcha, String secret) {
System.out.println("Ranveer Singh ");
HttpClient httpClient = context.getSession().getProvider(HttpClientProvider.class).getHttpClient();
HttpPost post = new HttpPost("https://www.google.com/recaptcha/api/siteverify");
List<NameValuePair> formparams = new LinkedList<>();
formparams.add(new BasicNameValuePair("secret", secret));
formparams.add(new BasicNameValuePair("response", captcha));
formparams.add(new BasicNameValuePair("remoteip", context.getConnection().getRemoteAddr()));
try {
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
InputStream content = response.getEntity().getContent();
try {
Map json = JsonSerialization.readValue(content, Map.class);
Object val = json.get("success");
success = Boolean.TRUE.equals(val);
} finally {
content.close();
}
} catch (Exception e) {
ServicesLogger.LOGGER.recaptchaFailed(e);
}
return success;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public void close() {
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "Adds Google Recaptcha button. Recaptchas verify that the entity that is registering is a human. This can only be used on the internet and must be configured after you add it.";
}
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(SITE_KEY);
property.setLabel("Recaptcha Site Key");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Google Recaptcha Site Key");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(SITE_SECRET);
property.setLabel("Recaptcha Secret");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Google Recaptcha Secret");
configProperties.add(property);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}
有什么我想念的嗎? 有人可以幫我在登錄頁面中獲取 recaptcha。 以前有人這樣做過嗎? 有人可以分享示例代碼,以便我可以查看並嘗試更多。
提前致謝。
基於@ghinea-alex 的回復,我們在這個Github Repository中制作了一個有效的keycloak jboss module
。
我們制作了一個 Maven 模塊,它也是一個 JBoss 模塊。
首先在RecpatchaUsernamePasswordFormFactory
UsernamePasswordForm
RecaptchaUsernamePasswordForm
了UsernamePasswordFormFatory
。
Recaptcha用戶名密碼表格:
<!-- language: java -->
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.Details;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import org.keycloak.util.JsonSerialization;
public class RecaptchaUsernamePasswordForm extends UsernamePasswordForm implements Authenticator{
public static final String G_RECAPTCHA_RESPONSE = "g-recaptcha-response";
public static final String RECAPTCHA_REFERENCE_CATEGORY = "recaptcha";
public static final String SITE_KEY = "site.key";
public static final String SITE_SECRET = "secret";
private static final Logger logger = Logger.getLogger(RecaptchaUsernamePasswordFormFactory.class);
@Override
public void authenticate(AuthenticationFlowContext context) {
context.getEvent().detail(Details.AUTH_METHOD, "auth_method");
if (logger.isInfoEnabled()) {
logger.info(
"validateRecaptcha(AuthenticationFlowContext, boolean, String, String) - Before the validation");
}
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
LoginFormsProvider form = context.form();
String userLanguageTag = context.getSession().getContext().resolveLocale(context.getUser()).toLanguageTag();
if (captchaConfig == null || captchaConfig.getConfig() == null
|| captchaConfig.getConfig().get(SITE_KEY) == null
|| captchaConfig.getConfig().get(SITE_SECRET) == null) {
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
return;
}
String siteKey = captchaConfig.getConfig().get(SITE_KEY);
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", siteKey);
form.addScript("https://www.google.com/recaptcha/api.js?hl=" + userLanguageTag);
super.authenticate(context);
}
@Override
public void action(AuthenticationFlowContext context) {
if (logger.isDebugEnabled()) {
logger.debug("action(AuthenticationFlowContext) - start");
}
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
boolean success = false;
context.getEvent().detail(Details.AUTH_METHOD, "auth_method");
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha)) {
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
String secret = captchaConfig.getConfig().get(SITE_SECRET);
success = validateRecaptcha(context, success, captcha, secret);
}
if (success) {
super.action(context);
} else {
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
// context.error(Errors.INVALID_REGISTRATION);
// context.validationError(formData, errors);
// context.excludeOtherErrors();
return;
}
if (logger.isDebugEnabled()) {
logger.debug("action(AuthenticationFlowContext) - end");
}
}
protected boolean validateRecaptcha(AuthenticationFlowContext context, boolean success, String captcha, String secret) {
HttpClient httpClient = context.getSession().getProvider(HttpClientProvider.class).getHttpClient();
HttpPost post = new HttpPost("https://www.google.com/recaptcha/api/siteverify");
List<NameValuePair> formparams = new LinkedList<>();
formparams.add(new BasicNameValuePair("secret", secret));
formparams.add(new BasicNameValuePair("response", captcha));
formparams.add(new BasicNameValuePair("remoteip", context.getConnection().getRemoteAddr()));
try {
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
InputStream content = response.getEntity().getContent();
try {
Map json = JsonSerialization.readValue(content, Map.class);
Object val = json.get("success");
success = Boolean.TRUE.equals(val);
} finally {
content.close();
}
} catch (Exception e) {
ServicesLogger.LOGGER.recaptchaFailed(e);
}
return success;
}
}
驗證用戶名密碼表單工廠:
<!-- language: java -->
import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
public class RecaptchaUsernamePasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "recaptcha-u-p-form";
public static final RecaptchaUsernamePasswordForm SINGLETON = new RecaptchaUsernamePasswordForm();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleUsernamePasswordAuthenticator.SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.PASSWORD;
}
@Override
public boolean isConfigurable() {
return true;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Recaptcha Username Password Form";
}
@Override
public String getHelpText() {
return "Validates a username and password from login form + google recaptcha";
}
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(RecaptchaUsernamePasswordForm.SITE_KEY);
property.setLabel("Recaptcha Site Key");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Google Recaptcha Site Key");
CONFIG_PROPERTIES.add(property);
property = new ProviderConfigProperty();
property.setName(RecaptchaUsernamePasswordForm.SITE_SECRET);
property.setLabel("Recaptcha Secret");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Google Recaptcha Secret");
CONFIG_PROPERTIES.add(property);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}
必須有一個META-INF
,其中必須有一個service\org.keycloak.authentication.AuthenticatorFactory
。 它的內容是:
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
org.keycloak.marjaa.providers.login.recaptcha.authenticator.RecaptchaUsernamePasswordFormFactory
而且所有獨立的可部署 jboss 模塊都必須有一個jboss-deployment-structure.xml
,它描述了這個模塊的依賴關系:
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.keycloak.keycloak-server-spi" export="true"/>
<module name="org.keycloak.keycloak-server-spi-private" export="true"/>
<module name="org.keycloak.keycloak-core" export="true"/>
<module name="org.jboss.logging" export="true"/>
<module name="org.keycloak.keycloak-services" export="true"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
在你的主題中的login.ftl
中,你應該在<form></form>
中添加這個:
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}">
</div>
</div>
</div>
</#if>
最后,您應該啟用外部來源https://google.com
,就像提到的 keycloaks 的Recaptcha 文檔中的方式一樣。
在這個github 存儲庫中,我們創建了一個易於使用的 maven 模塊和使用手冊。
只需克隆回購。 你應該安裝了java
和maven
。 對於構建,您需要運行mvn clean install
。 它將生成 jar target/recaptcha-login.jar
。 為了使它可以在 Keycloak 中訪問,您應該將此 jar 復制到 keycloaks standalone/deployment/
目錄中。 只是它。 如果你在 docker 環境中使用它,你應該將它安裝在/opt/jboss/keycloak/standalone/deployment/recaptcha-login.jar
中。 例如在我的 docker compose 文件中:
keycloak:
image: jboss/keycloak:4.2.1.Final
.
.
.
volumes:
- ./realm-config:/opt/jboss/keycloak/realm-config
- ./my-theme/:/opt/jboss/keycloak/themes/my-theme/
- ./kc-recaptcha-module/target/recaptcha-login.jar:/opt/jboss/keycloak/standalone/deployments/recaptcha-login.jar
在您的主題文件中,您應該在 login.ftl 模板文件中添加這段代碼:
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}">
</div>
</div>
</div>
</#if>
您應該在登錄模板 ( login.ftl
) 中將其粘貼到登錄<form></form>
中
最后,您應該啟用外部來源https://google.com
,就像提到的 keycloaks 的Recaptcha 文檔中的方式一樣。
然后為你自己創建一個 Flow 在這種情況下我的是BrowserWithRecaptcha
,它應該像 keycloaks 的默認Browser
,除了它有Recaptcha Username Password Form
而不是Username Password Form
:
然后根據您的 google recaptcha 鍵配置您的Recaptacha Uusername Password Form
:
然后在下一個選項卡中將您的Browser Flow
綁定到BrowserWithRecaptcha
:
並且必須允許 google.com 在Realm Settings
> Security Defences
中訪問:
您的實施存在一些問題。 如果您打算使用瀏覽器登錄功能,最好為 Recaptcha 創建一個新流程。 此流程將使用 UsernamePasswordForm 和 UsernamePasswordFormFactory,因此您需要擴展這兩個類。
除了擴展這兩個類之外,您還必須從 UsernamePasswordForm/Factory 調用構造函數到新的實現。 基本上,此解決方案為您提供用戶名 + 密碼 + 驗證碼。
在 RecaptchaFormFactory 中,您不需要 buildPage,它來自注冊驗證碼,也不需要 validateRecaptcha 和 validate(您將在操作方法中使用並從 RecaptchaForm 進行身份驗證)。
您在工廠中唯一需要的是配置和來自 UsernamePasswordFormFactory 的完全相同的方法。
在 RecaptchaForm 類中,您將擁有 action 方法、帶有 Override 的 authenticate 方法和 validateRecaptcha 方法。
第一個調用的方法是具有這種結構的身份驗證方法:
@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.AUTH_METHOD, "auth_method");
if (logger.isInfoEnabled()) {
logger.info(
"validateRecaptcha(AuthenticationFlowContext, boolean, String, String) - inainte de validation");
}
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
Map<String, String> econd = captchaConfig.getConfig();
logger.debug("Am in config in context: {}", econd);
context.form().addScript("https://www.google.com/recaptcha/api.js");
context.form().setAttribute("recaptchaRequired", true);
context.form().setAttribute("recaptchaSiteKey", econd.get(SITE_KEY));
super.authenticate(context);
}
validateRecaptcha 將具有與來自 RegistrationCaptcha 的結構相同的結構,並且操作方法只需將 getEvent 更改為來自 Registration 的 AUTH_METHOD。
@Override
public void action(AuthenticationFlowContext context) {
if (logger.isDebugEnabled()) {
logger.debug("action(AuthenticationFlowContext) - start");
}
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
boolean success = false;
context.getEvent().detail(Details.AUTH_METHOD, "auth_method");
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha)) {
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
String secret = captchaConfig.getConfig().get(SITE_SECRET);
success = validateRecaptcha(context, success, captcha, secret);
}
if (success) {
super.action(context);
} else {
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
// context.error(Errors.INVALID_REGISTRATION);
// context.validationError(formData, errors);
// context.excludeOtherErrors();
return;
}
if (logger.isDebugEnabled()) {
logger.debug("action(AuthenticationFlowContext) - end");
}
}
這是另一種方法,可能不像其他偉大的開發人員所建議的那樣優雅,但是它可能對那些正在尋找基於主題的解決方案的人有所幫助。
重要提示:您需要先為 keyCloak 注冊頁面啟用 google re-captcha。
themes --> base --> login.ftl
在第 46 行之后
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<script>
function verifyCaptcha() {
document.getElementById('kc-login').disabled = false;
}
</script>
<div class="g-recaptcha" data-sitekey="${properties.recaptchaSiteKey!}" data-callback="verifyCaptcha"></div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" disabled type="submit" value="${msg("doLogIn")}"/>
</div>
themes --> base --> theme.properties
recaptchaSiteKey=YourGoogleSiteKeyWhichYouShouldGetFromGoogleSite
刷新您的頁面並清除緩存,您將獲得有效的驗證碼。
它不是很強大,因為它只是禁用登錄按鈕,直到您/用戶從 google re-captcha 服務獲得成功回調。
此代碼中的所有內容都很好,但在 .ftl 模板中您應該添加標簽名稱,否則 keycloak 不會將在 catpcha 變量中創建的值帶入 spi: public static final String G_RECAPTCHA_RESPONSE = "g-recaptcha-response" ;
像這樣的東西:
<#if recaptchaRequired??> <div class="form-group"> <div class="${properties.kcInputWrapperClass!}"> <div class="g-recaptcha" data-size="compact" name="g-recaptcha-response" data-sitekey="${recaptchaSiteKey}"> </div> </div> </div> </#if>
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.