Wildfly 16远程EJB登录失败次数限制方案咨询
首先明确:Wildfly 16内置的数据库登录模块(不管是Legacy的PicketBox还是默认的Elytron实现)没有直接通过配置就能实现用户级失败次数限制+临时封禁的功能,得通过自定义扩展或者其他方案来实现。下面是几个可行的方案,其中自定义登录模块是最直接且符合你需求的方式:
方案一:自定义登录模块(推荐)
你提到的“维护用户ID与失败登录次数的内存映射”完全可行,甚至可以扩展为持久化存储来避免服务器重启丢失数据。具体实现步骤如下:
1. 编写自定义登录模块类
继承Wildfly的DatabaseServerLoginModule(Legacy安全域)或者Elytron的JdbcSecurityRealm相关类(如果你用Elytron),这里以Legacy的PicketBox为例:
import org.jboss.security.auth.spi.DatabaseServerLoginModule; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; public class BanningDatabaseLoginModule extends DatabaseServerLoginModule { // 内存存储:用户名 -> [失败次数, 最后失败时间戳, 封禁截止时间戳] private static final ConcurrentHashMap<String, long[]> FAILED_LOGINS = new ConcurrentHashMap<>(); // 配置参数:允许的最大失败次数,默认5次 private int maxFailedAttempts = 5; // 封禁时长,默认5分钟(毫秒) private long banDuration = TimeUnit.MINUTES.toMillis(5); @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { super.initialize(subject, callbackHandler, sharedState, options); // 读取配置的参数 if (options.containsKey("maxFailedAttempts")) { maxFailedAttempts = Integer.parseInt(options.get("maxFailedAttempts").toString()); } if (options.containsKey("banDuration")) { banDuration = Long.parseLong(options.get("banDuration").toString()); } } @Override protected boolean validatePassword(String inputPassword, String expectedPassword) { String username = getUsername(); // 先检查用户是否处于封禁期 long[] userData = FAILED_LOGINS.get(username); if (userData != null && System.currentTimeMillis() < userData[2]) { // 仍在封禁期,直接返回验证失败 return false; } // 调用父类方法验证密码 boolean isValid = super.validatePassword(inputPassword, expectedPassword); if (!isValid) { // 密码验证失败,更新失败次数和时间 handleFailedLogin(username); } else { // 验证成功,重置失败记录 FAILED_LOGINS.remove(username); } return isValid; } private void handleFailedLogin(String username) { long[] userData = FAILED_LOGINS.getOrDefault(username, new long[]{0, 0, 0}); userData[0]++; // 失败次数+1 userData[1] = System.currentTimeMillis(); // 更新最后失败时间 if (userData[0] >= maxFailedAttempts) { // 达到阈值,设置封禁截止时间 userData[2] = System.currentTimeMillis() + banDuration; } FAILED_LOGINS.put(username, userData); } }
2. 部署自定义模块到Wildfly
将编译好的类打包成JAR,放到wildfly-16.0.0.Final/modules/com/yourcompany/security/main/目录下,同时创建module.xml:
<?xml version="1.0" encoding="UTF-8"?> <module name="com.yourcompany.security" xmlns="urn:jboss:module:1.3"> <resources> <resource-root path="your-security-module.jar"/> </resources> <dependencies> <module name="org.jboss.security"/> <module name="javax.api"/> </dependencies> </module>
3. 配置Wildfly安全域
修改standalone.xml(或domain.xml)的安全域配置,替换原来的DatabaseServerLoginModule为你的自定义模块:
<security-domain name="yourEjbSecurityDomain" cache-type="default"> <authentication> <login-module code="com.yourcompany.security.BanningDatabaseLoginModule" flag="required"> <!-- 原数据库登录模块的参数 --> <module-option name="dsJndiName" value="java:jboss/datasources/YourDataSource"/> <module-option name="principalsQuery" value="SELECT password FROM users WHERE username=?"/> <module-option name="rolesQuery" value="SELECT role, 'Roles' FROM user_roles WHERE username=?"/> <!-- 自定义参数 --> <module-option name="maxFailedAttempts" value="5"/> <module-option name="banDuration" value="300000"/> <!-- 5分钟=300000毫秒 --> </login-module> </authentication> </security-domain>
扩展:持久化失败记录
如果担心服务器重启丢失封禁数据,可以把失败记录存在数据库中:
- 创建表
user_failed_logins:username VARCHAR(50) PRIMARY KEY, failed_count INT, last_failed_time BIGINT, banned_until BIGINT - 在自定义登录模块中,用JDBC操作这个表来读写失败记录,替代内存中的
ConcurrentHashMap
方案二:利用Elytron的事件监听(适用于Elytron安全域)
如果你使用Wildfly 16默认的Elytron安全框架,可以通过SecurityEventListener监听登录失败事件,在事件处理器中维护失败次数和封禁逻辑:
- 实现
SecurityEventListener接口,在authenticationFailed方法中处理失败次数累加 - 配置Elytron的
security-domain时关联这个监听器 - 可以结合Elytron的
CredentialStore或者自定义的存储来保存封禁数据
方案三:结合外部fail2ban(IP级封禁,补充方案)
如果你的需求也包含IP级的封禁,可以用传统的fail2ban工具:
- 配置Wildfly将登录失败日志输出到文件(比如
server.log),日志格式要包含用户名和IP - 配置fail2ban的规则,匹配日志中的登录失败条目,达到次数后封禁对应的IP
- 注意:这个方案是IP级的,不是用户ID级的,适合补充用户级封禁的不足
总结
最贴合你需求的是自定义登录模块,既可以用内存存储快速实现,也能扩展为持久化存储保证数据不丢失。实现起来难度不大,只需要继承内置的数据库登录模块,添加失败次数统计和封禁逻辑即可。
内容的提问来源于stack exchange,提问作者j-dimension

