springboot整合shiro最全功能

springboot整合shiro,实现登录认证,登录过期,权限拦截,单点登录

1、权限控制的两种方式:粗粒度基于URL级别权限控制、细粒度基于方法级别权限控制
2、基于Apache Shiro实现登录认证和权限控制,重点讲解shiro权限控制流程、自定义Realm对象控制系统认证和授权
3、Apache Shiro实现细粒度方法级别权限控制
4、动态系统菜单显示功能
5、对认证和授权数据进行缓存优化

粗粒度 URL 级别权限控制(基于页面的访问路径)

在这里插入图片描述
用户发送请求,访问页面,通过配置的Filter过滤器进行拦截,判断当前用户是否具有访问页面(Url)的权限(根据shiro配置,那些页面/资源需要进行认证,那些页面/资源可以直接访问),如果有放行,如果没有提示权限不足,禁止访问
可以基于 Filter 实现(基于页面访问路径)在数据库中存放 用户、权限、访问 URL 对应关系, 当前用户访问一个 URL 地址,查询数据库判断用户当前具有权限,是否包含这个 URL,如果包含允许访问,如果不包含 权限不足 !!!

细粒度方法级别权限控制(基于HTTP方法的访问)

在这里插入图片描述
用户发送请求,访问方法,在方法的Service(业务逻辑层),通过注解方式,判断此用户是否具有执行方法的权限,有就放行,没有进行拦截
可以代理、自定义注解实现,访问目标对象方法,在方法上添加权限注解信息,对目标对象创建代理对象,访问真实对象先访问代理对象,在代理对象查询数据库判断是否具有注解上描述需要权限,具有权限允许访问,不具有权限,拦截访问,提示权限不足

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

Apache Shiro 组件介绍

在这里插入图片描述
1) Authentication 认证: 用户认证,身份识别 (你是谁?)
2) Authorization 授权:用户具有哪些权限、角色 (你能做什么?)
3) Cryptography安全数据加密
4) Session Management 会话管理
5) Web Integration web系统集成
6) Integrations 集成其他应用,spring、缓存框架

ApacheShiro运行流程和权限控制方式分析

在这里插入图片描述
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自定义标签实现,页面显示权限控制

引入maven依赖

<!--springboot整合shiro -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.3.2</version>
		</dependency>
		<!-- shiro 缓存框架 -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>1.4.0</version>
		</dependency>
		<!-- thymeleaf支持shiro注解 -->
		<dependency>
			<groupId>com.github.theborakompanioni</groupId>
			<artifactId>thymeleaf-extras-shiro</artifactId>
			<version>2.0.0</version>
		</dependency>

配置shiroconfig 核心配置类

@Configuration
public class ShiroConfig {

