要限制用户登录尝试次数,必然要对用户名密码验证失败做记录,Shiro中用户名密码的验证交给了CredentialsMatcher
所以在CredentialsMatcher里面检查,记录登录次数是最简单的做法。
我们用Ehcache来记录用户登录次数的计数,继承HashedCredentialsMatcher,加入缓存,在每次验证用户名密码之前先验证用户名尝试次数,如果超过5次就抛出尝试过多异常,否则验证用户名密码,验证成功把尝试次数清零,不成功则直接退出。
package org.jstudioframework.freeway.shiro.authc.credential;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 验证器,增加了登录次数校验功能
*/
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
private static final Logger log = LoggerFactory.getLogger(RetryLimitCredentialsMatcher.class);
//集群中可能会导致出现验证多过5次的现象,因为AtomicInteger只能保证单节点并发
private Cache<String, AtomicInteger> loginRetryCache;
private int maxRetryCount = 5;
private String loginRetryCacheName;
public void setMaxRetryCount(int maxRetryCount) {
this.maxRetryCount = maxRetryCount;
}
public RetryLimitCredentialsMatcher(CacheManager cacheManager,String loginRetryCacheName) {
this.loginRetryCacheName = loginRetryCacheName;
loginRetryCache = cacheManager.getCache(loginRetryCacheName);
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
//retry count + 1
AtomicInteger retryCount = loginRetryCache.get(username);
if (null == retryCount) {
retryCount = new AtomicInteger(0);
loginRetryCache.put(username, retryCount);
}
if (retryCount.incrementAndGet() > 5) {
log.warn("username: " + username + " tried to login more than 5 times in period");
throw new ExcessiveAttemptsException("username: " + username + " tried to login more than 5 times in period"
);
}
boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
//clear retry data
loginRetryCache.remove(username);
}
return matches;
}
}
UserRealm继承AuthorizingRealm,在其父类AuthenticatingRealm的getAuthenticationInfo方法中会调用credentialsMatcher的 doCredentialsMatch 来验证用户输入用户名密码是否匹配。
Ehcache配置
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="loginRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
Spring Shiro配置
<!-- Realm实现 -->
<bean id="userRealm" class="org.jstudioframework.freeway.shiro.UserRealm">
<!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher"
class="org.jstudioframework.freeway.shiro.authc.credential.RetryLimitCredentialsMatcher">
<constructor-arg index="0" ref="cacheManager"/>
<constructor-arg index="1" value="5"/>
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1"/>
</bean>
<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:cache/shiro-ehcache.xml"/>
</bean>