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

x33g5p2x  于2021-12-18 转载在 其他  
字(5.7k)|赞(0)|评价(0)|浏览(414)

一、基本思路

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

      单机使用的是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<String, AtomicInteger> 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缓存对象的锁定时间。

<ehcache name="shiroCache">
    <!-- 磁盘上缓存的位置 -->
    <diskStore path="java.io.tmpdir"/>

    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            overflowToDisk="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="300"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />

    <!-- 登录验证缓存,缓存1分钟 -->
    <cache name="passwordRetryCache"
           maxElementsInMemory="10000"
           eternal="false"
           overflowToDisk="false"
           timeToIdleSeconds="60"
           timeToLiveSeconds="60"
           diskPersistent="false"
           diskExpiryThreadIntervalSeconds="120"
    />
</ehcache>

       (2)Spring配置CacheManager

<!-- securityManager 对象-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        ...
        <!-- 引入UserRealm -->
        <property name="realm" ref="userRealm"/>
        <!-- 引入ehcache缓存 -->
        <property name="cacheManager" ref="cacheManager"/>
    </bean>

    <!-- shiro的自带 EhCache缓存管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <!-- 凭证匹配器 -->
    <bean id="userCredentialsMatcher" class="com.jay.shiro.RetryLimitCredentialsMatcher">
        <!-- 为自定义Matcher注入缓存管理器-->
        <constructor-arg ref="cacheManager"/>
        <!-- 我们继承的HashedCredentialsMatcher类的构造函数中没有指明加密算法
             因此我们得手动配置,使用md5算法循环加密一次即可-->
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="1"/>
        <property name="storedCredentialsHexEncoded" value="true"/>
    </bean>

    <!-- 自定义Realm -->
    <bean id="userRealm" class="com.jay.shiro.UserRealm">
        <property name="cachingEnabled" value="true"/>
        <!-- 为自定义Realm注入密码匹配器-->
        <property name="credentialsMatcher" ref="userCredentialsMatcher"/>
        ...
    </bean>

(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 security限制登录尝试次数

Shiro限制登录尝试次数

相关文章