SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录

来源:http://dwz.date/bRkG
作者:Sans_

一.说明

Shiro是一个安全框架,项目中主要用它做认证,授权,加密,以及用户的会话管理,虽然Shiro没有SpringSecurity功能更丰富,但是它轻量,简单,在项目中通常业务需求Shiro也都能胜任.

二.项目环境

  • MyBatis-Plus版本: 3.1.0
  • SpringBoot版本:2.1.5
  • JDK版本:1.8
  • Shiro版本:1.4
  • Shiro-redis插件版本:3.1.0

数据表(SQL文件在项目中):数据库中测试号的密码进行了加密,密码皆为123456

数据表名 中文表名 备注说明
sys_user 系统用户表 基础表
sys_menu 权限表 基础表
sys_role 角色表 基础表
sys_role_menu 角色与权限关系表 中间表
sys_user_role 用户与角色关系表 中间表

Maven依赖如下:

    <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!-- AOP依赖,一定要加,否则权限拦截验证不生效 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <!-- lombok插件 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <!-- Redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
            </dependency>
            <!-- mybatisPlus 核心库 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.1.0</version>
            </dependency>
            <!-- 引入阿里数据库连接池 -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.6</version>
            </dependency>
            <!-- Shiro 核心依赖 -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
            <!-- Shiro-redis插件 -->
            <dependency>
                <groupId>org.crazycake</groupId>
                <artifactId>shiro-redis</artifactId>
                <version>3.1.0</version>
            </dependency>
            <!-- StringUtilS工具 -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.5</version>
            </dependency>
    </dependencies>

配置如下:

    # 配置端口
    server:
      port: 8764
    spring:
      # 配置数据源
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: root
        type: com.alibaba.druid.pool.DruidDataSource
      # Redis数据源
      redis:
        host: localhost
        port: 6379
        timeout: 6000
        password: 123456
        jedis:
          pool:
            max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
            max-wait: -1      # 连接池最大阻塞等待时间(使用负值表示没有限制)
            max-idle: 10      # 连接池中的最大空闲连接
            min-idle: 5       # 连接池中的最小空闲连接
    # mybatis-plus相关配置
    mybatis-plus:
      # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
      mapper-locations: classpath:mapper/*.xml
      # 以下配置均有默认值,可以不设置
      global-config:
        db-config:
          #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
          id-type: auto
          #字段策略 IGNORED:"忽略判断"  NOT_NULL:"非 NULL 判断")  NOT_EMPTY:"非空判断"
          field-strategy: NOT_EMPTY
          #数据库类型
          db-type: MYSQL
      configuration:
        # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
        map-underscore-to-camel-case: true
        # 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏
        call-setters-on-nulls: true
        # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

二.编写项目基础类

用户实体,Dao,Service等在这里省略,请参考源码

编写Exception类来处理Shiro权限拦截异常

    /**
     * @Description 自定义异常
     * @Author Sans
     * @CreateTime 2019/6/15 22:56
     */
    @ControllerAdvice
    public class MyShiroException {
        /**
         * 处理Shiro权限拦截异常
         * 如果返回JSON数据格式请加上 @ResponseBody注解
         * @Author Sans
         * @CreateTime 2019/6/15 13:35
         * @Return Map<Object> 返回结果集
         */
        @ResponseBody
        @ExceptionHandler(value = AuthorizationException.class)
        public Map<String,Object> defaultErrorHandler(){
            Map<String,Object> map = new HashMap<>();
            map.put("403","权限不足");
            return map;
        }
    }

创建SHA256Util加密工具

    /**
     * @Description Sha-256加密工具
     * @Author Sans
     * @CreateTime 2019/6/12 9:27
     */
    public class SHA256Util {
        /**  私有构造器 **/
        private SHA256Util(){};
        /**  加密算法 **/
        public final static String HASH_ALGORITHM_NAME = "SHA-256";
        /**  循环次数 **/
        public final static int HASH_ITERATIONS = 15;
        /**  执行加密-采用SHA256和盐值加密 **/
        public static String sha256(String password, String salt) {
            return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
        }
    }

