Shiro 入门笔记,整合SpringBoot,Redis

Shiro 入门笔记

视频地址:https://www.bilibili.com/video/BV1uz4y197Zm
感谢编程不良人的教程

1. 权限管理

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

身份认证

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

授权

即访问控制,控制已经登录的用户能访问那些资源。主体进行身份认证后需要分配权限才可以访问系统的资源,对于某些资源,没有权限是不能访问的。

2. Shiro 简介

apacheshiro(发音为“shee-roh”,日语中是“castle”)是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理,可用于保护任何应用程序,从命令行应用程序,移动应用程序到最大的web和企业应用程序。

  • Apache shiro 是 java 的一个安全(权限)框架
  • Shiro 可以非常容易地开发出足够好的应用环境,其不仅可以用在 JavaSE 环境,也可以在 JavaEE 环境
  • Shiro 可以实现:认证、授权、加密、会话管理、与 Web 集成、缓存等
  • 官网地址:https://shiro.apache.org/

功能简介

  • Authentication:身份认证/登录,验证用户是否拥有相应的的身份。
  • Authorization:授权,即权限验证,验证某个已经认证的用户,是否拥有某个权限;即判断用户是否能进行相关操作,如:验证某个用户是否拥有某个角色,或者细粒度地验证某个用户对某个资源是否具有某个权限。
  • Session Manger:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是 JavaSE 环境,也可以是 Web 环境的。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  • Web Support:Web 支持,可以非常容易集成到 Web 环境;
  • Cacheing:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次都去查询,这样可以提高效率。
  • Concurrency:Shiro 支持多线程应用的并发验证,如在一个线程中开启另一个线程,能把权限自动传播过去。
  • Testing:提供测试支持。
  • Run As:允许一个用户假装另一个用户(如果允许)的身份进行访问;
  • Remember Me:记住我,这是非常常见的功能,即一次登录后,下次再访问的话,不用再次登录。

Shiro 架构

  • 从外部来看 Shiro,也就是应用程序角度的看,如何使用 Shiro 完成工作

    在这里插入图片描述

  • 从 Shiro 内部来看

    在这里插入图片描述

  • Subject:任何可以与应用交互的 “用户”

  • SecurityManager:相当于 SpringMVC 中的 DispatcherServlet,是 Shiro 的心脏,所有的具体交互都通过 SecurityManager 进行控制,它管理着所有的 Subject,并且负责进行认证、授权、会话及缓存的管理

  • Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现,可以使用认证策略(Authentication Strategy),即什么情况下算是用户认证通过了

  • Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能

  • Realm:可以有一个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的,可以是 JDBC 实现,也可以是内存实现等等,由用户提供,所以在一般应用中都需要实现自己的 Realm

  • SessionManager:管理 Session 声明周期的组件,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境

  • CacheManager:缓存控制器,来管理如用户、角色,权限等的缓存的,因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能

  • Cryptography:密码模块,Shiro 提高了一下常见的加密组件,用于如密码的加密/解密

3. Shiro 认证

身份认证,就是判断一个用户是否为合法用户的处理过程

关键对象

  • Subject:主体

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

  • Principal:身份信息

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

  • credential:凭证信息

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

认证流程

在这里插入图片描述

认证案例

案例仓库地址:https://gitee.com/Crater/hello-shiro.git

  1. 引入 Maven 依赖

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.5.3</version>
    </dependency>
    
  2. resources 目录下新建 shiro.ini ,就是 shiro 的配置文件(整合 SpringBoot 和数据库后不需要这个文件)

    用来学习 shiro 的时候,书写系统中相关权限的数据

    [users]
    crater=123456
    ton=123
    jerry=456
    
  3. 测试代码

    // 1.创建安全管理器对象
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    
    // 2.给安全管理器设置 Realm
    //   因为信息保存在 ini 文件里,所以使用 IniRealm,认证时,去 shiro.ini 读取数据
    securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    
    // 3.给全局安全工具类设置安全管理器
    //   SecurityUtils 全局安全工具类
    SecurityUtils.setSecurityManager(securityManager);
    
    // 4.关键对象 Subject 主体
    Subject subject = SecurityUtils.getSubject();
    
    // 5.创建令牌
    UsernamePasswordToken token = new UsernamePasswordToken("crater", "123456");
    
    // 6.用户认证
    System.out.println("认证前——认证状态 >> " + subject.isAuthenticated());
    try {
        subject.login(token);
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("认证失败 >> 用户名不存在~");
    } catch (IncorrectCredentialsException e) {
        e.printStackTrace();
        System.out.println("认证失败 >> 密码错误~");
    }
    System.out.println("认证后——认证状态 >> " + subject.isAuthenticated());
    

