如何在keycloak登录页面上实现Recaptcha [英] How to implement Recaptcha on keycloak login page

查看:243
本文介绍了如何在keycloak登录页面上实现Recaptcha的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在keycloak登录页面(如注册页面)中实现Recaptcha.我用所需的工厂类扩展了UsernamePasswordForm类.我什至也要求行动需要上课.但我仍然看不到在提供程序选项卡中添加登录名.我也修改了现有的login.ftl,但没有运气.

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.

下面是我尝试过的.

我的身份验证器类:

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

}

有什么我想念的吗?有人可以帮助我在登录页面中获取验证码吗?有人做过吗?有人可以共享示例代码,以便我看到并尝试更多吗.

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.

谢谢.

推荐答案

基于@ 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:

这篇关于如何在keycloak登录页面上实现Recaptcha的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