创建Spring工具

    /**
     * @Description Spring上下文工具类
     * @Author Sans
     * @CreateTime 2019/6/17 13:40
     */
    @Component
    public class SpringUtil implements ApplicationContextAware {
        private static ApplicationContext context;
        /**
         * Spring在bean初始化后会判断是不是ApplicationContextAware的子类
         * 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去
         * @Author Sans
         * @CreateTime 2019/6/17 16:58
         */
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
        /**
         * 通过Name返回指定的Bean
         * @Author Sans
         * @CreateTime 2019/6/17 16:03
         */
        public static <T> T getBean(Class<T> beanClass) {
            return context.getBean(beanClass);
        }
    }

创建Shiro工具

    /**
     * @Description Shiro工具类
     * @Author Sans
     * @CreateTime 2019/6/15 16:11
     */
    public class ShiroUtils {
    
        /** 私有构造器 **/
        private ShiroUtils(){}
    
        private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
    
        /**
         * 获取当前用户Session
         * @Author Sans
         * @CreateTime 2019/6/17 17:03
         * @Return SysUserEntity 用户信息
         */
        public static Session getSession() {
            return SecurityUtils.getSubject().getSession();
        }
    
        /**
         * 用户登出
         * @Author Sans
         * @CreateTime 2019/6/17 17:23
         */
        public static void logout() {
            SecurityUtils.getSubject().logout();
        }
    
        /**
        * 获取当前用户信息
        * @Author Sans
        * @CreateTime 2019/6/17 17:03
        * @Return SysUserEntity 用户信息
        */
        public static SysUserEntity getUserInfo() {
          return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
        }
    
        /**
         * 删除用户缓存信息
         * @Author Sans
         * @CreateTime 2019/6/17 13:57
         * @Param  username  用户名称
         * @Param  isRemoveSession 是否删除Session
         * @Return void
         */
        public static void deleteCache(String username, boolean isRemoveSession){
            //从缓存中获取Session
            Session session = null;
            Collection<Session> sessions = redisSessionDAO.getActiveSessions();
            SysUserEntity sysUserEntity;
            Object attribute = null;
            for(Session sessionInfo : sessions){
                //遍历Session,找到该用户名称对应的Session
                attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
                if (attribute == null) {
                    continue;
                }
                sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
                if (sysUserEntity == null) {
                    continue;
                }
                if (Objects.equals(sysUserEntity.getUsername(), username)) {
                    session=sessionInfo;
                    break;
                }
            }
            if (session == null||attribute == null) {
                return;
            }
            //删除session
            if (isRemoveSession) {
                redisSessionDAO.delete(session);
            }
            //删除Cache,在访问受限接口时会重新授权
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
            Authenticator authc = securityManager.getAuthenticator();
            ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
        }
    }

创建Shiro的SessionId生成器

    /**
     * @Description 自定义SessionId生成器
     * @Author Sans
     * @CreateTime 2019/6/11 11:48
     */
    public class ShiroSessionIdGenerator implements SessionIdGenerator {
        /**
         * 实现SessionId生成
         * @Author Sans
         * @CreateTime 2019/6/11 11:54
         */
        @Override
        public Serializable generateId(Session session) {
            Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
            return String.format("login_token_%s", sessionId);
        }
    }

三.编写Shiro核心类

