利用shiro实现密码错误锁定账户功能
使用场景
用户多次输入密码错误,系统应给出密码错误提示和当前错误次数,并且提示密码剩余输入次数,当密码错误次数达到指定最大次数时,该账户将被锁定
shiro身份认证流程
如果简单了解过shiro身份认证的一些基本概念,都应该明白shiro的身份认证的流程,大致是这样的:当我们调用subject.login(token)的时候,首先这次身份认证会委托给Security Manager,而Security Manager又会委托给Authenticator,接着Authenticator会把传过来的token再交给我们自己注入的Realm进行数据匹配从而完成整个认证。如果不太了解这个流程建议再仔细读一下官方提供的Authentication说明文档:
http://shiro.apache.org/authentication.html上面提到了我们自己注入的realm会对用户名密码进行匹配,而在我们的adp框架中ShiroDbRealm类就是我们自己注入的realm,具体实现如下
public class ShiroDbRealm extends AuthorizingRealm{
//...
}
可以看出该类继承自AuthorizingRealm,打开AuthorizingRealm源码可对其初始化对象一目了然
/*-------------------------------------------
| C O N S T R U C T O R S |
============================================*/
public AuthenticatingRealm() {
this(null, new SimpleCredentialsMatcher());
}
public AuthenticatingRealm(CacheManager cacheManager) {
this(cacheManager, new SimpleCredentialsMatcher());
}
public AuthenticatingRealm(CredentialsMatcher matcher) {
this(null, matcher);
}
public AuthenticatingRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
authenticationTokenClass = UsernamePasswordToken.class;
//retain backwards compatibility for Shiro 1.1 and earlier. Setting to true by default will probably cause
//unexpected results for existing applications:
this.authenticationCachingEnabled = false;
int instanceNumber = INSTANCE_COUNT.getAndIncrement();
this.authenticationCacheName = getClass().getName() + DEFAULT_AUTHORIZATION_CACHE_SUFFIX;
if (instanceNumber > 0) {
this.authenticationCacheName = this.authenticationCacheName + "." + instanceNumber;
}
if (cacheManager != null) {
setCacheManager(cacheManager);
}
if (matcher != null) {
setCredentialsMatcher(matcher);
}
}
```
>可以看到AuthenticatingRealm对象构造器里面有一个CredentialsMatcher的对象,没错这就是密码匹配器
```java
/**
* Credentials matcher used to determine if the provided credentials match the credentials stored in the data store.
*/
private CredentialsMatcher credentialsMatcher;
```
>通过类图可以看出HashedCredentialsMatcher类为CredentialsMatcher接口的最终实现类,要更改密码匹配规则只需重写该类即可
![类图](image/liucheng.png "类图")
> ## 重写HashedCredentialsMatcher实现自定义密码匹配
> ## 密码次数验证规则
>当用户进行登录时,如果密码输入错误,会将改用户唯一标识(登录名)以key的形式存入缓存中,并且以value的形式存入登录错误次数,根据缓存中登录错误次数对账户进行提示或者锁定.
> ### 1.配置密码缓存
> 这里采用Ehcache缓存,并且将缓存加入shiro默认的缓存管理器中
```xml
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro/ehcache-shiro.xml" />
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="shiroDbRealm" />
</list>
</property>
<property name="cacheManager" ref="shiroEhcacheManager" />
</bean>
<div class="se-preview-section-delimiter"></div>
在ehcache-shiro.xml中添加密码缓存对象,该缓存存活时间为10分钟,对应账户锁定时间
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryCache_admp"
maxEntriesLocalHeap="20000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"
statistics="false">
</cache>
<div class="se-preview-section-delimiter"></div>
2.编写类实现
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义密码匹配规则
*
*/
@Component
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
private Cache<String, AtomicInteger> passwordRetryCache;
//依赖注入缓存管理器
@Autowired
private EhCacheManager shiroEhcacheManager;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//获取密码错误次数缓存对象
passwordRetryCache = shiroEhcacheManager.getCache("passwordRetryCache_admp");
//获取密码错误次数缓存对象存活时间
Long lockTime = shiroEhcacheManager.getCacheManager().getCache("passwordRetryCache_admp").getCacheConfiguration().getTimeToLiveSeconds();
//获取登录名
String username = (String) token.getPrincipal();
//使用AtomicInteger,该对象为线程安全对象
AtomicInteger retryCount = passwordRetryCache.get(username);
if (null == retryCount) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
//自增之后的值和最大密码错误次数对比,最大错误次数5次
if (retryCount.incrementAndGet() == 5) {
throw new LockedAccountException("账户被锁定,请于" + lockTime / 60 + "分后重新登录");
}
if (retryCount.get() > 5) {
throw new LockedAccountException("账户被锁定");
}
boolean matches = super.doCredentialsMatch(token, info);
//密码比对成功清除该用户密码错误次数缓存
if (matches) {
//clear retry data
passwordRetryCache.remove(username);
} else {
if (5 - retryCount.get() <= 3) {
throw new ExcessiveAttemptsException("用户名或密码错误,剩余" + (5 - retryCount.get()) + "次");
}
throw new ExcessiveAttemptsException("用户名或密码错误");
}
//返回结果为true表示验证通过
return matches;
}
}
<div class="se-preview-section-delimiter"></div>
3.编写配置文件
将该类注入到shiro的自定义realm中,并且定义加密规则
<!--自定义密码匹配器-->
<bean id="retryLimitCredentialsMatcher" class="com.hongguaninfo.hgdf.adp.shiro.RetryLimitCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA-1"></property>
<property name="hashIterations" value="1024"></property>
</bean>
<!-- 項目自定义的Realm -->
<bean id="shiroDbRealm" class="com.hongguaninfo.hgdf.adp.shiro.ShiroDbRealm">
<property name="credentialsMatcher" ref="retryLimitCredentialsMatcher"></property>
</bean>
<div class="se-preview-section-delimiter"></div>
最后将shiroDbRealm加入securityManager中
<!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="shiroDbRealm" />
</list>
</property>
<property name="cacheManager" ref="shiroEhcacheManager" />
</bean>
整个流程编写完毕
自行优化代码
当用户进行登录时会进入shiroDbRealm中通过查询数据库对用户名密码进行验证,这时候我们可以在查询数据库之前通过查询密码错误次数缓存对象对用户账户是否锁定进行判断,防止重复查询数据库
“`java
@Autowired
private EhCacheManager shiroEhcacheManager;
/**
* 认证回调函数, 登录时调用.
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
UsernamePasswordCaptchaToken token = (UsernamePasswordCaptchaToken) authcToken;
Cache passwordRetryCache = shiroEhcacheManager.getCache("passwordRetryCache_admp");
AtomicInteger retryCount = (AtomicInteger) passwordRetryCache.get(username);
if (null != retryCount && retryCount.get() > 5) {
throw new LockedAccountException("账户被锁定");
}
AdMamUser user = accountService.findUserByLoginName(username);
if (null == user) {
throw new UnknownAccountException("用户名错误.");
}
byte[] salt = new String(user.getLoginName()).getBytes();
return new SimpleAuthenticationInfo(new ShiroUser(user.getId(), user.getLoginName(), user.getUserName()),
user.getLoginPwd(), ByteSource.Util.bytes(salt), getName());
}
“`
ok啦