shiro-实战

权限管理

  • 什么是权限管理

    基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源

    权限管理包括用户身份认证授权两部分,简称认证授权,对于需要访问控制的资源用户首先要经过身份认证,认证通过后具有该资源的访问权限方可访问

  • 什么是身份认证

    身份认证 就是判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确,对于采用指纹等系统,则出示指纹,对于硬件key 等刷卡系统,则需要刷卡

  • 什么是授权

    授权、即访问控制控制谁能访问那些资源,主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法进行访问的

shiro

  • 架构图

    架构图(官网)
  • 解释

    • Subject

      Subject即主体,外部应用与subject 进行交互,subject 记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序,Subject shiro 中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject 进行认证授权,而subject 是通过SecurityManager 安全管理器进行认证授权

    • SecurityManager

      SecurityManager 即安全管理器,对外部的subject 进行安全管理,它是shiro 的核心,负责对所有的subject 进行安全管理,通过SecurityManager 可以完成subject 的认证、授权等,实质上SecurityManager 是通过Authenticator 进行认证,通过Authorizer 进行授权,通过SessionManager 进行会话管理等

      SecurityManager 是一个接口,继承了Authenticator、Authorizer、SessionManager 这三个接口

    • Authenticator

      Authenticator 即认证器,对用户身份进行认证,Authenticator 是一个接口,shiro 提供ModularRealmAuthenticator 实现类,通过ModularRealmAuthenticator 基本上可以满足大多数需求,也可以自定义认证器

    • Authorizer

      Authorizer即授权器用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否具有此功能的操作权限

    • Realm

      Realm即领域相当于datasource 数据源,securityManager 进行安全认证需要通过Realm 获取用户权限数据,比如: 如果用户身份数据在数据库那么realm 就需要从数据库获取用户身份信息

      注意: 不要把realm 理解成只是从数据源取数据,realm 中还有认证授权校验的相关的代码

    • SessionManager

      SessionManager即会话管理,shiro 框架定义了一套会话管理,他不依赖web 容器的session 可以使用在非web 应用上,也可以将分布式应用的会话集中在一点管理,此特性可以使他实现单点登录

    • SessionDAO

      SessionDAO即会话dao 是对session 会话操作的一套接口,比如要将session 存储到数据库,可以通过jdbc 将会话存储到数据库

    • CacheManager

      CacheManager即缓存管理将用户权限数据存储在缓存,这样可以提高性能

    • Cryptography

      Cryptography 即密码管理,shiro 提供了一套加密/解密的组件,方便开发,比如提供常用的散列,/解密等功能