    @Bean // shiroFilter 过滤器
    public ShiroFilterFactoryBean shirFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        System.out.println("====ShiroConfiguration.shirFilter()====");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
        shiroFilterFactoryBean.setLoginUrl("/");// 设置首页
        // 登录成功后要跳转的链接
        // shiroFilterFactoryBean.setSuccessUrl("/login/home");
        // // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/shiroError");
        // 添加shiro内置过滤器 filterChainDefinitionMap
        /*
         * anon:表示可以匿名使用。 authc:表示需要认证(登录)才能使用,没有参数
         * roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[
         * "admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
         * perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[
         * "user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
         * rest:根据请求的方法,相当于/admins/user/**=perms[user:method]
         * ,其中method为post,get,delete等。
         * port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,
         * 其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,
         * queryString是你访问的url里的?后面的参数。 authcBasic:没有参数表示httpBasic认证
         * ssl:表示安全的url请求,协议为https user:当登入操作时不做检查
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/", "anon"); // 可以匿名访问"/"方法
              filterChainDefinitionMap.put("/kickout", "anon"); //踢出用户方法,跳转到登录页
        filterChainDefinitionMap.put("/add", "authc"); // 必须认证才能访问 测试用
        filterChainDefinitionMap.put("/update", "authc"); // 必须认证才能访问 测试用
        filterChainDefinitionMap.put("/tologin", "anon");// 可以匿名访问"/tologin"方法
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/image/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/plugins/**", "anon");

        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout"); // 退出登录方法
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
       //user  验证通过或RememberMe登录的都可以 kickout 自定义过滤器(需要验证并发登录,踢出重复登录)
filterChainDefinitionMap.put("/**", "user,kickout");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//自定义拦截器  key:名称 value:filter(过滤器)
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
//设置自定义过滤器到shiro过滤器工厂
shiroFilterFactoryBean.setFilters(filtersMap);
        return shiroFilterFactoryBean;

    }

    /**
     * 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

/**
 * 自定义retryLimitCredentialsMatcher的作用是为了设置一个和密码加密算法一样的算法,替代了凭证管理器HashedCredentialsMatcher 
 * 在注册用户时会根据设置的加密方式和次数对明文密码加密保存
 * 在登录的时候会根据用户密码加密匹配DB密码,同时记录登录次数,超过限制次数限制登录时间
 */
@Bean
public RetryLimitCredentialsMatcher getRetryLimit(){
    //构造方法实例化RetryLimitCredentialsMatcher对象,从指定cache缓存中获取缓存信息
    RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(cacheManager());
    //设置缓存加密方式 默认MD5
    retryLimitCredentialsMatcher.setHashAlgorithmName("md5");
    //设置缓存加密次数
    retryLimitCredentialsMatcher.setHashIterations(2);
    retryLimitCredentialsMatcher.setStoredCredentialsHexEncoded(true);
    return retryLimitCredentialsMatcher;
}

    @Bean // 自定义Realm
    public ShiroRealm myShiroRealm() {
        //自定义Realm
        ShiroRealm myShiroRealm = new ShiroRealm();
        //设置MD5加密规则  在Realm登录认证的时候会加密用户输入的密码,是否和保存DB的密码一致
       //        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
//设置加密规则 在Realm登录认证的时候会加密用户输入的密码,是否和保存DB的密码一致,同时记录登录次数,限制多次登录
myShiroRealm.setCredentialsMatcher(getRetryLimit());
        return myShiroRealm;
    }

    /**
     * cookie对象; rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
     *
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        // 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setSecure(true);
        // 只能通过httpservlet访问,无法通过js脚本将无法读取到cookie信息
        simpleCookie.setHttpOnly(true);
        // <!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(60 * 10);
        return simpleCookie;
    }

    // 记住我的配置
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        rememberMeManager.setCookie(rememberMeCookie());
        rememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return rememberMeManager;
    }

    /**
     * 自定义shiroSession的cookie,如果不设置默认JSESSIONID,会与tomcat等默认cookie名重复,sessionIdCookie用于保存sessionId标识
     *
     * @return
     */
    @Bean
    public SimpleCookie sessionIdCookie() {
        // 这个参数是cookie的名称
        SimpleCookie simpleCookie = new SimpleCookie("sid");
        // setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
        // 设为true后,只能通过http访问,javascript无法访问
        // 防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        // simpleCookie.setPath("/");
        // maxAge=-1表示浏览器关闭时失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }

    /**
     * 配置session监听
     * shiroSession时间SessionListener监听,原来的HttpSessionListener接口监听Session的创建和失效
     *
     * @return
     */
    @Bean("sessionListener")
    public ShiroSessionListener sessionListener() {
        ShiroSessionListener sessionListener = new ShiroSessionListener();
        return sessionListener;
    }

    /**
     * 配置会话ID生成器
     *
     * @return
     */
    @Bean
    public SessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    // shiro 缓存
    @Bean
    public EhCacheManager cacheManager() {
        EhCacheManager cache = new EhCacheManager();
        // 设置ehcache缓存的配置文件
        cache.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
        return cache;
    }

    /**
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 MemorySessionDAO
     * 直接在内存中进行会话维护 EnterpriseCacheSessionDAO
     * 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
     * CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager
     *
     * @return
     */
    @Bean
    public SessionDAO sessionDAO() {
        EnterpriseCacheSessionDAO enterpriseCacheSessionDAO = new EnterpriseCacheSessionDAO();
        // 使用ehCacheManager 设置缓存配置
        enterpriseCacheSessionDAO.setCacheManager(cacheManager());
        // 设置session缓存的名字
        // 默认为shiro-activeSessionCache,在cacheManager缓存配置xml中name已经定义了
        // enterpriseCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
        // sessionId生成器  生成的sessionId
        enterpriseCacheSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        return enterpriseCacheSessionDAO;
    }

    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话
        sessionManager.setSessionValidationSchedulerEnabled(true); // session可以使用该定时调度器进行检测
        sessionManager.setSessionValidationInterval(10000); // 多长时间检查一次session有效性
        // 会话超时时间,单位:毫秒
        sessionManager.setGlobalSessionTimeout(180000); // 超时时间3600秒
        // 去掉shiro登录时url里的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        // 开启自定义cookie 指定sessionid
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(sessionIdCookie());

        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        // 配置监听
        listeners.add(sessionListener());
        sessionManager.setSessionListeners(listeners);
        // 设置session缓存持久化到内存
        sessionManager.setSessionDAO(sessionDAO());
        // Shiro提供SessionDAO用于会话的CRUD
        // sessionManager.setCacheManager(cacheManager());
        return sessionManager;
    }