认证流程源码

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

    在这里插入图片描述

  1. 最终执行用户比较在 SimpleAccountRealm 中,在 doGetAuthenticationInfo 方法中完成用户名校验。

    需要将用户信息存储在数据库中实现认证,可以模仿 SimpleAccountRealm 继承 AuthorizingRealm ,重写doGetAuthenticationInfo 方法,自定义实现 Realm。

  2. 最终密码校验是在 AuthenticatingRealm 中的 assertCredentialsMatch 方法中实现。

总结

  • AuthenticatingRealm : 认证realm doGetAuthenticationInfo 方法
  • AuthorizingRealm : 授权realm doGetAuthorizationInfo 方法

自定义 Realm

  • 自定义Realm

    /**
     * 自定义 Realm,将认证/授权数据的来源,改为数据库
     */
    public class CustomRealm extends AuthorizingRealm {
    
        /**
         * 授权方法
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
    
        /**
         * 认证方法
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 在 token中获取用户名
            String principal = (String) token.getPrincipal();
            System.out.println("token 中用户名 >> " + principal);
    
            // 在数据库中获取身份信息
            String username = "crater";
            String password = "123456";
            if (username.equals(principal)) {
                // 认证信息
                // 参数1:数据库中用户名
                // 参数2:数据库中密码
                // 参数3:当前 Realm 名字
                return new SimpleAuthenticationInfo(principal, password, this.getName());
            }
            // 返回null:用户名错误
            return null;
        }
    }
    

MD5 和 Salt

MD5 一般用来加密、签名(校验和),算法不可逆,相同的文本无论执行多少次 MD5 加密,生成结果始终一致,一些 MD5 解密工具网站就是把常用的密码加密后,进行穷举。

MD5 生成结果始终是一个 16进制的 32位字符串。

在实际开发中使用 MD5,注册过程需要在业务层使用 MD5 对密码加密。

单独使用 MD5 还有潜在而风险,如被穷举破解,所以需要使用 Salt(盐)配合 MD5减小风险:

  1. 在注册时,对用户的密码加盐,即在密码的任意位置按照加盐规则,添加随机字符,如 123456 加盐后变成 123456X*oq ,然后再使用 MD5 加密。
  2. 在登录时,根据对用户从前台传入的账号密码,再按照相同的加盐规则,对密码加盐,MD5 加密,再与数据库中注册信息对比,进而降低了风险。

MD5+Salt 实现

  • 测试 Md5

    // 使用Md5
    Md5Hash md5Hash = new Md5Hash("123456");
    System.out.println(md5Hash.toHex());	// e10adc3949ba59abbe56e057f20f883e
    
    // 使用Md5 + salt,默认加盐到后面
    Md5Hash md5Hash1 = new Md5Hash("123456", "X0*7ps");
    System.out.println(md5Hash1.toHex());	// e99a0dee78d3c1f71609cead42047675
    
    // 使用Md5 + salt + hash散列,第三个参数为散列程度
    Md5Hash md5Hash2 = new Md5Hash("123456", "X0*7ps", 1024);
    System.out.println(md5Hash2.toHex());	// 955224a95d4161ad8bd84f7ede979c02
    
  • 在 Shiro 中使用

    在认证方法模拟,注册时使用了加密

    /**
     * 认证方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取用户名
        String principal = (String) token.getPrincipal();
    
        // 用户名、据用户名查询到的密码(Md5)
        String username = "crater";
        String password01 = "e10adc3949ba59abbe56e057f20f883e"; // MD5
        String password02 = "e99a0dee78d3c1f71609cead42047675"; // MD5 + salt
        String password03 = "955224a95d4161ad8bd84f7ede979c02"; // MD5 + salt + hash
        if (username.equals(principal)) {
            // 认证信息
            return new SimpleAuthenticationInfo(
                principal,
                password03,
                ByteSource.Util.bytes("X0*7ps"),     // MD5 + salt 需要指明注册时生成密码的随机盐
                this.getName()
            );
        }
        // 返回null:用户名错误
        return null;
    }
    

    给安全管理器设置 Realm 时,Realm使用hash凭证匹配器

    // 2.给安全管理器设置 Realm
    CustomMd5Realm realm = new CustomMd5Realm();
    
    /*
     * 设置Realm使用hash的凭证匹配器
     */
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // 凭证匹配器策略
    credentialsMatcher.setHashAlgorithmName("md5");
    // MD5 + salt + hash 模式需要设置散列数
    credentialsMatcher.setHashIterations(1024);
    
    realm.setCredentialsMatcher(credentialsMatcher);
    securityManager.setRealm(realm);
    
    

4. Shiro 授权

授权

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

关键对象

授权可以简单理解为 who 对 what 进行 how 操作:

  • who:主体(Subject),主体需要访问系统中的资源。
  • what:资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括 资源类型资源实例 ,比如商品信息为资源类型,类型为 t01的商品为资源实例,编号为 001 的 商品也属于资源实例。
  • how:权限/许可(Permission),规定了主体对资源的操作许可, 权限离开资源没有意义 ,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为 001 用户的修改权限等,通过权限可知主体对哪些资源都有哪些许可。

授权流程

在这里插入图片描述

授权方式

  • 基于角色的访问控制

    • RBAC 基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

      if (subject.hasRole("admin")) {
          // 操作什么资源
      }
      
  • 基于资源的访问控制

    • RBAC 基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

      if (subject.hasPermission("user:update:01")) {	// 资源实例
          // 对用户01进行修改
      }
      if (subject.hasPermission("user:update:*")) {	// 资源类型
          // 对用户进行修改
      }
      

权限字符串

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

例子:

  • 用户创建权限:user:create,或 user:create:*
  • 用户修改实例 001 的权限:user:update:001
  • 用户实例 001 的所有权限:user:*:001

Shiro 授权实现

  • 编程式:

    Subject subject = SecurityUtils.getSubject();
    if (subject.hasRole("admin")) {
        // 有权限
    } else {
        // 无权限
    }
    
  • 注解式

    @RequiresRoles("admin")
    public void hello() {
        // 有权限
    }
    
  • 标签式

    JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:
    <shiro:hasRole name="admin">
    	<!-- 有权限 -->
    </shiro:hasRole>
    注意:Thymeleaf 中使用 shiro 需要额外集成
    

代码实例

  • 改写自定义 Realm 中的 doGetAuthorizationInfo 方法

    /**
     * 授权方法
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String principal = (String)principals.getPrimaryPrincipal();
        System.out.println("授权身份信息 >> " + principal);
    
        // 根据身份信息(用户名),获取当前用户的角色信息,以及权限信息
        // 假设 crater 拥有权限 admin、user
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    
        // 将数据库查询的【角色信息】赋予权限对象
        authorizationInfo.addRole("admin");
        authorizationInfo.addRole("user");
    
        // 将数据库查询的【资源信息】赋予权限对象
        authorizationInfo.addStringPermission("user:*:01");
        authorizationInfo.addStringPermission("product:create");
    
        return authorizationInfo;
    }
    
  • 认证后测试授权

    if (subject.isAuthenticated()) {
        System.out.println("===================== 基于角色 ======================");
        // 基于角色权限控制
        System.out.println("是否拥有admin权限 >> " +subject.hasRole("admin"));
    
        // 基于多角色权限控制
        boolean allRoles = subject.hasAllRoles(Arrays.asList("admin", "user"));
        System.out.println("是否拥有admin、user权限 >> " + allRoles);
    
        // 是否拥有其中一个角色
        boolean[] hasRoles = subject.hasRoles(Arrays.asList("admin", "user"));
        System.out.println("是否拥有admin、user其中一个权限 >> " + Arrays.toString(hasRoles));
    
        System.out.println("===================== 基于资源 ======================");
        // 基于资源权限控制
        System.out.println("是否拥有user:*:01权限 >> " + subject.isPermitted("user:*:01"));
    
        // 基于多资源权限控制
        boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create");
        System.out.println("是否拥有user:*:01、product:create权限 >> " + permittedAll);
    
        // 是否拥有其中一个资源
        boolean[] permitted = subject.isPermitted("user:*:01", "product:create:01");
        System.out.println("是否拥有user:*:01、product:create其中一个权限 >> " + Arrays.toString(permitted));
    }
    
  • 输出

    登录成功~
    ===================== 基于角色 ======================
    授权身份信息 >> crater
    是否拥有admin权限 >> true
    授权身份信息 >> crater
    授权身份信息 >> crater
    是否拥有admin、user权限 >> true
    授权身份信息 >> crater
    授权身份信息 >> crater
    是否拥有admin、user其中一个权限 >> [true, true]
    ===================== 基于资源 ======================
    授权身份信息 >> crater
    是否拥有user:*:01权限 >> true
    授权身份信息 >> crater
    授权身份信息 >> crater
    是否拥有user:*:01、product:create权限 >> true
    授权身份信息 >> crater
    授权身份信息 >> crater
    是否拥有user:*:01、product:create其中一个权限 >> [true, true]
    
    Process finished with exit code 0
    
    

5. 整合 SpringBoot

整合思路

在这里插入图片描述

配置 Shiro 环境

  1. 搭建 SpringBoot,并引入 shiro 依赖

    仓库地址:https://gitee.com/Crater/hello-shiro.git

    在这里插入图片描述

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</artifactId>
        <version>1.5.3</version>
    </dependency>
    

    访问 http://127.0.0.1:8080/shiro/index.jsp,可以看到主页

  2. 搭建 shiro 配置和自定义 Realm

    @Configuration
    public class ShiroConfig {
        // 1.创建shiroFilter
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    
            // 给Filter设置安全管理器
            shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    
            // 配置系统受限资源
            Map<String, String> map = new HashMap<>();
            map.put("/index.jsp", "authc");     // authc:请求这个资源需要认证和授权
            shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    
            // 默认认证界面,就算是不设置,未授权的请求也会重定向到 login.jsp
            shiroFilterFactoryBean.setLoginUrl("/login.jsp");
            return shiroFilterFactoryBean;
        }
    
        // 2.创建Web安全管理器
        @Bean
        public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
            DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    
            // 给安全管理器设置 Realm
            defaultSecurityManager.setRealm(realm);
    
            return defaultSecurityManager;
        }
    
        // 3.创建自定义realm
        @Bean(name = "realm")
        public Realm getRealm() {
            return new CustomRealm();
        }
    }
    
    public class CustomRealm extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            return null;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            return null;
        }
    }
    
  3. 再次访问 http://127.0.0.1:8080/shiro/login.jsp,因为未经授权,重定向到 login.jsp 页面

常见过滤器

shiro 提供多个默认的过滤器,可以使用这些过滤器来配置控制指定url的权限:

前两个用的比较多。

配置缩写对应的过滤器功能
anonAnonymousFilter指定url可以匿名访问
authcFromAuthenticationFilter指定url需要from表单登录,默认会从请求中获取username、password , rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做,录不了就会跳转到loginUrl配置的路径.我们也可以用这个过滤器做默认的登录逻辑,但是-般都是我们自己在控制器写登录逻辑的,默认的登录逻辑,但是-般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url,就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionAuthenticationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构建一个权限字符,感觉这个意义不大,有兴趣自己看源码
rolesRolesAuthenticationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要以登录或“记住我”用户才能访问

认证

表单认证退出

实现登录认证。过滤除登录之外所有请求,并重定向到登录页面。

  • 登录页面

    <body>
        <h1>登录页面</h1>
        <form action="${pageContext.request.contextPath}/user/login" method="post">
            账号:<input type="text" name="username"><br/>
            密码:<input type="text" name="password"><br/>
            <input type="submit" value="登录">
        </form>
    </body>
    
  • 系统主页

    <body>
        <h1>系统主页V1.0</h1>
        <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
        <ul>
            <li><a href="">用户管理</a></li>
            <li><a href="">商品管理</a></li>
            <li><a href="">订单管理</a></li>
            <li><a href="">物流管理</a></li>
        </ul>
    </body>
    
  • shiro 配置类

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    
        // 给Filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    
        // 配置认证和授权规则
        // 配置系统公共资源
        // 配置系统受限资源
        Map<String, String> map = new HashMap<>();
        map.put("/user/login", "anon");      // anon:设置为公共资源
        map.put("/**", "authc");             // authc:请求这个资源需要认证和授权,使用通配符过滤所有
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    
        // 默认认证界面,就是不设置,未授权的请求也会重定向到 login.jsp
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        return shiroFilterFactoryBean;
    }
    
注册(连接数据库)
  • 注册页面

    <body>
        <h1>注册页面</h1>
        <form action="${pageContext.request.contextPath}/user/register" method="post">
            账号:<input type="text" name="username"><br/>
            密码:<input type="text" name="password"><br/>
            <input type="submit" value="立即注册">
        </form>
    </body>
    
  • 新建数据库

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for t_user
    -- ----------------------------
    DROP TABLE IF EXISTS `t_user`;
    CREATE TABLE `t_user`  (
      `id` int(6) NOT NULL AUTO_INCREMENT,
      `username` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `password` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
  • 引入依赖

    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.22</version>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.2</version>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.11</version>
    </dependency>
    
  • 修改配置文件

    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8&serverTimezone=GMT%2b8&useSSL=false&failOverReadOnly=false
    spring.datasource.username=root
    spring.datasource.password=123456
    
    mybatis.type-aliases-package=com.crater.entity
    mybatis.mapper-locations=classpath:/mapper/*.xml
    
  • 随机盐工具类

    /**
     * 生成salt
     */
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            sb.append(c);
        }
        return sb.toString();
    }
    
  • 编写业务层

    @Override
    public void register(User user) {
        // 1.生成随机盐并保存
        String salt = SaltUtils.getSalt(8);
        user.setSalt(salt);
        // 2.明文密码进行 md5 + salt + hash散列
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
        user.setPassword(md5Hash.toHex());
        userDao.save(user);
    }
    
  • 编写 controller

    /**
     * 注册
     */
    @RequestMapping("/register")
    public String register(User user) {
        try {
            userService.register(user);
            return "redirect:/login.jsp";
        } catch (Exception e) {
            e.printStackTrace();
            return "redirect:/register.jsp";
        }
    }
    
  • 测试注册,注册成功,跳转到登录页面,数据库也存储了用户信息