创建Realm用于授权和认证

    /**
     * @Description Shiro权限匹配和账号密码匹配
     * @Author Sans
     * @CreateTime 2019/6/15 11:27
     */
    public class ShiroRealm extends AuthorizingRealm {
        @Autowired
        private SysUserService sysUserService;
        @Autowired
        private SysRoleService sysRoleService;
        @Autowired
        private SysMenuService sysMenuService;
        /**
         * 授权权限
         * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
         * @Author Sans
         * @CreateTime 2019/6/12 11:44
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
            //获取用户ID
            Long userId =sysUserEntity.getUserId();
            //这里可以进行授权和处理
            Set<String> rolesSet = new HashSet<>();
            Set<String> permsSet = new HashSet<>();
            //查询角色和权限(这里根据业务自行查询)
            List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
            for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
                rolesSet.add(sysRoleEntity.getRoleName());
                List<SysMenuEntity> sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
                for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
                    permsSet.add(sysMenuEntity.getPerms());
                }
            }
            //将查到的权限和角色分别传入authorizationInfo中
            authorizationInfo.setStringPermissions(permsSet);
            authorizationInfo.setRoles(rolesSet);
            return authorizationInfo;
        }
        
        /**
         * 身份认证
         * @Author Sans
         * @CreateTime 2019/6/12 12:36
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //获取用户的输入的账号.
            String username = (String) authenticationToken.getPrincipal();
            //通过username从数据库中查找 User对象,如果找到进行验证
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            SysUserEntity user = sysUserService.selectUserByName(username);
            //判断账号是否存在
            if (user == null) {
                throw new AuthenticationException();
            }
            //判断账号是否被冻结
            if (user.getState()==null||user.getState().equals("PROHIBIT")){
                throw new LockedAccountException();
            }
            //进行验证
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    user,                                  //用户名
                    user.getPassword(),                    //密码
                    ByteSource.Util.bytes(user.getSalt()), //设置盐值
                    getName()
            );
            //验证成功开始踢人(清除缓存和Session)
            ShiroUtils.deleteCache(username,true);
            return authenticationInfo;
        }
    }

创建SessionManager类

    /**
     * @Description 自定义获取Token
     * @Author Sans
     * @CreateTime 2019/6/13 8:34
     */
    public class ShiroSessionManager extends DefaultWebSessionManager {
        //定义常量
        private static final String AUTHORIZATION = "Authorization";
        private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
        //重写构造器
        public ShiroSessionManager() {
            super();
            this.setDeleteInvalidSessions(true);
        }
        /**
         * 重写方法实现从请求头获取Token便于接口统一
         * 每次请求进来,Shiro会去从请求头找Authorization这个key对应的Value(Token)
         * @Author Sans
         * @CreateTime 2019/6/13 8:47
         */
        @Override
        public Serializable getSessionId(ServletRequest request, ServletResponse response) {
            String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
            //如果请求头中存在token 则从请求头中获取token
            if (!StringUtils.isEmpty(token)) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
                return token;
            } else {
                // 这里禁用掉Cookie获取方式
                // 按默认规则从Cookie取Token
                // return super.getSessionId(request, response);
                return null;
            }
        }
    }
    

