基于Shiro框架的权限系统,包括登录权限、角色权限、菜单权限,spring-boot + mybatis + shiro 20190809

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u012888052/article/details/98984036

一、简介

  • Shiro 这个框架,先甩一张网上流行的架构图,看起来很高大上,然后呢,就不用细看了,知道哪个里面嵌套哪个,了解下大体结构,方便理解后面的代码结构:
    在这里插入图片描述
  • 博主对权限系统理解有限,此入门项目涵盖三部分权限场景,包括:登录、接口权限、菜单权限

二、环境搭建

三、项目讲解

项目太抽象的话,不方便理解,所以我们假设一个场景,每个曾经上学很怀念的场景:
用户账号:zhuren(教务处王主任,可以叫他老王)shuxue(数学张老师,曾经拿过全国奥数奖的老张)
用户角色:zhuren(主任角色,权限很大,各种阴谋诡计)teacher(老师角色,备备课,讲讲题,顺便拖拖堂)
用户接口权限:shoubanfei(收班费,没准还能收点贿赂)jiaoxuesheng(教学生,假期还能办个补习班赚外快)
用户菜单权限:教务管理 课程安排

  • 根据上方假设的场景,我们来建立权限系统,并通过Shiro框架实现权限控制
  • 源码中还包含 强制T人、禁用账号、多端互T 等功能,博客中就不一一介绍了,详见源码
  • 登录权限
    • 账号密码登录
    • 账号密码登录成功后,返回权限对应的菜单
    • 账号密码带验证码登录
  • 接口权限
    • 单一角色限制
    • 多个角色限制
    • 单一权限限制
    • 多个权限限制

四、项目搭建

  • 环境搭建好后,我们先把表关系理清楚,大致分为下面几个表:

tb_user:用户表
tb_role:角色表
tb_permission:权限表
tb_menu:菜单表
tb_user_role:用户-角色关联表
tb_role_permission:角色-权限关联表
tb_role_menu:角色-菜单关联表

  • 把表分别拆开关联的好处是可扩展,并且可实现多对多的关联关系,应对后续复杂的业务场景很适用
  • 数据库的SQL文件会在文末源码中,带初始数据
  • 接下来贴上项目完成后的目录结构
    在这里插入图片描述
    在这里插入图片描述
  • 新建项目,在pom.xml文件中,引入下方的maven代码进行构建,每一个都有注释,使用时,可以思考为什么要使用这个:
<dependencies>
        <!-- Apache Shiro依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 连接mysql数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- JDBC连接数据库 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- 添加web支持   包含SpringMVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--@ConfigurationProperties注解-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- redis依赖commons-pool 这个依赖一定要添加 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- google kaptcha验证码 -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>javax.servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>
  • 必要的包构建完成后,打开配置文件(这里使用的是yml的配置文件语法),配置好参数,比如端口号、数据库链接、redis链接、shiro的一些自定义缓存参数等
# 端口号 port
server:
  port: 8080

# 数据库配置 jdbc
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
    username: root
    password: root

  # 缓存配置 redis
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password:

mybatis:
  mapper-locations: classpath:/mappers/*.xml
  type-aliases-package: com.example.shiro.model

# session cache config
shiro:
  redis:
    sessionLive: 30
    sessionPrefix: shiro_redis_session_
    cacheLive: 30
    cachePrefix: shiro_redis_cache_
    kickoutPrefix: shiro_redis_kickout_
    # 验证码缓存时间
    verificationCodeTime: 5
    # 踢出缓存key
    kickOutKey: out

  • 接下来,我们可以先实现前面7张表的增删改查操作(可以适当少写点代码,不一定增删改查都会用到的),也就是项目目录中的:service、serviceImpl、dao、model、mapper 此处省略2000行代码,不细讲增删改查了,练就一身CV大法的我已经疲惫
  • 直接讲重点,核心文件就一个,没有之一:ShiroConfig
  • 它里面创建了各种Bean,根据最开始贴的shiro架构图,可以看出每一个Bean所在的位置,分别将redis、自定义的配置,填充到Shiro框架中了,代码中有每个Bean的注释,可以看到每一个Bean的作用
package com.shiro.api.config;

import com.shiro.api.core.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import javax.servlet.Filter;

import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.DelegatingFilterProxy;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro配置文件
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
@Configuration
public class ShiroConfig {

    // session缓存时间
    @Value("${shiro.redis.sessionLive}")
    private long sessionLive;
    // session前缀
    @Value("${shiro.redis.sessionPrefix}")
    private String sessionPrefix;
    // redis缓存时间
    @Value("${shiro.redis.cacheLive}")
    private long cacheLive;
    // redis缓存前缀
    @Value("${shiro.redis.cachePrefix}")
    private String cachePrefix;
    // 验证码缓存前缀
    @Value("${shiro.redis.kickoutPrefix}")
    private String kickoutPrefix;

    /**
     * 自定义redis缓存管理器
     *
     * @param redisTemplate redis模版
     * @return redis缓存管理器对象
     */
    @Bean(name = "redisCacheManager")
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        // 将redis缓存时间及前缀,放到配置中
        redisCacheManager.setCacheLive(cacheLive);
        redisCacheManager.setCacheKeyPrefix(cachePrefix);
        redisCacheManager.setRedisTemplate(redisTemplate);
        return redisCacheManager;
    }

    /**
     * 凭证匹配器(密码加密)
     *
     * @return 凭证匹配器对象
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 加密算法,指定MD5加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 加密的次数为2次,相当于 MD5(MD5())
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * Session ID生成管理器
     *
     * @return sessionId 生成器对象
     */
    @Bean(name = "sessionIdGenerator")
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    /**
     * 自定义RedisSessionDAO
     *
     * @param sessionIdGenerator sessionId 生成器
     * @param redisTemplate      redis模版
     * @return RedisSessionDAO 对象
     */
    @Bean(name = "redisSessionDAO")
    public RedisSessionDAO redisSessionDAO(JavaUuidSessionIdGenerator sessionIdGenerator, RedisTemplate redisTemplate) {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        // 配置sessionId生成器
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator);
        // 配置session缓存时间及缓存前缀
        redisSessionDAO.setSessionLive(sessionLive);
        redisSessionDAO.setSessionKeyPrefix(sessionPrefix);
        // 配置redis模版
        redisSessionDAO.setRedisTemplate(redisTemplate);
        return redisSessionDAO;
    }

    /**
     * 自定义session管理器
     *
     * @param redisSessionDAO redisSessionDAO
     * @return session管理器 对象
     */
    @Bean(name = "sessionManager")
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        MySessionManager mySessionManager = new MySessionManager();
        // 配置 自定义的RedisSessionDAO 对象
        mySessionManager.setSessionDAO(redisSessionDAO);
        return mySessionManager;
    }

    /**
     * 自定义域
     *
     * @return 自定义域 对象
     */
    @Bean(name = "myRealm")
    public MyRealm myRealm() {
        MyRealm myRealm = new MyRealm();
        // 启用缓存,默认为false
        myRealm.setCachingEnabled(true);
        return myRealm;
    }

    /**
     * 安全管理器
     *
     * @param sessionManager    session管理器
     * @param redisCacheManager redis缓存管理器
     * @return 安全管理器
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager(SessionManager sessionManager, RedisCacheManager redisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置自定义域
        securityManager.setRealm(myRealm());
        // 配置session管理器
        securityManager.setSessionManager(sessionManager);
        // 配置redis缓存管理器
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    /**
     * 多端互T控制过滤器
     *
     * @param sessionManager session管理器
     * @param redisTemplate  redis模版
     * @return 互T控制过滤器
     */
    @Bean(name = "outSessionControlFilter")
    public OutSessionControlFilter outSessionControlFilter(SessionManager sessionManager, RedisTemplate redisTemplate) {
        OutSessionControlFilter outSessionControlFilter = new OutSessionControlFilter();
        outSessionControlFilter.setSessionManager(sessionManager);
        outSessionControlFilter.setRedisTemplate(redisTemplate);
        outSessionControlFilter.setKickOutPrefix(kickoutPrefix);
        return outSessionControlFilter;
    }

    /**
     * shiro过滤器
     *
     * @param securityManager         安全管理器
     * @param outSessionControlFilter 多端互T控制过滤器
     * @return shiro过滤工厂Bean
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OutSessionControlFilter outSessionControlFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap(2);
        filters.put("out", outSessionControlFilter);
        shiroFilterFactoryBean.setFilters(filters);
        // 注意拦截链配置顺序,不能颠倒
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
        // 退出
        filterChainDefinitionMap.put("/logout", "logout");
        // 可匿名访问,无需登录,即可访问的路径
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginByMenu", "anon");
        filterChainDefinitionMap.put("/loginByCaptcha", "anon");
        filterChainDefinitionMap.put("/captcha", "anon");
        // 拦截所有请求
        filterChainDefinitionMap.put("/**", "out,authc");
        // 未认证 跳转未认证页面
        shiroFilterFactoryBean.setLoginUrl("/unAuthen");
        // 未授权 跳转未权限页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthor");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 将springboot中的过滤器替换成shiro过滤器,不替换会报错
     *
     * @return 过滤器注册Bean
     */
    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        // 设置servlet容器来管理其生命周期,默认false,spring来管理其生命周期
        proxy.setTargetFilterLifecycle(true);
        // 配置改为shiro过滤器
        proxy.setTargetBeanName("shiroFilter");
        filterRegistrationBean.setFilter(proxy);
        return filterRegistrationBean;
    }

    /**
     * 默认创建代理类Bean
     * 在这里将代理改为cglib代理的方式
     *
     * @return 默认创建代理类Bean
     */
    @Bean(name = "advisorAutoProxyCreator")
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 设置成true,是以cglib动态代理生成代理类;设置成false,就是默认用JDK动态代理生成代理类
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * shiro中的权限拦截器
     *
     * @param securityManager 权限管理器
     * @return shiro中的权限拦截器
     */
    @Bean(name = "authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        // 配置权限管理器
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

  • Redis配置文件,RedisConfig:
package com.shiro.api.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置文件
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 配置自定义redis模版
     *
     * @return redis模版
     */
    @Bean(name = "redisTemplate")
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }
}
  • 验证码配置文件,KaptchaConfig:
package com.shiro.api.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * google验证码
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
@Configuration
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha producer() {
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "10");
        properties.put("kaptcha.textproducer.char.length","4");
        properties.put("kaptcha.image.height","34");
        properties.put("kaptcha.textproducer.font.size","25");
        properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
  • 自定义域,MyRealm:
package com.shiro.api.core;

import com.shiro.api.model.TbUser;
import com.shiro.api.service.LoginService;
import com.shiro.api.service.TbUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Objects;

/**
 * 自定义域
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private TbUserService userService;

    @Autowired
    private LoginService loginService;

    /**
     * 认证,出现异常会被ControllerExceptionHandler捕获
     *
     * @param token 用户token
     * @return 认证信息
     * @throws AuthenticationException 认证异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName = (String) token.getPrincipal();
        TbUser user = userService.getByUserName(userName);
        if (Objects.isNull(user)) {
            throw new UnknownAccountException("该用户名称不存在!");
        } else if (Objects.isNull(user.getForbidden()) || user.getForbidden().equals(1)) {
            throw new UnknownAccountException("该用户已经被锁定!");
        } else {
            String password = new String((char[]) token.getCredentials());
            // 校验传入的密码,是否等于数据库中的密码
            if (user.getPassword().equals(password)) {
                AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                        user.getUserName(), user.getPassword(), user.getUserName());
                // 将user对象放到session属性中
                SecurityUtils.getSubject().getSession().setAttribute("currentUser", user);
                return authenticationInfo;
            } else {
                throw new IncorrectCredentialsException("密码错误!");
            }
        }
    }

    /**
     * 授权,出现异常会被ControllerExceptionHandler捕获
     *
     * @param principals shiro框架中用户信息对象
     * @return 授权信息
     * @throws AuthorizationException 授权异常
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        // 根据用户名授予相应的权限,用户名需唯一
        return loginService.getRolesAndPermissionsByUserName(userName);
    }
}
  • 自定义session管理,MySessionManager:
package com.shiro.api.core;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 自定义session管理
 *
 * Created by Happy王佳乐 on 2019/8/02.
 */
public class MySessionManager extends DefaultWebSessionManager {

    // 前端请求头传这个参数,用于获取SessionId
    private static final String AUTHORIZATION = "Authorization";

    public MySessionManager() {
        super();
    }

    /**
     * 获取sessionId
     *
     * @param request  请求request
     * @param response 返回response
     * @return SessionId
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // 从请求头中获取SessionId
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        // 如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            // 参考DefaultWebSessionManager源码中getSessionId方法
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            // 否则按默认规则(DefaultWebSessionManager)从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}
  • 多端互T过滤器,OutSessionControlFilter:
package com.shiro.api.core;

import com.shiro.api.enums.ErrorCode;
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.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 多端互T过滤器
 *
 * Created by Happy王佳乐 on 2019/8/02.
 */
public class OutSessionControlFilter extends AccessControlFilter {

    @Value("${shiro.redis.kickOutKey}")
    private String kickOutKey;

    private String kickOutPrefix;
    private RedisTemplate redisTemplate;
    private SessionManager sessionManager;

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        Subject subject = getSubject(servletRequest, servletResponse);
        // 如果没有登录,不进行多出登录判断
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            return true;
        }
        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();
        // 获取redis中数据
        List<Serializable> sessionIdList = redisTemplate.opsForList().range(kickOutPrefix + username, 0, -1);
        if (sessionIdList == null || sessionIdList.size() == 0) {
            sessionIdList = new ArrayList<>();
        }
        // 如果队列里没有此sessionId,且用户没有被踢出,当前session放入队列
        if (!sessionIdList.contains(sessionId) && Objects.isNull(session.getAttribute(kickOutKey))) {
            sessionIdList.add(sessionId);
            redisTemplate.opsForList().leftPush(kickOutPrefix + username, sessionId);
        }
        // 如果队列里的sessionId数大于1,开始踢人
        while (sessionIdList.size() > 1) {
            // 获取第一个sessionId(限转成LinkedList,保证顺序)
            Serializable outSessionId = sessionIdList.get(0);
            sessionIdList.remove(outSessionId);
            System.out.println("移除---sessionId: " + outSessionId);
            System.out.println("剩余---sessionId: " + sessionIdList.get(0));
            redisTemplate.opsForList().remove(kickOutPrefix + username, 1, outSessionId);
            try {
                DefaultSessionKey defaultSessionKey = new DefaultSessionKey(outSessionId);
                Session outSession = sessionManager.getSession(defaultSessionKey);
                // 设置会话的out属性表示踢出了
                if (outSession != null) {
                    outSession.setAttribute(kickOutKey, true);
                }
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
        // session包含out属性,T出
        if (session.getAttribute(kickOutKey) != null) {
            try {
                subject.logout();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
            saveRequest(servletRequest);
            // 返回错误码,以及错误文案
            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
            httpResponse.setStatus(HttpStatus.OK.value());
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.getWriter().write("{\"code\":" + ErrorCode.UNAUTHENTIC.getCode() + ", \"msg\":\"" + "您已被强制下线!" + "\"}");
            return false;
        }
        return true;
    }

    public void setKickOutPrefix(String kickOutPrefix) {
        this.kickOutPrefix = kickOutPrefix;
    }
    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
  • 自定义redisCache,RedisCache:
package com.shiro.api.core;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 自定义redisCache
 *
 * Created by Happy王佳乐 on 2019/8/02.
 */
public class RedisCache<K, V> implements Cache<K, V> {

    private long cacheLive;
    private String cacheKeyPrefix;
    private RedisTemplate redisTemplate;

    @Override
    public V get(K k) throws CacheException {
        return (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
    }

    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(this.getRedisCacheKey(k), v, cacheLive, TimeUnit.MINUTES);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        V obj = (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
        redisTemplate.delete(this.getRedisCacheKey(k));
        return obj;
    }

    @Override
    public void clear() throws CacheException {
        Set keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
        if (null != keys && keys.size() > 0) {
            Iterator iterator = keys.iterator();
            this.redisTemplate.delete(iterator.next());
        }
    }

    @Override
    public int size() {
        Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
        return keys.size();
    }

    @Override
    public Set<K> keys() {
        return this.redisTemplate.keys(this.cacheKeyPrefix + "*");
    }

    @Override
    public Collection<V> values() {
        Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
        Set<V> values = new HashSet<V>(keys.size());
        for (K key : keys) {
            values.add((V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(key)));
        }
        return values;
    }

    private String getRedisCacheKey(K key) {
        Object redisKey = this.getStringRedisKey(key);
        if (redisKey instanceof String) {
            return this.cacheKeyPrefix + redisKey;
        } else {
            return String.valueOf(redisKey);
        }
    }

    private Object getStringRedisKey(K key) {
        Object redisKey;
        if (key instanceof PrincipalCollection) {
            redisKey = this.getRedisKeyFromPrincipalCollection((PrincipalCollection) key);
        } else {
            redisKey = key.toString();
        }
        return redisKey;
    }

    private Object getRedisKeyFromPrincipalCollection(PrincipalCollection key) {
        List realmNames = this.getRealmNames(key);
        Collections.sort(realmNames);
        Object redisKey = this.joinRealmNames(realmNames);
        return redisKey;
    }

    private List<String> getRealmNames(PrincipalCollection key) {
        ArrayList realmArr = new ArrayList();
        Set realmNames = key.getRealmNames();
        Iterator i$ = realmNames.iterator();
        while (i$.hasNext()) {
            String realmName = (String) i$.next();
            realmArr.add(realmName);
        }
        return realmArr;
    }

    private Object joinRealmNames(List<String> realmArr) {
        StringBuilder redisKeyBuilder = new StringBuilder();
        for (int i = 0; i < realmArr.size(); ++i) {
            String s = realmArr.get(i);
            redisKeyBuilder.append(s);
        }
        String redisKey = redisKeyBuilder.toString();
        return redisKey;
    }

    public RedisCache(RedisTemplate redisTemplate, long cacheLive, String cachePrefix) {
        this.redisTemplate = redisTemplate;
        this.cacheLive = cacheLive;
        this.cacheKeyPrefix = cachePrefix;
    }
}
  • 自定义cache管理器,RedisCacheManager:
package com.shiro.api.core;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 自定义cache管理器
 *
 * Created by Happy王佳乐 on 2019/8/02.
 */
public class RedisCacheManager implements CacheManager {

    private long cacheLive;
    private String cacheKeyPrefix;
    private RedisTemplate redisTemplate;

    private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>();

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        Cache cache = this.caches.get(name);
        if (cache == null) {
            // 自定义shiro缓存
            cache = new RedisCache<K, V>(redisTemplate, cacheLive, cacheKeyPrefix);
            this.caches.put(name, cache);
        }
        return cache;
    }

    public void setCacheLive(long cacheLive) {
        this.cacheLive = cacheLive;
    }

    public void setCacheKeyPrefix(String cacheKeyPrefix) {
        this.cacheKeyPrefix = cacheKeyPrefix;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
  • 自定义sessionDAO,RedisSessionDAO:
package com.shiro.api.core;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 自定义sessionDAO
 *
 * Created by Happy王佳乐 on 2019/8/02.
 */
public class RedisSessionDAO extends AbstractSessionDAO {

    private long sessionLive;
    private String sessionKeyPrefix;
    private RedisTemplate redisTemplate;

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        redisTemplate.opsForValue().set(sessionKeyPrefix + sessionId, session, sessionLive, TimeUnit.MINUTES);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        return (Session) redisTemplate.opsForValue().get(sessionKeyPrefix + sessionId);
    }

    @Override
    public void update(Session session) {
        this.redisTemplate.opsForValue().set(sessionKeyPrefix + session.getId(), session, sessionLive, TimeUnit.MINUTES);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            return;
        }
        this.redisTemplate.delete(sessionKeyPrefix + session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<Session>();
        Set<Serializable> keys = redisTemplate.keys(sessionKeyPrefix + "*");
        if (keys != null && keys.size() > 0) {
            for (Serializable key : keys) {
                Session s = (Session) redisTemplate.opsForValue().get(key);
                sessions.add(s);
            }
        }
        return sessions;
    }

    public void setSessionLive(long sessionLive) {
        this.sessionLive = sessionLive;
    }

    public void setSessionKeyPrefix(String sessionKeyPrefix) {
        this.sessionKeyPrefix = sessionKeyPrefix;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
  • 自定义返回结果集,Response:
package com.shiro.api.util;

import java.io.Serializable;

/**
 * 自定义返回结果集
 *
 * @param <T>
 */
public class Response<T> implements Serializable {

    private static final long serialVersionUID = 1998307887673028548L;

    private int code;
    private String msg;
    private T data;

    public Response() {
    }

    public Response(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Response(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }


    public boolean equals(Object obj) {
        if (!(obj instanceof Response)) {
            return false;
        }
        return this.getCode() == ((Response) obj).getCode();
    }

    public int hashCode() {
        return this.code;
    }
}
  • 结果集工具类,ResponseUtil:
package com.shiro.api.util;

import com.shiro.api.enums.ErrorCode;

public class ResponseUtil {

    public ResponseUtil() {
    }

    public static Response makeFail(String message) {
        return makeResponse(1, message, (Object)null);
    }

    public static Response makeSuccess(Object obj) {
        return makeResponse(0, "", obj);
    }

    public static Response makeSuccess(Object obj, String msg) {
        return makeResponse(0, msg, obj);
    }

    public static Response makeFail(Object obj) {
        return makeResponse(1, "", obj);
    }

    public static Response makeError(ErrorCode errorCode) {
        return makeResponse(errorCode.getCode(), errorCode.getMsg(), (Object)null);
    }

    public static Response makeError(ErrorCode errorCode, Object obj) {
        return makeResponse(errorCode.getCode(), errorCode.getMsg(), obj);
    }

    public static Response makeAdminError(ErrorCode errorCode) {
        return makeResponse(errorCode.getCode(), errorCode.getMsg(), (Object)null);
    }

    public static Response makeAdminError(ErrorCode errorCode, Object obj) {
        return makeResponse(errorCode.getCode(), errorCode.getMsg(), obj);
    }

    public static Response makeResponse(int code, String msg, Object obj) {
        Response result = new Response();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(obj);
        return result;
    }

    public static boolean isOk(Response response) {
        return response != null && response.getCode() == 0;
    }
}
  • 接下来,项目controller包中,建立两个controller,分别是LoginController(用于验证登录权限、菜单权限)、HelloController(用于验证访问接口权限)
package com.shiro.api.controller;

import com.shiro.api.enums.ErrorCode;
import com.shiro.api.model.*;
import com.shiro.api.service.*;
import com.shiro.api.util.Response;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.shiro.api.util.ResponseUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 业务接口Controller
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
@RestController
public class LoginController {

    @Autowired
    DefaultKaptcha producer;

    @Autowired
    private LoginService loginService;

    /**
     * 账号、密码登录
     *
     * @param tbUser 用户对象
     * @return Response
     */
    @PostMapping(value = "/login")
    public Response login(TbUser tbUser) {
        // 校验账号密码是否传入
        if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword())) {
            return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
        }
        Subject subject = SecurityUtils.getSubject();
        // 生成token
        UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
        try {
            subject.login(token);
            return ResponseUtil.makeSuccess(subject.getSession().getId());
        } catch (Exception e) {
            return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
        }
    }

    /**
     * 账号、密码登录,带菜单信息
     *
     * @param tbUser 用户对象
     * @return Response
     */
    @PostMapping(value = "/loginByMenu")
    public Response loginByMenu(TbUser tbUser) {
        // 校验账号密码是否传入
        if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword())) {
            return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
        }
        Subject subject = SecurityUtils.getSubject();
        // 生成token
        UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
        try {
            subject.login(token);
            // 根据用户名称,查询菜单权限
            List<TbMenu> menuList = loginService.getMenuByUserName(tbUser.getUserName());
            // 封装结果
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("token", subject.getSession().getId());
            resultMap.put("menuList", menuList);
            return ResponseUtil.makeSuccess(resultMap);
        } catch (Exception e) {
            return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
        }
    }

    /**
     * 账号、密码登录,带验证码
     *
     * @param tbUser  用户对象
     * @param sToken  验证码对应的token
     * @param textStr 用户输入的验证码
     * @return Response
     */
    @PostMapping(value = "/loginByCaptcha")
    public Response loginByCaptcha(TbUser tbUser, String sToken, String textStr) {
        // 校验账号、密码、验证码等是否传入
        if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword()) ||
            StringUtils.isEmpty(sToken) || StringUtils.isEmpty(textStr)) {
            return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
        }
        Subject subject = SecurityUtils.getSubject();
        // 生成token
        UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
        // 校验验证码
        boolean flag = loginService.checkCodeToken(sToken, textStr);
        if(!flag) {
            return ResponseUtil.makeError(ErrorCode.CAPTCHA_CHECK_ERROR);
        }
        try {
            subject.login(token);
            return ResponseUtil.makeSuccess(subject.getSession().getId());
        } catch (Exception e) {
            return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
        }
    }

    /**
     * 当前用户退出登录
     *
     * @return Response
     */
    @PostMapping("/logout")
    public Response logout() {
        // 从缓存中删除缓存
        loginService.removeSessionBySessionId(SecurityUtils.getSubject().getSession().getId().toString());
        SecurityUtils.getSubject().logout();
        return ResponseUtil.makeSuccess(ErrorCode.LAY_OUT_SUCCESS);
    }

    /**
     * 生成验证码
     *
     * @return Response
     */
    @PostMapping("/captcha")
    public Response captcha() {
        try {
            return ResponseUtil.makeSuccess(loginService.generateVerificationCode());
        } catch (Exception e) {
            return ResponseUtil.makeError(ErrorCode.CAPTCHA_ERROR);
        }
    }

    /**
     * 获取在线用户
     *
     * @return Response
     */
    @PostMapping("/listOnLine")
    public Response listOnLine() {
        return ResponseUtil.makeSuccess(loginService.listOnLineUser());
    }

    /**
     * 踢出用户
     *
     * @param userName
     * @return
     */
    @RequestMapping("/kickOutUser")
    @ResponseBody
    public Response kickOutUser(String userName) {
        return ResponseUtil.makeSuccess(loginService.forbiddenByUserName(userName));
    }

    /**
     * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
     *
     * @return
     */
    @RequestMapping(value = "/unAuthen")
    public Response unAuthen() {
        return ResponseUtil.makeError(ErrorCode.UNAUTHENTIC);
    }

    /**
     * 未授权
     *
     * @return
     */
    @RequestMapping(value = "/unAuthor")
    public Response unAuthor() {
        return ResponseUtil.makeError(ErrorCode.UNAUTHORIZED);
    }
}
package com.shiro.api.controller;

import com.shiro.api.util.Response;
import com.shiro.api.util.ResponseUtil;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 业务接口Controller
 *
 * Created by Happy王子乐 on 2019/8/02.
 */
@RestController
public class HelloController {

    /**
     * 没有角色、权限限制,只要登录即可访问
     *
     * @return Response
     */
    @RequestMapping("/hello")
    public Response hello() {
        return ResponseUtil.makeSuccess("无角色、权限限制接口,任何登录账号都可以请求该方法");
    }

    // 一些角色组合用法:
    // 属于user角色
    // @RequiresRoles("user")
    // 必须同时属于user和admin角色
    // @RequiresRoles({"user","admin"})
    // 属于user或者admin之一;修改logical为OR 即可
    // @RequiresRoles(value={"user","admin"},logical=Logical.OR)

    /**
     * 后面接口按照傻瓜场景进行权限演示:
     * 角色:zhuren(主任) 、 teacher(老师)
     * 权限:jiaoxuesheng(教学生)、shouxuefei(收学费)
     *
     * 于是:
     * 主任可以教学生(宝刀未老的主任是由老师升职上去的),可以收学费
     * 老师可以教学生,不可以收学费
     */

    /**
     * 拥有“zhuren”角色,登录后可正常访问
     *
     * @return Response
     */
    @RequiresRoles("zhuren")
    @RequestMapping("/role1")
    public Response role1() {
        return ResponseUtil.makeSuccess("zhuren角色正确,可以请求该方法");
    }

    /**
     * 拥有“teacher”角色,登录后可正常访问
     *
     * @return Response
     */
    @RequiresRoles("teacher")
    @RequestMapping("/role2")
    public Response role2() {
        return ResponseUtil.makeSuccess("teacher角色正确,可以请求该方法");
    }

    /**
     * 拥有“zhuren”角色和“teacher”角色,登录后可正常访问
     *
     * @return Response
     */
    @RequiresRoles({"teacher", "zhuren"})
    @RequestMapping("/role3")
    public Response role3() {
        return ResponseUtil.makeSuccess("zhuren角色和teacher角色都有,可以请求该方法");
    }

    /**
     * 拥有“zhuren”角色或“teacher”角色,登录后可正常访问
     *
     * @return Response
     */
    @RequiresRoles(value={"teacher", "zhuren"}, logical=Logical.OR)
    @RequestMapping("/role4")
    public Response role4() {
        return ResponseUtil.makeSuccess("有zhuren角色或者有teacher角色,可以请求该方法");
    }

    /**
     * 拥有“jiaoxuesheng”权限,登录后可正常访问
     *
     * @return Response
     */
    @RequiresPermissions("jiaoxuesheng")
    @RequestMapping("/permission1")
    public Response permission1() {
        return ResponseUtil.makeSuccess("jiaoxuesheng权限正确,可以请求该方法");
    }

    /**
     * 拥有“shoubanfei”权限,登录后可正常访问
     *
     * @return Response
     */
    @RequiresPermissions("shoubanfei")
    @RequestMapping("/permission2")
    public Response permission2() {
        return ResponseUtil.makeSuccess("shoubanfei权限正确,可以请求该方法");
    }

    /**
     * 拥有"jiaoxuesheng"权限和"shoubanfei"权限,登录后可正常访问
     *
     * @return Response
     */
    @RequiresPermissions({"shoubanfei", "jiaoxuesheng"})
    @RequestMapping("/permission3")
    public Response permission3() {
        return ResponseUtil.makeSuccess("拥有jiaoxuesheng权限和shoubanfei权限,可以请求该方法");
    }

    /**
     * 拥有"jiaoxuesheng"权限或"shoubanfei"权限,登录后可正常访问
     *
     * @return Response
     */
    @RequiresPermissions(value={"shoubanfei", "jiaoxuesheng"}, logical=Logical.OR)
    @RequestMapping("/permission4")
    public Response permission4() {
        return ResponseUtil.makeSuccess("拥有jiaoxuesheng权限或shoubanfei权限,可以请求该方法");
    }
}
  • 接下来启动项目,运行成功后,可以看到8080端口已经启动

五、登录权限验证

  • 当登录时,会进行认证操作,贴出相关代码:
    在这里插入图片描述
  • 启动Postman,我们先让“主任”登录:
    在这里插入图片描述
  • 可以看到,已经返回了token,登录成功
  • 当账号/密码错误时(正确的密码是:zhuren),是禁止登录的,会进入拦截:
    在这里插入图片描述
  • 当账号登录管理后台时,需要带上菜单列表返回,主任的菜单权限是“教务管理”、“课程安排”,数学张老师的菜单权限是“课程安排”:
    在这里插入图片描述
    在这里插入图片描述
  • 验证码登录,首先我们先生成验证码图片,调用接口时,在后台打印出生成的验证码数字(PS:方便后面传参):
    在这里插入图片描述
    在这里插入图片描述
  • 调用登录接口,将生成验证码的token和验证码一起传入(PS:cToken 就是生成验证码的token):
    在这里插入图片描述

六、接口请求权限验证

  • 首先先贴出接口请求的过滤器,需要根据实际情况进行区分配置
    在这里插入图片描述
  • 接口代码1,当用户登录就可以请求:
    在这里插入图片描述
  • 当用户未登录时,请求接口:
    在这里插入图片描述
  • 反之,当用户登录后请求:
    在这里插入图片描述
  • 接口代码2,当用户符合角色,就可以请求接口:
    在这里插入图片描述
  • 当用户第一次请求角色/权限接口时,会进行初始授权
    在这里插入图片描述
  • 授权时,会进行数据库权限查询,并赋值到授权信息中(如果授权后想重新授权,需要用户退出登录):
    在这里插入图片描述
  • 主任分别请求接口(PS:第一次请求角色类型接口,会初始角色权限):
    在这里插入图片描述
    在这里插入图片描述
  • 角色相关的接口还可以进行“与、或”组合,接口代码3:
    在这里插入图片描述
  • 主任角色分别请求接口(role3由于没有teacher角色,权限不足;role4因为角色是或的关系,请求正常):
    在这里插入图片描述
    在这里插入图片描述
  • 角色验证完毕,我们来验证权限,接口代码4:
    在这里插入图片描述
  • 主任用户分别请求接口:
    在这里插入图片描述
    在这里插入图片描述
  • 数学张老师,分别请求接口(PS:老师收班费没有权限,所以请求被拒绝了):
    在这里插入图片描述
    在这里插入图片描述
  • 同理,权限也可以进行 “与、或”操作,进行组合限制,接口代码5:
    在这里插入图片描述
  • 主任、数学张老师,请求省略。。。这里不贴请求结果了,自己试试吧

基于shiro的权限控制系统,包括 登录权限、接口权限、菜单权限
如果该项目对您有帮助,您可以点右上角 “Star” 支持一下 谢谢!
或者您可以 “follow” 一下,该项目将持续更新,不断完善功能。
转载还请注明出处,谢谢了
QQ:820155406

猜你喜欢

转载自blog.csdn.net/u012888052/article/details/98984036