How to implement Recaptcha on keycloak login page

无人久伴 提交于 2021-02-07 04:16:41

问题


I want to implement recaptcha in keycloak login page like registration page. I extended UsernamePasswordForm class with desired factory class. I even implenmented action required classes as well. but still i can not see that in provider tab to add in login. i modified existing login.ftl also but no luck.

below is what i tried.

My Authenticator class:

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() {

    }
}

My Factory Class:

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;
    }

}

Is there anything that i am missing ? can somebody help me to get recaptcha in login page. Has anybody done this before ? can someone share sample code so that i can see and try more.

Thanks in advance.


回答1:


Based On @ghinea-alex response, we made a working keycloak jboss module in this Github Repository.

We made a maven module which is also a JBoss Module.

first extended UsernamePasswordForm in RecaptchaUsernamePasswordForm and also extended UsernamePasswordFormFatory in RecpatchaUsernamePasswordFormFactory.

RecaptchaUsernamePasswordForm:

<!-- 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;
    }    

}

RecaptchaUsernamePasswordFormFactory:

<!-- 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;
    }

}

There must be a META-INF in which there must be a service\org.keycloak.authentication.AuthenticatorFactory . which it's content is:

#
# 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

and also all standalone deployable jboss modules must have a jboss-deployment-structure.xml, which describes this modules dependencies:

<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>

and in your login.ftl in your theme you shoud add this inside <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>

And finally you should enable external origin https://google.com like the way in keycloaks' Recaptcha Documentation mentioned.

how to use

in this github repo we've created an easy to use maven module and Usage manual.

Just clone the repo. you should have java and maven installed. for building you need to run mvn clean install. it will produce the jar target/recaptcha-login.jar. for making it accessible in Keycloak you should copy this jar into keycloaks standalone/deployment/ directory. just it. if you're using it in docker environment you should mount it in /opt/jboss/keycloak/standalone/deployment/recaptcha-login.jar. for example in my docker compose file:

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

and in your theme file you should add this piece of code in your login.ftl template file:

<#if recaptchaRequired??>
    <div class="form-group">
        <div class="${properties.kcInputWrapperClass!}">
            <div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}">            
            </div>
        </div>
    </div>
</#if>

you should past it inside your login <form></form> in your login template (login.ftl)

And finally you should enable external origin https://google.com like the way in keycloaks' Recaptcha Documentation mentioned.

for enabling it IN GUI do these: Go To authentication

then create a Flow For yourself in this case mine is BrowserWithRecaptcha, it should be like keycloaks' default Browser except that it has Recaptcha Username Password Form instead of Username Password Form:

Then Config your Recaptacha Uusername Password Form according your google recaptcha keys in:

and then Bind your Browser Flow to BrowserWithRecaptcha in the next tab:

and it's also Obligatory to allow google.com to access in Realm Settings>Security Defences:




回答2:


There are a couple of things wrong with your implementation. If you intend to use the Browser Login Feature is better if you just create a new flow for Recaptcha. This Flow will use the UsernamePasswordForm and UsernamePasswordFormFactory, so you need to extend those 2 classes.

Besides extending those 2 classes, you also have to call the constructors from the UsernamePasswordForm/Factory to the new implementation. Basically, this solution gives you Username + Password + Recaptcha.

In the RecaptchaFormFactory you don't need the buildPage, that's from the registration captcha, neither the validateRecaptcha and validate (which you will use in action method and authenticate from RecaptchaForm).

The only things you need in the Factory are the config and the exact same methods from UsernamePasswordFormFactory.

In the RecaptchaForm class, you will have the action method, the authenticate method both with Override and the validateRecaptcha method.

The first method called is the authentication method with this structure:

@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);
}

The validateRecaptcha will have the same structure as the one from RegistrationCaptcha and the action method just needs to change the getEvent to AUTH_METHOD from Registration.

@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");
    }
}



回答3:


Here is another approach, may be not elegant as suggested by other great developer however, it may be helpful for those who are looking for theme based solution.

Important: You need to enable google re-captcha for the keyCloak registration page first.

  • Goto theme and add below line of code into themes --> base --> login.ftl after line number 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>

  • Goto theme and add below line of code into themes --> base --> theme.properties
recaptchaSiteKey=YourGoogleSiteKeyWhichYouShouldGetFromGoogleSite

Refresh your page and with clearing cache, you will get working captcha.

It's not very strong, as its just disable login button until you/user get success call back from google re-captcha service.



来源:https://stackoverflow.com/questions/46974448/how-to-implement-recaptcha-on-keycloak-login-page

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!