    @Bean // 安全管理器
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义realm.  全局安全管理期设置自定义realm
        securityManager.setRealm(myShiroRealm());
        // 配置自定义session管理 全局安全管理期设置session管理
        securityManager.setSessionManager(sessionManager());
        // 配置记住我 全局安全管理器设置记住我
        securityManager.setRememberMeManager(rememberMeManager());
        // 全局安全管理器设置缓存
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

/**
 * 限制同一账号登录同时登录人数控制
 * @return 过滤器
 */
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
    KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
    //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
    //这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
    //也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
    kickoutSessionControlFilter.setCacheManager(cacheManager());
    //用于根据会话ID,获取会话进行踢出操作的;
    kickoutSessionControlFilter.setSessionManager(sessionManager());
    //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
    kickoutSessionControlFilter.setKickoutAfter(false);
    //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
    kickoutSessionControlFilter.setMaxSession(1);

    //被踢出后重定向到的地址;
    kickoutSessionControlFilter.setKickoutUrl("/kickout");
    return kickoutSessionControlFilter;
}

    /**
     * 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持,解决spingboot环境中使用Shiro注解(如@RequiresRoles,@RequiresPermissions)无效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        sourceAdvisor.setSecurityManager(securityManager);
        return sourceAdvisor;
    }

    /**
     * thymeleaf模板使用shiro注解
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() { // thymeleaf模板使用shiro注解
        return new ShiroDialect();
    }

}

自定义KickoutSessionControlFilter 继承shiro拦截器AccessControlFilter

判断登录用户是否重复登录,踢出最早登录的

/**
 * shiro强大的自定义访问控制拦截器:AccessControlFilter
 * isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
 *
 * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
 *
 * onPreHandle:会自动调用这两个方法决定是否继续处理;
 *
 */
