SpringBoot 整合Shiro實(shí)現(xiàn)動(dòng)態(tài)權(quán)限加載更新+Session共享+單點(diǎn)登錄

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

一.說明

Shiro是一個(gè)安全框架,項(xiàng)目中主要用它做認(rèn)證,授權(quán),加密,以及用戶的會(huì)話管理,雖然Shiro沒有SpringSecurity功能更豐富,但是它輕量,簡單,在項(xiàng)目中通常業(yè)務(wù)需求Shiro也都能勝任.

二.項(xiàng)目環(huán)境

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

數(shù)據(jù)表(SQL文件在項(xiàng)目中):數(shù)據(jù)庫中測(cè)試號(hào)的密碼進(jìn)行了加密,密碼皆為123456

數(shù)據(jù)表名 中文表名 備注說明
sys_user 系統(tǒng)用戶表 基礎(chǔ)表
sys_menu 權(quán)限表 基礎(chǔ)表
sys_role 角色表 基礎(chǔ)表
sys_role_menu 角色與權(quán)限關(guān)系表 中間表
sys_user_role 用戶與角色關(guān)系表 中間表

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依賴,一定要加,否則權(quán)限攔截驗(yàn)證不生效 -->
            <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>
            <!-- 引入阿里數(shù)據(jù)庫連接池 -->
            <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:
      # 配置數(shù)據(jù)源
      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數(shù)據(jù)源
      redis:
        host: localhost
        port: 6379
        timeout: 6000
        password: 123456
        jedis:
          pool:
            max-active: 1000  # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
            max-wait: -1      # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
            max-idle: 10      # 連接池中的最大空閑連接
            min-idle: 5       # 連接池中的最小空閑連接
    # mybatis-plus相關(guān)配置
    mybatis-plus:
      # xml掃描,多個(gè)目錄用逗號(hào)或者分號(hào)分隔(告訴 Mapper 所對(duì)應(yīng)的 XML 文件位置)
      mapper-locations: classpath:mapper/*.xml
      # 以下配置均有默認(rèn)值,可以不設(shè)置
      global-config:
        db-config:
          #主鍵類型 AUTO:"數(shù)據(jù)庫ID自增" INPUT:"用戶輸入ID",ID_WORKER:"全局唯一ID (數(shù)字類型唯一ID)", UUID:"全局唯一ID UUID";
          id-type: auto
          #字段策略 IGNORED:"忽略判斷"  NOT_NULL:"非 NULL 判斷")  NOT_EMPTY:"非空判斷"
          field-strategy: NOT_EMPTY
          #數(shù)據(jù)庫類型
          db-type: MYSQL
      configuration:
        # 是否開啟自動(dòng)駝峰命名規(guī)則映射:從數(shù)據(jù)庫列名到Java屬性駝峰命名的類似映射
        map-underscore-to-camel-case: true
        # 返回map時(shí)true:當(dāng)查詢數(shù)據(jù)為空時(shí)字段返回為null,false:不加這個(gè)查詢數(shù)據(jù)為空時(shí),字段將被隱藏
        call-setters-on-nulls: true
        # 這個(gè)配置會(huì)將執(zhí)行的sql打印出來,在開發(fā)或測(cè)試的時(shí)候可以用
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

二.編寫項(xiàng)目基礎(chǔ)類

用戶實(shí)體,Dao,Service等在這里省略,請(qǐng)參考源碼

編寫Exception類來處理Shiro權(quán)限攔截異常

    /**
     * @Description 自定義異常
     * @Author Sans
     * @CreateTime 2019/6/15 22:56
     */
    @ControllerAdvice
    public class MyShiroException {
        /**
         * 處理Shiro權(quán)限攔截異常
         * 如果返回JSON數(shù)據(jù)格式請(qǐng)加上 @ResponseBody注解
         * @Author Sans
         * @CreateTime 2019/6/15 13:35
         * @Return Map<Object> 返回結(jié)果集
         */
        @ResponseBody
        @ExceptionHandler(value = AuthorizationException.class)
        public Map<String,Object> defaultErrorHandler(){
            Map<String,Object> map = new HashMap<>();
            map.put("403","權(quán)限不足");
            return map;
        }
    }

創(chuàng)建SHA256Util加密工具

    /**
     * @Description Sha-256加密工具
     * @Author Sans
     * @CreateTime 2019/6/12 9:27
     */
    public class SHA256Util {
        /**  私有構(gòu)造器 **/
        private SHA256Util(){};
        /**  加密算法 **/
        public final static String HASH_ALGORITHM_NAME = "SHA-256";
        /**  循環(huán)次數(shù) **/
        public final static int HASH_ITERATIONS = 15;
        /**  執(zhí)行加密-采用SHA256和鹽值加密 **/
        public static String sha256(String password, String salt) {
            return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
        }
    }

創(chuàng)建Spring工具

    /**
     * @Description Spring上下文工具類
     * @Author Sans
     * @CreateTime 2019/6/17 13:40
     */
    @Component
    public class SpringUtil implements ApplicationContextAware {
        private static ApplicationContext context;
        /**
         * Spring在bean初始化后會(huì)判斷是不是ApplicationContextAware的子類
         * 如果該類是,setApplicationContext()方法,會(huì)將容器中ApplicationContext作為參數(shù)傳入進(jìn)去
         * @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);
        }
    }

創(chuàng)建Shiro工具

    /**
     * @Description Shiro工具類
     * @Author Sans
     * @CreateTime 2019/6/15 16:11
     */
    public class ShiroUtils {
    
        /** 私有構(gòu)造器 **/
        private ShiroUtils(){}
    
        private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
    
        /**
         * 獲取當(dāng)前用戶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();
        }
    
        /**
        * 獲取當(dāng)前用戶信息
        * @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,找到該用戶名稱對(duì)應(yīng)的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,在訪問受限接口時(shí)會(huì)重新授權(quán)
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
            Authenticator authc = securityManager.getAuthenticator();
            ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
        }
    }

創(chuàng)建Shiro的SessionId生成器

    /**
     * @Description 自定義SessionId生成器
     * @Author Sans
     * @CreateTime 2019/6/11 11:48
     */
    public class ShiroSessionIdGenerator implements SessionIdGenerator {
        /**
         * 實(shí)現(xiàn)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核心類

創(chuàng)建Realm用于授權(quán)和認(rèn)證

    /**
     * @Description Shiro權(quán)限匹配和賬號(hào)密碼匹配
     * @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;
        /**
         * 授權(quán)權(quán)限
         * 用戶進(jìn)行權(quán)限驗(yàn)證時(shí)候Shiro會(huì)去緩存中找,如果查不到數(shù)據(jù),會(huì)執(zhí)行這個(gè)方法去查權(quán)限,并放入緩存中
         * @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();
            //這里可以進(jìn)行授權(quán)和處理
            Set<String> rolesSet = new HashSet<>();
            Set<String> permsSet = new HashSet<>();
            //查詢角色和權(quán)限(這里根據(jù)業(yè)務(wù)自行查詢)
            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());
                }
            }
            //將查到的權(quán)限和角色分別傳入authorizationInfo中
            authorizationInfo.setStringPermissions(permsSet);
            authorizationInfo.setRoles(rolesSet);
            return authorizationInfo;
        }
        
        /**
         * 身份認(rèn)證
         * @Author Sans
         * @CreateTime 2019/6/12 12:36
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //獲取用戶的輸入的賬號(hào).
            String username = (String) authenticationToken.getPrincipal();
            //通過username從數(shù)據(jù)庫中查找 User對(duì)象,如果找到進(jìn)行驗(yàn)證
            //實(shí)際項(xiàng)目中,這里可以根據(jù)實(shí)際情況做緩存,如果不做,Shiro自己也是有時(shí)間間隔機(jī)制,2分鐘內(nèi)不會(huì)重復(fù)執(zhí)行該方法
            SysUserEntity user = sysUserService.selectUserByName(username);
            //判斷賬號(hào)是否存在
            if (user == null) {
                throw new AuthenticationException();
            }
            //判斷賬號(hào)是否被凍結(jié)
            if (user.getState()==null||user.getState().equals("PROHIBIT")){
                throw new LockedAccountException();
            }
            //進(jìn)行驗(yàn)證
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    user,                                  //用戶名
                    user.getPassword(),                    //密碼
                    ByteSource.Util.bytes(user.getSalt()), //設(shè)置鹽值
                    getName()
            );
            //驗(yàn)證成功開始踢人(清除緩存和Session)
            ShiroUtils.deleteCache(username,true);
            return authenticationInfo;
        }
    }

創(chuàng)建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";
        //重寫構(gòu)造器
        public ShiroSessionManager() {
            super();
            this.setDeleteInvalidSessions(true);
        }
        /**
         * 重寫方法實(shí)現(xiàn)從請(qǐng)求頭獲取Token便于接口統(tǒng)一
         * 每次請(qǐng)求進(jìn)來,Shiro會(huì)去從請(qǐng)求頭找Authorization這個(gè)key對(duì)應(yīng)的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);
            //如果請(qǐng)求頭中存在token 則從請(qǐng)求頭中獲取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獲取方式
                // 按默認(rèn)規(guī)則從Cookie取Token
                // return super.getSessionId(request, response);
                return null;
            }
        }
    }
    

創(chuàng)建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基礎(chǔ)配置
         * @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<>();
            // 注意過濾器配置順序不能顛倒
            // 配置過濾:不會(huì)被攔截的鏈接
            filterChainDefinitionMap.put("/static/**", "anon");
            filterChainDefinitionMap.put("/userLogin/**", "anon");
            filterChainDefinitionMap.put("/**", "authc");
            // 配置shiro默認(rèn)登錄界面地址,前后端分離中登錄界面跳轉(zhuǎn)應(yīng)由前端路由控制,后臺(tái)僅返回json數(shù)據(jù)
            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實(shí)現(xiàn)
            securityManager.setCacheManager(cacheManager());
            // 自定義Realm驗(yàn)證
            securityManager.setRealm(shiroRealm());
            return securityManager;
        }
    
        /**
         * 身份驗(yàn)證器
         * @Author Sans
         * @CreateTime 2019/6/12 10:37
         */
        @Bean
        public ShiroRealm shiroRealm() {
            ShiroRealm shiroRealm = new ShiroRealm();
            shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return shiroRealm;
        }
    
        /**
         * 憑證匹配器
         * 將密碼校驗(yàn)交給Shiro的SimpleAuthenticationInfo進(jìn)行處理,在這里做匹配配置
         * @Author Sans
         * @CreateTime 2019/6/12 10:48
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
            // 散列算法:這里使用SHA256算法;
            shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
            // 散列的次數(shù),比如散列兩次,相當(dāng)于 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存儲(chǔ)權(quán)限和角色標(biāo)識(shí)
         * @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里面的實(shí)體類必須有個(gè)id標(biāo)識(shí)
            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;
        }
    }

四.實(shí)現(xiàn)權(quán)限控制

Shiro可以用代碼或者注解來控制權(quán)限,通常我們使用注解控制,不僅簡單方便,而且更加靈活.Shiro注解一共有五個(gè):

注解名稱 說明
RequiresAuthentication 使用該注解標(biāo)注的類,方法等在訪問時(shí),當(dāng)前Subject必須在當(dāng)前session中已經(jīng)過認(rèn)證.
RequiresGuest 使用該注解標(biāo)注的類,方法等在訪問時(shí),當(dāng)前Subject可以是“gust”身份,不需要經(jīng)過認(rèn)證或者在原先的session中存在記錄.
RequiresUser 驗(yàn)證用戶是否被記憶,有兩種含義:一種是成功登錄的(subject.isAuthenticated()結(jié)果為true);另外一種是被記憶的(subject.isRemembered()結(jié)果為true).
RequiresPermissions 當(dāng)前Subject需要擁有某些特定的權(quán)限時(shí),才能執(zhí)行被該注解標(biāo)注的方法.如果沒有權(quán)限,則方法不會(huì)執(zhí)行還會(huì)拋出AuthorizationException異常.
RequiresRoles 當(dāng)前Subject必須擁有所有指定的角色時(shí),才能訪問被該注解標(biāo)注的方法.如果沒有角色,則方法不會(huì)執(zhí)行還會(huì)拋出AuthorizationException異常.

一般情況下我們?cè)陧?xiàng)目中做權(quán)限控制,使用最多的是RequiresPermissions和RequiresRoles,允許存在多個(gè)角色和權(quán)限,默認(rèn)邏輯是AND,也就是同時(shí)擁有這些才可以訪問方法,可以在注解中以參數(shù)的形式設(shè)置成OR

    示例
    //擁有一個(gè)角色就可以訪問
    @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
    //擁有所有權(quán)限才可以訪問
    @RequiresPermissions(value={"sys:user:info","sys:role:info"},logical = Logical.AND)

使用順序:Shiro注解是存在順序的,當(dāng)多個(gè)注解在一個(gè)方法上的時(shí)候,會(huì)逐個(gè)檢查,知道全部通過為止,默認(rèn)攔截順序是:

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

    示例
    //擁有ADMIN角色同時(shí)還要有sys:role:info權(quán)限
    @RequiresRoles(value={"ADMIN")
    @RequiresPermissions("sys:role:info")

創(chuàng)建UserRoleController角色攔截測(cè)試類

    /**
     * @Description 角色測(cè)試
     * @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;
    
        /**
         * 管理員角色測(cè)試接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @RequestMapping("/getAdminInfo")
        @RequiresRoles("ADMIN")
        public Map<String,Object> getAdminInfo(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","這里是只有管理員角色能訪問的接口");
            return map;
        }
    
        /**
         * 用戶角色測(cè)試接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @RequestMapping("/getUserInfo")
        @RequiresRoles("USER")
        public Map<String,Object> getUserInfo(){
            Map<String,Object> map = new HashMap<>();
            map.put("code",200);
            map.put("msg","這里是只有用戶角色能訪問的接口");
            return map;
        }
    
        /**
         * 角色測(cè)試接口
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @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;
        }
    
        /**
         * 登出(測(cè)試登出)
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @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;
        }
    }

創(chuàng)建UserMenuController權(quán)限攔截測(cè)試類

    /**
     * @Description 權(quán)限測(cè)試
     * @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> 返回結(jié)果
         */
        @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> 返回結(jié)果
         */
        @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;
        }
    
        /**
         * 獲取權(quán)限信息集合
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @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;
        }
    
        /**
         * 獲取所有數(shù)據(jù)
         * @Author Sans
         * @CreateTime 2019/6/19 10:38
         * @Return Map<String,Object> 返回結(jié)果
         */
        @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;
        }
    
        /**
         * 添加管理員角色權(quán)限(測(cè)試動(dòng)態(tài)權(quán)限更新)
         * @Author Sans
         * @CreateTime 2019/6/19 10:39
         * @Param  username 用戶ID
         * @Return Map<String,Object> 返回結(jié)果
         */
        @RequestMapping("/addMenu")
        public Map<String,Object> addMenu(){
            //添加管理員角色權(quán)限
            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","權(quán)限添加成功");
            return map;
        }
    }

創(chuàng)建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<>();
            //進(jìn)行身份驗(yàn)證
            try{
                //驗(yàn)證身份和登陸
                Subject subject = SecurityUtils.getSubject();
                UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
                //驗(yàn)證成功進(jìn)行登錄操作
                subject.login(token);
            }catch (IncorrectCredentialsException e) {
                map.put("code",500);
                map.put("msg","用戶不存在或者密碼錯(cuò)誤");
                return map;
            } catch (LockedAccountException e) {
                map.put("code",500);
                map.put("msg","登錄失敗,該用戶已被凍結(jié)");
                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;
        }
        /**
         * 添加一個(gè)用戶演示接口
         * 這里僅作為演示不加任何權(quán)限和重復(fù)查詢校驗(yàn)
         * @Author Sans
         * @CreateTime 2020/1/6 9:22
         */
        @RequestMapping("/testAddUser")
        public Map<String,Object> testAddUser(){
            // 設(shè)置基礎(chǔ)參數(shù)
            SysUserEntity sysUser = new SysUserEntity();
            sysUser.setUsername("user1");
            sysUser.setState("NORMAL");
            // 隨機(jī)生成鹽值
            String salt = RandomStringUtils.randomAlphanumeric(20);
            sysUser.setSalt(salt);
            // 進(jìn)行加密
            String password ="123456";
            sysUser.setPassword(SHA256Util.sha256(password, sysUser.getSalt()));
            // 保存用戶
            sysUserService.save(sysUser);
            // 保存角色
            SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();
            sysUserRoleEntity.setUserId(sysUser.getUserId()); // 保存用戶完之后會(huì)把ID返回給用戶實(shí)體
            sysUserRoleService.save(sysUserRoleEntity);
            // 返回結(jié)果
            Map<String,Object> map = new HashMap<>();
            map.put("code",0);
            map.put("msg","添加成功");
            return map;
        }
    }

五.POSTMAN測(cè)試

登錄成功后會(huì)返回TOKEN,因?yàn)槭菃吸c(diǎn)登錄,再次登陸的話會(huì)返回新的TOKEN,之前Redis的TOKEN就會(huì)失效了

image

當(dāng)?shù)谝淮卧L問接口后我們可以看到緩存中已經(jīng)有權(quán)限數(shù)據(jù)了,在次訪問接口的時(shí)候,Shiro會(huì)直接去緩存中拿取權(quán)限,注意訪問接口時(shí)候要設(shè)置請(qǐng)求頭.

image

image

ADMIN這個(gè)號(hào)現(xiàn)在沒有sys:info:all這個(gè)權(quán)限的,所以無法訪問getInfoAll接口,我們要?jiǎng)討B(tài)分配權(quán)限后,要清掉緩存,在訪問接口時(shí)候,Shiro會(huì)去重新執(zhí)行授權(quán)方法,之后再次把權(quán)限和角色數(shù)據(jù)放入緩存中

image

訪問添加權(quán)限測(cè)試接口,因?yàn)槭菧y(cè)試,我把增加權(quán)限的用戶ADMIN寫死在里面了,權(quán)限添加后,調(diào)用工具類清掉緩存,我們可以發(fā)現(xiàn),Redis中已經(jīng)沒有緩存了

image

image

再次訪問getInfoAll接口,因?yàn)榫彺嬷袥]有數(shù)據(jù),Shiro會(huì)重新授權(quán)查詢權(quán)限,攔截通過

image

六.項(xiàng)目源碼

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

謝謝大家閱讀,如果喜歡,請(qǐng)收藏點(diǎn)贊,多給些star,文章不足之處,也請(qǐng)給出寶貴意見.

推薦

學(xué)習(xí)資料分享

12 套 微服務(wù)、Spring Boot、Spring Cloud 核心技術(shù)資料,這是部分資料目錄:

  • Spring Security 認(rèn)證與授權(quán)
  • Spring Boot 項(xiàng)目實(shí)戰(zhàn)(中小型互聯(lián)網(wǎng)公司后臺(tái)服務(wù)架構(gòu)與運(yùn)維架構(gòu))
  • Spring Boot 項(xiàng)目實(shí)戰(zhàn)(企業(yè)權(quán)限管理項(xiàng)目))
  • Spring Cloud 微服務(wù)架構(gòu)項(xiàng)目實(shí)戰(zhàn)(分布式事務(wù)解決方案)
  • ...

公眾號(hào)后臺(tái)回復(fù)arch028獲取資料::

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容