第十六节 Shiro限制密码重试次数限制

一、基本思路

        不管是单机还是集群,我们都得把用户的登录次数记录下来,放到缓存里面。

        单机使用的是Ehcache缓存,集群使用的是Redis缓存。单机或集群对于缓存来说,只是CacheManager接口的实现方式不同。

        我们可以按照如下的思路来限制登录次数:

        先查看是否系统中是否已有登录次数缓存。缓存对象结构预期为:"用户名--登录次数"。

        如果之前没有登录缓存,则创建一个登录次数缓存。

        将缓存记录的登录次数加1。

        如果缓存次数已经超过限制,则驳回本次登录请求。

        将缓存次数其保存到缓存中。

        验证用户本次输入的帐号密码,如果登录登录成功,则清除掉登录次数的缓存。

        代码只是思路的翻译。我们按照上述思路还编写代码。

        用户名可以从Shiro的token中获取,登录次数可以使用原子类AtomicInteger保证线程安全。

package com.jay.shiro;

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 java.util.concurrent.atomic.AtomicInteger;

import static org.slf4j.LoggerFactory.getLogger;

/**
 * @author jay.zhou
 * @date 2019/1/17
 * @time 9:28
 */
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {

    /**
     * 集群中可能会导致出现验证多过5次的现象,因为AtomicInteger只能保证单节点并发
     */
    private Cache passwordRetryCache;
    private static final Logger LOGGER = getLogger(RetryLimitCredentialsMatcher.class);
    private static final String RETRY_CACHE_NAME = "passwordRetryCache";
    private static final Integer MAX_RETRY_COUNT = 5;

    /**
     * cacheManager对象由外部注入
     * 可以是Ehcache的CacheManager
     * 也可以注入自定义的CacheManager
     *
     * @param cacheManager cacheManager
     */
    private RetryLimitCredentialsMatcher(CacheManager cacheManager) {
        /**
         * 此处从CacheManager中获取缓存Cache对象
         * 本例中获取的缓存对象是从Ehcache.xml配置中获取
         * 如果是我们自定义CacheManager的话,
         * 可用下面的实现思路:
         * 先尝试从缓区池中获取名为RETRY_CACHE_NAME的缓存对象
         * 如果缓存池中没有名为RETRY_CACHE_NAME的缓存对象
         * 那么则创建名为RETRY_CACHE_NAME的缓存对象,并放入到缓存池中
         * 保证本类属性passwordRetryCache不为空
         */
        passwordRetryCache = cacheManager.getCache(RETRY_CACHE_NAME);
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        final String clientUserName = (String) token.getPrincipal();
        //先查看是否系统中是否已有登录次数缓存
        AtomicInteger retryCount = passwordRetryCache.get(clientUserName);
        // 如果之前没有登录缓存,则创建一个登录次数缓存。
        if (retryCount == null) {
            retryCount = new AtomicInteger(0);
        }
        //将缓存记录的登录次数加1
        retryCount.incrementAndGet();
        //如果有且次数已经超过限制,则驳回本次登录请求。
        if (retryCount.get() > MAX_RETRY_COUNT) {
            LOGGER.error("登录次数超过限制");
            throw new ExcessiveAttemptsException("用户:" + clientUserName + "登录次数已经超过限制");
        }
        //并将其保存到缓存中
        passwordRetryCache.put(clientUserName, retryCount);
        //debug
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("用户:{},尝试登录次数:{}", clientUserName, retryCount.get());
        }
        //调用超类验证器,判断是否登录成功
        boolean isMatcher = super.doCredentialsMatch(token, info);
        //如果成功则清除缓存
        if (isMatcher) {
            passwordRetryCache.remove(clientUserName);
        }
        return isMatcher;
    }
}

       (1) 在Ehcache中配置名为passwordRetryCache缓存对象的锁定时间。


    
    

    

    
    

         (2)Spring配置CacheManager


    
        ...
        
        
        
        
    

    
    
        
    

    
    
        
        
        
        
        
        
    

    
    
        
        
        
        ...
    

 (3)自定义Realm中的配置

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //还记得吗,token封装了客户端的帐号密码,由Subject拉客并最终带到此处
        String clientUsername = (String) token.getPrincipal();
        //从数据库中查询帐号密码
        String passwordFromDB = userService.findPasswordByName(clientUsername);
        if (passwordFromDB == null) {
            //如果根据用户输入的用户名,去数据库中没有查询到相关的密码
            throw new UnknownAccountException();
        }

        //使用相同的加密算法,md5加密,默认加密一次
        Md5Hash md5Hash = new Md5Hash(passwordFromDB);

        return new SimpleAuthenticationInfo(clientUsername, md5Hash.toString(), "UserRealm");
    }

        数据库中存放的密码是123456,通过MD5加密循环加密1次后为:e10adc3949ba59abbe56e057f20f883e。并将此密文那过去与密码凭证器中的解析出来的密文进行对比,看是否一致。本例仅为实例项目,在实际项目中数据库的密码是加密后的密文。

          更多关于Shiro加密的操作可参考:第三节 Shiro对加密的支持

二、测试

        启动项目后,来到项目的根目录。点击第一个超链接尝试从后台获取JSON数据,因为没有登录,所以请求被重定向到登录页面。在登录页面中模拟多次输入错误的帐号密码。正确的帐号密码是"jay / 123456" 或者 "sunny / 654321"。输入超过五次错误密码后,限制再次登录,并提示用户等待一段时间后重试。

        第十六节 Shiro限制密码重试次数限制_第1张图片

三、源码下载

        本章节项目源码:点击我下载源码 

        大宇能够成功实现密码登录限制,很大一部分原因就是站在巨人的肩膀上。特此鸣谢下方博客与博主。 

        参考文章:

        Shiro security限制登录尝试次数

        Shiro限制登录尝试次数

        阅读更多:跟着大宇学Shiro目录贴

 

你可能感兴趣的:(跟着大宇学Shiro)