public class KickoutSessionControlFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);


    /**
     * 踢出后到的地址
     */
    private String kickoutUrl;

    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickoutAfter = false;
    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;

    private String kickoutAttrName = "kickout";

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /**
     *     设置Cache的key的前缀  shiro-kickout-session cache配置
     */
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("shiro-kickout-session");
    }

    @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
            throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
            throws Exception {
        Subject subject = getSubject(request, response);
        //isAuthenticated 成功登录 isRemembered 被记忆的
        if(!subject.isAuthenticated() && !subject.isRemembered())
        {
            //如果没有登录,直接进行之后的流程
            return true;
        }

        Session session = subject.getSession();
        User user = (User) subject.getPrincipal();
        String username = user.getUserName();
        Serializable sessionId = session.getId();

        logger.info("进入KickoutControl, sessionId:{}", sessionId);
        //读取缓存 根据用户名 查询缓存cache对象 获取value(缓存的sessionId) 没有就存入
        Deque<Serializable> deque = cache.get(username);
        if(deque == null) {
            deque = new LinkedList<Serializable>();
            cache.put(username, deque);
        }

        //如果队列里没有此sessionId,且用户没有被踢出;放入队列deque
        if(!deque.contains(sessionId) && session.getAttribute(kickoutAttrName) == null) {
            //将sessionId存入队列
            deque.push(sessionId);
        }
        logger.info("缓存登录用户的SessionId--deque.size:{}",deque.size());
        //如果队列里的sessionId数超出最大会话数,开始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) {
                //如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else {
                //否则踢出前者
                kickoutSessionId = deque.removeLast();
            }

            //踢出后再更新下缓存队列
            cache.put(username, deque);
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute(kickoutAttrName, true);
                }
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute(kickoutAttrName) != null && (Boolean)session.getAttribute(kickoutAttrName) == true) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) {
                logger.warn(e.getMessage());
                e.printStackTrace();
            }
            saveRequest(request);
            //重定向
            logger.info("用户登录人数超过限制, 重定向到{}", kickoutUrl);
            String reason = URLEncoder.encode("账户已超过登录人数限制", "UTF-8");
            String redirectUrl = kickoutUrl  + (kickoutUrl.contains("?") ? "&" : "?") + "shiroLoginFailure=" + reason;
            WebUtils.issueRedirect(request, response, redirectUrl);
            return false;
        }
        return true;
    }

}

自定义RetryLimitCredentialsMatcher 继承HashedCredentialsMatcher

实现明文密码加密,登录密码匹配,限制登录尝试次数,5次后限制10分钟登录

**
 * @author 张江丰
 * 密码验证+登录次数限制
 */
@Component
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
    /**
     * 密码输入错误次数就被冻结
     */
    private Integer errorPasswordTimes=5;

    /**
     * 缓存对象Cache 根据缓存配置名称:passwordRetryCache 获取指定的缓存信息
     */
    private Cache<String, AtomicInteger> passwordRetryCache;

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

    /**
     * 方法名: doCredentialsMatch
     * 方法描述: 用户登录错误次数方法.
     * @param token  登录用户信息对象
     * @param info   shiro认证用户信息对象
     * @return boolean
     * @throws
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token,
                                      AuthenticationInfo info) {
        logger.info("密码登录次数验证执行了......");
        //获取登录用户名
        String username = (String) token.getPrincipal();
        //passwordRetryCache(ehcache缓存对象),获取所有keys(缓存的用户)的set集合
//        Set<String> keys = passwordRetryCache.keys();

        //AtomicInteger 高并发下使用的线程安全的int类  从passwordRetryCache缓存map结构中 根据key(登录用户名)获取value(登录次数)
        AtomicInteger retryCount = passwordRetryCache.get(username);
        //retryCount=null  第一次登录 设置登录对象的缓存信息
        if (retryCount == null) {
            retryCount = new AtomicInteger(0);
            //设置缓存信息 用户名:key 登录次数:value
            passwordRetryCache.put(username, retryCount);
        }

        //retryCount.incrementAndGet()方法,每次判断都会把用户的登录次数缓存+1,如果登录成功会清空缓存信息
        //如果登录次数>5次,抛出异常在登录的controller拦截此异常处理
        if (retryCount.incrementAndGet() > errorPasswordTimes) {
            // if retry count > 5 throw
            throw new ExcessiveAttemptsException();
        }
        //匹配token(登录用户信息)和info(shiro认证信息),这里token密码加盐与info比较
        boolean matches = super.doCredentialsMatch(token,info);
        if (matches) {
            // clear retry count  如果信息匹配上(登录成功)  删除缓存里的用户信息
            passwordRetryCache.remove(username);
        }
        return matches;
    }
}

自定义Relam 实现AuthorizingRealm接口,重写授权和认证的逻辑

public class ShiroRealm extends AuthorizingRealm {

    private static final Log logger = LogFactory.getLog(ShiroRealm.class);
    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private PermissionsService permissionsService;

    /**
     * @author 张江丰 授权 完成登录认证后会执行授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("shiro 授权管理");
        // 授权管理对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 查询出用户
        User user = (User) principals.getPrimaryPrincipal();
        if (user.getUserName().equals("admin")) {
            // 设置超级管理员
            info.addRole("admin");
            info.addStringPermission("admin");
        } else {
            // List<Permissions> pers = null;
            // 查询用户-->角色
            List<Role> roles = roleService.userByRole(user.getId());
            // info.addRoles((Collection) roles);
            for (Role role : roles) {
                // 查询角色对应的权限
                info.addRole(role.getDescr());
                // pers.addAll(permissionsService.userBypermisson(role.getId()));
                List<Permissions> userBypermisson = permissionsService.userBypermisson(role.getId());
                for (Permissions permissions : userBypermisson) {
                    info.addStringPermission(permissions.getDescr());
                }
            }
        }
        // return null;

        return info;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("shiro 认证管理");
        // 获取用户登录信息
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        User user = userService.findByUsername(usernamePasswordToken.getUsername());
        if (user == null) {
            // 用户名不存在 抛出指定异常,跳转到登录页面
            return null;
        } else {
            setSession("user", user);
            //获取盐值 与用户输入密码进行加密是否匹配DB密码  ps:salt盐值,是新增用户时随机创建并保存的数据
            ByteSource salt = ByteSource.Util.bytes(user.getSalt());
            //身份认证返回对象 参数:s1 用户对象 s2:用户密码  s3:加密盐值 s4:realm名称
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
            return simpleAuthenticationInfo;
        }
    }


    /**
     * 将一些数据放到ShiroSession中,以便于其它地方使用,将用户存放到session中
     *
     * @see // 比如Controller,使用时直接用HttpSession.getAttribute(key)就可以取到
     */
    private void setSession(Object key, Object value) {
        Subject currentUser = SecurityUtils.getSubject();
        if (currentUser != null) {
            Session session = currentUser.getSession();
            System.out.println("==========" + session.getId() + "=============");
            if (null != session) {
                // 2小时
                // session.setTimeout(60 * 10);// 3600秒超时
                session.setAttribute(key, value);
            }
        }
    }

}

