Shiro

是一个权限认证的框架,Web安全性包括用户认证和用户授权

用户认证:验证某个用户是否是一个合法的用户,通常用来通过用户名和密码验证这个用户是否能登录

用户授权:验证某个用户是否有权限执行某个操作

链接

核心架构

最重要的是安全管理器

img

  • authenticator是用来认证使用的

  • authorizer是用来实现授权的

  • session manager是在web中使用的,用来管理web的web会话

  • session dao对session进行增删改查的

  • cache manager 用来缓存认证和授权的数据

  • pluggable realms用来获取认证、授权的数据,完成授权操作

  • cryptography是算法生成器,提供了密码加密的算法

认证

shiro认证的关键对象:

  • subject
    • 中文为课程、题目、主题,读音为 səbˈdʒekt
    • 在这里代指为主体,通常主体为:用户(当前访问系统的用户)、程序等
  • principal
    • 中文为最主要的、主要的,读音为ˈprɪnsəpl
    • 是主体subject进行身份认证的表示,这个标识必须具有唯一性
    • 可以有多个身份,但必须要有一个主身份
    • 例如用户名、手机号码、邮箱等
  • credential
    • 中文为资质、凭据,读音为krəˈdenʃl
    • 通常称为凭证信息,通常密码、口令等

引入依赖:

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.9.0</version>
</dependency>

配置文件

配置文件使用.ini结尾的文件,可以将权限数据暂时写死到这个文件中

例如以下是3个用户:

[users]
admin=123456
user=user123
username=password

认证的实现

  • 创建安全管理器对象
    • SecurityManager接口,可以使用实现类DefaultSecurityManager
  • 给安全管理器提供用户的数据
    • realm中文为领域,读音为relm
    • 可以使用DefaultSecurityManager类提供的setRealm设置数据
  • 使用全局安全工具类SecurityUtils完成认证
    • 给这个工具类设置安全管理器
    • 获取Subject主体
    • 创建一个令牌(UsernamePasswordToken类),通过在令牌中设置用户名和密码
    • 通过Subject主体中的login(令牌)的方式进行尝试登录
      • 如果登录失败,并且密码错误,将会抛出org.apache.shiro.authc.IncorrectCredentialsException异常(不正确的凭据异常)
      • 如果用户名不存在,将会抛出org.apache.shiro.authc.UnknownAccountException异常
      • subject.isAuthenticated()方法可以判断认证状态
  • 以上流程可以概括为:
    • 创建一个安全管理器
    • 在安全管理器中设置已存在的账号密码
    • 为全部安全工具类设置一个安全管理器
    • 设置一个令牌
    • 通过安全管理器获取一个主体
    • 通过主体验证令牌
public static void main(String[] args) {
    // 创建安全管理器
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    // 设置数据
    securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    // 设置安全工具类的安全管理器
    SecurityUtils.setSecurityManager(securityManager);
    // 获取一个主体
    Subject subject = SecurityUtils.getSubject();
    // 设置一个令牌
    UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
    // 尝试登录
    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        System.out.println("登录失败");
        e.printStackTrace();
    }

    System.out.println("认证状态:" + subject.isAuthenticated());

}

底层源码实现

底层调用的是安全管理器中的login()方法

  • 向下执行,会发现先判断令牌是不是空的,如果是空的就直接抛异常,如果不是继续往下

  • 进行断言realms是不是空的,如果是空的就直接抛出异常,如果不是空的,继续向下执行

  • 获取所有的realms并放到List集合中,判断一下List集合的大小,如果大小是1,就直接在集合中取出这个realm

    • image-20220513163458713
    • 当大小是1时,先判断当前的realm是否支持这个token(令牌),如果不支持就打个日志,并抛出异常,如果支持 继续向下执行
    • 首先在缓存中取出认证信息,如果是空的就通过用户名拿到这个用户信息,拿到之后判断一下用户是不是锁定了或者密码是不是过期了,如果有相应的情况就抛出相应的异常,如果没有相应的情况直接返回这个用户信息,需要注意这个时候并没有进行密码的校验
    • 如果取出的用户信息不为空,那么将返回的用户信息放入到缓存中
    • 这个时候会进行断言密码是否匹配
      • image-20220513164926045
      • 如果不匹配就抛出异常
    • 当想要进行从数据库中读取用户名和密码时,可以将SimpleAccountRealm类中的doGetAuthenticationInfo(AuthenticationToken token)方法换成自己的实现即可,又由于SimpleAccountRealm类继承自AuthorizingRealm,所以自定义的类可以继承AuthorizingRealm
  • 总结:

    image-20220513170653365