shiro中的认证

  • 认证

    身份认证,就是判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令是否一致,来判断用户身份是否正确

  • shiro 中认证的关键对象

    • Subject 主体

      访问系统的用户,程序等、进行认证的都称为主体

    • Principal 身份信息

      是主体进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份

    • credential 凭证信息

      是只有主体自己知道的安全信息,如密码、证书等

  • 认证流程

    认证流程
  • 认证的开发

    • 创建项目(maven-archetype-quickstart)并引入依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.9.0</version>
      </dependency>
    • 引入shiro 配置文件并加入如下配置

      1
      2
      3
      # 临时数据
      [users]
      coderitl=root
    • 开发认证代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package com.coderitl.shiro;

      import org.apache.shiro.SecurityUtils;
      import org.apache.shiro.authc.AuthenticationException;
      import org.apache.shiro.authc.IncorrectCredentialsException;
      import org.apache.shiro.authc.UnknownAccountException;
      import org.apache.shiro.authc.UsernamePasswordToken;
      import org.apache.shiro.mgt.DefaultSecurityManager;
      import org.apache.shiro.realm.text.IniRealm;
      import org.apache.shiro.subject.Subject;

      public class TestAuthenticator {
      public static void main(String[] args) {
      // 1. 创建安全管理器 => SecurityManager
      DefaultSecurityManager securityManager = new DefaultSecurityManager();
      // 2. 给安全管理器设置 realm
      securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
      // 3. SecurityUtils 给安全工具类设置安全管理器
      SecurityUtils.setSecurityManager(securityManager);
      // 4. 关键对象 subject 主体
      Subject subject = SecurityUtils.getSubject();
      // 5. 创建令牌
      UsernamePasswordToken token = new UsernamePasswordToken("coderitl", "root");
      try {
      // 用户认证
      subject.login(token);
      // true => 认证通过
      System.out.println("认证状态: " + subject.isAuthenticated());
      } catch (UnknownAccountException e) {
      e.printStackTrace();
      System.out.println("认证失败: 用户名不存在~");
      } catch (IncorrectCredentialsException e) {
      System.out.println("认证失败: 密码错误~");
      }
      }
      }

    • 源码实现

    • 根据认证源码认证使用的是SimpleAccountRealm

      SimpleAccountRealm
    • 自定义realm

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      package com.coderitl.shiro.realm;

      import jdk.nashorn.internal.parser.Token;
      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;

      // 自定义 realm 实现 将认证/授权数据来源转为数据库的实现
      public class CustomerRealm extends AuthorizingRealm {
      // 1. => 认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      System.out.println("认证...............");
      // 1. 在 token 中获取用户名
      String principal = (String) authenticationToken.getPrincipal();
      // 根据身份信息使用 jdbc | mybatis 查询相关数据库
      if ("coderitl".equals(principal)) {
      // SimpleAuthenticationInfo 的属性可以赋值用户名和密码
      // protected PrincipalCollection principals; 用户名
      // protected Object credentials; 密码
      // 参数1: 返回数据库中正确的用户名 参数2: 返回数据库中正确的密码 参数3: 提供当前 realm 的名称(底层生成) => this.getName() 获取
      SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("coderitl","root",this.getName());
      return simpleAuthenticationInfo;
      }
      return null;
      }

      // 2. => 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      System.out.println("授权...............");
      return null;
      }


      }

      • 测试自定义realm

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        package com.coderitl.shiro;

        import com.coderitl.shiro.realm.CustomerRealm;
        import org.apache.shiro.SecurityUtils;
        import org.apache.shiro.authc.IncorrectCredentialsException;
        import org.apache.shiro.authc.UnknownAccountException;
        import org.apache.shiro.authc.UsernamePasswordToken;
        import org.apache.shiro.mgt.DefaultSecurityManager;
        import org.apache.shiro.subject.Subject;

        public class TestCustomerRealm {
        public static void main(String[] args) {
        // 创建 SecurityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        // 设置自定义的 realm
        defaultSecurityManager.setRealm(new CustomerRealm());
        // 将安全工具类设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 通过安全工具类获取 subject
        Subject subject = SecurityUtils.getSubject();
        // 创建 token
        UsernamePasswordToken token = new UsernamePasswordToken("coderitl", "root");
        try {
        // 用户认证
        subject.login(token);
        System.out.println("认证状态: " + subject.isAuthenticated());
        } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("认证失败: 用户名不存在~");
        } catch (IncorrectCredentialsException e) {
        System.out.println("认证失败: 密码错误~");
        }
        }
        }

  • 加密

    • md5 Salt

      实际应用是将盐和散列后的值存在数据库中,自动realm 从数据库取出盐和加密后的值由shiro 完成密码校验

      • md5 作用: 一般用来加密 或者 签名

      • 特点: MD5 算法不可逆,如果内容相同无论执行多少次md5 生成结果始终是一致

      • 基础使用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        @Test
        public void shouldAnswerWithTrue() {
        // 不能使用 setBytes => 未实现 md5 加密
        Md5Hash md5Hash1 = new Md5Hash();
        md5Hash1.setBytes("root".getBytes());
        System.out.println("md5Hash1 = " + md5Hash1);

        // 使用构造 => 实现 md5 加密
        Md5Hash md5Hash2 = new Md5Hash("root".getBytes());
        System.out.println("md5Hash2 = " + md5Hash2);

        // md5 + salt
        Md5Hash md5Hash3 = new Md5Hash("root".getBytes(), "coderitl");
        System.out.println("md5Hash3 = " + md5Hash3);

        // md5 + salt + hash散列(最终用法)
        Md5Hash md5Hash4 = new Md5Hash("root".getBytes(), "coderitl", 1024);
        System.out.println("md5Hash4 = " + md5Hash4);
        }
    • 综合使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      package com.coderitl.shiro;

      import com.coderitl.shiro.realm.CustomerMDSalt;
      import org.apache.shiro.SecurityUtils;
      import org.apache.shiro.authc.IncorrectCredentialsException;
      import org.apache.shiro.authc.UnknownAccountException;
      import org.apache.shiro.authc.UsernamePasswordToken;
      import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
      import org.apache.shiro.mgt.DefaultSecurityManager;
      import org.apache.shiro.subject.Subject;

      public class TestCustomerMD5Salt {
      public static void main(String[] args) {
      // 创建安全管理器
      DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
      // 注入 realm
      CustomerMDSaltHash realm = new CustomerMDSaltHash();
      // 设置 realm 使用 hash 凭证匹配器
      HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
      // 设置使用的 md5 算法
      hashedCredentialsMatcher.setHashAlgorithmName("md5");
      // 散列次数
      hashedCredentialsMatcher.setHashIterations(1024);
      realm.setCredentialsMatcher(hashedCredentialsMatcher);
      defaultSecurityManager.setRealm(realm);

      // 将 安全管理器注入安全工具
      SecurityUtils.setSecurityManager(defaultSecurityManager);
      // 通过安全工具类获取 subject
      Subject subject = SecurityUtils.getSubject();
      // 认证
      UsernamePasswordToken token = new UsernamePasswordToken("coderitl", "root");
      try {
      // 用户认证
      subject.login(token);
      System.out.println("登录成功~");
      } catch (UnknownAccountException e) {
      e.printStackTrace();
      System.out.println("认证失败: 用户名不存在~");
      } catch (IncorrectCredentialsException e) {
      System.out.println("认证失败: 密码错误~");
      }
      }
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      package com.coderitl.shiro.realm;

      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;
      import org.apache.shiro.util.ByteSource;

      public class CustomerMDSaltHash extends AuthorizingRealm {
      // 1. => 认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      System.out.println("认证...............");
      // 1. 获取身份信息
      String principal = (String) authenticationToken.getPrincipal();
      // 根据用户名查询数据库
      if ("coderitl".equals(principal)) {
      return new SimpleAuthenticationInfo(
      // 数据库中的用户名
      "coderitl",
      // 数据库中的密码
      "6d0b13265ab36f227e1341c97a3fe4f1",
      // 随机盐
      ByteSource.Util.bytes("coderitl"),
      // realm 名称
      this.getName());
      }
      return null;
      }

      // 2. => 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      System.out.println("授权...............");

      return null;
      }
      }

      凭证解析器