认证(连接数据库)
  • 修改自定义 CustomRealm 中的认证方法

    // 获取身份信息
    String principal = (String) authenticationToken.getPrincipal();
    // 获取service对象
    UserService userService = SpringUtils.getBean("userService", UserService.class);
    User user = userService.findByUserName(principal);
    
    if (!ObjectUtils.isEmpty(user)) {
        return new SimpleAuthenticationInfo(
            user.getUsername(),
            user.getPassword(),
            ByteSource.Util.bytes(user.getSalt()),
            this.getName());
    }
    return null;
    
  • 修改 ShiroConfig 中 getRealm 的规则

    // 3.创建自定义realm
    @Bean(name = "realm")
    public Realm getRealm() {
        CustomRealm customRealm = new CustomRealm();
        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        customRealm.setCredentialsMatcher(credentialsMatcher);
        return customRealm;
    }
    
  • 测试成功,实现了基于MD5 + salt 认证!

授权

角色授权
  • 改写自定义 Realm 下的 doGetAuthorizationInfo 方法

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("调用授权验证 >> " + primaryPrincipal);
        if ("crater".equals(primaryPrincipal)) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addRole("user");
            return simpleAuthorizationInfo;
        }
        return null;
    }
    
  • 改写 index.jsp 页面

    <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
    ... ...
    <body>
        <h1>系统主页V1.0</h1>
        <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
        <ul>
            <shiro:hasAnyRoles name="user,admin">
                <li><a href="">用户管理</a></li>
            </shiro:hasAnyRoles>
            <shiro:hasRole name="admin">
                <li><a href="">商品管理</a></li>
                <li><a href="">订单管理</a></li>
                <li><a href="">物流管理</a></li>
            </shiro:hasRole>
        </ul>
    </body>
    
  • 测试,只有拥有相应的角色,才能看到相应的资源,user 角色只能看到用户管理

    在这里插入图片描述