自定义Realm实现

  • 自定义类继承AuthorizingRealm类,实现抽象方法

  • 主要重写protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)方法

    • 通过调用token.getPrincipal()将其值强制转换为String,此值将作为用户名
    • 连接数据库,通过用户名获取用户,如果用户为空,直接返回空即可
    • 如果不为空,需要返回AuthenticationInfo实例,可以使用实现类SimpleAuthenticationInfo的实例进行返回,构造方法有3个参数,分别为:
      • 参数1为用户名
      • 参数2为密码(口令)
      • 参数3为一个名字,可以用父类的getName()获取名称
  • SecurityManagerrealm设置为自定义类的实例

    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    public class MyRealm extends AuthorizingRealm {
    
        //    授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
            return null;
        }
    
        //    认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String principal = (String) token.getPrincipal();
            System.out.println("token中的用户名为:" + principal);
            // 接下来可以访问数据库通过用户名获取整个用户信息
            if (!"hello".equals(principal)) {
                // 代表此时不存在这个用户
                return null;
            }
            // 参数1为用户名;参数2为密码(口令);参数3为一个名字,可以用父类的getName()获取名称
            return new SimpleAuthenticationInfo("hello", "123456", getName());
        }
    }
    
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置数据
securityManager.setRealm(new MyRealm());
// 设置安全工具类的安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取一个主体
Subject subject = SecurityUtils.getSubject();
// 设置一个令牌
UsernamePasswordToken token = new UsernamePasswordToken("hello", "1234567");
// 尝试登录
try {
    subject.login(token);
} catch (AuthenticationException e) {
    System.out.println("登录失败");
    e.printStackTrace();
}

System.out.println("认证状态:" + subject.isAuthenticated());

加密

MD5

  • 一般用来加密、签名
  • 算法是不可逆的,同一条数据无论加密多少次,得到的结果都是一致的
  • 如果想要求出原始字符串,那就只能通过穷举法
  • 生成结果始终是16进制的32位的字符串
  • 一般在Service层完成密码的加密

Salt

  • 如果用户输入的密码是一个比较简单的密码,那么使用穷举进行求出原始串时也会变的比较容易,所以为了密码的进一步安全性,通常在注册时需要在密码前边或者后边加入一个随机的字符串,然后在对这个密码做MD5加密,随机加的字符串称为Salt,在设计数据库时还需要将Salt作为一个字段进行存储
  • 在登录时,需要在数据库中取出这个salt,将其拼接到密码中,在对这段密码进行MD5加密,将加密后的结果再与数据库中的进行比对

在Shiro中对密码进行加密

错误的用法如下:

Md5Hash md5Hash = new Md5Hash();
md5Hash.setBytes("admin".getBytes());
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());

结果为md5Hash.toHex() = 61646d696e


正确的用法:

使用构造方法

Md5Hash md5Hash = new Md5Hash("admin");
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());

使用salt:

String uuid = UUID.randomUUID().toString();
Md5Hash md5Hash = new Md5Hash("admin", uuid);
System.out.println("uuid = " + uuid);
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());}

第二个参数为salt


也可以对结果进行再次散列:

构造方法的第3个参数为对结果进行散列的次数

Md5Hash md5Hash = new Md5Hash("admin", uuid, 10240);

如果想要在Realm中实现通过MD5进行比较,还需要进行切换凭据匹配器