shiro中的授权

  • 授权

    授权,即访问控制,控制谁能访问那些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的

  • 关键对象

    授权可简单理解为who what(which) 进行How 操作

    who即主体(Subject) 主体需要访问系统中的资源

    what即资源(Resource) 如系统菜单,页面,按钮、类方法、系统商品信息等,资源包括资源类型和资源实例,比如商品信息为资源类型,类型为51010 的商品为资源实例,变为为001 的商品信息也属于资源实例

    How权限/许可规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限,用户添加权限,某个类方法的调用权限,编号为001 用户的修改权限等,通过权限可知主体对那些资源操作许可

  • 授权流程

    授权流程
  • 授权的方式

    • 基于角色的访问控制
      • RBAC 基于角色的访问控制,是以角色为中心进行访问控制
    • 基于资源的访问控制
      • RBAC 基于资源的访问控制,是以资源为中心进行访问控制
  • 权限字符串

    权限字符串的规则是: 资源标识符:操作:资源实例标识符意思是对那个资源的那个实例具有什么操作: 是资源/操作/实例的分隔符,权限字符串也可以使用* 通配符

    例:

    • 用户创建权限user:create | user:create:*
    • 用户修改实例001 的权限:user:update:001
    • 用户实例001 的所有权限:user:*:001
  • 授权编程实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 自定义的 
    // 2. => 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // PrincipalCollection principalCollection => 身份的集合
    System.out.println("授权...............");
    // 主身份用户信息
    String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
    System.out.println("用户名: " + primaryPrincipal);
    // 根据身份信息(用户名) 获取当前用户的角色信息以及权限信息
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    // 将数据库中查询角色信息赋值给权限对象
    simpleAuthorizationInfo.addRole("admin");
    simpleAuthorizationInfo.addRole("root");
    // 将数据库中查询权限信息赋值给权限对象
    simpleAuthorizationInfo.addStringPermission("user:*:01");
    return simpleAuthorizationInfo;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 认证后 => 授权
    if (subject.isAuthenticated()) {
    // 1. (单一权限)基于角色的权限控制
    System.out.println("单一角色权限: " + subject.hasRole("admin"));
    System.out.println("******************************");
    // 2. 基于多角色的权限控制
    System.out.println("多角色权限: " + subject.hasAllRoles(Arrays.asList("admin", "root")));
    System.out.println("<------------------------------------------>");
    // 3. 是否具有某一角色权限
    boolean[] booleans = subject.hasRoles(Arrays.asList("root", "admin", "super"));
    for (boolean aBoolean : booleans) {
    System.out.println("aBoolean = " + aBoolean);
    }
    System.out.println("=================================");
    // 基于权限字符串的访问控制
    System.out.println(subject.isPermitted("user:*:*"));

    // 分别具有那些权限
    boolean[] permitted = subject.isPermitted("user:*01", "order:*:01");
    for (boolean b : permitted) {
    System.out.println(b);
    }
    // 同时具有那些权限
    boolean permittedAll = subject.isPermittedAll("user:*01", "order:*:01");
    System.out.println("permittedAll = " + permittedAll);
    }