权限授权
  • 改写自定义 Realm 下的 doGetAuthorizationInfo 方法,赋予权限

    if ("crater".equals(primaryPrincipal)) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole("user");
    
        simpleAuthorizationInfo.addStringPermission("user:find:*");
        simpleAuthorizationInfo.addStringPermission("user:update:*");
    
        return simpleAuthorizationInfo;
    }
    
  • 改写 index.jsp 页面,CURD都需要不同的权限才可以看到

    <shiro:hasAnyRoles name="user,admin">
        <li><a href="">用户管理</a>
            <ul>
                <shiro:hasPermission name="user:add:*">
                    <li>添加</li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:delete:*">
                    <li>删除</li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:update:*">
                    <li>修改</li>
                </shiro:hasPermission>
                <shiro:hasPermission name="user:find:*">
                    <li>查询</li>
                </shiro:hasPermission>
            </ul>
        </li>
    </shiro:hasAnyRoles>
    
  • 测试

    在这里插入图片描述

  • 在后端判断当前主体的权限,有两种方式

    // 1. 获取主体,进行判断
    Subject subject = SecurityUtils.getSubject();
    boolean hasRole = subject.hasRole("admin");
    boolean permitted = subject.isPermitted("user:find:*");
    
    // 2. 注解的方式
    @RequiresRoles("admin")
    @RequiresPermissions("user:find:*")
    