可以在自定义的realm类中通过:

  • getCredentialsMatcher();获取一个凭据匹配器

  • setCredentialsMatcher(CredentialsMatcher credentialsMatcher);设置一个凭据匹配器

    • CredentialsMatcher是一个接口,可以使用HashedCredentialsMatcher作为凭据匹配器实现MD5比较

    • Realm实现类的构造方法中设置凭据匹配器为setCredentialsMatcher(new HashedCredentialsMatcher("md5"));

    • 其他地方代码保持不变,只是返回的密码变成了MD5

    • import org.apache.shiro.authc.AuthenticationException;
      import org.apache.shiro.authc.AuthenticationInfo;
      import org.apache.shiro.authc.AuthenticationToken;
      import org.apache.shiro.authc.SimpleAuthenticationInfo;
      import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
      import org.apache.shiro.authz.AuthorizationInfo;
      import org.apache.shiro.realm.AuthorizingRealm;
      import org.apache.shiro.subject.PrincipalCollection;
      
      public class MyRealm extends AuthorizingRealm {
      
          public MyRealm() {
              setCredentialsMatcher(new HashedCredentialsMatcher("md5"));
          }
      
          //    授权
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
      
              return null;
          }
      
          //    认证
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
              String principal = (String) token.getPrincipal();
              System.out.println("token中的用户名为:" + principal);
              // 接下来可以访问数据库通过用户名获取整个用户信息
              if (!"hello".equals(principal)) {
                  // 代表此时不存在这个用户
                  return null;
              }
              // 参数1为用户名;参数2为密码(口令);参数3为一个名字,可以用父类的getName()获取名称
              return new SimpleAuthenticationInfo("hello", "e10adc3949ba59abbe56e057f20f883e", getName());
          }
      }
      
      

如果采用了Salt对密码进行加密,需要在返回SimpleAuthenticationInfo值的构造方法上进行添加:

  • ByteSource.Util.bytes("Salt字符串")
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String principal = (String) token.getPrincipal();
    System.out.println("token中的用户名为:" + principal);
    // 接下来可以访问数据库通过用户名获取整个用户信息
    if (!"hello".equals(principal)) {
        // 代表此时不存在这个用户
        return null;
    }
    // 参数1为用户名;参数2为密码(口令);参数3为通过ByteSource转换的salt;参数4为一个名字,可以用父类的getName()获取名称
    return new SimpleAuthenticationInfo("hello", "d83dd1679625748d33a1ce05de8aa1aa", ByteSource.Util.bytes("00e1adff-cb31-4f83-8dc0-93d1101ffdef"), getName());
}

如果密码散列了多次,还需要在自定义Relam类的构造方法设置凭据匹配器时设置散列次数:

  • public MyRealm() {
        HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
        md5.setHashIterations(1024);
        setCredentialsMatcher(md5);
    }
    

授权

控制谁能访问哪些资源,对于某些资源没有权限是无法访问的

授权可以理解为whowhat进行how操作

  • who:主体subject,需要访问系统中的资源
  • what:为资源例如商品信息、某个类方法等
  • how:主体最资源的操作许可、操作权限,权限不能离开资源

授权一般是在认证成功后进行授权

授权的方式:

  • 基于角色访问控制

    • RBACRole-Based Access Control

    • 检查某个用户是否有相应权限

      if(subject.hasRole("用户")) {
          ....
      }
      
  • 基于资源访问控制

    • RBACResource-Based Access Control

    • 检查这个资源是否有权限,具体含义为检查检查某个模块是否有操作某个资源的权限

      if(subject.isPermission("资源标识符:操作名称:具体的资源实例标识")) {
          ...
      }
      

权限字符串:资源标识符:操作:资源实例标识符

  • 权限字符串可以用*标识,*代表所有权限

授权的实现

实现方案有两种:

  • 编程式

    • if (subject.hasRole("admin")) {
          
      } else {
          
      }
      
  • 注解式

    • @RequiresRoles("admin")
      public void method() {
      
      }
      
      @RequiresPermissions("*:*:*")
      public void method() {
          
      }
      

只有认证通过的用户才能够判断是否有某个权限,否则进行判断是否有某个权限时,结果使用为false

基于单角色的权限访问控制

即只需要判断一个用户只需要有其中的一种角色就可以