配置自定义的shirosession拦截器 实现SessionListener接口

项目启动,用户访问创建session,就会触发自定义ShiroSessionListener,分别拦截session创建,退出,销毁方法

/**
 * shiroSession时间SessionListener监听,原来的HttpSessionListener接口监听Session的创建和失效
 * 
 * @author 张江丰
 *
 */
public class ShiroSessionListener implements SessionListener {

   private static final Log logger = LogFactory.getLog(ShiroSessionListener.class);

   /**
    * 统计在线人数 juc包下线程安全自增
    */
   private final AtomicInteger sessionCount = new AtomicInteger(0);

   /**
    * 会话创建时触发
    * 
    * @param session
    */
   @Override
   public void onStart(Session session) {
      // 会话创建,在线人数加一
      sessionCount.incrementAndGet();
      logger.info("=============shiroSession会话创建成功" + session.getId() + "=================");
   }

   /**
    * 退出会话时触发
    * 
    * @param session
    */
   @Override
   public void onStop(Session session) {
      // 会话退出,在线人数减一
      sessionCount.decrementAndGet();
      logger.info("==============shiroSession会话退出" + session.getId() + "================");
   }

   /**
    * 会话过期时触发
    * 
    * @param session
    */
   @Override
   public void onExpiration(Session session) {
      // 会话过期,在线人数减一
      sessionCount.decrementAndGet();
      logger.info("==============shiroSession会话过期" + session.getId() + "================");
   }

   /**
    * 获取在线人数使用
    * 
    * @return
    */
   public AtomicInteger getSessionCount() {
      return sessionCount;

   }

}