角色授权(连接数据库)
  • 数据库设计:用户 >> 角色 >> 权限 >> 资源

    在这里插入图片描述

    搭建数据库并初始化

    
    -- ----------------------------
    -- Table structure for t_perms
    -- ----------------------------
    DROP TABLE IF EXISTS `t_perms`;
    CREATE TABLE `t_perms`  (
      `id` int(6) NOT NULL AUTO_INCREMENT,
      `name` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Table structure for t_role
    -- ----------------------------
    DROP TABLE IF EXISTS `t_role`;
    CREATE TABLE `t_role`  (
      `id` int(6) NOT NULL AUTO_INCREMENT,
      `name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Table structure for t_role_perms
    -- ----------------------------
    DROP TABLE IF EXISTS `t_role_perms`;
    CREATE TABLE `t_role_perms`  (
      `id` int(6) NOT NULL AUTO_INCREMENT,
      `roleid` int(6) NULL DEFAULT NULL,
      `permsid` int(6) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Table structure for t_user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `t_user_role`;
    CREATE TABLE `t_user_role`  (
      `id` int(6) NOT NULL AUTO_INCREMENT,
      `userid` int(6) NULL DEFAULT NULL,
      `roleid` int(6) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    
  • 编写 t_role 和 t_perms 的数据库查询

    public class Role {
        private Integer id;
        private String name;
    }
    

    改写 User 实体类

    public class User {
        private String id;
        private String username;
        private String password;
        private String salt;
    
        // 角色集合
        private List<Role> roles;
    }
    

    编写sql

    <resultMap id="userMap" type="User">
        <id column="uid" property="id"/>
        <result column="username" property="username"/>
        <collection property="roles" javaType="list" ofType="Role">
            <id column="id" property="id"/>
            <result column="name" property="name"/>
        </collection>
    </resultMap>
    
    <select id="findRolesByUsername" parameterType="String" resultMap="userMap">
        select u.id as uid,u.username,r.id,r.name
        from t_user u
        left join t_user_role ur
        on u.id = ur.userid
        left join t_role r
        on ur.roleid = r.id
        where u.username=#{username}
    </select>
    
  • 改写授权过程

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取身份信息
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("调用授权验证 >> " + primaryPrincipal);
        // 获取角色
        UserService userService = SpringUtils.getBean("userService", UserService.class);
        List<Role> roles = userService.findRolesByUsername(primaryPrincipal).getRoles();
        System.out.println(roles);
    
        if (!CollectionUtils.isEmpty(roles)) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role -> {
                simpleAuthorizationInfo.addRole(role.getName());
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }
    
  • 测试结果,具有 admin 角色可以看到所有资源,具有 user 角色只可以看到 用户管理

权限授权(连接数据库)
  • 新建 Perms 实体

    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class Perms {
        private Integer id;
        private String name;
        private String url;
    }
    
  • 改写Role实体

    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class Role {
        private Integer id;
        private String name;
    
        private List<Perms> perms;
    }
    
  • 权限数据库初始化

    数据库中一共3个权限:user:*:*,product:*:01,order:*.*
    admin角色拥有所有权限
    user角色拥有user:*:*
    product角色拥有order:*.*
    
  • 编写SQL

    <select id="findPermsByRoleId" parameterType="java.lang.Integer" resultType="Perms">
        select p.*
        from t_role r
        left join t_role_perms rp
        on r.id = rp.roleid
        left join t_perms p
        on rp.permsid = p.id
        where r.id = #{id}
    </select>
    
  • 改写认证,添加对权限的操作

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
        // 获取身份信息
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("调用授权验证 >> " + primaryPrincipal);
        // 获取角色
        UserService userService = SpringUtils.getBean("userService", UserService.class);
        List<Role> roles = userService.findRolesByUsername(primaryPrincipal).getRoles();
    
        // 授权角色信息
        if (!CollectionUtils.isEmpty(roles)) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.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;
    }
    
  • 测试,只有拥有admin角色的admin用户可以看到所有资源,其他用户只能看到部分资源

6. CacheManager

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

  • 流程:

    在这里插入图片描述

EhCache实现缓存

使用shiro中默认的EhCache实现缓存

  1. 引入依赖

    <!-- EnCache -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.5.3</version>
    </dependency>
    
  2. 在自定义 Realm中开启缓存管理

    // 3.创建自定义realm
    @Bean(name = "realm")
    public Realm getRealm() {
        CustomRealm customRealm = new CustomRealm();
        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        customRealm.setCredentialsMatcher(credentialsMatcher);
    
        // 开启缓存管理
        customRealm.setCacheManager(new EhCacheManager());
        customRealm.setCachingEnabled(true);                            // 开启全局缓存
        customRealm.setAuthenticationCachingEnabled(true);              // 开启认证缓存
        customRealm.setAuthenticationCacheName("authenticationCache");
        customRealm.setAuthorizationCachingEnabled(true);               // 开启授权缓存
        customRealm.setAuthorizationCacheName("authorizationCache");
    
        return customRealm;
    }
    
  3. 测试,登录后刷新,除了第一次请求数据库,之后不再请求

Redis实现缓存

  1. 引入依赖

    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 配置 redis 连接

    spring.redis.port=6379
    spring.redis.host=localhost
    spring.redis.database=0
    
  3. 自定义shiro缓存管理器

    /**
     * 自定义shiro缓存管理器
     */
    public class RedisCacheManager implements CacheManager {
        /**
         * @param s 缓存统一名称
         * @return Redis缓存
         */
        @Override
        public <K, V> Cache<K, V> getCache(String s) throws CacheException {
            return new RedisCache<K, V>();
        }
    }
    
  4. 自定义redis缓存的实现

    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 {
            System.out.println("get key:" + k);
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.setStringSerializer(new StringRedisSerializer());
            return (V) redisTemplate.opsForHash().get(this.cacheName, k.toString());
        }
    
        @Override
        public V put(K k, V v) throws CacheException {
            System.out.println("put key:" + k);
            System.out.println("put val:" + v);
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.setStringSerializer(new StringRedisSerializer());
            redisTemplate.opsForHash().put(this.cacheName, k.toString(), v);
            return null;
        }
    
        ... ...
    
        private RedisTemplate getRedisTemplate(){
            RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class);
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
    
  5. 测试报错

    在这里插入图片描述

    由于shiro中提供的simpleByteSource实现没有实现序列化,所有在认证时出现错误信息,需要自动salt实现序列化

  6. 自动salt实现序列化

    package com.crater.shiro.salt;
    
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.codec.CodecSupport;
    import org.apache.shiro.codec.Hex;
    import org.apache.shiro.util.ByteSource;
    
    import java.io.File;
    import java.io.InputStream;
    import java.io.Serializable;
    import java.util.Arrays;
    
    //自定义salt实现 实现序列化接口
    public class MyByteSource implements ByteSource, Serializable {
        private byte[] bytes;
        private String cachedHex;
        private String cachedBase64;
    
        public MyByteSource() {
    
        }
    
        public MyByteSource(byte[] bytes) {
            this.bytes = bytes;
        }
    
        public MyByteSource(char[] chars) {
            this.bytes = CodecSupport.toBytes(chars);
        }
    
        public MyByteSource(String string) {
            this.bytes = CodecSupport.toBytes(string);
        }
    
        public MyByteSource(ByteSource source) {
            this.bytes = source.getBytes();
        }
    
        public MyByteSource(File file) {
            this.bytes = (new com.crater.shiro.salt.MyByteSource.BytesHelper()).getBytes(file);
        }
    
        public MyByteSource(InputStream stream) {
            this.bytes = (new com.crater.shiro.salt.MyByteSource.BytesHelper()).getBytes(stream);
        }
    
        public static boolean isCompatible(Object o) {
            return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
        }
    
        public byte[] getBytes() {
            return this.bytes;
        }
    
        public boolean isEmpty() {
            return this.bytes == null || this.bytes.length == 0;
        }
    
        public String toHex() {
            if (this.cachedHex == null) {
                this.cachedHex = Hex.encodeToString(this.getBytes());
            }
    
            return this.cachedHex;
        }
    
        public String toBase64() {
            if (this.cachedBase64 == null) {
                this.cachedBase64 = Base64.encodeToString(this.getBytes());
            }
    
            return this.cachedBase64;
        }
    
        public String toString() {
            return this.toBase64();
        }
    
        public int hashCode() {
            return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
        }
    
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            } else if (o instanceof ByteSource) {
                ByteSource bs = (ByteSource) o;
                return Arrays.equals(this.getBytes(), bs.getBytes());
            } else {
                return false;
            }
        }
    
        private static final class BytesHelper extends CodecSupport {
            private BytesHelper() {
            }
    
            public byte[] getBytes(File file) {
                return this.toBytes(file);
            }
    
            public byte[] getBytes(InputStream stream) {
                return this.toBytes(stream);
            }
        }
    }
    
  7. 在realm中使用自定义salt

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
        // 获取身份信息
        String principal = (String) authenticationToken.getPrincipal();
    
        // 获取service对象
        UserService userService = SpringUtils.getBean("userService", UserService.class);
        User user = userService.findByUserName(principal);
    
        if (!ObjectUtils.isEmpty(user)) {
            return new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                new MyByteSource(user.getSalt()),
                this.getName());
        }
    
        return null;
    }
    
  8. 测试又报错,看第6步骤

    org.apache.shiro.authc.SimpleAuthenticationInfo cannot be cast to org.apache.shiro.authz.AuthorizationInfo
    
    AuthenticationInfo(认证)被转换成AuthorizationInfo(授权)了
    根本原因是先往redis里面放入认证的数据,后来授权的数据把认证的数据给覆盖了(key相同),取出来的时候就会报这个转换的错误。
    
    解决方案:
    redisTemplate.opsForHash().get(this.cacheName, k.toString());
    redisTemplate.opsForHash().put(this.cacheName, k.toString(), v);
    
  9. 测试,成功

  10. 其他的方法

    @Override
    public V remove(K k) throws CacheException {
        // 退出时调用
        System.out.println("=============remove=============");
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }
    
    @Override
    public void clear() throws CacheException {
        System.out.println("=============clear==============");
        getRedisTemplate().delete(this.cacheName);
    }
    
    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }
    
    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }
    
    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值