且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何在keycloak登录页面上实现Recaptcha

更新时间:2023-12-01 16:05:28

基于@ ghinea-alex响应,我们在此

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

我们制作了一个Maven模块,它也是一个JBoss模块.

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

首先在RecaptchaUsernamePasswordForm中扩展了UsernamePasswordForm,还在RecpatchaUsernamePasswordFormFactory中扩展了UsernamePasswordFormFatory.

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

RecaptchaUsernamePasswordForm:

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:

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

}

必须有一个META-INF,其中必须有一个service\org.keycloak.authentication.AuthenticatorFactory.它的内容是:

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

以及所有独立的可部署jboss模块都必须具有jboss-deployment-structure.xml,它描述了此模块的依赖性:

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>

,在主题的login.ftl中,应在<form></form>内添加此内容:

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>

最后,您应该启用外部来源https://google.com,就像在密钥斗篷的

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

在此 github存储库中,我们创建了一个易于使用的maven模块和使用手册.

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

只需克隆存储库即可. 您应该已安装javamaven. 要进行构建,您需要运行mvn clean install.它将产生jar target/recaptcha-login.jar. 为了使其可以在Keycloak中访问,您应该将此jar复制到keycloaks standalone/deployment/目录中. 只是它. 如果在docker环境中使用它,则应将其安装在/opt/jboss/keycloak/standalone/deployment/recaptcha-login.jar中. 例如在我的docker compose文件中:

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

在主题文件中,您应该在login.ftl模板文件中添加以下代码:

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>

您应该将其粘贴到登录模板(login.ftl)中的登录<form></form>

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

最后,您应该启用外部来源https://google.com,就像在密钥斗篷的

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

要在GUI中启用它,请执行以下操作: 转到身份验证

for enabling it IN GUI do these: Go To authentication

然后创建一个流程对于这种情况,您自己的是BrowserWithRecaptcha,它应该类似于keycloaks的默认Browser,只是它具有Recaptcha Username Password Form而不是Username Password Form:

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:

然后根据您的Google Recaptcha密钥配置您的Recaptacha Uusername Password Form:

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

,然后在下一个选项卡中将Browser Flow绑定到BrowserWithRecaptcha:

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

,还必须允许google.com在> Security Defences中进行访问:

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