zoukankan      html  css  js  c++  java
  • 【shiro】初识与集成

    个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

    如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充

    前言

    权限管理对于一个成熟的项目是极为基础且必要的部分,我们必须知道来者何人,才能判断这个人能做什么不能做什么

    而shiro正是解决方案中最常见的一种

    请留意,本文主要整理shiro相关思路,为方便理解,示例代码并不完整,更谈不上严谨

    若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云

    1.介绍

    • shiro是Apache下的一个开源项目,提供认证、授权、加密和会话管理等功能
    • shiro属于轻量级框架,相当于SpringSecurity的精简版,更加轻便,当然内容也更少,不过足以应付大量场景

    2.结构

    shiro的三大核心组件为Subject、SecurityManager 和 Realm

    2.1.Subject:认证主体

    包括两个信息,便可辨认出需要认证的身份

    • Principals:身份。用于标识登录主体,通常为用户名、手机号等等

    • Credentials:凭证。用于验证主体身份,通常为密码、数字证书等等

    2.2.SecurityManager:安全管理器

    为shiro的核心,负责管理所有的subject和与之先关的交互操作

    2.3.Realm:数据域

    即数据来源,shiro会从这里回去安全数据,用于验证身份和分配权限,通常使用缓存或者数据库来实现(数据库和redis都是常见的实现方案)

    可以看出,shiro并不提供和维护安全数据,仅仅是进行验证身份和分配权限,需要开发者去维护安全数据

    3.细节功能

    • Authentication:身份认证/登录(账号密码验证)。

    • Session Manager:会话管理,用户登录后的session相关管理。

    • Cryptography:加密,密码加密等。

    • Web Support:Web支持,集成Web环境。

    • Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。

    • Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。

    • Testing:测试支持;

    • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

    • Remember Me:记住我,登录后,下次再来的话不用登录了。

    4.主要业务

    4.1.拦截

    拦截器会拦截请求,并根据配置,决定下一步业务是拒绝访问重定向进入控制层/业务层

    4.2.登录

    流程如下:

    • 控制层/业务层根据用户传递的数据创建token,即认证主体Subject
    • 控制层/业务层发起登录行为
    • shiro根据token中的数据,查询数据库账号相关数据,生成认证信息。没有数据则认证失败
    • shiro将认证信息认证主体进行匹配,不匹配则认证失败
    • shiro通过认证主体中的信息,查询数据库里权限数据,并将其写入数据域保存

    换言之,通过用户传递的数据生成认证主体,再通过查询数据库生成认证信息,对两者进行匹配,成功就再查询权限信息保存,不匹配就再见

    4.3.身份验证

    流程如下:

    • 根据请求头中的token在数据域查询认证信息,未查询到则认证失败
    • 对认证信息中的角色、权限或其他自定义信息进行匹配,任意一个不符合则认证失败
    • 认证成功则进入控制层/业务层,且仍可通过代码控制进行身份验证和认证数据读取

    也就是,通过token查询认证相关数据,没查到就是没认证

    5.集成

    5.1.添加依赖

     <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.5.3</version>
    	</dependency>
    

    5.2.建立相关数据库表

    此处建立三张表:用户表user_info、角色表role_info、权限表permission_info,并生成对应实体类

    (为方便调试,仅生成实体类,并模拟查询过程)

    • UserInfo

      @Data
      @AllArgsConstructor
      public class UserInfo {
          /**
           * 主键id
           */
          private Integer id;
      
          /**
           * 用户名
           */
          private String username;
      
          /**
           * 密码
           */
          private String password;
      
          /**
           * 外键关联role表
           */
          private Integer roleId;
      }
      
    • RoleInfo

      @Data
      @AllArgsConstructor
      public class RoleInfo {
          /**
           * 主键id
           */
          private Integer id;
      
          /**
           * 角色名称
           */
          private String roleName;
      }
      
    • PermissionInfo

      @Data
      @AllArgsConstructor
      public class PermissionInfo {
          /**
           * 主键id
           */
          private Integer id;
      
          /**
           * 权限名
           */
          private String permissionName;
      
          /**
           * 外键关联role
           */
          private Integer roleId;
      }
      
    • 查询方法(模拟查询,方便调试)

          public UserInfo getUserInfo(String userName) {
              UserInfo userInfo;
              switch (userName) {
                  case "user1":
                      userInfo = new UserInfo(1, "user1", "pwd1", 1);
                      break;
                  case "user2":
                      userInfo = new UserInfo(2, "user2", "pwd2", 2);
                      break;
                  case "user3":
                      userInfo = new UserInfo(3, "user3", "pwd3", 3);
                      break;
                  default:
                      userInfo = null;
                      break;
              }
              return userInfo;
          }
      
          public List<PermissionInfo> getPermissionList(String userName) {
              List<PermissionInfo> list = new ArrayList<>();
              list.add(new PermissionInfo(1, "student", 1));
              list.add(new PermissionInfo(2, "teacher", 2));
              list.add(new PermissionInfo(3, "teacher", 3));
              list.add(new PermissionInfo(4, "student", 3));
      
              UserInfo userInfo = getUserInfo(userName);
              List<PermissionInfo> result = new ArrayList<>();
              for (PermissionInfo permissionInfo : list) {
                  if (permissionInfo.getRoleId() == userInfo.getRoleId()) {
                      result.add(permissionInfo);
                  }
              }
              return result;
          }
      

      如上述数据中

      • user1拥有student权限
      • user2拥有teacher权限
      • user3拥有两种权限student&teacher

    5.3.自定义Realm

    集成AuthorizingRealm类,重写两个方法

    • doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
    • doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
    package com.yezi_tool.basic_project.shiro;
    
    import com.yezi_tool.basic_project.commons.model.UserRealmInfo;
    import com.yezi_tool.basic_project.entity.PermissionInfo;
    import com.yezi_tool.basic_project.entity.RoleInfo;
    import com.yezi_tool.basic_project.entity.UserInfo;
    import lombok.extern.slf4j.Slf4j;
    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.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    /**
     * @author Echo_Ye
     * @title 自定义realm
     * @description 自定义数据域
     * @date 2020/8/17 9:25
     * @email echo_yezi@qq.com
     */
    @Slf4j
    public class MyRealm extends AuthorizingRealm {
        /**
         * 分配权限
         *
         * @param principalCollection 数据源
         * @return 权限和角色信息
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            log.info("-----------------访问授权---------------------");
            //认证信息未登录,可能是用户非正常退出
            if (!SecurityUtils.getSubject().isAuthenticated()) {
                doClearCache(principalCollection);
                SecurityUtils.getSubject().logout();
                return null;
            }
            //获取认证主体
            String username = (String) principalCollection.getPrimaryPrincipal();
            if (username == null) {
                //认证错误
                return null;
            }
            //添加角色和权限
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<PermissionInfo> permissionInfoList = getPermissionList(username);
            Set<String> permissionSet = permissionInfoList.stream().map(m -> m.getPermissionName()).collect(Collectors.toSet());
            simpleAuthorizationInfo.setStringPermissions(permissionSet);
            return simpleAuthorizationInfo;
        }
    
        /**
         * 进行认证
         *
         * @param authenticationToken 数据源
         * @return 认证结果
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
            // 根据 Token 获取数据
            String username = (String) authenticationToken.getPrincipal();
            // 根据用户名从数据库中查询该用户
            UserInfo user = getUserInfo(username);
            if (user != null) {
                // 把当前用户存到 Session 中
                SecurityUtils.getSubject().getSession().setAttribute("user", user);
                // 传入用户名和密码进行身份认证,并返回认证信息
                AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), this.getName());
                return authInfo;
            } else {
                //未查找到用户信息,认证失败
                return null;
            }
        }
    }
    

    5.4.配置shiro

    package com.yezi_tool.basic_project.config;
    
    import com.yezi_tool.basic_project.shiro.MyRealm;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author Echo_Ye
     * @title shiro配置
     * @description shiro相关配置
     * @date 2020/8/17 11:28
     * @email echo_yezi@qq.com
     */
    @Slf4j
    @Configuration
    public class ShiroConfig {
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            // 定义bean
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // 必须设置 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
            shiroFilterFactoryBean.setLoginUrl("/login.html");
            // 认证失败url
            shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
    
            // 设置拦截器
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 开放静态资源权限
            filterChainDefinitionMap.put("/webjars/**", "anon");
            filterChainDefinitionMap.put("/assets/**", "anon");
            filterChainDefinitionMap.put("/img/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/css/**", "anon");
            // 开放swagger页面权限
            filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
            filterChainDefinitionMap.put("/swagger-ui.html", "anon");
            // 开放登陆接口
            filterChainDefinitionMap.put("/index", "anon");
            filterChainDefinitionMap.put("/login", "anon");
            filterChainDefinitionMap.put("/logout", "logout");
            // 开放公共接口
            filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
            filterChainDefinitionMap.put("/sms/**", "anon");
    
            // 其他页面权限限制
            // 其余全部接口必须进行身份验证,此处方便测试限制到shiroTest开头
            filterChainDefinitionMap.put("/shiroTest/**", "authc");
            // "/teacher/"相关接口必须有teacher权限
            filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
            // "/student/"相关接口必须有student权限
            filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");
    
            //设置拦截器
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    
            log.info("-----------------shiro配置注入完成---------------------");
            return shiroFilterFactoryBean;
        }
    
        /**
         * 注入 securityManager
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置realm
            securityManager.setRealm(customRealm());
            return securityManager;
        }
    
        /**
         * 自定义身份认证 realm
         */
        @Bean
        public MyRealm customRealm() {
            return new MyRealm();
        }
    
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        /**
         * 开启aop注解支持
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
    }
    

    拦截器常用权限参数列表

    • anon:开放权限,可直接访问
    • authc:需要认证
    • logout:注销,执行后跳转至登录页面,即shiroFilterFactoryBean.setLoginUrl()所设置的页面
    • roles[role1, role2...]:所需角色。若有多个角色,则需满足所有角色才可放行。
    • perms[permission1, permission2...]:所需权限。若有多个权限,则需满足所有权限才可放行。

    6.使用样例(仅供参考思路,正常使用请参考贴在后面的整理后的核心代码)

    6.1.登录接口(正常使用请勿返回账号数据)

    package com.yezi_tool.basic_project.controller;
    
    import com.yezi_tool.basic_project.commons.model.ReturnMsg;
    import com.yezi_tool.basic_project.entity.UserInfo;
    import lombok.Data;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.UnauthorizedException;
    import org.apache.shiro.subject.Subject;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * @author Echo_Ye
     * @title 登录接口
     * @description 用于登录相关接口
     * @date 2020/8/17 9:39
     * @email echo_yezi@qq.com
     */
    @Controller
    @RequestMapping("/login")
    public class LoginController {
    
        @Data
        public static class LoginRequest {
            private String username;
            private String password;
        }
    
    
        @PostMapping("/login")
        @ResponseBody
        public ReturnMsg login(@RequestBody LoginRequest loginRequest) {
            ReturnMsg returnMsg = new ReturnMsg();
            //组装token
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(loginRequest.username, loginRequest.password);
            try {
                //执行登录
                subject.login(token);
                //获取验证后的对象
                UserInfo userInfo = (UserInfo) subject.getPrincipal();
                //检查权限,无权限将会抛出异常
    //            subject.checkPermission("teacher");
                returnMsg.setData(userInfo);
            } catch (UnknownAccountException e) {
                returnMsg = ReturnMsg.error("账号不存在");
            } catch (IncorrectCredentialsException e) {
                returnMsg = ReturnMsg.error("密码错误");
            } catch (LockedAccountException e) {
                returnMsg = ReturnMsg.error("账号被锁定");
            } catch (AuthenticationException e) {
                returnMsg = ReturnMsg.error("认证错误");
            } catch (UnauthorizedException e) {
                returnMsg = ReturnMsg.error("权限错误");
            }
    
            return returnMsg;
        }
    }
    

    6.2.测试权限接口

    package com.yezi_tool.basic_project.controller;
    
    import com.yezi_tool.basic_project.commons.model.ReturnMsg;
    import org.apache.shiro.SecurityUtils;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * @author Echo_Ye
     * @title shiro测试接口
     * @description 用于shiro相关测试
     * @date 2020/8/17 11:54
     * @email echo_yezi@qq.com
     */
    @Controller
    @RequestMapping("/shiroTest")
    public class ShiroTestController {
    
        @GetMapping("teacher/testShiro")
        @ResponseBody
        public ReturnMsg teacherTest(HttpServletRequest request) {
            return ReturnMsg.success();
        }
    
        @GetMapping("student/testShiro")
        @ResponseBody
        public ReturnMsg studentTest(HttpServletRequest request) {
            return ReturnMsg.success();
        }
    
        @GetMapping("checkPermission")
        @ResponseBody
        public ReturnMsg checkPermission(HttpServletRequest request, String permission) {
            SecurityUtils.getSubject().checkPermission(permission);
            return ReturnMsg.success();
        }
        
        @GetMapping("checkSuperAdmin")
        @ResponseBody
        @RequiresPermissions({"teacher","student"})
        public ReturnMsg checkSuperAdmin(HttpServletRequest request) {
            return ReturnMsg.success();
        }
    }
    
    

    6.3.进行测试

    使用postman,每次使用同一个token进行测试,操作和结果如下

    • 发起请求/shiroTest/student/testShiro,结果为404的ModelAndView对象,提示/login.html不存在

      未进行登录,故跳转至登录页面,而测试项目并没有这个页面

      image-20200817141203215

    • 发起请求LoginController/login,结果为用户信息,即登录成功

      image-20200817141500256

    • 再次发起请求/shiroTest/student/testShiro,结果为请求成功,则证明权限验证有效

      image-20200817141814725

    • 发起请求/shiroTest/checkSuperAdmin,结果为操作失败,后端报错Subject does not have permission [teacher]

      该接口使用了注解,需要studentteacher两个权限,缺少任意一个则会抛出异常

      image-20200817142721827

    image-20200817142824970

    7.补充

    7.1.SimpleAuthorizationInfo权限信息常用操作

    • addRole(String role)addRoles(Collection<String> roles)setRoles(Set<String> roles):添加/批量添加/批量设置角色
    • addStringPermission(String permission)addStringPermissions(Collection<String> permissions)setStringPermissions(Set<String> roles):添加/批量添加/批量设置权限

    7.2.认证主体Subject常用操作

    • login(AuthenticationToken var1):登录,失败会抛出异常
    • logout():登出,注销认证信息
    • getPrincipal():获取认证身份信息,通常读取账号或者全部账号数据
    • getSession().setAttribute(Object var1, Object var2)getSession().getAttribute(Object var1):设置/获取session缓存数据
    • getSession().setTimeout(long var1)getSession().getTimeout():设置/获取session有限时限
    • isPermitted(String var1):是否满足权限,参数也可为Permission var1String... var1List<Permission> var1
    • isPermittedAll(String... var1):是否满足全部权限,参数也可为Collection<Permission> var1
    • checkPermission(String var1):检查权限,权限不足时抛出异常,参数也可为Permission var1String... var1Collection<Permission> var1
    • hasRole(String var1):是否满足角色,参数也可为List<String> var1
    • hasAllRoles(Collection<String> var1):是否满足全部角色
    • checkRole(String var1):检查角色,角色不足时抛出异常
    • checkRoles(String... var1):检查角色,角色不足时抛出异常,参数也可为Collection<String> var1
    • isRemembered():是否记住登录状态
    • isAuthenticated():是否已认证,没错,认证主体并不一定已被认证

    7.3.拦截器常用权限参数列表(再重提一遍,很重要)

    Filter 解释
    anon 无参,开放权限,可以理解为匿名用户或游客
    authc 无参,需要认证
    logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl();设置的 url
    authcBasic 无参,表示 httpBasic 认证
    user 无参,表示必须存在用户,当登入操作时不做检查
    ssl 无参,表示安全的URL请求,协议为 https
    perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
    roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
    rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
    port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

    此外,权限参数可以自定义,详情请自行查阅资料,或者等我啥时候有空了整理。。。

    7.4.控制层注解

    • @RequiresAuthentication

      验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。

    • @RequiresUser

      验证用户是否被记忆,user有两种含义:

      一种是成功登录的(subject.isAuthenticated() 结果为true);

      另外一种是被记忆的(subject.isRemembered()结果为true)。

    • @RequiresGuest

      验证是否是一个guest的请求,与@RequiresUser完全相反。

      换言之,RequiresUser == !RequiresGuest。

      此时subject.getPrincipal() 结果为null.

    • @RequiresRoles

      例如:

      • @RequiresRoles("roleA")
      • @RequiresRoles({"roleA", “roleB"})

      验证角色,任意一个不符合则抛出异常

    • @RequiresPermissions

      例如:

      • @RequiresPermissions("file")

      • @RequiresPermissions({"file:read", "file:write"} )

      验证权限,任意一个不符合则抛出异常

    8.整理后代码

    8.1.整理内容

    • 优化代码结构
    • 查询账号方法修正为从数据库查询(使用mybatisPlus)
    • 密码改为密文(MD5加密+盐)
    • 登录结果仅提示成功或失败
    • shiro相关异常使用全局异常捕获
    • 自定义token对象,并添加其余参数(如验证码,登录端),并进行验证
    • 通用方法写到BaseController
    • 增加多地登录踢下线机制
    • 缓存改用redis
    • 增加RememberMe机制

    相关内容会另起他文整理,此处不做赘述,有兴趣的不妨瞅一下我更新没。。。

    8.2.核心源码:

    ShiroConfig.java

    package com.yezi_tool.basic_project.config;
    
    import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
    import com.yezi_tool.basic_project.shiro.KickoutSessionControlFilter;
    import com.yezi_tool.basic_project.shiro.MyRealm;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.web.mgt.CookieRememberMeManager;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.servlet.SimpleCookie;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.crazycake.shiro.RedisCacheManager;
    import org.crazycake.shiro.RedisManager;
    import org.crazycake.shiro.RedisSessionDAO;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.springframework.context.annotation.DependsOn;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import javax.servlet.Filter;
    
    /**
     * @author Echo_Ye
     * @title shiro配置
     * @description shiro相关配置
     * @date 2020/8/17 11:28
     * @email echo_yezi@qq.com
     */
    @Slf4j
    @Configuration
    public class ShiroConfig {    //获取application.properties参数,此处不能加static关键字
        @Value("${spring.redis.port}")
        private String port;
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.password}")
        private String redisPassword;
    
        /**
         * Shiro生命周期处理器
         */
        @Bean(name = "lifecycleBeanPostProcessor")
        public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
            log.info("-----------------Shiro生命周期周期处理器设置---------------------");
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
         */
        @Bean
        @DependsOn("lifecycleBeanPostProcessor")
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        /**
         * 开启aop注解支持
         */
        @Bean
        @ConditionalOnMissingBean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            // 定义bean
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            // 必须设置 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
            shiroFilterFactoryBean.setLoginUrl("/login.html");
            // 认证失败url
            shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
            // 登录成功url
    //        shiroFilterFactoryBean.setSuccessUrl("/index");
            //当访问受限url
    //        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
    
            //自定义拦截器
            Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
            //限制同一帐号同时在线的个数。
            filtersMap.put("kickout", kickoutSessionControlFilter());
            shiroFilterFactoryBean.setFilters(filtersMap);
    
            // 设置拦截器
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 开放静态资源权限
            filterChainDefinitionMap.put("/webjars/**", "anon");
            filterChainDefinitionMap.put("/assets/**", "anon");
            filterChainDefinitionMap.put("/img/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/css/**", "anon");
            // 开放swagger页面权限
            filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
            filterChainDefinitionMap.put("/swagger-ui.html", "anon");
            // 开放登陆接口
            filterChainDefinitionMap.put("/index", "anon");
            filterChainDefinitionMap.put("/login/**", "anon");
            filterChainDefinitionMap.put("/logout", "logout");
            // 开放公共接口
            filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
            filterChainDefinitionMap.put("/sms/**", "anon");
    
            // 其他页面权限限制
            // 其余全部接口必须进行身份验证,且强制下线
            filterChainDefinitionMap.put("/**", "user,kickout");
            // "/teacher/"相关接口必须有teacher权限
            filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
            // "/student/"相关接口必须有student权限
            filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");
            //设置拦截器
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    
            log.info("-----------------shiro配置注入完成---------------------");
            return shiroFilterFactoryBean;
        }
    
    
        /**
         * 注入 securityManager
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置realm
            securityManager.setRealm(customRealm());
            // 自定义缓存实现 使用redis
            securityManager.setCacheManager(redisCacheManager());
            // 自定义session管理 使用redis
            securityManager.setSessionManager(sessionManager());
            // 自定义rememberMe管理
            securityManager.setRememberMeManager(rememberMeManager());
            return securityManager;
        }
    
        /**
         * 自定义身份认证 realm
         */
        @Bean
        public MyRealm customRealm() {
            MyRealm myRealm = new MyRealm();
            //设置加密方式
            myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return myRealm;
        }
    
        /**
         * 加密策略
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
            //指定加密方式
            credentialsMatcher.setHashAlgorithmName(ConfigConstants.SHIRO_ENCODE_MODE);
            //加密次数
            credentialsMatcher.setHashIterations(ConfigConstants.SHIRO_ENCODE_TIMES);
            //此处的设置,true加密用的hex编码,false用的base64编码
            credentialsMatcher.setStoredCredentialsHexEncoded(true);
            return credentialsMatcher;
        }
    
        /**
         * cacheManager 缓存 redis实现
         * 使用的是shiro-redis开源插件
         */
        public RedisCacheManager redisCacheManager() {
            log.info("-----------------创建缓存管理器---------------------");
    
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
            redisCacheManager.setPrincipalIdFieldName("id");
    //        用户权限信息缓存时间
    //        redisCacheManager.setExpire(200000);
            return redisCacheManager;
        }
    
        /**
         * Session Manager
         * 使用的是shiro-redis开源插件
         */
        @Bean
        public DefaultWebSessionManager sessionManager() {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
            sessionManager.setSessionDAO(redisSessionDAO());
            return sessionManager;
        }
    
        /**
         * RedisSessionDAO shiro sessionDao层的实现 通过redis
         * 使用的是shiro-redis开源插件
         */
        @Bean
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            return redisSessionDAO;
        }
    
    
        /**
         * 配置shiro redisManager
         * 使用的是shiro-redis开源插件
         */
        public RedisManager redisManager() {
            log.info("-----------------创建RedisManager,连接Redis..URL= " + host + ":" + port + "---------------------");
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host + ":" + port);//老版本是分别setHost和setPort,新版本只需要setHost就可以了
            if (!StringUtils.isEmpty(redisPassword)) {
                redisManager.setPassword(redisPassword);
            }
            return redisManager;
        }
    
        /**
         * 限制同一账号登录同时登录人数控制
         */
        @Bean
        public KickoutSessionControlFilter kickoutSessionControlFilter() {
            KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
            kickoutSessionControlFilter.setCacheManager(redisCacheManager());
            kickoutSessionControlFilter.setSessionManager(sessionManager());
            kickoutSessionControlFilter.setKickoutAfter(false);
            kickoutSessionControlFilter.setMaxSession(ConfigConstants.SHIRO_SESSION_KICKOUT_MAX_SESSION);
            kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
            return kickoutSessionControlFilter;
        }
    
        /**
         * Cookie
         */
        @Bean
        public SimpleCookie rememberMeCookie() {
            //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
            SimpleCookie simpleCookie = new SimpleCookie(ConfigConstants.SHIRO_COOKIE_KEY_REMEMBER_ME);
            //如果httpOnly设置为true,则客户端不会暴露给客户端脚本代码,使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击;
            simpleCookie.setHttpOnly(true);
            //记住我cookie生效时间,单位是秒
            simpleCookie.setMaxAge(600);
            return simpleCookie;
        }
    
        /**
         * cookie管理器;
         */
        @Bean
        public CookieRememberMeManager rememberMeManager() {
            log.info("-----------------创建RememberMe管理器---------------------");
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            //rememberme cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
            byte[] cipherKey = Base64.decode(ConfigConstants.SHIRO_COOKIE_CIPHER_KEY);
            cookieRememberMeManager.setCipherKey(cipherKey);
            cookieRememberMeManager.setCookie(rememberMeCookie());
            return cookieRememberMeManager;
        }
    
    }
    

    MyRealm.java

    package com.yezi_tool.basic_project.shiro;
    
    import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
    import com.yezi_tool.basic_project.entity.UserInfo;
    import com.yezi_tool.basic_project.service.IUserInfoService;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.util.ByteSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    /**
     * @author Echo_Ye
     * @title 自定义realm
     * @description 自定义数据域
     * @date 2020/8/17 9:25
     * @email echo_yezi@qq.com
     */
    @Slf4j
    public class MyRealm extends AuthorizingRealm {
    
        /**
         * 用户信息业务层
         */
        @Autowired
        @Qualifier("userInfoService")
        private IUserInfoService userInfoService;
    
        /**
         * 分配权限
         *
         * @param principalCollection 数据源
         * @return 权限和角色信息
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            log.info("-----------------访问授权---------------------");
            //认证信息未登录,可能是用户非正常退出
            if (!SecurityUtils.getSubject().isAuthenticated()) {
                doClearCache(principalCollection);
                SecurityUtils.getSubject().logout();
                return null;
            }
            //获取认证主体
            UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
            if (userInfo == null) {
                //认证错误
                return null;
            }
            String username = userInfo.getUsername();
            //添加角色和权限
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<String> permissionInfoList = userInfoService.queryPermission(username);
            Set<String> permissionSet = permissionInfoList.stream().collect(Collectors.toSet());
            simpleAuthorizationInfo.setStringPermissions(permissionSet);
    
            // 存储信息到session,根据自身需求添加
            SecurityUtils.getSubject().getSession().setAttribute(ConfigConstants.SHIRO_SESSION_KEY_USER_INFO, userInfo);
            return simpleAuthorizationInfo;
        }
    
        /**
         * 进行认证
         *
         * @param authenticationToken 数据源
         * @return 认证结果
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
            //获取自定义token
            CustomAuthenticationToken token = (CustomAuthenticationToken) authenticationToken;
            // 根据 Token 获取数据
            String username = (String) token.getPrincipal();
            // 根据用户名从数据库中查询该用户
            UserInfo user = userInfoService.selectByUserName(username);
            if (user != null) {
                // 传入用户名和密码进行身份认证,并返回认证信息
                //自定义盐值
                ByteSource salt = ByteSource.Util.bytes(user.getSalt());
                AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, this.getName());
                return authInfo;
            } else {
                //未查找到用户信息,认证失败
                return null;
            }
        }
    
    }
    

    ConfigConstants.java

    package com.yezi_tool.basic_project.commons.constants;
    
    /**
     * @author Echo_Ye
     * @title 配置信息常量
     * @description 用于各种配置
     * @date 2020/8/19 18:13
     * @email echo_yezi@qq.com
     */
    public class ConfigConstants {
        /**
         * 序列化相关
         */
        public static final long SERIAL_VERSION_UID     = 1L;    //序列号
    
        /**
         * shiro相关
         */
        public static final String SHIRO_ENCODE_MODE                    = "MD5";    //加密方式
        public static final int SHIRO_ENCODE_TIMES                      = 1;    //加密次数
        public static final String SHIRO_REDIS_PREFIX_CACHE             = "shiro_redis_cache";    //缓存前缀
        public static final String SHIRO_SESSION_PREFIX_KICKOUT         = "kickout";    //缓存前缀
        public static final int SHIRO_SESSION_KICKOUT_MAX_SESSION       = 1;    //踢下线最多人数
        public static final String SHIRO_SESSION_KEY_USER_INFO          = "user_info";    //缓存-用户信息
        public static final String SHIRO_COOKIE_KEY_REMEMBER_ME         = "rememberMe";    //cookie-记住我
        public static final String SHIRO_COOKIE_CIPHER_KEY              = "qU7b1ChYNwqbrEwTlPbO9Q==";    //cookie-加密秘钥
    
        /**
         * redis相关
         */
        public static final String REDIS_KEY_CAPTCHA="captcha";
    }
    

    BaseExceptionHandler.java

    package com.yezi_tool.basic_project.interceptors;
    
    import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
    import com.yezi_tool.basic_project.commons.exception.BaseException;
    import com.yezi_tool.basic_project.commons.model.ReturnMsg;
    import com.yezi_tool.basic_project.commons.utils.ObjectMapperFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.ShiroException;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.LockedAccountException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authz.UnauthorizedException;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.util.NestedServletException;
    
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;
    
    /**
     * @title 统一异常处理
     * @description 统一处理项目内的异常
     * @author Echo_Ye
     * @date 2020/8/24 10:42
     * @email echo_yezi@qq.com
     */
    @Slf4j
    @ControllerAdvice
    public class BaseExceptionHandler {
        /**
         * 统一异常处理,仅处理NestedServletException
         */
        @ExceptionHandler({NestedServletException.class})
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        public ModelAndView servletException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
            ModelAndView mav = new ModelAndView();
            ReturnMsg message = ReturnMsg.FAIL;
            out(response, message);
            return mav;
        }
    
    
        /**
         * 统一异常处理,仅处理ShiroException
         */
        @ExceptionHandler({ShiroException.class})
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        public ReturnMsg shiroException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
            // 打印异常信息至控制台,开始处理异常
            log.error("异常统一处理-ShiroException:" + exception.getLocalizedMessage(), exception);
            //异常默认为是操作失败
            ReturnMsg message = ReturnMsg.FAIL;
            //确认错误类型
            if (exception instanceof UnknownAccountException) {
                //账号错误
                message.setMsg(ResponseConstants.SHIRO_MSG_UNKNOWN_ACCOUNT);
            } else if (exception instanceof IncorrectCredentialsException) {
                //密码错误
                message.setMsg(ResponseConstants.SHIRO_MSG_INCORRECT_CREDENTIALS);
            } else if (exception instanceof LockedAccountException) {
                //账号被锁定
                message.setMsg(ResponseConstants.SHIRO_MSG_LOCKED_ACCOUNT);
            } else if (exception instanceof AuthenticationException) {
                //认证错误
                message.setMsg(ResponseConstants.SHIRO_MSG_AUTHENTICATION_ERROR);
            } else if (exception instanceof UnauthorizedException) {
                //权限不足
                message.setMsg(ResponseConstants.SHIRO_MSG_UNAUTHORIZED);
            }
            //返回消息体
            return message;
        }
    
    
        /**
         * 统一异常处理
         */
        @ExceptionHandler({Exception.class})
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        public ReturnMsg processException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
            // 打印异常信息至控制台,开始处理异常
            log.error("异常统一处理-Exception:" + exception.getLocalizedMessage(), exception);
            //异常默认为是操作失败
            ReturnMsg message = ReturnMsg.FAIL;
            // 检查异常的类型
            if (exception instanceof NestedServletException) {
                // 异步请求错误,已处理
            } else if (exception instanceof BaseException) {
                // 自定义类型的异常,转换为自定义异常
                message = ((BaseException) exception).asReturnMsg();
            } else {
                // 非自定义类型异常,打印错误信息至日志,封装ReturnMsg对象
                log.error(request.getRequestURI(), exception);
                message = ReturnMsg.FAIL;
            }
            //返回消息体
            return message;
        }
    
        /**
         * response 输出JSON
         */
        public static void out(ServletResponse response, ReturnMsg returnMsg) {
            PrintWriter out = null;
            try {
                response.setContentType("application/json;charset=utf-8");
                out = response.getWriter();
                out.println(ObjectMapperFactory.getInstance().writeValueAsString(returnMsg));
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (null != out) {
                    out.flush();
                    out.close();
                }
            }
        }
    }
    
    

    CustomAuthenticationToken.java

    package com.yezi_tool.basic_project.shiro;
    
    import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
    import lombok.Data;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.web.util.WebUtils;
    
    import javax.servlet.ServletRequest;
    
    /**
     * @author Echo_Ye
     * @title 自定义token
     * @description 自定义token,适用于shiro
     * @date 2020/8/20 11:56
     * @email echo_yezi@qq.com
     */
    @Data
    public class CustomAuthenticationToken extends UsernamePasswordToken {
        /**
         * 序列号
         */
        private static final long serialVersionUID = ConfigConstants.SERIAL_VERSION_UID;
    
        /**
         * 验证码字段
         */
        public static final String DEFAULT_CAPTCHA_PARAM = "captcha";
    
        /**
         * 登录类型
         */
        private int loginType;
    
        /**
         * 登录方式
         */
        private String loginMode;
    
        /**
         * 手机验证码
         */
        private String code;
    
        /**
         * 验证码
         */
        private String captcha;
    
    
        /**
         * 从request请求中获取验证码
         */
        public String getCaptcha(ServletRequest request) {
            return WebUtils.getCleanParam(request, DEFAULT_CAPTCHA_PARAM);
        }
    
    
        public CustomAuthenticationToken(String username, String password) {
            super(username, password);
        }
    
        public CustomAuthenticationToken(String username, String password, String captcha) {
            super(username, password);
            this.captcha = captcha;
        }
    }
    

    KickoutSessionControlFilter.java

    package com.yezi_tool.basic_project.shiro;
    
    import com.alibaba.fastjson.JSON;
    import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
    import com.yezi_tool.basic_project.entity.UserInfo;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheManager;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.mgt.DefaultSessionKey;
    import org.apache.shiro.session.mgt.SessionManager;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.web.filter.AccessControlFilter;
    import org.apache.shiro.web.util.WebUtils;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.io.Serializable;
    import java.util.Deque;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.Map;
    
    public class KickoutSessionControlFilter extends AccessControlFilter {
    
        private String kickoutUrl; //踢出后到的地址
        private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
        private int maxSession = 1; //同一个帐号最大会话数 默认1
        private String kickoutKey = ConfigConstants.SHIRO_SESSION_PREFIX_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的前缀
        public void setCacheManager(CacheManager cacheManager) {
            this.cache = cacheManager.getCache(ConfigConstants.SHIRO_REDIS_PREFIX_CACHE);
        }
    
        @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);
            if (!subject.isAuthenticated() && !subject.isRemembered()) {
                //如果没有登录,直接进行之后的流程
                return true;
            }
    
            Session session = subject.getSession();
            UserInfo user = (UserInfo) subject.getPrincipal();
            String username = user.getUsername();
            Serializable sessionId = session.getId();
    
            //读取缓存   没有就存入
            Deque<Serializable> deque = cache.get(username);
    
            //如果此用户没有session队列,也就是还没有登录过,缓存中没有
            //就new一个空队列,不然deque对象为空,会报空指针
            if (deque == null) {
                deque = new LinkedList<Serializable>();
            }
    
            //如果队列里没有此sessionId,且用户没有被踢出;放入队列
            if (!deque.contains(sessionId) && session.getAttribute(kickoutKey) == null) {
                //将sessionId存入队列
                deque.push(sessionId);
                //将用户的sessionId队列缓存
                cache.put(username, deque);
            }
    
            //如果队列里的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(kickoutKey, true);
                    }
                } catch (Exception e) {//ignore exception
                }
            }
    
            //如果被踢出了,直接退出,重定向到踢出后的地址
            if (session.getAttribute(kickoutKey) != null) {
                //会话被踢出了
                try {
                    //退出登录
                    subject.logout();
                } catch (Exception e) { //ignore
                }
                saveRequest(request);
    
                Map<String, String> resultMap = new HashMap<String, String>();
                //判断是不是Ajax请求
                if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                    resultMap.put("code ", "300");
                    resultMap.put("message", "您已经在其他地方登录,请重新登录!");
                    //输出json串
                    out(response, resultMap);
                } else {
                    //重定向
                    WebUtils.issueRedirect(request, response, kickoutUrl);
                }
                return false;
            }
            return true;
        }
    
        private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException {
            try {
                hresponse.setCharacterEncoding("UTF-8");
                PrintWriter out = hresponse.getWriter();
                out.println(JSON.toJSONString(resultMap));
                out.flush();
                out.close();
            } catch (Exception e) {
                System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
            }
        }
    }
    

    BaseController.java

    package com.yezi_tool.basic_project.controller;
    
    import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
    import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
    import com.yezi_tool.basic_project.commons.exception.BaseException;
    import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
    import org.apache.shiro.SecurityUtils;
    import org.springframework.stereotype.Controller;
    
    @Controller
    public class BaseController {
    
        /**
         * 校验验证码
         *
         * @param token token对象
         * @throws Exception 校验失败则抛出异常
         */
        public void checkCaptcha(CustomAuthenticationToken token) throws Exception {
            String captcha = token.getCaptcha();
            String exitCode = (String) SecurityUtils.getSubject().getSession().getAttribute(ConfigConstants.REDIS_KEY_CAPTCHA);
            if (null == captcha || !captcha.equalsIgnoreCase(exitCode)) {
                throw new BaseException(ResponseConstants.RETURN_MSG_INCORRECT_CAPTCHA);
            }
        }
    
        /**
         * 获取认证者身份
         *
         * @return 认证者身份信息
         */
        public Object getPrimaryPrincipal() {
            return SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
        }
    
        /**
         * 获取session字段值
         *
         * @param key session的字段key
         * @return session的字段值
         */
        public Object getSessionAttribute(String key) {
            return SecurityUtils.getSubject().getSession().getAttribute(key);
        }
    }
    

    LoginController.java

    package com.yezi_tool.basic_project.controller;
    
    import com.yezi_tool.basic_project.commons.model.ReturnMsg;
    import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
    import lombok.Data;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.subject.Subject;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * @author Echo_Ye
     * @title 登录接口
     * @description 用于登录相关接口
     * @date 2020/8/17 9:39
     * @email echo_yezi@qq.com
     */
    @Controller
    @RequestMapping("/login")
    public class LoginController extends BaseController {
    
        @Data
        private static class LoginRequest {
            private String username;
            private String password;
            private Boolean rememberMe;
        }
    
    
        @PostMapping("/login")
        @ResponseBody
        public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
            //组装token
            Subject subject = SecurityUtils.getSubject();
            CustomAuthenticationToken token = new CustomAuthenticationToken(loginRequest.username, loginRequest.password);
            // 设置rememberMe字段
            if(loginRequest.rememberMe!=null) {
                token.setRememberMe(loginRequest.rememberMe);
            }
            //判断验证码,暂不启用
    //        checkCaptcha(token);
            //执行登录
            subject.login(token);
    
            return ReturnMsg.success();
        }
    
    }
    

    8.3.全部代码:

    demo地址:https://gitee.com/echo_ye/shiro-demo

    demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~

    BB两句

    真的是了解越多,越觉得其强大,越觉得自己弱鸡。。。


    作者:Echo_Ye

    WX:Echo_YeZ

    EMAIL :echo_yezi@qq.com

    个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

  • 相关阅读:
    美国和日本不是盟友吗,为什么拒绝出售F-22战斗机给日本?
    刚刚大学毕业,是选择创业还是选择公司上班?
    现今的社会一定要结婚吗?
    事必躬亲果真是无能的表现?
    课外辅导机构,就让疫情灭了吧!
    这些年,我读书越来越少了!
    Python操作日志、加密、发送邮件、线程、生产者消费者
    Python模块操作之re、MySQL、Excel
    Python面向对象、迭代器、range和切片的区分
    Python模块、文件读写、异常
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13563259.html
Copyright © 2011-2022 走看看