第十六节 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<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限制登录尝试次数

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

你看我都这么努力的分享知识给你了,鼓励一下又何妨O(∩_∩)O

你的打赏是对我最好的支持!

                    

### Shiro 中实现密码错误次数控制 为了在 Apache Shiro 中实现密码错误次数限制,可以利用 `AuthenticatingRealm` 类中的 `doCredentialsMatch` 方法来处理认证逻辑。当用户尝试登录时,如果输入的密码不匹配,则记录该次失败并更新用户的错误计数器。 #### 修改 Realm 实现类 通过继承自 `AuthorizingRealm` 或者其他类型的 realm 来覆盖默认行为,在此过程中加入对密码错误次数的判断: ```java public class CustomizedShiroRealm extends AuthorizingRealm { private static final int MAX_FAILED_ATTEMPTS = 5; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String username = token.getUsername(); User user = userService.findByUsername(username); if(user != null && !isAccountLocked(user)){ SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //principal user.getPassword(),//credentials getName() //realm name ); return authenticationInfo; } throw new LockedAccountException("账号已被锁定"); } /** * 判断账户是否被锁住 */ private boolean isAccountLocked(User user){ Integer failedAttempts = user.getFailedAttemptCount(); LocalDateTime lockTime = user.getAccountLockTime(); // 如果超过最大允许失败次数,并且当前时间仍在锁定时间内则返回true表示已锁定 return failedAttempts >= MAX_FAILED_ATTEMPTS && Objects.nonNull(lockTime) && LocalDateTime.now().compareTo(lockTime.plusMinutes(30)) < 0; } } ``` 上述代码展示了如何创建一个定制化的 Realm 并在其内部定义了 `MAX_FAILED_ATTEMPTS` 常量用于设置最大允许的连续失败次数[^2]。 #### 更新服务层业务逻辑 每当发生一次成功的或失败的身份验证请求之后都需要相应地调整数据库中存储的相关字段值(比如增加失败次数),这通常是在 Service 层完成的工作: ```java @Service @Transactional(readOnly=true) public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; @Transactional public void updateLoginFailure(String username){ Optional<User> optionalUser = userRepository.findByUsername(username); optionalUser.ifPresentOrElse((user)->{ user.setFailedAttemptCount(user.getFailedAttemptCount()+1); userRepository.saveAndFlush(user); }, ()->{ log.warn("{} 尝试登录但找不到对应用户",username); }); } @Transactional public void resetFailuresAfterSuccessfulLogin(String username){ Optional<User> optionalUser = userRepository.findByUsername(username); optionalUser.ifPresentOrElse((user)->{ user.setFailedAttemptCount(0); user.setAccountLockTime(null); userRepository.saveAndFlush(user); }, ()->{ log.warn("{} 成功登录但是找不到对应的用户实体对象.",username); }); } } ``` 这段 Java 代码片段说明了如何根据不同的情况去修改用户的属性,如增加失败次数或将这些数值重置为零以便于下次正常登录操作[^4]。 #### 登录控制器部分 最后一步就是确保每次调用 Shiro 的 `subject.login()` 后都能正确触发相应的事件处理器来进行必要的状态变更: ```java @PostMapping("/login") @ResponseBody public Result login(@RequestParam String username,@RequestParam String password){ Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password); try { currentUser.login(token); userService.resetFailuresAfterSuccessfulLogin(username); return Result.ok("登录成功!"); } catch (UnknownAccountException | IncorrectCredentialsException ex) { userService.updateLoginFailure(username); return Result.fail(ex.getMessage()); } } ``` 以上示例展示了一个简单的 RESTful API 接口用来接收来自前端表单提交的数据,并执行实际的身份验证过程以及后续的状态管理动作[^3]。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小大宇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值