Springboot整合

整合思路
  • 流程

    流程
环境搭建
  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    </dependency>
    <!-- springboot 整合 shiro 依赖 -->
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.9.1</version>
    </dependency>

    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.11</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
    </dependency>
    <!-- 整合 jsp -->
    <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
    <dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    </dependency>
    </dependencies>
  • 创建shiro 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    package com.coderitl.shiro.config;

    import com.coderitl.shiro.realms.CustomerRealm;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.realm.Realm;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

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

    @Configuration
    public class ShiroConfig {

    // 1. 创建 shiroFilter
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 给 filter 设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    // 配置系统受限资源
    // 配置系统公共资源
    Map<String, String> map = new HashMap<>();
    // 参数一: 受限资源路径 参数二: 限制级别 authc=> 代表这个资源需要认证和授权
    map.put("/user/**", "anon"); // 所有 /user 为 anon =>不认证,可直接访问
    map.put("/register.jsp", "anon");
    map.put("/", "authc");
    map.put("/index.jsp", "authc");
    // 默认认证界面路径
    shiroFilterFactoryBean.setLoginUrl("/login.jsp");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    return shiroFilterFactoryBean;
    }

    // 2. 创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    // 设置 realm
    defaultWebSecurityManager.setRealm(realm);
    return defaultWebSecurityManager;
    }

    // 3. 创建自定义 realm
    @Bean
    public Realm getRealm() {
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为 md5
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");
    // 设置散列次数
    hashedCredentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
    return customerRealm;
    }

    }

    • 常见过滤器

      配置缩写 对应的过滤器 功能
      anno AnonymousFilter 指定url 可以匿名访问
      authc FormAuthenticationFilter 指定url 需要form 表单登录,默认会从请求中获取username,password,remeberMe 等参数并尝试登录,如果登陆不了就会跳转到loginURL 配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑,自己写的话出错可以定制信息
      authcNasic BasicHttpAuthenticationFilter 指定url 需要basic 登录
      logout LogoutFilter 退出过滤器,配置指定的url 就可以实现推出功能,非常方便
      noSessionCreation NoSessionCreationFilter 禁止创建会话
      perms HttpMethodPermissionFilter 需要指定的权限才能访问
      port PortFilter 需要指定的端口才能访问
      rest HttpMethodPermissionFilter http 请求方法转换成相应的动词来构造一个权限字符串,意义不大
      roles RolesAuthorizationFilter 需要指定角色才能访问
      ssl SslFilter 需要https 请求才能访问
      user UserFilter 需要已登录或记住我的用户才能访问
  • 创建自定义realms (认证 、授权)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.coderitl.shiro.realms;

    import com.coderitl.shiro.dao.UserDao;
    import com.coderitl.shiro.entity.User;
    import com.coderitl.shiro.service.UserService;
    import com.coderitl.shiro.service.impl.UserServiceImpl;
    import com.coderitl.shiro.utilos.ApplicationContextUtils;
    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;
    import org.apache.shiro.util.ByteSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.util.ObjectUtils;

    public class CustomerRealm extends AuthorizingRealm {
    // 1. => 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("认证...............");
    String principal = (String) authenticationToken.getPrincipal();
    // 在工厂中获取 service 对象
    UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
    // 获取数据库对象
    User user = userService.findUserByUserName(principal);
    // 设置 md5 + salt + hash
    if (!ObjectUtils.isEmpty(user)) {
    return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
    }
    return null;
    }

    // 2. => 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("授权...............");
    return null;
    }
    }


    • 授权

      • 编程式

        1
        2
        3
        4
        5
        6
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("admin")){
        // 有 admin 角色权限时执行该逻辑
        }else{
        // 无该角色权限
        }
      • 注解式

        1
        2
        3
        4
        5
        6
        7
        8
        9
        // 角色
        @RequireRoles("admin")
        public void hello(){
        // 有 admin角色权限时执行该逻辑
        }

        // 权限字符串
        @RequirePermissions

      • 标签式

        1
        2
        3
        4
        <shiro:hasRole name="admin">
        <!-- 有权限 -->
        </shiro:hasRole>
        注意: Thymeleaf 中使用 shiro 需要额外集成!
    • 编码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 自定义 realm => 假数据模拟 
      // 2. => 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      System.out.println("授权...............");
      // 获取身份信息
      String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
      // 根据主身份信息获取角色 和 权限
      if("coder-itl".equals(primaryPrincipal)){
      SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
      // 添加角色权限
      simpleAuthorizationInfo.addRole("admin");
      return simpleAuthorizationInfo;
      }
      return null;
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <!-- 添加 tag -->
      <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

      <ul>
      <li><a href="#"> 用户管理
      <shiro:hasRole name="admin">
      <li><a href="#"> 商品管理
      <li><a href="#"> 订单管理
      <li><a href="#"> 物流管理
      </shiro:hasRole>
      </ul>

      <!-- 拥有任意角色权限可访问 -->
      <shiro:hasAnyRoles name="user,admin">
      <li><a href="#"> 用户管理
      </shiro:hasAnyRoles>


      • 访问

        受限资源
    • 权限字符串

      1
      2
      3
      4
      5
      // 添加角色权限
      simpleAuthorizationInfo.addRole("user");
      // realm 权限字符串
      simpleAuthorizationInfo.addStringPermission("user:find:*");
      simpleAuthorizationInfo.addStringPermission("user:delete:*");
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <shiro:hasAnyRoles name="user,admin">
      <li>
      <a href="#"> 用户管理
      <ul>
      <shiro:hasPermission name="user:delete:*">
      <li><a href=""> 删除 </a></li>
      </shiro:hasPermission>
      <shiro:hasPermission name="user:update:*">
      <li><a href=""> 修改 </a></li>
      </shiro:hasPermission>
      <shiro:hasPermission name="user:find:*">
      <li><a href=""> 查询 </a></li>
      </shiro:hasPermission>
      </ul>
      </li>
      </shiro:hasAnyRoles>
      • 访问

        权限字符串
  • 创建工具类

    • 获取随机盐

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      package com.coderitl.shiro.utilos;

      import java.util.Random;

      /**
      * 生成随机盐
      */
      public class SaltUtils {
      public static String getSalt(int n) {
      // TODO: 快速转换大小写快捷键 ctrl + shift + U
      char[] chars = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@#$%^&*&()_[];',./|".toCharArray();
      StringBuilder stringBuilder = new StringBuilder();
      for (int i = 0; i < n; i++) {
      char aChar = chars[new Random().nextInt(chars.length)];
      stringBuilder.append(aChar);
      }
      return stringBuilder.toString();
      }
      }

    • 获取实例对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.coderitl.shiro.utilos;

      import org.springframework.beans.BeansException;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.ApplicationContextAware;
      import org.springframework.stereotype.Component;

      @Component
      public class ApplicationContextUtils implements ApplicationContextAware {
      private static ApplicationContext context;

      @Override
      public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
      context = applicationContext;
      }

      // 根据 bean 名字获取工厂中指定 bean 对象
      public static Object getBean(String beanName) {
      return context.getBean(beanName);
      }
      }

  • 数据库实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;
    }

  • dao

    1
    2
    3
    4
    5
    6
    7
    @Mapper
    public interface UserDao {
    void save(User user);

    User findUserByUserName(String username);
    }

  • service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package com.coderitl.shiro.service.impl;

    import com.coderitl.shiro.dao.UserDao;
    import com.coderitl.shiro.entity.User;
    import com.coderitl.shiro.service.UserService;
    import com.coderitl.shiro.utilos.SaltUtils;
    import org.apache.shiro.crypto.hash.Md5Hash;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    @Service("userServiceImpl")
    @Transactional
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public void register(User user) {
    // 生成随机盐
    String salt = SaltUtils.getSalt(8);
    // 将随机盐保存到数据
    user.setSalt(salt);
    // 明文密码加密 + salt + hash
    Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
    user.setPassword(md5Hash.toHex());
    // 处理业务调用 dao
    userDao.save(user);

    }

    @Override
    public User findUserByUserName(String username) {
    return userDao.findUserByUserName(username);

    }
    }

  • 创建控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    package com.coderitl.shiro.controller;

    import com.coderitl.shiro.entity.User;
    import com.coderitl.shiro.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.subject.Subject;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;

    @Slf4j
    @Controller
    @RequestMapping("/user")
    public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/login")
    public String login(String username, String password) {
    log.info("username: [{}],password: [{}]", username, password);
    // 获取主体对象 web 环境会自动注入
    Subject subject = SecurityUtils.getSubject();
    try {
    // 用户认证
    subject.login(new UsernamePasswordToken(username, password));
    log.info("认证状态: {}", subject.isAuthenticated());
    // 重定向到 index.jsp
    return "redirect:/index.jsp";
    } catch (UnknownAccountException e) {
    e.printStackTrace();
    log.info("认证失败: 用户名不存在~");
    } catch (IncorrectCredentialsException e) {
    log.info("认证失败: 密码错误~");
    }
    // 失败回到登录页面
    return "redirect:/login.jsp";
    }

    // 退出
    @RequestMapping("/logout")
    public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/login.jsp";
    }


    @RequestMapping("/register")
    public String register(User user) {
    log.info("user: [{}]", user.toString());
    try {
    userService.register(user);
    return "redirect:/login.jsp";
    } catch (Exception e) {
    e.printStackTrace();
    return "redirect:/register.jsp";
    }
    }
    }

  • 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    server:
    port: 8888
    servlet:
    context-path: /shiro # context-path: 必须 /xxx 开头
    # 配置视图解析器
    spring:
    application:
    name: shiro
    mvc:
    view:
    prefix: / # 前缀
    suffix: .jsp # 后缀
    # 数据源
    datasource:
    druid:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?autoReconnect=true&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    initial-size: 1
    min-idle: 1
    max-active: 20
    mybatis:
    type-aliases-package: com.coderitl.shiro.entity # 实体类所在包名
    mapper-locations: classpath:/mapper/*.xml # mybatis 映射文件所在路径
    # 配置自定义包的日志级别
    logging:
    level:
    com.coderitl.shiro.dao: debug

  • 授权数据持久化

    • 数据库设计

      • 用户 ->角色 ->权限 ->资源(*)

        ER
      • 用户 -> 角色

      • 用户 -> 权限

      • 用户 -> 角色

      • 用户 -> 权限

    • 权限表设计

      权限表设计
    • 授权持久化实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      // 2. => 授权
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      System.out.println("授权...............");
      // 获取身份信息
      String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
      // 根据主身份信息获取角色 和 权限
      // 在工厂中获取 service 对象
      UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
      User user = userService.findRoleUserByUserName(primaryPrincipal);
      // 授权角色信息
      if (!CollectionUtils.isEmpty(user.getRoles())) {
      SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
      user.getRoles().forEach(role -> {
      // 添加角色信息
      simpleAuthorizationInfo.addRole(role.getName());
      // 添加权限信息
      List<Perms> perms = userService.findPermsByRoleId(role.getId());
      if (!CollectionUtils.isEmpty(perms)) {
      perms.forEach(perm -> {
      simpleAuthorizationInfo.addStringPermission(perm.getName());
      });
      }
      });
      return simpleAuthorizationInfo;
      }
      return null;
      }

shiro 整合SpringBoot 缓存之EhCache实现

  • Cache 作用

    • Cache 缓存: 计算机内存中一段数据

    • 作用: 用来减轻DB 的访问压力,从而提高系统的查询效率

    • 流程

      流程
    • 使用shiro 中默认EhCache 实现缓存

      • 引入依赖

        1
        2
        3
        4
        5
        6
        <!--   默认缓存: shiro + EhCache -->
        <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.5.3</version>
        </dependency>
      • 开启缓存,自定义realm 中开启

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        // 开启缓存管理器 
        customerRealm.setCacheManager(new EhCacheManager());
        // 开启全局缓存
        customerRealm.setCachingEnabled(true);
        // 开启认证缓存
        customerRealm.setAuthenticationCachingEnabled(true);
        // 设置认证缓存名称
        customerRealm.setAuthenticationCacheName("authenticationCache");
        // 开启授权缓存
        customerRealm.setAuthorizationCachingEnabled(true);
        // 设置授权缓存名称
        customerRealm.setAuthorizationCacheName("authorizationCache");

整合 Redis

  • 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 实体类添加序列化

  • 缓存管理器

    • 自定义缓存管理器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      package com.coderitl.shiro.cache;

      import lombok.extern.slf4j.Slf4j;
      import org.apache.shiro.cache.Cache;
      import org.apache.shiro.cache.CacheException;
      import org.apache.shiro.cache.CacheManager;

      // 自定义 shiro 缓存管理器
      @Slf4j
      public class RedisCacheManager implements CacheManager {
      @Override
      public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
      log.info("cacheName: [{}]", cacheName);
      return new RedisCache<K, V>(cacheName);
      }
      }

    • 实现Cache

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      package com.coderitl.shiro.cache;


      import com.coderitl.shiro.utilos.ApplicationContextUtils;
      import lombok.extern.slf4j.Slf4j;
      import org.apache.shiro.cache.Cache;
      import org.apache.shiro.cache.CacheException;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.serializer.StringRedisSerializer;

      import java.util.Collection;
      import java.util.Set;

      // 自定义 redis 缓存
      @Slf4j
      public class RedisCache<k, v> implements Cache<k, v> {
      private String cacheName;

      public RedisCache() {
      }

      public RedisCache(String cacheName) {
      this.cacheName = cacheName;
      }


      @Override
      public v get(k k) throws CacheException {
      log.info("redis get use.............");
      log.info("get Key: [{}]", k);
      // return (v) getRedisTemplate().opsForValue().get(k.toString());
      // hash
      return (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
      }

      @Override
      public v put(k k, v v) throws CacheException {
      log.info("redis put use.............");

      log.info("put Key: [{}]", k);
      log.info("put Value: [{}]", v);
      // getRedisTemplate().opsForValue().set(k.toString(), v);
      getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
      return null;
      }

      @Override
      public v remove(k k) throws CacheException {
      log.info("redis remove use.............");
      return (v) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
      }

      @Override
      public void clear() throws CacheException {
      log.info("redis clear use.............");

      // 删除 hash => Map<K,Map<k,v>>
      getRedisTemplate().delete(this.cacheName);

      }

      @Override
      public int size() {
      log.info("redis size use.............");

      // size() => long => intValue => int
      return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
      }

      @Override
      public Set<k> keys() {
      log.info("redis keys use.............");

      return getRedisTemplate().opsForHash().keys(this.cacheName);
      }

      @Override
      public Collection<v> values() {
      log.info("redis values use.............");

      return getRedisTemplate().opsForHash().values(this.cacheName);
      }

      private RedisTemplate getRedisTemplate() {
      RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
      // 序列化 k
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      // hash 的序列化 k
      redisTemplate.setHashKeySerializer(new StringRedisSerializer());

      return redisTemplate;
      }
      }

  • 自定义salt

    1
    2
    3
    4
    5
    6
    7
    8
    // 自定义 salt 实现,实现序列化接口 => 基础使用
    public class MyByteSource extends SimpleByteSource implements Serializable {
    public MyByteSource(String str) {
    super(str);
    }
    }

    // 更换 md5 处的 SourceBytes
  • 更换缓存管理器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 3. 创建自定义 realm
    @Bean
    public Realm getRealm() {
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为 md5
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");
    // 设置散列次数
    hashedCredentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

    // 开启缓存管理器 => redis
    customerRealm.setCacheManager(new RedisCacheManager());
    // 开启全局缓存
    customerRealm.setCachingEnabled(true);
    // 开启认证缓存
    customerRealm.setAuthenticationCachingEnabled(true);
    // 设置认证缓存名称
    customerRealm.setAuthenticationCacheName("authenticationCache");
    // 开启授权缓存
    customerRealm.setAuthorizationCachingEnabled(true);
    // 设置授权缓存名称
    customerRealm.setAuthorizationCacheName("authorizationCache");


    return customerRealm;
    }

  • 标签

    1
    2
    <!-- 获取主体信息 -->
    登录用户: <shiro:principal/>

Thymeleaf 整合Shiro