创建ShiroConfig配置类

    /**
     * @Description Shiro配置类
     * @Author Sans
     * @CreateTime 2019/6/10 17:42
     */
    @Configuration
    public class ShiroConfig {
    
        private final String CACHE_KEY = "shiro:cache:";
        private final String SESSION_KEY = "shiro:session:";
    
        //Redis配置
        @Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private int port;
        @Value("${spring.redis.timeout}")
        private int timeout;
        @Value("${spring.redis.password}")
        private String password;
    
        /**
         * 开启Shiro-aop注解支持
         * @Attention 使用代理方式所以需要开启代码支持
         * @Author Sans
         * @CreateTime 2019/6/12 8:38
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        /**
         * Shiro基础配置
         * @Author Sans
         * @CreateTime 2019/6/12 8:42
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // 注意过滤器配置顺序不能颠倒
            // 配置过滤:不会被拦截的链接
            filterChainDefinitionMap.put("/static/**", "anon");
            filterChainDefinitionMap.put("/userLogin/**", "anon");
            filterChainDefinitionMap.put("/**", "authc");
            // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
            shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
        /**
         * 安全管理器
         * @Author Sans
         * @CreateTime 2019/6/12 10:34
         */
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 自定义Ssession管理
            securityManager.setSessionManager(sessionManager());
            // 自定义Cache实现
            securityManager.setCacheManager(cacheManager());
            // 自定义Realm验证
            securityManager.setRealm(shiroRealm());
            return securityManager;
        }
    
        /**
         * 身份验证器
         * @Author Sans
         * @CreateTime 2019/6/12 10:37
         */
        @Bean
        public ShiroRealm shiroRealm() {
            ShiroRealm shiroRealm = new ShiroRealm();
            shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return shiroRealm;
        }
    
        /**
         * 凭证匹配器
         * 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
         * @Author Sans
         * @CreateTime 2019/6/12 10:48
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
            // 散列算法:这里使用SHA256算法;
            shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
            // 散列的次数,比如散列两次,相当于 md5(md5(""));
            shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
            return shaCredentialsMatcher;
        }
    
        /**
         * 配置Redis管理器
         * @Attention 使用的是shiro-redis开源插件
         * @Author Sans
         * @CreateTime 2019/6/12 11:06
         */
        @Bean
        public RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host);
            redisManager.setPort(port);
            redisManager.setTimeout(timeout);
            redisManager.setPassword(password);
            return redisManager;
        }
    
        /**
         * 配置Cache管理器
         * 用于往Redis存储权限和角色标识
         * @Attention 使用的是shiro-redis开源插件
         * @Author Sans
         * @CreateTime 2019/6/12 12:37
         */
        @Bean
        public RedisCacheManager cacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            redisCacheManager.setKeyPrefix(CACHE_KEY);
            // 配置缓存的话要求放在session里面的实体类必须有个id标识
            redisCacheManager.setPrincipalIdFieldName("userId");
            return redisCacheManager;
        }
    
        /**
         * SessionID生成器
         * @Author Sans
         * @CreateTime 2019/6/12 13:12
         */
        @Bean
        public ShiroSessionIdGenerator sessionIdGenerator(){
            return new ShiroSessionIdGenerator();
        }
    
        /**
         * 配置RedisSessionDAO
         * @Attention 使用的是shiro-redis开源插件
         * @Author Sans
         * @CreateTime 2019/6/12 13:44
         */
        @Bean
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
            redisSessionDAO.setKeyPrefix(SESSION_KEY);
            redisSessionDAO.setExpire(timeout);
            return redisSessionDAO;
        }
    
        /**
         * 配置Session管理器
         * @Author Sans
         * @CreateTime 2019/6/12 14:25
         */
        @Bean
        public SessionManager sessionManager() {
            ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
            shiroSessionManager.setSessionDAO(redisSessionDAO());
            return shiroSessionManager;
        }
    }

四.实现权限控制

Shiro可以用代码或者注解来控制权限,通常我们使用注解控制,不仅简单方便,而且更加灵活.Shiro注解一共有五个:

注解名称 说明
RequiresAuthentication 使用该注解标注的类,方法等在访问时,当前Subject必须在当前session中已经过认证.
RequiresGuest 使用该注解标注的类,方法等在访问时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录.
RequiresUser 验证用户是否被记忆,有两种含义:一种是成功登录的(subject.isAuthenticated()结果为true);另外一种是被记忆的(subject.isRemembered()结果为true).
RequiresPermissions 当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法.如果没有权限,则方法不会执行还会抛出AuthorizationException异常.
RequiresRoles 当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法.如果没有角色,则方法不会执行还会抛出AuthorizationException异常.

一般情况下我们在项目中做权限控制,使用最多的是RequiresPermissions和RequiresRoles,允许存在多个角色和权限,默认逻辑是AND,也就是同时拥有这些才可以访问方法,可以在注解中以参数的形式设置成OR

    示例
    //拥有一个角色就可以访问
    @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
    //拥有所有权限才可以访问
    @RequiresPermissions(value={"sys:user:info","sys:role:info"},logical = Logical.AND)

使用顺序:Shiro注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止,默认拦截顺序是:

RequiresRoles->RequiresPermissions->RequiresAuthentication->RequiresUser->RequiresGuest

    示例
    //拥有ADMIN角色同时还要有sys:role:info权限
    @RequiresRoles(value={"ADMIN")
    @RequiresPermissions("sys:role:info")

创建UserRoleController角色拦截测试类

    /**
     * @Description 角色测试
     * @Author Sans
     * @CreateTime 2019/6/19 11:38
     */
    @RestController
    @RequestMapping("/role")
    public class UserRoleController {
    
        @Autowired
        private SysUserService sysUserService;
        @Autowired
        private SysRoleService sysRoleService;
        @Autowired
        private SysMenuService sysMenuService;
        @Autowired
        private SysRoleMenuService sysRoleMenuService;
    
        /**
         * 管理员角色测试接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getAdminInfo")
        @RequiresRoles("ADMIN")
        public Map<String,Object> getAdminInfo(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","这里是只有管理员角色能访问的接口");
            return map;
        }
    
        /**
         * 用户角色测试接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getUserInfo")
        @RequiresRoles("USER")
        public Map<String,Object> getUserInfo(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","这里是只有用户角色能访问的接口");
            return map;
        }
    
        /**
         * 角色测试接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getRoleInfo")
        @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
        @RequiresUser
        public Map<String,Object> getRoleInfo(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");
            return map;
        }
    
        /**
         * 登出(测试登出)
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getLogout")
        @RequiresUser
        public Map<String,Object> getLogout(){
            ShiroUtils.logout();
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","登出");
            return map;
        }
    }

创建UserMenuController权限拦截测试类

    /**
     * @Description 权限测试
     * @Author Sans
     * @CreateTime 2019/6/19 11:38
     */
    @RestController
    @RequestMapping("/menu")
    public class UserMenuController {
    
        @Autowired
        private SysUserService sysUserService;
        @Autowired
        private SysRoleService sysRoleService;
        @Autowired
        private SysMenuService sysMenuService;
        @Autowired
        private SysRoleMenuService sysRoleMenuService;
        
        /**
         * 获取用户信息集合
         * @Author Sans
         * @CreateTime 2019/6/19 10:36
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getUserInfoList")
        @RequiresPermissions("sys:user:info")
        public Map<String,Object> getUserInfoList(){
            Map<String,Object> map = new HashMap<>();
            List<SysUserEntity> sysUserEntityList = sysUserService.list();
            map.put("sysUserEntityList",sysUserEntityList);
            return map;
        }
    
        /**
         * 获取角色信息集合
         * @Author Sans
         * @CreateTime 2019/6/19 10:37
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getRoleInfoList")
        @RequiresPermissions("sys:role:info")
        public Map<String,Object> getRoleInfoList(){
            Map<String,Object> map = new HashMap<>();
            List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
            map.put("sysRoleEntityList",sysRoleEntityList);
            return map;
        }
    
        /**
         * 获取权限信息集合
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getMenuInfoList")
        @RequiresPermissions("sys:menu:info")
        public Map<String,Object> getMenuInfoList(){
            Map<String,Object> map = new HashMap<>();
            List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
            map.put("sysMenuEntityList",sysMenuEntityList);
            return map;
        }
    
        /**
         * 获取所有数据
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/getInfoAll")
        @RequiresPermissions("sys:info:all")
        public Map<String,Object> getInfoAll(){
            Map<String,Object> map = new HashMap<>();
            List<SysUserEntity> sysUserEntityList = sysUserService.list();
            map.put("sysUserEntityList",sysUserEntityList);
            List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
            map.put("sysRoleEntityList",sysRoleEntityList);
            List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
            map.put("sysMenuEntityList",sysMenuEntityList);
            return map;
        }
    
        /**
         * 添加管理员角色权限(测试动态权限更新)
         * @Author Sans
         * @CreateTime 2019/6/19 10:39
         * @Param  username 用户ID
         * @Return Map<String,Object> 返回结果
         */
        @RequestMapping("/addMenu")
        public Map<String,Object> addMenu(){
            //添加管理员角色权限
            SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
            sysRoleMenuEntity.setMenuId(4L);
            sysRoleMenuEntity.setRoleId(1L);
            sysRoleMenuService.save(sysRoleMenuEntity);
            //清除缓存
            String username = "admin";
            ShiroUtils.deleteCache(username,false);
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","权限添加成功");
            return map;
        }
    }

创建UserLoginController登录类

    /**
     * @Description 用户登录
     * @Author Sans
     * @CreateTime 2019/6/17 15:21
     */
    @RestController
    @RequestMapping("/userLogin")
    public class UserLoginController {
    
        /**
         * 登录
         * @Author Sans
         * @CreateTime 2019/6/20 9:21
         */
        @RequestMapping("/login")
        public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
            Map<String,Object> map = new HashMap<>();
            //进行身份验证
            try{
                //验证身份和登陆
                Subject subject = SecurityUtils.getSubject();
                UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
                //验证成功进行登录操作
                subject.login(token);
            }catch (IncorrectCredentialsException e) {
                map.put("code",500);
                map.put("msg","用户不存在或者密码错误");
                return map;
            } catch (LockedAccountException e) {
                map.put("code",500);
                map.put("msg","登录失败,该用户已被冻结");
                return map;
            } catch (AuthenticationException e) {
                map.put("code",500);
                map.put("msg","该用户不存在");
                return map;
            } catch (Exception e) {
                map.put("code",500);
                map.put("msg","未知异常");
                return map;
            }
            map.put("code",0);
            map.put("msg","登录成功");
            map.put("token",ShiroUtils.getSession().getId().toString());
            return map;
        }
        /**
         * 未登录
         * @Author Sans
         * @CreateTime 2019/6/20 9:22
         */
        @RequestMapping("/unauth")
        public Map<String,Object> unauth(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",500);
            map.put("msg","未登录");
            return map;
        }
        /**
         * 添加一个用户演示接口
         * 这里仅作为演示不加任何权限和重复查询校验
         * @Author Sans
         * @CreateTime 2020/1/6 9:22
         */
        @RequestMapping("/testAddUser")
        public Map<String,Object> testAddUser(){
            // 设置基础参数
            SysUserEntity sysUser = new SysUserEntity();
            sysUser.setUsername("user1");
            sysUser.setState("NORMAL");
            // 随机生成盐值
            String salt = RandomStringUtils.randomAlphanumeric(20);
            sysUser.setSalt(salt);
            // 进行加密
            String password ="123456";
            sysUser.setPassword(SHA256Util.sha256(password, sysUser.getSalt()));
            // 保存用户
            sysUserService.save(sysUser);
            // 保存角色
            SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();
            sysUserRoleEntity.setUserId(sysUser.getUserId()); // 保存用户完之后会把ID返回给用户实体
            sysUserRoleService.save(sysUserRoleEntity);
            // 返回结果
            Map<String,Object> map = new HashMap<>();
            map.put("code",0);
            map.put("msg","添加成功");
            return map;
        }
    }

五.POSTMAN测试

登录成功后会返回TOKEN,因为是单点登录,再次登陆的话会返回新的TOKEN,之前Redis的TOKEN就会失效了

image

当第一次访问接口后我们可以看到缓存中已经有权限数据了,在次访问接口的时候,Shiro会直接去缓存中拿取权限,注意访问接口时候要设置请求头.

image

image

ADMIN这个号现在没有sys:info:all这个权限的,所以无法访问getInfoAll接口,我们要动态分配权限后,要清掉缓存,在访问接口时候,Shiro会去重新执行授权方法,之后再次把权限和角色数据放入缓存中

image

访问添加权限测试接口,因为是测试,我把增加权限的用户ADMIN写死在里面了,权限添加后,调用工具类清掉缓存,我们可以发现,Redis中已经没有缓存了

image

image

再次访问getInfoAll接口,因为缓存中没有数据,Shiro会重新授权查询权限,拦截通过

image

六.项目源码

码云: gitee.com/liselotte/s…
GitHub: github.com/xuyulong201…

谢谢大家阅读,如果喜欢,请收藏点赞,多给些star,文章不足之处,也请给出宝贵意见.

推荐

学习资料分享

12 套 微服务、Spring Boot、Spring Cloud 核心技术资料,这是部分资料目录:

  • Spring Security 认证与授权
  • Spring Boot 项目实战(中小型互联网公司后台服务架构与运维架构)
  • Spring Boot 项目实战(企业权限管理项目))
  • Spring Cloud 微服务架构项目实战(分布式事务解决方案)
  • ...

公众号后台回复arch028获取资料::

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343