ehcache 缓存xml配置,shiro自带的缓存框架,用于持久化ShiroSession信息

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
   <!--
       name:缓存名称。
       maxElementsInMemory:缓存最大数目
       maxElementsOnDisk:硬盘最大缓存个数。
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。
       overflowToDisk:是否保存到磁盘,当系统当机时
       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
        clearOnFlush:内存数量最大时是否清除。
         memoryStoreEvictionPolicy:
            Ehcache的三种清空策略;
            FIFO,first in first out,这个是大家最熟的,先进先出。
            LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
            LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
    -->
   <diskStore path="java.io.tmpdir/ehcache"/>
   <!--默认缓存 -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
     />
  <!-- 登录记录缓存 锁定10分钟 -->
     <cache name="passwordRetryCache"
        maxElementsInMemory="2000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true" >
     </cache>
   <!-- 用户队列缓存10分钟,缓存登录用户,踢出重复登录 -->
   <cache name="shiro-kickout-session"
          maxEntriesLocalHeap="2000"
          eternal="false"
          timeToIdleSeconds="3600"
          timeToLiveSeconds="0"
          overflowToDisk="false"
          statistics="true">
   </cache>
</ehcache>

自定义shiro全局异常处理,通过Controller层继承,子类在抛出shiro异常后,父类进行处理

/**
 * shiro 全局异常处理 Controller继承BaseController公共异常类,在抛出异常时会拦截异常处理器
 *
 * @ControllerAdvice aop切面技术,此注解相当于声明了切面 basePackages = "com.supplier.utils"相当于切点指定捕获异常的类(可以省略,spring自动扫描识别) @ExceptionHandler相当于通知 具体方法
 * @ExceptionHandler 全局异常处理器  全局异常捕获,捕获规则 s1:根据异常继承的层级关系优先捕获被父类捕获) s2:子类抛异常只能小于或等于父类的异常
 *                   登录认证异常:UnauthenticatedException,AuthenticationException
 *                   权限认证异常:UnauthorizedException,AuthorizationException
 *
 * @author 张江丰
 *
 */
@ControllerAdvice(basePackages = "com.supplier.utils")
public class BaseController {
   /**
    * 登录认证异常
    * 
    * @param request
    * @param response
    * @return
    */
   // @ExceptionHandler({ UnauthenticatedException.class,
   // AuthenticationException.class })
   // public String authenticationException(HttpServletRequest request,
   // HttpServletResponse response) {
   // Map<String, Object> map = new HashMap<>();
   // map.put("status", "-1000");
   // map.put("message", "未登录");
   // writeJson(map, response);
   // return null;
   // }

   /**
    * 权限异常
    *
    * @ExceptionHandler 全局异常捕获,捕获规则(根据异常继承的层级关系优先捕获被父类捕获),子类抛异常只能小于或等于父类的异常
    * UnauthorizedException   父类拦截未授权异常
    * AuthorizationException  父类拦截授权异常(不匹配)
    * @return
    */
   @ExceptionHandler({ UnauthorizedException.class, AuthorizationException.class })
   public void authorizationException(HttpServletRequest request, HttpServletResponse response) {
      Map<String, Object> map = new HashMap<>();
      map.put("code", "-1001");
      map.put("msg", "无操作权限");
      writeJson(map, response);
   }

   private void writeJson(Map<String, Object> map, HttpServletResponse response) {
      // 字符打印流
      PrintWriter out = null;
      try {
         response.setCharacterEncoding("UTF-8");
         response.setContentType("application/json; charset=utf-8");
         out = response.getWriter();
         JSONObject mapJson = JSONObject.fromObject(map);
         String string = mapJson.toString();
         out.write(string);
      } catch (IOException e) {
      } finally {
         if (out != null) {
            out.close();
         }
      }
   }

}

controller层调用,实现登录,退出,创建用户

/**
 * @author 张江丰
 */
@Controller
public class UserController extends BaseController {
    private static final Log logger = LogFactory.getLog(UserController.class);

    @Autowired
    private UserService userService;
    @Autowired
    private RedisUtil redisUtil;
    // @Autowired
    // private CookieUtil cookieUtil;

    @RequestMapping("/")
    public String hello() {
        logger.info("-----------首页入口执行-----------");
        return "login";
    }

     @RequestMapping("/kickout")
     public String kickout() {
          logger.info("-----------踢出重复登录用户-----------");
          return "login";
     };