进行判断权限时,将会调用protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 方法,每进行一个权限判断,都会执行一次这个方法

  • 在这个方法的参数中,可以调用String primaryPrincipal = (String) principals.getPrimaryPrincipal();获取主身份(用户名)

  • 通过主身份可以查询数据库中的用户的权限,并将查到的权限放入到一个Set<String>集合中

  • 通过return new SimpleAuthorizationInfo(Set<String> roles)将权限列表返回

  • 如果返回的这个对象中没有任何的内容或者null,代表此时无任何权限

  • public static void main(String[] args) {
        SecurityUtils.setSecurityManager(new DefaultSecurityManager(new MyRealm2()));
        Subject subject = SecurityUtils.getSubject();
        System.out.println(subject.hasRole("管理员"));
    
        subject.login(new UsernamePasswordToken("hello", "123456"));
        if (subject.isAuthenticated()) {
            System.out.println(subject.hasRole("管理员"));
        }
    }
    
  • @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        // 查询数据库语句
        return new SimpleAuthorizationInfo(Set.of("管理员", "用户"));
    }
    

基于多角色的权限访问控制

需要判断一个用户有多个角色时,只需要修改授权判断就可以

if (subject.isAuthenticated()) {
    System.out.println(subject.hasAllRoles(List.of("管理员", "用户")));
}

以上代码中,只有这个用户同时拥有管理员和用户权限时,结果为true

也可以通过subject.hasRoles(List.of("管理员", "用户", "老师"))判断这个主体针对多个角色是否有相应的权限,结果返回一个boolean[]

基于权限字符串的权限访问控制

权限字符串:资源标识符:操作:资源实例标识符,例如:

  • 返回权限如果最后是*,那么可以省略,但必须要保证至少有一个
    • 例如user:update:*可以写为user:update
    • user:*:*可以写为user
    • 但当某个角色拥有全部权限时,不能写为 ,必须要有一个标识,此时可以写为*
  • 返回的权限:user:update,那么此时user:update:*user:update:1user:update:akfasdkfja等都是合法的,但user:*:1user:insert:3等都是不合法的
  • 需要判断的权限只要比返回的权限小或者相等,那么这时候都是合法的

可以通过subject.isPermitted("权限字符串")进行判断是否有某项权限

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)方法中的返回值中可以添加一些权限的字符串

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    // 查询数据库语句

    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(Set.of("管理员", "用户"));
    // 添加权限字符串
    simpleAuthorizationInfo.addStringPermission("user:update:*");
    return simpleAuthorizationInfo;
}
System.out.println("=============基于权限字符串==============");
if (subject.isAuthenticated()) {
    // 判断有单个权限
    System.out.println("subject.isPermitted(\"user:update:1\") = " + subject.isPermitted("user:update:1"));
    // 判断是否拥有一下全部权限
    System.out.println("subject.isPermittedAll(\"user:update:2\", \"user:update:3\", \"user:insert:3\", \"user:update:*\", \"user:update\") = " + subject.isPermittedAll("user:update:2", "user:update:3", "user:insert:3", "user:update:*", "user:update"));
    // 判断对各项权限的拥有情况
    System.out.println("Arrays.toString(subject.isPermitted(\"user:update:2\", \"user:update:3\", \"user:insert:3\", \"user:update:*\", \"user:update\")) = " + Arrays.toString(subject.isPermitted("user:update:2", "user:update:3", "user:insert:3", "user:update:*", "user:update")));
}

Spring Boot整合Shiro

将所有的请求都交给ShiroFilter处理

image-20220515161530165

引入依赖:

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.9.0</version>
</dependency>

需要一个配置类:

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        shiroFilterFactoryBean.setLoginUrl("/main/login");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(new HashMap<>(){
            {
                put("/", "authc");
            }
        });
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(MyRealm2 myRealm2) {
        return new DefaultWebSecurityManager(myRealm2);
    }

    @Bean
    public MyRealm2 myRealm2() {
        return new MyRealm2();
    }

}

以上代码的第11行有authc字样:

  • 这是一个过滤器的简称,shiro提供了多个过滤器
  • image-20220518183013835
  • Spring Boot会自定将安全管理器注入到SecurityUtils
  • subject.logout可以退出登录

前后端分离的项目中,在配置ShiroFilterFactoryBean时,无需再进行配置拦截路径

是通过JSESSIONID判断登录状态的

