SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号

版权声明:欢迎关注我的微信公众号 java持续实践,获取最新学习资料 https://blog.csdn.net/qq_33229669/article/details/87945020

一 Shiro的执行流程


1、核心介绍
1)Application Code用户编写代码
2)Subject就是shiro管理的用户
3)SecurityManager安全管理器,就是shiro权限控制核心对象,在编程时,只需要操作Subject方法,底层调用SecurityManager方法,无需直接编程操作SecurityManager
4)Realm应用程序和安全数据之间连接器,应用程序进行权限控制读取安全数据(数据表、文件、网络…)通过Realm对象完成
2、Shiro执行流程
应用程序(就是你自己的项目)—>Subject—>SecurityManager—>Realm—>安全数据
3、Shiro进行权限控制的四种主要方式
1)在程序中通过Subject编程方式进行权限控制
2)配置Filter实现URL级别粗粒度权限控制
3)配置代理,基于注解实现细粒度权限控制
4)在页面中使用shiro自定义标签实现,页面显示权限控制

Shiro执行登录的流程如下图.

大致的思路如下, 在Controller层接收前端输入的用户名和密码. 调用Shiro的SecurityUtils.getSubject()方法获取Subject对象.
之后用Subject对象调用login方法,其Shiro底层会进行密码的验证, 传入UsernamePasswordToken对象,此对象封装了前端传入的用户名和密码.

接着Shiro的SecurityManager会去调用自定义的Realm的AuthenticationInfo方法进行登录的验证, 此方法会返回一个SimpleAuthenticationInfo对象,此对象封装了ShiroUser ,数据库中存储的当前用户的密码, 密码加盐的值,Realm的名称, 即把数据库中的当前的用户, 与用户输入的用户名密码即存储在 UsernamePasswordToken进行比较,如果密码正确,登录成功,密码不正确登录失败.


调用自定义Realm的AuthenticationInfo完了之后, 调用RetryLimitCredentialsMatcher类中的doCredentialsMatch方法, 进行密码匹配次数的记录. 并用EhCache作为缓存, 把当前登录的用户名作为key,key的过期时间按照需求设置即可, 把登录的次数作为值.首先通过用户名,获取登录次数,如果登录次数为0, 那么先给当前用户设置一个缓存,登录次数+1,之后判断是否大于限定的登录错误次数,如果超过了限定次数,则抛出异常,用全局的异常拦截器,拦截此异常, 记录登录错误次数的异常, 并封装登录次数过多的提示,给客户端. 具体的代码在下面.

二.Controller层接收登录请求

 /**
     * 点击登录执行的动作
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String loginVali(HttpServletRequest request) {
   
        String username = super.getPara("username").trim();

        //获取加密的密码
        String password = super.getPara("password").trim();
  
        //获取Subjec 对象,用于登录和授权操作
        Subject subject = ShiroKit.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken(username, password.toCharArray());


        //执行登录验证,如果出现异常,代表登录失败
        subject.login(token);

        ShiroUser shiroUser = ShiroKit.getUser();
        super.getSession().setAttribute("shiroUser", shiroUser);
        super.getSession().setAttribute("username", shiroUser.getAccount());

        LogManager.me().executeLog(LogTaskFactory.loginLog(shiroUser.getId(), getIp()));

        ShiroKit.getSession().setAttribute("sessionFlag", true);

        //登录成功,进行重定向到 / 接口.  跳转到index.html 登录后的首页
        return REDIRECT + "/";
    }

三.自定义的Realm

import cn.stylefeng.roses.core.util.HttpContext;
import cn.stylefeng.roses.core.util.ToolUtil;
import cn.utry.govaffairs.core.shiro.service.UserAuthService;
import cn.utry.govaffairs.core.shiro.service.impl.UserAuthServiceServiceImpl;
import cn.utry.govaffairs.modular.system.model.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class ShiroDbRealm extends AuthorizingRealm {


    @Autowired
    private SessionDAO sessionDAO;

    /**
     * 登录认证  证明 鉴定
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
            throws AuthenticationException {

        // 获取shirorealm所需数据的Service层
        UserAuthService shiroFactory = UserAuthServiceServiceImpl.me();

        //获取Controller层传递的token,包含了前端输入的用户名和密码 一个简单的用户名/密码身份验证令牌
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;

        //获取登录的用户名
        String username = token.getUsername();

        //根据前端输入的账号, 去数据库查询用户信息. 此时如果账号不存在(包括逻辑删除)或被冻结,直接抛出异常,终止登录
        User user = shiroFactory.user(token.getUsername());

        //进行用户的验证
        ShiroUser shiroUser = shiroFactory.shiroUser(user);
		
		 // 获取需要登录的用户在数据库中存储的加盐的密码
        String credentials = user.getPassword();

        //  获取需要登录的用户在数据库中存储的密码的盐值
        String source = user.getSalt();
        ByteSource credentialsSalt = new Md5Hash(source);
        // 创建SimpleAuthenticationInfo 返回给shiro的安全管理器去比较当前登录用户输入的密码,与数据库中存储的加盐的密码是否一致
        // 即在登录的Controller层 UsernamePasswordToken 中存储了当前输入的用户名与密码, 与此SimpleAuthenticationInfo 进行比较
        // 如果密码一致,代表登录成功, 密码不一致,则报密码错误的异常
        return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
       
    }

    /**
     * 权限认证  授权,认可
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
       return null;
    }

四.密码验证器增加登录次数校验功能

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 org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 验证器,增加了登录次数校验功能
 */

