根据 @ghinea-alex 的回复,我们做了一个工作keycloak jboss module
在这个Github 存储库 https://github.com/raptor-group/keycloak-login-recaptcha.
我们制作了一个 Maven 模块,它也是一个 JBoss 模块。
首先扩展UsernamePasswordForm
in RecaptchaUsernamePasswordForm
并且还延伸了UsernamePasswordFormFatory
in RecpatchaUsernamePasswordFormFactory
.
重新验证用户名密码形式:
<!-- 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
就像钥匙斗篷的方式一样验证码文档 https://www.keycloak.org/docs/latest/server_admin/index.html#_recaptcha提及。
如何使用
in this github 仓库 https://github.com/raptor-group/keycloak-login-recaptcha我们创建了一个易于使用的 Maven 模块和使用手册。
只需克隆存储库即可。
你应该有java
and maven
安装。
为了构建你需要运行mvn clean install
。它会产生罐子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>
你应该把它放在你的登录名中<form></form>
在您的登录模板中(login.ftl
)
最后你应该启用外部来源https://google.com
就像钥匙斗篷的方式一样验证码文档 https://www.keycloak.org/docs/latest/server_admin/index.html#_recaptcha提及。
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:
并且还必须允许 google.com 访问Realm Settings
>Security Defences
: