shiro使用

为什么要使用shiro?

①使用动态权限管理,页面自动生成菜单,已经看不见自己不能访问的地址了为什么还要在服务器里配置filter过滤?这是为了防止他知道自己不能访问的地址直接在地址栏中手动输入地址访问。但是过滤器是静态的,只能处理提前写进去的一些路径匹配规则,但是我们实际需要的是根据不同的角色限制不同的访问内容,这角色又是写入数据库中的,可以通过管理给角色授权的,不可能直接写个静态的规则在filter里面。所以这时候就需要一个动态的权限管理框架----shiro。
②当然了,过滤器也能实现动态的,就是每次访问过滤器的时候就去查用户拥有哪些角色哪些权限,然后比对当前的访问路径是不是在我查出来的这些权限里面,在就访问通过。

1.基础

1.1 名词解析

  1. subject :当前安全对象,可以是人,是爬虫是机器人,负责认证和授权,但是它又委托给了安全管理器。
  2. SecurityManager:安全管理器,作用①接收并管理对象②与其他组件进行交互,类似于前端控制器
  3. Realm:Shiro 从从 Realm 获取安全数据(如用户、角色、权限)可以把 Realm 看成 DataSource,即安全数据源。

可以把subject和securityManager看成service和serviceImpl,是一个门面模式,很多时候调用subject里面的方法,实际执行的是securityManager实现的方法。

1.2 执行流程

对于我们而言,最简单的一个 Shiro 应用:

  1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
  2. 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

1.3 内部架构

  1. Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
  2. SecurityManager:它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
  3. Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;
  4. Authrizer:授权器,或者访问控制器,即控制着用户能访问应用中的哪些功能;
  5. Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的
  6. SessionManager:session就是会话域,不同的环境都有自己独立的session域,不方便数据间的交互,所以Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据。比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器)
  7. SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
  8. CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
  9. Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。

1.4 boot环境依赖

       <!-- 核心包-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>
         <!-- 与spring整合 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

2.身份验证

2.1 名词解析

  1. principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。实际功能邮箱登录、账号登录、手机登录
  2. credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
  3. 身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。最常见的 principals 和 credentials 组合就是用户名 / 密码了
  4. Token:一组用户名和密码

2.2 认证流程

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

2.3 简单应用

public class Shiro {

    public static void main(String[] args) {
        //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager 
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        /*2、得到SecurityManager实例 并绑定给SecurityUtils。
        通过 SecurityUtils 得到 Subject,其会自动绑定到当前线程;
        如果在 web 环境在请求结束时需要解除当前线程的绑定; */
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        //3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
        UsernamePasswordToken token = new UsernamePasswordToken("root", "123456");
        System.out.println("是否登录:" + subject.getPrincipal());
        try {
            //4、登录,即身份验证
            subject.login(token);
        } catch (AuthenticationException e) {
            //5、身份验证失败
            /* 对于页面的错误消息展示,最好使用如 “用户名 / 密码错误” 
            而不是 “用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;*/
        }
        //判断是否登录
        System.out.println("是否登录:" + subject.getPrincipal());
        //判断用户有没有角色
        System.out.println("用户角色"+subject.hasRole("admin"));
        //判断有没有权限
        System.out.println("包含权限"+subject.isPermitted("user.html"));
        //6、退出,调用 subject.logout 退出,其会自动委托给 SecurityManager.logout 方法退出
        subject.logout();
    }
}

3.Realm

3.1 介绍

Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。如我们之前的 ini 配置方式将使用 org.apache.shiro.realm.text.IniRealm。Realm是一个接口,通过继承它来自定义Realm

org.apache.shiro.realm.Realm 接口如下:

//返回一个唯一的Realm名字
String getName(); 
//判断此Realm是否支持此Token
boolean supports(AuthenticationToken token); 
 //根据Token获取认证信息
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throws AuthenticationException; 

3.2 jdbcRealm

jdbcRealm:类似于读取静态ini配置文件,通过它来读取数据库中的相关信息。已经实现的默认jdbcRealm是根据user_name字段进行关联,比如说用户表有id,user_name,password三个属性,用户与角色多对多会有一个用户角色表,通常我们的做法是通过user表的id和role表的id进行关联,但是jdbcRealm默认是以user_name、role_name进行关联。为什么非得用用户名关联呢?因为jdbcRealm源码里面用的是根据用户名进行查询的sql语句,跟实际开发中是不符合的,实际开发是通过主键id进行关联的。

3.3 自定义Realm

  1. 自定义Reaml是实现AuthorizingRealm这个抽象类,它下面又各种操作数据库的实现,自定义自己的Realm跟其他如jdbcRealm一样,也是需要继承AuthorizingRealm。这就是多态,实际声明的对象是AuthorizingRealm,但是具体执行的类是它指向的子类。

    public class DivRolem extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
    }
    @Override
    //登录的时候调用
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //父类类型做参数可以接收它的各种子类对象,UsernamePasswordToken是AuthenticationToken的子类
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    return null;
    }
    }

4. 整合项目

整合内容:数据得从dao里面查,dao现在又归spring管,所以必须把spring和dao整合到一起。

4.1 自定义Realm

public class DivRealm extends AuthorizingRealm {

    private UserDao userDao;

    /**
     * 交给spring管理需要set方法注入属性
     * @param userDao
     */
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    /**
     * 验证用户权限的时候,获取用户权限
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    @Override
    /**
     * 登录的时候调用,主要是查询用户信息
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.父类类型做参数可以接收它的各种子类对象,UsernamePasswordToken是AuthenticationToken的子类
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //2.token里面存放的是用户名和密码,但是这里只需要用户名在dao里面查数据
        String email = token.getUsername();
        //3.调用dao,查询出密码和token里面的密码进行比较
        User user= userDao.findByUserName(email);
        //4.没查到代表用户名不存在,返回一个null就行了
        if (user==null){
            return null;
        }
        //5.也可以不比较,封装一个SimpleAuthenticationInfo对象,把这个对象给shiro可以自动进行比较
        //第一个参数是用户的principals,传id、email、user对象都可以,就如同session,后面可以获取使用
        //第二个参数就是凭证,如密码、数字证书
        //第三个参数就是当前realm的名字,因为shiro是支持多个realm的,
        //它会按顺序在多个realm里面查找,最后会告诉你在哪个realm找到的
        SimpleAuthenticationInfo info=new                  SimpleAuthenticationInfo(user.getUserId(),user.getPassword(),getName());
        //shiro它已经知道了用户传的用户名和密码了,只需要把数据库中正确的密码给shiro,就能进行判断了
        return info;
    }
}

4.2 自定义shiro过滤器

   shiro想要帮我们拦截请求,首先得知道菜单和角色对应的关系,shiro拦截的时候是按照url进行拦截的,它拦截的时候不拦截参数,如/menu.html?act=add,现在只需要把数据库的角色id全部存到shiro就行,然后根据角色id去role_menu里面查对应的menu,最后存储的形式为 /menu.html=roles[1,2]这样一个表。现在需要把以前filter里面写的静态规则变成从数据库里面加载的。

//主要作用是初始化规则
public class DivShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    private MenuDao menuDao;
    private PermsDao permsDao;

    public void setMenuDao(MenuDao menuDao) {
        this.menuDao = menuDao;
    }
    public void setPermsDao(PermsDao permsDao) {
        this.permsDao = permsDao;
    }

    /**
     * 重新的shiro过滤器规则
     * 需要注意:
     *   动态加载的路径与静态路径的书写的先后顺序,先加载静态后加载动态
     *   静态里面不能写最高的规则,不然会跑到自定义规则前面去,就无法执行动态规则
     * @param definitions 配置文件里面传来的静态过滤规则
     */
    @Override
    public void setFilterChainDefinitions(String definitions) {
        Ini ini = new Ini();
        ini.load(definitions);
        //url初始化,只拦截有url的东西
        Ini.Section section = ini.getSection("urls");
        if (CollectionUtils.isEmpty(section)) {
            //最终保存权限和对应关系的地方,里面的数据结构类似于map,其实就是继承了一个map
            //维护着菜单和对应角色的关系
            section = ini.getSection("");
        }
        //1.查询所有的菜单
        List<Menu> menuList=menuDao.findAll();
        //2.遍历菜单,如果url不是空,就要对权限进行一个写入
        for (Menu menu : menuList) {
            //3.这个api的意思是不为空且长度大于0
            if (StringUtils.hasText(menu.getUrl())){
                //4.最终要写成的模式 /url=roles[1,2...],现在已经知道当前菜单id,根据id查询角色
                List<Integer> roleIds = permsDao.getRoleIdByMenuId(menu.getId());
                //【注意】roleIds有可能为空,先要排除为空的情况,不然toString会报空指针
                if (roleIds!=null && roleIds.size()>0){
                    //5.需要把拿回来的id用逗号隔开,直接使用toString方法就可以拿到我们想要的格式
                    String idStr = "roles"+roleIds.toString();
                    //6.可以直接使用section的put方法,key就是请求路径,value就是拼接好权限的字符串
                    section.put(menu.getUrl(),idStr);
                }
            }
            //7.必须把最高规则放到最后
            section.put("/**","authc");
        }
        this.setFilterChainDefinitionMap(section);
    }
}

【注意】这里有一个问题,ShiroFilterFactoryBean是在系统启动的时候初始化的,那么就有一个问题,一个菜单固定的拥有这些角色,它的角色列表在系统启动的时候就已经写到shiro里面去了,我在后面给角色配置的权限虽然已经写进数据库了,但是shiro不会重新读取。给用户添加角色可以,每次重新登录就会重新查的。ShiroFilterFactoryBean的父类有一个update方法,只需要重写这个更新方法就可以了。

【使用】重写更新方法后,在角色权限发生改变后调用,在需要调用的地方需要注入一个shiro过滤器对象来调用。

    //重新调用setFilterChainDefinitions(String definitions)需要传一个静态规则
    //这里我可以把第一次调用时传来的规则用一个静态变量存起来,再次调用传这个静态变量
    private static String filterChainDefinitions=null;
    @Override
    public void setFilterChainDefinitions(String definitions) {
        filterChainDefinitions=definitions;
      ...//这里省略部分内容
    }
// 当重新给角色授权时使用,因为菜单的权限在项目启动的时候就加载了,所以后追加的权限需要再次追加进来
    public synchronized void update() {
        try {
            AbstractShiroFilter shiroFilter = (AbstractShiroFilter) this.getObject();
            PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) shiroFilter
                    .getFilterChainResolver();
            // 过滤管理器
            DefaultFilterChainManager manager = (DefaultFilterChainManager) resolver.getFilterChainManager();
            // 清除权限配置,就是把原来的过滤规则全清空
            manager.getFilterChains().clear();
            this.getFilterChainDefinitionMap().clear();

            // 重新设置权限,就是再调用自己上面写的那个方法
            this.setFilterChainDefinitions(filterChainDefinitions);
            Map<String, String> chains = this.getFilterChainDefinitionMap();
            if (!CollectionUtils.isEmpty(chains)) {
                Iterator var12 = chains.entrySet().iterator();
                while (var12.hasNext()) {
                    Map.Entry<String, String> entry = (Map.Entry) var12.next();
                    String url = (String) entry.getKey();
                    String chainDefinition = (String) entry.getValue();
                    manager.createChain(url, chainDefinition);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

4.3 自定义角色过滤器

   shiro里面最终规则会以/url=roles[1,2...]存储,在匹配的时候默认是角色里面的全部角色都得拥有才会匹配上。但是我们在实际开发中只要用户有其中一个角色就能匹配,所以就需要重新定义匹配的规则。原来是一个与的关系就是所有角色都满足,现在要改成一个或的关系。  

public class DivShiroRoleFilter extends AuthorizationFilter {
    /**
     * 自定义角色过滤器
     *  想让这个过滤器生效还需要把他写入到shiro过滤器里面去
     *  通过shiro过滤器的setFilters配置进去
     *  但是这个方法要的是个map,所以要先封装成map对象
     *  map的key我过滤器类型,value为过滤器
     *
     * @param servletRequest 请求对象
     * @param servletResponse 响应对象
     * @param o 代表当前访问的url对应的角色列表,就是roles[]里面的集合
     * @return 这返回false代表不能进,true代表能进
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest,
                                      ServletResponse servletResponse, Object o) throws Exception {
        //1. 拿到当前请求对象
        Subject subject = SecurityUtils.getSubject();

        //2. 拿到当前请求路径所需的角色列表,也就是权限列表,相对于菜单的权限
        String[] roleIds=(String[]) o;

        //3. 遍历roleIds,只要当前对象有其中一个角色,就满足要求,这个角色权限时在reals里写入保存的
        for (String roleId : roleIds) {
            //4.只要有一个满足就直接返回true就可以了
            if (subject.hasRole(roleId)){
                return true;
            }
        }
        return false;
    }
}

在DivShiroFilterFactoryBean里注册角色过滤器

        //注册自定义角色过滤器,因为setFilters参数是一个map,所以先封装一个map
        Map<String, Filter> filterMap=new HashMap<>();

        //这里的key跟这个过滤器里面的String idStr = "roles"+roleIds.toString()那个"roles"一致
        //map的key为过滤器类型,value为过滤器
        filterMap.put("roles",new DivShiroRoleFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

4.4 开启shiro缓存

   第二点就是现在每次点击一个链接都会去执行Realm的doGetAuthorizationInfo方法,但是实际是在用户第一次登陆的时候就已经知道了这个用户有哪些角色,所有这样是不对的,效率比较低。所有我们可以开启shiro的缓存功能。缓存功能就是只查一次就不查了,如果权限变动了就退出登录重新登录就好了

/**
     * 开启默认的缓存,不用每次请求都去查询用户权限
     * 开启之后交给安全管理器管理
     * @return
     */
    @Bean
    public MemoryConstrainedCacheManager cacheManager(){
        return new MemoryConstrainedCacheManager();
    }
    @Bean
    public DefaultWebSecurityManager securityManager(DivRealm realm,
                                                     MemoryConstrainedCacheManager cacheManager){
        //1.创建安全管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //2.需要告诉安全管理器进行数据验证的时候去哪个数据源找数据
        securityManager.setRealm(realm);
        //3.开启缓存
        securityManager.setCacheManager(cacheManager);
        return securityManager;
    }

4.5 spring配置文件

需要交给spring管理的bean:

①首先肯定是自己自定义的Realm

②要把shiro的核心组件交给spring,即DefaultWebSecurityManager,它是web环境的过滤器

③已经交给spring管理了,现在就需要把它和springMVC整合到一起,即ShiroFilterFactoryBean

//静态配置说明版(不完整)
@Configuration
public class ShiroConfig {

    /**
     * 把realm交给spring管理
     * @param userDao
     * @return
     */
    @Bean
    public DivRealm realm(UserDao userDao){
        //1.创建一个数据源
        DivRealm realm=new DivRealm();
        //2.注入userDao
        realm.setUserDao(userDao);
        return realm;
    }

    /**
     * 开启默认的缓存,不用每次请求都去查询用户权限
     * 开启之后交给安全管理器管理
     * @return
     */
    @Bean
    public MemoryConstrainedCacheManager cacheManager(){
        return new MemoryConstrainedCacheManager();
    }
    /**
     * web环境下面的shiro核心控制器,在用户请求controller的时候它会先去校验用户的权限
     */
    @Bean
    public DefaultWebSecurityManager securityManager(DivRealm realm,
                                                     MemoryConstrainedCacheManager cacheManager){
        //1.创建安全管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //2.需要告诉安全管理器进行数据验证的时候去哪个数据源找数据
        securityManager.setRealm(realm);
        //3.开启缓存
        securityManager.setCacheManager(cacheManager);
        return securityManager;
    }

    /**
     * Shiro过滤器,它是由springMVC控制的一个核心,就是一个过滤器
     * ①需要把shiro核心控制器交给它管理
     * ②它里面定义了当前系统里的过滤规则
     * ③当跳转的页面不对一般在此处查看
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        //1.创建shiro过滤器
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //2.注入shiro核心
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //3.静态规则配置
        //如果用户没有登录跳转的地址
        shiroFilterFactoryBean.setLoginUrl("/sys/user/jump/login");
        //当用户没有权限跳转的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
        //4.设置过滤规则,比如有一些页面不需要验证,比如说登录
        //authc必须登录,anon不需要登录就可以访问,不同规则之间是用回车来进行区分也就是\n
        //如 /login.html=anon /dologin=anon /static/**不需要密码,其他都需要密码访问/**=authc
        //注意:匹是按照书写顺序进行匹配,不能把/**写到最前面,一般是把小范围需要放行的写前面
        //     如果没有找到路径也会进入错误页面,不能直接访问的页面需要通过控制器来读取返回
        shiroFilterFactoryBean.setFilterChainDefinitions(
                        "/login.html=anon\n" +
                        "/sys/user/login=anon\n" +
                        "/static/**=anon\n" +
                         "/**=authc"
        );
        return shiroFilterFactoryBean;
    }
}

  上面配置是手动的把路由规则写到过滤器里面,是静态的,但是我们想要的是动态的,那么靠默认的ShiroFilterFactoryBean是实现不了我的需求了,所以需要自定义一个动态的过滤器。能够在写路由规则的时候从数据库中根据userID动态的查询权限,然后拼接到setFilterChainDefinitions参数字符串里面。里面的配置写法,比如有 /sys/menu.html=roles[1,2]代表有两个角色可以访问它,然后只需要判断用户有没有这个角色就可以访问它,原来是查出这个用户全部菜单进行匹配,现在权限和角色已经全部查询出来,对应关系也已经明确了,只需要判断当前的用户又没有这个角色就可以访问了。

//动态配置完整版
@Configuration
public class ShiroConfig {

    /**
     * 把realm交给spring管理
     */
    @Bean
    public DivRealm realm(UserDao userDao,PermsDao permsDao){
        DivRealm realm=new DivRealm();
        realm.setUserDao(userDao);
        realm.setPermsDao(permsDao);
        return realm;
    }
    /**
     * 开启默认的缓存,不用每次请求都去查询用户权限
     */
    @Bean
    public MemoryConstrainedCacheManager cacheManager(){
        return new MemoryConstrainedCacheManager();
    }
    /**
     * web环境下面的shiro核心控制器,在用户请求controller的时候它会先去校验用户的权限
     */
    @Bean
    public DefaultWebSecurityManager securityManager(DivRealm realm,MemoryConstrainedCacheManager cacheManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setCacheManager(cacheManager);
        return securityManager;
    }

    /**
     * Shiro过滤器,它是由springMVC控制的一个核心,就是一个过滤器
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, MenuDao menuDao, PermsDao permsDao){
        DivShiroFilterFactoryBean shiroFilterFactoryBean = new DivShiroFilterFactoryBean();
        shiroFilterFactoryBean.setMenuDao(menuDao);
        shiroFilterFactoryBean.setPermsDao(permsDao);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/sys/user/jump/login");
        shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
        shiroFilterFactoryBean.setFilterChainDefinitions(
                        "/login.html=anon\n" +
                        "/sys/user/login=anon\n" +
                        "/static/**=anon"
        );
        Map<String, Filter> filterMap=new HashMap<>();
        filterMap.put("roles",new DivShiroRoleFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }
}

4.6 控制器登录方法

  1. 登录验证

    @RequestMapping("/login")
    public ModelAndView doLogin(String email, String password){
    //shiro进行登录的时候得先拿到subject,即当前用户。从SecurityUtils拿当前绑定到线程上的用户
    Subject subject = SecurityUtils.getSubject();
    //把当前拿到的作为token的标识和凭证放入token
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(email, password);
    try {
    //把token放入shiro,让shiro调用doGetAuthenticationInfo登录验证,并对它进行异常捕获
    subject.login(usernamePasswordToken);
    return new ModelAndView(new RedirectView("/index.do"));
    } catch (AuthenticationException e) {
    e.printStackTrace();
    return new ModelAndView("/login",“message”,
    new Message<>(500,“用户名和密码错误”,null,null));
    }
    }

  2. 取subject里面数据

@RequestMapping("/side.do")
@ResponseBody
public Object side(){
Subject subject = SecurityUtils.getSubject();
//拿realm里通过SimpleAuthenticationInfo存放的第一个参数Principal
Integer userId = (Integer)subject.getPrincipal();
//根据id查询菜单返回
return null;
}

5. shiro注解

①粗粒度的权限:为url级别的,控制用户能访问哪些url不能访问哪些url,有url是跳进页面的菜单,没有url的这些是url上的按钮,对于这些按钮就需要用细粒度的权限控制。

②细粒度的权限:限制在方法级别的了。可以把每个方法的每个路径写成不一样的,如menu/add.html,但是通常在控制器里面用的是参数param=“act=add”,参数就没办法直接写到路径里面去,所以就需要shiro提供的方法级别注解

权限perms也是要变的,也是人为添加的,为什么它就可以通过@RequiresPermissions(“sys:user:login”)写到代码里面?所以这里要这里注意一下,每添加一个菜单或者一个url都需要添加一个页面和控制器,这时候代码都改了为什么不直接写一个注解之类的呢,所以菜单表一定是程序员自己维护的。但是角色是部门领导分配的,我总不能要领导来改代码吧?所以动态东西不能写在代码里面。

5.1 开启注解

让shiro注解生效需要在配置文件中注册一个AuthorizationAttributeSourceAdvisor,从后缀可以看出这是一个advisor,是一个前置通知。

  @Bean
    public AuthorizationAttributeSourceAdvisor      authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }  

5.2 获取权限

   第4章节只是添加了用户角色,没有添加权限,如果要用细粒度权限控制的话在realm授权方法里查询出来即可,查出来后因为开启了缓存会自动存进去。到时候只要配置了注解的方法就会去用户权限里面找有没有匹配的权限,有就可以执行,没有就抛出没有权限异常

 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("doGetAuthorizationInfo");
        Integer userId = (Integer) principalCollection.getPrimaryPrincipal();
        List<Integer> roleIds = permsDAO.getUserRoleIds(userId);//用户拥有的角色id
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        for (Integer roleId : roleIds) {
            info.addRole(roleId.toString());
        }
        //这里拿到菜单表里面当前用户,且该角色在有效状态,且权限不为空的全部权限。
        //这里因为拿到的就是String类型,所以不需要像上面那样转换了
        List<String> perms = permsDAO.getUserPerms(userId);
        info.addStringPermissions(perms);
        return info;
    }

xml配置查询当前用户权限的方法:

①角色失效了也不能删,只能用有效无效,因为方便以后查历史

②删除id之后会造成数据库中数据树重新排列,这样就会造成非常大的一个磁盘io到操作,效率很低

③同时登陆的时候需要加判断,无效的用户不能登陆,直接在mapper里面加改sql

④查用户权限的时候也是必须要有效角色对应的菜单,所以需要关联角色表看status

⑤删除一个菜单的时候,中间表怎么办?把role_menu根据菜单id也一起删了

 <select id="getUserPerms" resultType="java.lang.String">
        /*根据用户id查询角色状态是有效且菜单表perms不为空的权限*/
        SELECT DISTINCT m.perms
        FROM menu as m
        inner join role_menu as rm on menu_id=m.id
        inner join role_user as ru on rm.role_id=ru.role_id
        inner join role as r on r.role_id=ru.role_id
        where ru.user_id=#{userId} and r.status=1 and m.perms is not null
    </select>

5.3 注解使用

用户能进这个url,但是能不能添加必须要有sys:user:add这个权限,没有权限就会报AuthorizationException。

可以把每一个处理方法上写上权限,权限要对应数据库里面的perms字段里的内容来写。

    //添加编辑用户
    @RequiresPermissions("sys:user:add")
    @PostMapping
    @ResponseBody
    public Object editUser(User user){
        boolean s=false;
        if (user.getUserId()!=null){
            System.out.println("修改功能");
        }else {
            System.out.println("添加功能");
            s=userService.addUser(user);
        }
        if (s){
            return new Message<Object>(0,"添加成功",0L,null);
        }else{
            return new Message<Object>(500,"添加失败",0L,null);
        }
    }

5.4 自定义权限过滤器

默认匹配权限的规则也是与的关系,现在要改成或的关系

//跟角色过滤器继承同一个类重写同一个方法
public class DivShiroPermFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        //这里因为在Realm里面权限数据通过 info.addStringPermissions(perms)来添加
        //所以在注册filterMap.put("perms",new DivShiroPermFilter())权限过滤器时
        //就把过滤器和Realm里面的权限数据绑定在了一起
        //所以这里的o拿到就是perms这个集合
        String[] perms=(String[]) o;
        for (String p : perms) {
            if (subject.isPermitted(p)){
                return true;
            }
        }
        return false;
    }
}

注册过滤器

 @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, MenuDao menuDao, PermsDao permsDao) {
        DivShiroFilterFactoryBean shiroFilterFactoryBean = new DivShiroFilterFactoryBean();
        shiroFilterFactoryBean.setMenuDao(menuDao);
        shiroFilterFactoryBean.setPermsDao(permsDao);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/sys/user/jump/login");
        shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
        shiroFilterFactoryBean.setFilterChainDefinitions(
                "/login.html=anon\n" +
                        "/sys/user/login=anon\n" +
                        "/static/**=anon"
        );
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("roles", new DivShiroRoleFilter());
        //注册权限过滤器
        filterMap.put("perms",new DivShiroPermFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }

5.5 页面按钮控制

   如果没有权限,在页面上就不应该让用户看到按钮,现在就需要把用户没有权限的按钮隐藏起来。那么在查询全部菜单返回得页面的时候,应该把权限也查出来。页面的按钮不要写成动态根据数据库里数据生成的那种,因为页面上有很多事件样式,写成动态的不方便。

①先遍历菜单把权限单独放一个list集合

②作为按钮的菜单不能显示到左侧菜单栏里面,就是在生成左侧菜单栏的时候加个判断条件要不是按钮

③前台把拿到的用户perms存在在window或者本地存储里面

④页面上没个按钮上固定的权限写上面,if语句判断该按钮的权限perms里面有没有

总结:用户的权限perms在前端页面控制特定按钮能否看见,写在后端方法上就是特定方法能够执行。登录过后进入首页就把当前用户的全部权限放window或者sessionStorage.setItem(“perms”,perms)

public Map<String,Object> getMenuTree(List<Menu> list) {
        HashMap<Integer, Menu> map = new HashMap<>();
        List<Menu> menuList = new ArrayList<>();
        ArrayList<String> perms = new ArrayList<>();
        for (Menu menu : list) {
            //根节点不能是按钮
            if (menu.getParentId() == null && menu.getType()!=3) {
                menuList.add(menu);
            }
            map.put(menu.getId(), menu);
            //放入每一个菜单的权限
            perms.add(menu.getPerms());
        }
        for (Menu menu : list) {
            //类型为按钮的不能加到菜单栏
            if (menu.getParentId() != null && menu.getType()!=3) {
                if (map.containsKey(menu.getParentId())) {
                    map.get(menu.getParentId()).getChildren().add(menu);
                }
            }
        }
        //最后的结果需要返回两部分,一部分是生成的菜单树,一部分是菜单所有的权限,所有返回一个map
        Map<String,Object> permsData=new HashMap<>();
        //用户所有的菜单
        permsData.put("menu",menuList);
        //用户所有的权限
        permsData.put("perms",perms);
        return permsData;
    }

前端获取并存储perms

$.ajax({
            url: "/get",
            method: "post",
            dataType:'json',
            success: function (json) {
                var perms=json.perms;
                //返回的是一个map,map里面有menu菜单集合和权限perms集合
                sessionStorage.setItem("perms",perms);
                alert(perms);
                $.each(json.menu, function (i, obj) {
                    str += '<li class="layui-nav-item layui-nav-itemed">';
                    if (obj.type == 2) {
                        str += '<a class="" href="javascript:;" onclick="openRight(\'' + 
                            obj.url + '\')">' + obj.name + '</a>';
                    } else {
                        str += '<a class="" href="javascript:;">' + obj.name + '</a>';
                    }

                    if (obj.children.length > 0) {
                        makeMenu(obj.children);
                    }
                    str += '</li>'
                });
                $("#left-menu").html(str);
                element.render()
            }
        })
  //每次验证调用的代码,会拿到存储的perms对象进行比较
  function hasPermission(str) {
    var perms=sessionStorage.getItem("perms");
        if (perms.indexOf(str) > -1) {
            return true;
        } else {
            return false;
        }
    }

把当前按钮的权限定死在页面里,通过用户查询的权限集合来比对,有就显示该按钮,没有就不显示。因为是用的layui的dom,所以得用layui提供的模板引擎{{d.数据}} {{# 代码片段}}

<!--头工具栏-->
<script type="text/html" id="role-toolbar">
    <div class="layui-btn-container">
        {{# if(hasPermission('sys:menu:add')){  }}
        <button class="layui-btn" style="margin-left: 10px" lay-event="add">
            <i class="layui-icon">&#xe654;</i> 添加
        </button>
        {{ } }}
        {{# if(hasPermission('sys:menu:delete')){ }}
        <button class="layui-btn layui-btn-danger" lay-event="delete">
            <i class="layui-icon">&#xe640;</i> 删除
        </button>
        {{ } }}
    </div>
</script>

6. 完整代码

6.1 Realm

public class DivRealm extends AuthorizingRealm {

    private UserDao userDao;
    private PermsDao permsDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void setPermsDao(PermsDao permsDao) {
        this.permsDao = permsDao;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        Integer userId = (Integer) principalCollection.getPrimaryPrincipal();
        List<Integer> roleIds=permsDao.getRoleUserIdsByUserId(userId);
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        for (Integer roleId : roleIds) {
            info.addRole(roleId.toString());
        }
        List<String> perms = permsDao.getUserPerms(userId);
        info.addStringPermissions(perms);
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String email = token.getUsername();
        User user= userDao.findByUserName(email);
        System.out.println(user);
        if (user==null){
            return null;
        }
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user.getUserId(),user.getPassword(),getName());
        return info;
    }
}

6.2 ShiroFilterFactoryBean

public class DivShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    private MenuDao menuDao;
    private PermsDao permsDao;

    public void setMenuDao(MenuDao menuDao) {
        this.menuDao = menuDao;
    }

    public void setPermsDao(PermsDao permsDao) {
        this.permsDao = permsDao;
    }

    private static String filterChainDefinitions = null;

    @Override
    public void setFilterChainDefinitions(String definitions) {
        filterChainDefinitions = definitions;
        Ini ini = new Ini();
        ini.load(definitions);
        Ini.Section section = ini.getSection("urls");
        if (CollectionUtils.isEmpty(section)) {
            section = ini.getSection("");
        }
        List<Menu> menuList = menuDao.findAll();
        for (Menu menu : menuList) {
            if (StringUtils.hasText(menu.getUrl())) {
                List<Integer> roleIds = permsDao.getRoleIdByMenuId(menu.getId());
                if (roleIds != null && roleIds.size() > 0) {
                    String idStr = "roles" + roleIds.toString();
                    section.put(menu.getUrl(), idStr);
                }
            }
            section.put("/**", "authc");
        }
        this.setFilterChainDefinitionMap(section);
    }

    public synchronized void update() {
        try {
            AbstractShiroFilter shiroFilter = (AbstractShiroFilter) this.getObject();
            PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) shiroFilter
                    .getFilterChainResolver();
            DefaultFilterChainManager manager = (DefaultFilterChainManager) resolver.getFilterChainManager();
            manager.getFilterChains().clear();
            this.getFilterChainDefinitionMap().clear();
            this.setFilterChainDefinitions(filterChainDefinitions);
            Map<String, String> chains = this.getFilterChainDefinitionMap();
            if (!CollectionUtils.isEmpty(chains)) {
                Iterator var12 = chains.entrySet().iterator();

                while (var12.hasNext()) {
                    Map.Entry<String, String> entry = (Map.Entry) var12.next();
                    String url = (String) entry.getKey();
                    String chainDefinition = (String) entry.getValue();
                    manager.createChain(url, chainDefinition);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

6.3 ShiroPermFilter

public class DivShiroPermFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        String[] perms=(String[]) o;
        System.out.println(Arrays.toString(perms));
        for (String p : perms) {
            if (subject.isPermitted(p)){
                return true;
            }
        }
        return false;
    }
}

6.4 ShiroRoleFilter

public class DivShiroRoleFilter extends AuthorizationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest,
                                      ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        String[] roleIds=(String[]) o;
        for (String roleId : roleIds) {
            if (subject.hasRole(roleId)){
                return true;
            }
        }
        return false;
    }
}

6.5 ShiroConfig

@Configuration
public class ShiroConfig {
    @Bean
    public DivRealm realm(UserDao userDao, PermsDao permsDao) {
        DivRealm realm = new DivRealm();
        realm.setUserDao(userDao);
        realm.setPermsDao(permsDao);
        return realm;
    }
    @Bean
    public MemoryConstrainedCacheManager cacheManager() {
        return new MemoryConstrainedCacheManager();
    }
    @Bean
    public DefaultWebSecurityManager securityManager(DivRealm realm, MemoryConstrainedCacheManager cacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setCacheManager(cacheManager);
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, MenuDao menuDao, PermsDao permsDao) {
        DivShiroFilterFactoryBean shiroFilterFactoryBean = new DivShiroFilterFactoryBean();
        shiroFilterFactoryBean.setMenuDao(menuDao);
        shiroFilterFactoryBean.setPermsDao(permsDao);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/sys/user/jump/login");
        shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
        shiroFilterFactoryBean.setFilterChainDefinitions(
                "/login.html=anon\n" +
                        "/sys/user/login=anon\n" +
                        "/static/**=anon"
        );
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("roles", new DivShiroRoleFilter());
        filterMap.put("perms",new DivShiroPermFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BossTimor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值