    /**
     * shiro 权限注解 value可以多值通过","分开,logical表示权限是并且和或的关系
新增用户
     *
     * @return
     */
    @RequiresPermissions(value = {"admin"}, logical = Logical.OR)
    @RequestMapping("/add")
    @ResponseBody
        String userName = request.getParameter("userName");
        String password = request.getParameter("password");
        //随机生成盐值  salt
        String salt = getRandomString();
        //原生密码混合salt
//        password=salt.charAt(1)+salt.charAt(3)+password+salt.charAt(5)+salt.charAt(6);
        //调用封装shiro的MD5加密方法(s1:密码,s2:随机盐值,s3:加密次数)
        String Md5Password = getPassword(password, salt, 2);
        User user = new User();
        user.setUserName(userName);
        user.setPassword(Md5Password);
        user.setPhone("15629048422");
        user.setIdNumber("420116199308160074");
        user.setCreateTime(new Date());
        user.setSalt(salt);
        user.setNickName("汪汪汪");
        user.setState("0");
        user.setGroupId(1);
        user.setSuperMan("0");
        user.setParentId(1);

        userService.add(user);
        return "新增方法";
    }

    /**
     * 密码加密方法
     * @param password 密码
     * @param salt 盐值
     * @param hashTimes 加密次数
     * @return
     */
    public static String getPassword(String password,String salt,int hashTimes){
//        SimpleHash md5Hsh = new SimpleHash(password,salt,hashTimes);
        Md5Hash md5Hash = new Md5Hash(password,salt,hashTimes);
        return md5Hash.toString();
    }

    /**
     *  盐值字符串
     */
    String range;

    {
        range = "0123456789abcdefghijklmnopqrstuvwxyz";
    }

    /**
     * 获取随机盐值
     * @return
     */
    public String getRandomString() {
        Random random = new Random();

        StringBuffer result = new StringBuffer();
        //要生成几位,就把这里的数字改成几
        for (int i = 0; i < 7; i++) {

            result.append(range.charAt(random.nextInt(range.length())));

        }

        return result.toString();
    }
    //登录
    @RequestMapping("/tologin")
    @ResponseBody
    public CommonResponse login(HttpServletRequest request, HttpServletResponse response,
                                Map<String, Object> paramMap) {
        String username = request.getParameter("userName");
        String password = request.getParameter("password");
        boolean rememberMe = request.getParameter("rememberMe") != null;
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // RememberMe这个参数设置为true后,在登陆的时候就会在客户端设置remenberme的相应cookie。
        // 下次访问带上这个cookie,访问链接为user链接器的,就不需要进行登录验证,直接进入权限验证。
        if (rememberMe) {
            token.setRememberMe(true);
        }
        Subject subject = SecurityUtils.getSubject();
        String error = null;
        User user = null;
        try {
            subject.login(token);
            //这里的catch到的异常会被继承的父类BaseController处理
     } catch (UnknownAccountException | IncorrectCredentialsException e) {
            error = "用户名或密码错误";
         }catch (ExcessiveAttemptsException e){
            error ="登录错误次数超过五次,请十分钟后登录!";
        } catch (AuthenticationException e) {
            error = "其它错误:" + e.getMessage();
        }
        logger.error("错误信息:" + error);
        //获取shiro保存的用户
        user = (User) subject.getPrincipal();
        if (error != null) {
            paramMap.put("error", error);
            return CommonResponseUtil.success(paramMap);
            // return "login";
        } else {

            paramMap.put("user", user.getNickName());
            return CommonResponseUtil.success(paramMap);
            // return "admin/index";
        }

    }

    @RequestMapping("/logout")
    @ResponseBody
    public CommonResponse logout(){
        logger.info("执行shiro的logout退出登录操作!");
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return  CommonResponseUtil.success("退出成功!");

    }

}
Springboot整合shiro全功能,欢迎提问交流!!!
发布了32 篇原创文章 · 获赞 53 · 访问量 2496

猜你喜欢

转载自blog.csdn.net/qq_41714882/article/details/102902705
今日推荐