本文主要实践用spring boot + shiro+ ehcache实现用户登录错误次数的判断,关于spring boot, shiro, ehcache的理论知识这里将不作为介绍,这些知识可以在它们的官网或其它博客上能够找到 。下面将进入正文:
pom.xml
本文主要引用的jar如下:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
用于我们将使用shiro + ehache配合使用,所以可以不用单独再引用ehcache.jar了,使用shiro-ehcache时,会自动添加ehcache-core 2.6.11。
ehcache.xml
由于我们使用了ehcache,所以我们需要添加一个配置文件ehcache.xml,此文件目录为src/main/resources/config/ehcache.xml,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false"
dynamicConfig="true"
name="ehcache_retry">
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir/ehcache" />
<!-- 默认缓存 -->
<defaultCache maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap" />
</defaultCache>
<!-- helloworld缓存 -->
<cache name="passwordRetryCache"
maxElementsInMemory="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"
statistics="true" />
</ehcache>
关于以上属性代表的含义,可以在官方文档中找到http://www.ehcache.org/documentation/ehcache-2.6.x-documentation.pdf
配置中不能对ehcache标签添加monitoring="autodetect",否侧缓存将无法保存。
注:
timeToIdleSeconds – The maximum number of seconds an element can exist in the cache without being accessed. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTI eviction takes place (infinite lifetime).
timeToLiveSeconds – The maximum number of seconds an element can exist in the cache regardless of use. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTL eviction takes place (infinite lifetime).
maxElementsOnDisk – The maximum sum total number of elements (cache entries) allowed for a distributed cache in all Terracotta clients. If this target is exceeded, eviction occurs to bring the count within the allowed target. The default value is 0, which means no eviction takes place (infinite size is allowed). Note that this value reflects storage allocated on the Terracotta Server Array. A setting of 0 means that no eviction of the cache's entries takes place on Terracotta Server Array, and consequently can cause the servers to run out of disk space.
eternal – If the cacheâ–s eternal flag is set, it overrides any finite TTI/TTL values that have been set
ShiroRealm.java
public class ShiroRealm extends AuthorizingRealm{
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
/* 登录验证
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String)token.getPrincipal();
User user = this.userService.findByUserName(userName);
if (user != null) {
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
info.setCredentialsSalt(new SimpleByteSource(ByteSource.Util.bytes(MD5Util.SALT)));
return info;
}
return null;
}
}
由于doGetAuthorizationInfo主要用于用户授权部分,所以这里没有贴出代码,读者可以根据自己的业务需求来完善该部分。对于方法doGetAuthenticationInfo中,读者注意到info.setCredentialsSalt(new SimpleByteSource(ByteSource.Util.bytes(MD5Util.SALT)));,这个是因为我在保存密码时,用的shiro MD5+salt的方式加密,所以在这里验证的时候,必须与加密时的算法保持一致,这样才能够验证通过。由于HashedCredentialsMatcher不提供salt的设置,所以在返回AuthenticationInfo的时候需要把salt带上一起返回出去。
RetryLimitCredentialsMatcher.java
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher{
private static final int MAX_LOGIN_RETRY_TIMES = 5;
private Cache<String, AtomicInteger> passwordRetryCache;
public RetryLimitCredentialsMatcher(EhCacheManager ehCacheManager) {
passwordRetryCache = ehCacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws ExcessiveAttemptsException{
String userName = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(userName);
if (retryCount == null) {
// 高并发下使用的线程安全的int类
retryCount = new AtomicInteger(0);
passwordRetryCache.put(userName, retryCount);
}
if (retryCount.incrementAndGet() > MAX_LOGIN_RETRY_TIMES) {
throw new ExcessiveAttemptsException();
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(userName);
}
return match;
}
}
这个类的主要作用就是计算并缓存用户尝试登陆的次数,如果大于了5次,那么该用户将被禁止登陆直到10分钟以后。这个时间在ehcache.xml中timeToIdleSeconds设置。
ShiroConfig.java
对shiro进行配置:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "user");
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/**", "authc");//if authc, all url should be authenticated
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(this.retryLimitCredentialsMatcher());
return shiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(this.shiroRealm());
return securityManager;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache.xml");
return ehCacheManager;
}
@Bean
public CredentialsMatcher retryLimitCredentialsMatcher() {
RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(this.ehCacheManager());
retryLimitCredentialsMatcher.setHashAlgorithmName(MD5Util.ALGORITH_NAME);
retryLimitCredentialsMatcher.setHashIterations(MD5Util.HASH_ITERATIONS);
retryLimitCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return retryLimitCredentialsMatcher;
}
}
注:在这个类中,org.apache.shiro.cache.ehcache.EhCacheManager的作用是为了加载ehcache.xml,另外,retryLimitCredentialsMatcher的作用是为了设置一个和密码加密算法一样的算法,其中一定要设置Hash Algorithm Name(我用的是MD5),否则会有exception提示HashAlgorithmName必须设置,这是因为我们使用的ehcache版本为2.5及以上,在此时,如果还有异常
net.sf.ehcache.CacheException: Another CacheManager with same name 'MD5' already exists in the same VM.
这是由于在对密码加密的时候也是使用的HashAlgorithmName = MD5,这个时候内存中已经存在了一个叫做MD5 CacheManager ,同时我们为了让加密算法和验证时候的算法也一样,我们不得不在此也设置为MD5,所以需要在ehcache.xml中为cache设置一个name="ehcache_retry",这也是为什么读者在前文中ehcache.xml中看到有name。
为了能够使用RetryLimitCredentialsMatcher,我们需要在shiroRealm中将其设置进去,这样一个用户登录尝试错误次数的判断器就算做好了。
当然,我们使用的是spring boot,同时开启了自动编译,所以当代码改动后,会自动重启服务器,这个时候你会发现又会出现以下exception
Caused by: net.sf.ehcache.CacheException: Another CacheManager with same name 'ehcache_retry' already exists in the same VM. Please provide unique names for each CacheManager in the config or do one of following:
1. Use one of the CacheManager.create() static factory methods to reuse same CacheManager with same name or create one if necessary
2. Shutdown the earlier cacheManager before creating new one with same name.
The source of the existing CacheManager is: InputStreamConfigurationSource [stream=java.io.BufferedInputStream@4e28fd64]
这是用于之前的EhCacheManager还存在,并没有因自动重启而失效(除非是手动的关闭再重启),在重启的时候回再次创建一个EhCacheManager,导致重复,为了解决这个问题,我们需要添加一个Listener来监听当前application是否是关闭了又重启。如果是,那么我们将之前的EhCacheManager也关掉。
SpringEhcacheShutdownListener.java
@Component
public class SpringEhcacheShutdownListener implements ApplicationListener<ApplicationEvent>{
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
ApplicationContext context = ((ContextClosedEvent) event).getApplicationContext();
EhCacheManager ehCacheManager = (EhCacheManager) context.getBean("ehCacheManager");
if (ehCacheManager != null) {
ehCacheManager.destroy();
}
}
}
}
本文是我初次写博客,如有不好之处,请多多指教。若文章有疑问之处,可以留言一起讨论。
版权声明:本文为时间_老人作者的原创文章,未经时间_老人允许不得转载。