如何在Keycloak中添加自定义FTL页面并触发自定义Java代码?
我来一步步帮你搞定这个需求,Keycloak扩展自定义页面和后端逻辑有固定的套路,咱们分四个核心步骤来实现:
1. 搭建Keycloak扩展项目结构
首先你需要创建一个Maven项目(或者Gradle),用来打包自定义逻辑成JAR包部署到Keycloak里。项目依赖要包含Keycloak的核心SPI库,pom.xml里加这些依赖(版本和你的Keycloak版本保持一致):
<dependencies> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-core</artifactId> <version>22.0.5</version> <!-- 替换成你的Keycloak版本 --> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi</artifactId> <version>22.0.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi-private</artifactId> <version>22.0.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-services</artifactId> <version>22.0.5</version> <scope>provided</scope> </dependency> </dependencies>
2. 编写自定义FTL页面
在项目的src/main/resources/theme/base/login/目录下创建special-number.ftl文件,内容就是带数字输入框的表单,提交地址指向Keycloak的登录动作端点,同时带上自定义action标识:
<!DOCTYPE html> <html> <head> <title>输入特殊数字</title> ${include('login/includes/header.ftl')} </head> <body class="login-pf"> <div class="container"> <div class="row"> <div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-6 col-lg-offset-3"> <div class="card-pf"> <h1 class="card-pf-title">请输入特殊数字</h1> <form id="special-number-form" action="${url.loginAction}" method="post"> ${csrfToken} <!-- 必须加这个防止CSRF攻击 --> <div class="form-group"> <label for="specialNumber">特殊数字:</label> <input type="number" id="specialNumber" name="specialNumber" class="form-control" required> </div> <div class="form-actions"> <input type="hidden" name="action" value="specialNumberSubmit"> <button type="submit" class="btn btn-primary btn-block">提交</button> </div> </form> </div> </div> </div> </div> ${include('login/includes/footer.ftl')} </body> </html>
注意:要引入Keycloak自带的header和footer,保持页面风格和系统一致,同时必须加上
${csrfToken}防止跨站请求伪造。
3. 编写Java后端逻辑(处理表单提交)
我们用Keycloak的Authenticator SPI来实现,这个SPI专门用来处理认证流程中的自定义步骤,完美匹配你的需求:
3.1 实现Authenticator接口
创建com/yourcompany/keycloak/authenticator/SpecialNumberAuthenticator.java:
package com.yourcompany.keycloak.authenticator; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import javax.ws.rs.core.Response; public class SpecialNumberAuthenticator implements Authenticator { @Override public void authenticate(AuthenticationFlowContext context) { // 当进入这个认证步骤时,渲染自定义FTL页面 Response response = context.form() .createForm("special-number.ftl"); context.challenge(response); } @Override public void action(AuthenticationFlowContext context) { // 用户提交表单后,触发这个方法处理逻辑 String specialNumber = context.getHttpRequest().getDecodedFormParameters().getFirst("specialNumber"); // 这里写你的自定义业务逻辑,比如验证数字、调用外部API、更新用户属性等 boolean isNumberValid = checkSpecialNumber(specialNumber); if (isNumberValid) { // 验证通过,继续后续认证流程 context.success(); } else { // 验证失败,返回错误信息并重新渲染页面 context.form() .setError("invalid-special-number", "请输入正确的特殊数字") .createForm("special-number.ftl"); } } // 示例:自定义验证逻辑 private boolean checkSpecialNumber(String number) { return "666888".equals(number); // 替换成你的真实逻辑 } // 以下是接口默认实现,按需修改 @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {} @Override public void close() {} }
3.2 注册Authenticator工厂
创建com/yourcompany/keycloak/authenticator/SpecialNumberAuthenticatorFactory.java,用来把自定义Authenticator注册到Keycloak:
package com.yourcompany.keycloak.authenticator; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; public class SpecialNumberAuthenticatorFactory implements AuthenticatorFactory { public static final String ID = "special-number-authenticator"; private static final SpecialNumberAuthenticator INSTANCE = new SpecialNumberAuthenticator(); @Override public String getId() { return ID; } @Override public String getDisplayType() { return "特殊数字验证"; } // 后台显示的名称 @Override public String getHelpText() { return "验证用户输入的特殊数字"; } @Override public String getReferenceCategory() { return "custom"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return new AuthenticationExecutionModel.Requirement[] { AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED }; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List<ProviderConfigProperty> getConfigProperties() { return null; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) {} @Override public void postInit(KeycloakSessionFactory factory) {} @Override public void close() {} }
3.3 配置SPI服务注册
在src/main/resources/META-INF/services/目录下创建文件org.keycloak.authentication.AuthenticatorFactory,内容是你的工厂类全路径:
com.yourcompany.keycloak.authenticator.SpecialNumberAuthenticatorFactory
4. 部署并配置Keycloak触发页面
4.1 打包部署
用Maven打包项目:mvn clean package,把生成的JAR包放到Keycloak的standalone/deployments/目录下,Keycloak会自动热部署(或者重启Keycloak确保生效)。
4.2 配置认证流程触发页面
登录Keycloak后台,按以下步骤配置:
- 进入你的Realm → 点击左侧菜单Authentication → Flows
- 复制默认的
Browser流程(避免修改默认流程出问题),命名为Custom Browser Flow - 展开复制后的流程,点击Add execution,选择刚才注册的特殊数字验证,设置为
Required - 把这个新流程拖到合适的位置(比如放在
Username Password Form之后) - 点击左侧菜单Authentication → Bindings,把
Browser Flow设置为刚才创建的Custom Browser Flow
现在用户登录时,输入完用户名密码后,就会自动跳转到你的special-number.ftl页面,输入数字提交后就会触发你写的Java逻辑了!
内容的提问来源于stack exchange,提问作者Jack