public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {

    /**
     * 密码输入错误次数就被冻结
     */
    private Integer errorPasswordTimes=5;

    private Cache<String, AtomicInteger> passwordRetryCache;

    /**
     * 构造方法 创建对象,传入缓存的管理器
     * @param cacheManager
     */
    public RetryLimitCredentialsMatcher(CacheManager cacheManager) {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }

    /**
     * 方法名: doCredentialsMatch
     * 方法描述: 用户登录错误次数方法.
     * 修改日期: 2019/2/26 20:19
      * @param token
     * @param info
     * @return boolean
     * @throws
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token,
                                      AuthenticationInfo info) {
        String username = (String) token.getPrincipal();
        Set<String> keys = passwordRetryCache.keys();

        // retry count + 1
        AtomicInteger retryCount = passwordRetryCache.get(username);
        if (retryCount == null) {
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(username, retryCount);
        }
        if (retryCount.incrementAndGet() > errorPasswordTimes) {
            // if retry count > 5 throw
            throw new ExcessiveAttemptsException();
        }

        boolean matches = super.doCredentialsMatch(token, info);
        if (matches) {
            // clear retry count
            passwordRetryCache.remove(username);
        }
        return matches;
    }
}

五.ShiroConfig的配置类

在此配置类中, 要注意的是把ShiroDbRealm的bean中要调用set方法注入retryLimitCredentialsMatcher,否则密码错误次数的校验不会生效.

@Configuration
public class ShiroConfig {
	 /**
     * Shiro生命周期处理器:
     * 用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调(例如:UserRealm)
     * 在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调(例如:DefaultSecurityManager)
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 方法名: getDefaultAdvisorAutoProxyCreator
     * 方法描述:  开启Shiro的注解模式
     * 修改日期: 2019/2/25 16:03
      * @param
     * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
     * @author taohongchao
     * @throws
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }

    /**
     * 安全管理器
     */
    @Bean
    public DefaultWebSecurityManager securityManager(CookieRememberMeManager rememberMeManager,
                                                     CacheManager cacheShiroManager,
                                                     SessionManager sessionManager,
                                                     RetryLimitCredentialsMatcher retryLimitCredentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //把自定义的Realm注入安全管理器中
        securityManager.setRealm(this.shiroDbRealm(retryLimitCredentialsMatcher));
        securityManager.setCacheManager(cacheShiroManager);
        securityManager.setRememberMeManager(rememberMeManager);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

	  /**
     * 缓存管理器 使用Ehcache实现
     */
    @Bean
    public CacheManager getCacheShiroManager(EhCacheManagerFactoryBean ehcache) {
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManager(ehcache.getObject());
        ehCacheManager.setCacheManagerConfigFile("ehcache.xml");
        return ehCacheManager;
    }
	
	 @Bean
    public RetryLimitCredentialsMatcher getRetryLimit(CacheManager cacheManager){
        RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(cacheManager);
        retryLimitCredentialsMatcher.setHashAlgorithmName(ShiroKit.HASH_ALGORITHM_NAME);
        retryLimitCredentialsMatcher.setHashIterations(ShiroKit.HASHITERATIONS);
        retryLimitCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return retryLimitCredentialsMatcher;
    }
    /**
     * 项目自定义的Realm
     */
    @Bean
    public ShiroDbRealm shiroDbRealm(RetryLimitCredentialsMatcher retryLimitCredentialsMatcher) {
        ShiroDbRealm shiroDbRealm = new ShiroDbRealm();
        shiroDbRealm.setCredentialsMatcher(retryLimitCredentialsMatcher);
        return shiroDbRealm;
    }
}

六.EhCache 的配置

EhCache.xml中的配置, 其中设置了名称为passwordRetryCache的缓存,用于冻结密码输入错误次数多过的缓存.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="true" >
         
    <diskStore path="java.io.tmpdir/ehcache"/>

    <defaultCache
            maxElementsInMemory="50000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="true"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />

    <!-- 登录记录缓存 锁定10分钟 -->
    <cache name="passwordRetryCache"
           eternal="false"
           timeToIdleSeconds="600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true"
           maxEntriesLocalHeap="0">
    </cache>

</ehcache>

EhCacheConfig的配置类

import net.sf.ehcache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

/**
 * ehcache配置
 *
 * @author
 * @date 2017-05-20 23:11
 */
@Configuration
@EnableCaching
public class EhCacheConfig {

    /**
     * EhCache的配置
     */
    @Bean
    public EhCacheCacheManager cacheManager(CacheManager cacheManager) {
        return new EhCacheCacheManager(cacheManager);
    }

    /**
     * EhCache的配置
     */
    @Bean
    public EhCacheManagerFactoryBean ehcache() {
        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        return ehCacheManagerFactoryBean;
    }
}

七. 全局异常的配置

当密码输入错误次数过多时,抛出ExcessiveAttemptsException异常,被此异常拦截器拦截

@ControllerAdvice
@Order(-1)
public class GlobalExceptionHandler {
	/**
     * 方法名: excessiveAttemptsException
     * 方法描述:  登录错误次数过多异常	
     * @throws
     */
    @ExceptionHandler(ExcessiveAttemptsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String excessiveAttemptsException(ExcessiveAttemptsException e, Model model) {
        String username = getRequest().getParameter("username");
        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "登录错误次数超过五次", getIp()));
        model.addAttribute("tips", "登录错误次数超过五次,请十分钟后登录!");
        return "/login.html";
    }
}

最终的效果如图所示

猜你喜欢

转载自blog.csdn.net/qq_33229669/article/details/87945020