1.添加依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.6</version>
</dependency>
</dependencies>
2.配置Ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<diskStore path="java.io.tmpdir"/>
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryEhcache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
3.编写自定义的CredentialsMatcher
package com.github.zhangkaitao.shiro.chapter5.credentials;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
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 java.util.concurrent.atomic.AtomicInteger;
/**
* @author 杨郑耀
* @description
* @create 2019-06-14-9:26
*/
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private Ehcache passwordRetryEhcache;
public RetryLimitHashedCredentialsMatcher(){
CacheManager cacheManager = CacheManager.newInstance(CacheManager.class.getClassLoader().getResource("ehcache.xml"));
passwordRetryEhcache = cacheManager.getCache("passwordRetryEhcache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//1.通过令牌获取用户名
String username = (String)token.getPrincipal();
//2.判断缓存中是否包含含有此用户名的Element
Element element = passwordRetryEhcache.get(username);
//3.如果没有 ,就创建包含该用户名的Element并添加到缓存
if (element == null){
element = new Element(username,new AtomicInteger(0));
passwordRetryEhcache.put(element);
}
//4.如果有 就通过该Element获取AutomicInteger
AtomicInteger atomicInteger = (AtomicInteger)element.getObjectValue();
//5.如果AutomicInteger的incrementAndGet大于5 就抛异常
if (atomicInteger.incrementAndGet() > 5){
throw new ExcessiveAttemptsException();
}
//6.如果小于5 就调用父类的doCredentialsMatch方法进行密码验证
boolean matchs = super.doCredentialsMatch(token, info);
//7.如果验证通过,就将该username从缓存中清除
if (matchs){
passwordRetryEhcache.remove(username);
}
return matchs;
}
}
4.编写自定义的Realm
package com.github.zhangkaitao.shiro.chapter5.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* @author 杨郑耀
* @description
* @create 2019-06-13-17:12
*/
public class MyRealm2 extends AuthorizingRealm {
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//模拟从数据库获取的用户名及密码
String username = "liu"; //用户名及salt1
String salt2 = "0072273a5d87322163795118fdd7c45e"; //盐
String password = "be320beca57748ab9632c4121ccac0db"; //加密后的密码
SimpleAuthenticationInfo ai = new SimpleAuthenticationInfo(username, password, getName());
ai.setCredentialsSalt(ByteSource.Util.bytes(username+salt2)); //盐是用户名+随机数
return ai;
}
}
5.配置ini文件
[main]
#自定义credentialsMatcher
credentialsMatcher=com.github.zhangkaitao.shiro.chapter5.credentials.RetryLimitHashedCredentialsMatcher
#算法设置为MD5
credentialsMatcher.hashAlgorithmName=md5
#迭代次数
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true
#自定义realm
myRealm=com.github.zhangkaitao.shiro.chapter5.realm.MyRealm2
#将自定义的credentialsMatcher注入到myRealm
myRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$myRealm
6.编写测试类
package com.github.zhangkaitao.shiro.chapter5.test;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.apache.shiro.util.ThreadContext;
import org.junit.After;
/**
* @author 杨郑耀
* @description
* @create 2019-06-12-11:16
*/
public abstract class BaseTest {
@After
public void tearDown() throws Exception{
ThreadContext.unbindSubject();//解除Subject绑定到线程
}
protected void login(String configFile, String name, String password) throws AuthenticationException {
//1.获取SecurityManager工厂,通过Ini配置初始化ecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory(configFile);
//2.获取SecurityManager实例,并绑定SecurityUtils
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3.得到Subject及创建用户/密码 身份验证
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name,password);
subject.login(token);
}
//获取Subject
public Subject subject(){
return SecurityUtils.getSubject();
}
}
package com.github.zhangkaitao.shiro.chapter5.test;
import com.sun.javafx.css.converters.EnumConverter;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.converters.AbstractConverter;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.junit.Test;
/**
* @author 杨郑耀
* @description
* @create 2019-06-13-15:57
*/
public class PasswordTest extends BaseTest{
//利用HashedCredentialsMatcher 防止密码被暴力破解 密码验证失败5次 就禁用
@Test(expected = ExcessiveAttemptsException.class)
public void testRetryLimitHashCredentialsMatcherWithMyRealm(){
//模拟5次验证失败
for (int i = 1; i <= 5; i++){
try {
// 模拟密码错误 所以每次都是IncorrectCredentialsException
login("classpath:shiro-retryLimitHashedCredentialsMatcher.ini","liu","234");
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
//失败5次后再次验证 即使密码正确 也会报错ExcessiveAttemptsException
login("classpath:shiro-retryLimitHashedCredentialsMatcher.ini","liu","123");
}
}
总结:主要思想是,自定义Realm提供原始数据,包括用户名、加密后的密码、盐,然后将这些原始数据添加到认证信息AuthenticationInfo中。自定义CredentialsMatche,实现调用父类doCredentialsMatch方法进行密码验证之前,对密码验证次数进行判定,当次数大于限制数就抛异常。Ehcache提供用户名缓存服务,当该用户名存在于缓存中就将其AtomicInteger加1,不存在就将该用户名添加到缓存中。如果验证通过,将该用户名从缓存清除。