注解式权限控制:

  • 以下代码为简单模拟的用户名之后带有n的用户才会被赋予角色n和权限admin:*:*

  • @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        if (primaryPrincipal.endsWith("n")) {
            simpleAuthorizationInfo.addRole("n");
            simpleAuthorizationInfo.addStringPermission("admin:*:*");
        }
        return simpleAuthorizationInfo;
    }
    
  • @GetMapping("/status")
    @RequiresRoles("n")
    @RequiresPermissions("admin:add:user")
    public String status() {
        return "有权限";
    }
    

    如果没有权限,将会被控制器执行这个方法时抛出org.apache.shiro.authz.UnauthorizedException(没有权限)异常,如果在其他有权限的方法中调用这个方法,会正常执行,将不会抛出异常!!

    例如admina用户是没有这个权限的,所以admina请求/status时会抛出没有权限的异常,但admina有权限访问/login/login的处理方法中的第6行调用了status方法,此时将会正常的执行

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        Subject subject = SecurityUtils.getSubject();
        subject.login(new UsernamePasswordToken(username, password));
        System.out.println("subject.isAuthenticated() = " + subject.isAuthenticated());
        status();
        return "登录";
    }
    
    @GetMapping("/status")
    @RequiresRoles("n")
    @RequiresPermissions("admin:add:user")
    public String status() {
        System.out.println("执行了");
        return "有权限";
    }
    @ExceptionHandler({AuthenticationException.class, Exception.class})
    public String processException(Exception e) {
        e.printStackTrace();
        if (e instanceof AuthenticationException) {
            return "用户名不存在或者密码错误";
        }
        return "没有权限";
    }
    

数据库设计

用户->角色->权限->资源

用户和角色、角色和权限是多对多的关系

image-20220519200707201

在设计实体类时,将角色的List类型的集合放入到用户类中,将权限的List类型的集合放入到角色类中

Shiro中的缓存

如果一个用户经过了权限认证,那么可以将这个数据暂时的缓存起来,以减轻数据库的压力,一般放入缓存的数据都是增删改比较少的数据

Shiro提供了CacheManager,以用来管理缓存

image-20220520154539647

可以使用ehCache作为缓存管理器:

  • 在配置类中返回自定义Realm实例的方法中进行配置

  • 引入依赖

    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.9.0</version>
    </dependency>
    
  • 配置信息大致如下:

    @Bean
    public MyRealm myRealm() {
        MyRealm myRealm = new MyRealm();
        myRealm.setCacheManager(new EhCacheManager());
        // 开启全局缓存
        myRealm.setCachingEnabled(true);
        // 开启认证缓存
        myRealm.setAuthorizationCachingEnabled(true);
        // 开启授权缓存
        myRealm.setAuthorizationCachingEnabled(true);
        myRealm.setAuthenticationCacheName("设置认证缓存的名字");
        myRealm.setAuthorizationCacheName("设置授权缓存的名字");
        return myRealm;
    }
    

整合Redis

  • 引入Spring Boot操作Redis的依赖
  • 自定义一个类实现CacheManager接口,并实现相应方法
    • 将返回值设置为new 下一步的实现类
  • 再使用自定义类实现Cache接口
@Bean
public MyRealm myRealm() {
    MyRealm myRealm = new MyRealm();
    // 设为自己的缓存管理器
    myRealm.setCacheManager(new MyCacheManager());
    // 开启全局缓存
    myRealm.setCachingEnabled(true);
    // 开启认证缓存
    myRealm.setAuthorizationCachingEnabled(true);
    // 开启授权缓存
    myRealm.setAuthorizationCachingEnabled(true);
    myRealm.setAuthenticationCacheName("设置认证缓存的名字");
    myRealm.setAuthorizationCacheName("设置授权缓存的名字");
    return myRealm;
}
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

public class MyCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        System.out.println("getCache:" + s);
        return new MyRedisCache<>();
    }
}
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import java.util.Collection;
import java.util.Set;

public class MyRedisCache<K, V> implements Cache<K, V> {
    @Override
    public V get(K k) throws CacheException {
        return null;
    }

    @Override
    public V put(K k, V v) throws CacheException {
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
        return null;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}
  • 此时需要重写以上的方法,K的中的值通常为用户名
  • V类型为SimpleAuthorizationInfo,可以通过getRoles()获取到所有的角色信息,通过getObjectPermissions()获取到所有的权限信息
  • 可以使用Redis存储二进制信息,通过反序列化将对象取出来

Q.E.D.


念念不忘,必有回响。