云小杰

相对于绝对成功的汲汲渴求,越无杂质的奔赴,越是动人。

要一个黄昏, 满是风, 和正在落下的夕阳。如此, 足够我爱这破碎泥泞的人间。


Download the theme

学习shiro

Shiro

1. 权限的管理

1.1 什么是权限管理

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

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

1.2 什么是身份认证

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

1.3 什么是授权

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

2. 什么是Shiro

​ Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

​ Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

3. Shiro的核心架构

Shiro Architecture Diagram

3.1 Subject

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

3.2 SecurityManager

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

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

3.3 Authenticator

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

3.4 Authorizer

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

3.5 Realm

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

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

3.6 SessionManager

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

3.7 SessionDAO

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

3.8 CacheManager

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

3.9 Cryptography

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

4. Shiro认证

4.1 认证

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

4.2 Shiro中认证的关键对象

  • Subject:主体
    • 访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
  • Principal:身份信息
    • 是主体(subject)进行身份认证的标识,标识必须具有 唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
  • credential:凭证信息
    • 是只有主体自己知道的安全信息,如密码、证书等。

4.3 认证流程

img

4.4 认证的开发

4.4.1 创建项目并引入依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.5.3</version>
        </dependency>

4.4.2 引入Shiro配置文件并加入以下配置

配置文件:名称随意,以 .ini 结尾,放在 resources 目录下

注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手,可以把权限相关的数据写在 .ini 文件中。

[users]
zhangsan=123456
lisi=456789

4.4.3 开发认证代码

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.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

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

4.5 常见的异常类型

  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)等

4.6 源码分析与自定义Realm

**
认证:
1. 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
2. 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法 中
****
总结:
> AuthenticatingRealm 认证  realm doGetAuthenticationInf
> AuthorizingRealm    授权  realm doGetAuthorizationInfo
**

上边的程序使用的是 Shiro 自带的 IniRealmIniRealmini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 realm

  1. 源码分析

img

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

img

SimpleAccountRealm的部分源码中有两个方法,一个是认证 一个是 授权:

public class SimpleAccountRealm extends AuthorizingRealm {
		//.......省略
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {

            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }

        }

        return account;
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
}
  1. 自定义Realm

自定义 Realm 的作用:放弃使用 .ini 文件,使用数据库查询。

package 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;

/**
 * 自定义Realm实现 将认证/授权数据得来源转为数据库的实现
 */
public class customerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认账
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 从token中获取用户名
        String principal = (String) authenticationToken.getPrincipal();
        System.out.println("=====================================");
        System.out.println(principal);
        System.out.println("=====================================");

        //实际开发中应当 根据身份信息使用jdbc mybatis查询相关数据库
        //在这里只做简单的演示
        //假设username,password是从数据库获得的信息
        String username = "zhangsan";
        String password = "123456";
        if (username.equals(principal)) {
            //参数1:返回数据库中正确的用户名
            //参数2:返回数据库中正确密码
            //参数3:提供当前realm的名字 this.getName();
            SimpleAuthenticationInfo simpleAuthenticationInfo =
                    new SimpleAuthenticationInfo(principal, password, this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}
  1. 使用自定义 Realm 认证
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;
import realm.customerRealm;

/**
 * 测试自定义的Realm
 */
public class testCustomerRealm {
    public static void main(String[] args) {
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(new customerRealm());
        SecurityUtils.setSecurityManager(manager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
        try {
            subject.login(token);//用户登录
            System.out.println("登录成功~~");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误!!!");
        }
    }
}
=====================================
zhangsan
=====================================
登录成功~~

4.7 使用MD5+Salt

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

补充:MD5算法

作用:一般用来加密或者签名(校验和)

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

网络上提供的MD5在线解密一般是用穷举的方法

生成结果:始终是一个16进制32位长度字符串

4.7.0 Shiro中的md5加密

import org.apache.shiro.crypto.hash.Md5Hash;

public class testShiroMD5 {
    public static void main(String[] args) {

        // 使用md5算法
        Md5Hash md5Hash = new Md5Hash("123");
        System.out.println(md5Hash.toHex()); // 202cb962ac59075b964b07152d234b70

        // 使用md5 + Salt
        Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
        System.out.println(md5Hash1.toHex()); // 8a83592a02263bfe6752b2b5b03a4799

        // //使用md5 + salt + hash散列(参数代表要散列多少次,一般是 1024或2048)
        Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
        System.out.println(md5Hash2.toHex()); // e4f9bf3e0c58f045e62c23c533fcf633
    }
}
import org.apache.shiro.crypto.hash.Md5Hash;

public class testShiroMD5 {
    public static void main(String[] args) {

        // 创建md5算法
//        Md5Hash md5Hash = new Md5Hash();
//        md5Hash.setBytes("123".getBytes());
//        String s = md5Hash.toHex();
//        System.out.println(s); // 313233

        // 使用md5算法
        Md5Hash md5Hash = new Md5Hash("123");
        System.out.println(md5Hash.toHex()); // 202cb962ac59075b964b07152d234b70

        // 使用md5 + Salt
        Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
        System.out.println(md5Hash1.toHex()); // 8a83592a02263bfe6752b2b5b03a4799

        // //使用md5 + salt + hash散列(参数代表要散列多少次,一般是 1024或2048)
        Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
        System.out.println(md5Hash2.toHex()); // e4f9bf3e0c58f045e62c23c533fcf633
    }
}

202cb962ac59075b964b07152d234b70
8a83592a02263bfe6752b2b5b03a4799
e4f9bf3e0c58f045e62c23c533fcf633

4.7.1 自定义md5+salt的realm

package 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;

/**
 * 使用自定义realm 加入md5 + salt +hash
 */
public class customerMd5Realm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户信息
        String principal = (String) authenticationToken.getPrincipal();
        //假设这是从数据库查询到的信息
        String username="zhangsan";
        String password="7268f6d32ec8d6f4c305ae92395b00e8";//加密后

        //根据用户名查询数据库
        if (username.equals(principal)) {
            //参数1:数据库用户名
            //参数2:数据库md5+salt之后的密码
            //参数3:注册时的随机盐
            //参数4:realm的名字
            return new SimpleAuthenticationInfo(principal,
                    password,
                    ByteSource.Util.bytes("@#$*&QU7O0!"),
                    this.getName());
        }
        return null;
    }
}

测试

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;
import realm.customerMd5Realm;

public class testCustomerMd5Realm {
    public static void main(String[] args) {
        //1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        //2.注入realm
        customerMd5Realm realm = new customerMd5Realm();

        //3.设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);

        //4.将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        //5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();

        //6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");

        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

5. Shiro授权

5.1 授权

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

5.2 关键对象

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

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

5.3 授权流程

img

5.4 授权方式

  • 基于角色的访问控制

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

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

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

    • if(subject.isPermission("user:update:01")){ //资源实例
        //对资源01用户具有修改的权限
      }
      if(subject.isPermission("user:update:*")){  //资源类型
        //对 所有的资源 用户具有更新的权限
      }
      

5.5 权限字符串

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

例如:

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

5.6 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需要额外集成!
      

5.7 开发授权

5.7.1 开发Realm

package 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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

/**
 * 使用自定义realm 加入md5 + salt +hash
 */
public class customerMd5Realm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("身份信息: " + primaryPrincipal); //用户名

        //根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //假设 admin,user 是从数据库查到的 角色信息
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        //假设 ... 是从数据库查到的 权限信息赋值给权限对象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("prodect:*");//第三个参数为*省略

        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户信息
        String principal = (String) authenticationToken.getPrincipal();
        //假设这是从数据库查询到的信息
        String username = "zhangsan";
        String password = "7268f6d32ec8d6f4c305ae92395b00e8";//加密后

        //根据用户名查询数据库
        if (username.equals(principal)) {
            //参数1:数据库用户名
            //参数2:数据库md5+salt之后的密码
            //参数3:注册时的随机盐
            //参数4:realm的名字
            return new SimpleAuthenticationInfo(principal,
                    password,
                    ByteSource.Util.bytes("@#$*&QU7O0!"),
                    this.getName());
        }
        return null;
    }
}

5.7.2 判断权限

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;
import realm.customerMd5Realm;

import java.util.Arrays;

public class testCustomerMd5Realm {
    public static void main(String[] args) {
        //1.创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

        //2.注入realm
        customerMd5Realm realm = new customerMd5Realm();

        //3.设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        defaultSecurityManager.setRealm(realm);

        //4.将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);

        //5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();

        //6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");

        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }

        //授权
        if (subject.isAuthenticated()){
            //基于角色权限控制
            System.out.println(subject.hasRole("admin"));
            //基于多角色的权限控制
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));//true
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "manager")));//false
            //是否具有其中一个角色
            boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "manager"));
            for (boolean aBoolean : booleans) {
                System.out.println(aBoolean);
            }

            System.out.println("====这是一个分隔符====");

            //基于权限字符串的访问控制  资源标识符:操作:资源类型
            //用户具有的权限 user:*:01  prodect:*
            System.out.println("权限:"+subject.isPermitted("user:update:01"));
            System.out.println("权限:"+subject.isPermitted("prodect:update:02"));

            //分别具有哪些权限
            boolean[] permitted = subject.isPermitted("user:*:01", "user:update:02");
            for (boolean b : permitted) {
                System.out.println(b);
            }

            //同时具有哪些权限
            boolean permittedAll = subject.isPermittedAll("prodect:*:01", "prodect:update:03");
            System.out.println(permittedAll);
        }
    }
}

6. SpringBoot实战

6.1 整合思路

img

6.2 环境搭建

6.2.1 新建springboot项目

image-20220318164906797

6.2.2 添加依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

6.2.3 简单使用

  1. 配置类

    • package com.lyj.springboot_jsp_shiro.config;
           
      import com.lyj.springboot_jsp_shiro.shiro.CustomerRealm;
      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;
           
      /**
       * 整合shiro框架相关的配置类
       */
      @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", "authc");
           
              // 设置默认的认证界面
              shiroFilterFactoryBean.setLoginUrl("/login"); //authc 请求这个资源需要认证和授权
           
              shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
              return shiroFilterFactoryBean;
          }
           
          // 2. 创建安全管理器
          @Bean
          public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
              DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
              //给安全管理器设置
              defaultWebSecurityManager.setRealm(realm);
           
              return defaultWebSecurityManager;
          }
           
          // 3. 自定义realm
          @Bean
          public Realm getRealm() {
              CustomerRealm customerRealm = new CustomerRealm();
              return customerRealm;
          }
      }
      
  2. 自定义 realm

    • package com.lyj.springboot_jsp_shiro.shiro;
           
      import org.apache.shiro.authc.AuthenticationException;
      import org.apache.shiro.authc.AuthenticationInfo;
      import org.apache.shiro.authc.AuthenticationToken;
      import org.apache.shiro.authz.AuthorizationInfo;
      import org.apache.shiro.realm.AuthorizingRealm;
      import org.apache.shiro.subject.PrincipalCollection;
           
      public class CustomerRealm extends AuthorizingRealm {
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              return null;
          }
           
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
              return null;
          }
      }
      
  3. 测试

    • 输入 http://localhost:8080/index的时候因为没有认证,自动跳转到登录页面
    • image-20220318173933440

6.3 常见过滤器

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

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

6.4 认证和退出

6.4.1 登录

UserController

package com.lyj.springboot_jsp_shiro.controller;

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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {

    @RequestMapping("login")
    public String login(String username, String password) {
        // 获取主体对象
        Subject subject = SecurityUtils.getSubject();
        try{
            subject.login(new UsernamePasswordToken(username, password));
            return "index";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误!");
        }
        return "login";
    }
}

CustomerRealm

package com.lyj.springboot_jsp_shiro.shiro;

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 CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("=================授权================");
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("=================认证================");
        String principal = (String) token.getPrincipal();
        if ("xiaochen".equals(principal)) {
            return new SimpleAuthenticationInfo(principal,
                    "123",
                    this.getName());
        }
        return null;
    }
}

image-20220318182658912

6.4.2 退出

UserController

package com.lyj.springboot_jsp_shiro.controller;

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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {
    // 登录省略
    /**
     * 退出登录
     * @return
     */
    @RequestMapping("logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();;
        return "login";
    }
}

6.5 MD5+Salt的注册

  1. 添加依赖

    •         <!--mysql-->
              <dependency>
                  <groupId>mysql</groupId>
                  <artifactId>mysql-connector-java</artifactId>
                  <version>8.0.26</version>
              </dependency>
              <!--druid-->
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid</artifactId>
                  <version>1.1.19</version>
              </dependency>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
              </dependency>
      
  2. 修改配置

    • server.port=8080
      #server.servlet.context-path=/shiro
      #spring.application.name=shiro
      #
      #spring.mvc.view.prefix=/WEB-INF/jsp/
      #spring.mvc.view.suffix=.jsp
      spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
      spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
      spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      spring.datasource.username=root
      spring.datasource.password=7012+2
           
      mybatis.type-aliases-package=com.lyj.springboot_jsp_shiro.entity
      mybatis.mapper-locations=classpath:mapper/*.xml
      
  3. 新建数据库

    • 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) DEFAULT NULL,
        `password` varchar(40) DEFAULT NULL,
        `salt` varchar(255) DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
           
      SET FOREIGN_KEY_CHECKS = 1;
      
  4. 新建实体类

    • package com.lyj.springboot_jsp_shiro.entity;
           
      import lombok.Data;
           
      @Data
      public class User {
          private Integer id;
          private String username;
          private String password;
          private String salt;
      }
      
  5. 创建 DAO 接口

    • @Mapper
      public interface UserDAO {
          void save(User user);
      }
      
  6. 开发mapper配置文件

    • <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
           
      <mapper namespace="com.lyj.springboot_jsp_shiro.dao.UserDAO">
           
          <insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
              insert into t_user
              values (#{id}, #{username}, #{password}, #{salt})
          </insert>
      </mapper>
      
  7. 开发 service 接口

    • package com.lut.service;
           
      import com.lut.entity.User;
           
      public interface UserService {
          //注册用户方法
          void register(User user);
      }
      
  8. 创建 salt 工具类

    • package com.lyj.springboot_jsp_shiro.utils;
           
      import java.util.Random;
           
      public class SaltUtils {
           
          /**
           * 生成salt静态方法
           */
          public static String getSalt(int n) {
              char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < n; i++) {
                  char aChar = chars[new Random().nextInt(chars.length)];
                  sb.append(aChar);
              }
              return sb.toString();
          }
      }
      
  9. 开发 service 实现类

    • package com.lyj.springboot_jsp_shiro.service;
           
      import com.lyj.springboot_jsp_shiro.dao.UserDAO;
      import com.lyj.springboot_jsp_shiro.entity.User;
      import com.lyj.springboot_jsp_shiro.utils.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
      @Transactional
      public class UserServiceImpl implements UserService{
           
          @Autowired
          private UserDAO userDAO;
           
          @Override
          public void register(User user) {
              //处理业务调用dao
              //1.生成随机盐
              String salt = SaltUtils.getSalt(8);
              //2.将随机盐保存到数据
              user.setSalt(salt);
              //3.明文密码进行md5 + salt + hash散列
              Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
              user.setPassword(md5Hash.toHex());
              userDAO.save(user);
          }
      }
      
  10. 开发 controller 类

    • package com.lyj.springboot_jsp_shiro.controller;
            
      import com.lyj.springboot_jsp_shiro.entity.User;
      import com.lyj.springboot_jsp_shiro.service.UserService;
      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;
            
      @Controller
      @RequestMapping("user")
      public class UserController {
            
          @Autowired
          UserService userService;
          /**
           * 注册
           */
          @RequestMapping("register")
          public String register(User user) {
              try {
                  userService.register(user);
                  return "redirect:login";
              }catch (Exception e){
                  e.printStackTrace();
                  return "register";
              }
          }
      }
      

image-20220318214341304

6.6 MD5+Salt的登录

  1. 开发 DAO

    • @Mapper
      public interface UserDAO {
          void save(User user);
           
          User findByUserName(String username);
      }
      
  2. 开发 mapper 配置文件

    •     <select id="findByUserName" parameterType="String" resultType="User">
              select id, username, password, salt
              from t_user
              where username = #{username}
          </select>
      
  3. 开发 service 接口

    •     // 根据用户名查询业务
          User findByUserName(String username);
      
  4. service 实现类

    •     @Override
          public User findByUserName(String username) {
              User user = userDAO.findByUserName(username);
              return user;
          }
      
  5. 修改自定义realm

    • package com.lyj.springboot_jsp_shiro.shiro;
           
      import com.lyj.springboot_jsp_shiro.entity.User;
      import com.lyj.springboot_jsp_shiro.service.UserService;
      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;
           
      public class CustomerRealm extends AuthorizingRealm {
           
          @Autowired
          UserService userService;
           
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              System.out.println("=================授权================");
              return null;
          }
           
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
              System.out.println("=================认证================");
              String principal = (String) token.getPrincipal();
              if (principal == null) {
                  return null;
              }
              User user = userService.findByUserName(principal);
              if (user!=null) {
                  return new SimpleAuthenticationInfo(user.getUsername(),
                          user.getPassword(),
                          ByteSource.Util.bytes(user.getSalt()),this.getName());
              }
              return null;
          }
      }
      
  6. 修改 shiro 配置类

    •     // 3. 自定义realm
          @Bean
          public Realm getRealm() {
              CustomerRealm customerRealm = new CustomerRealm();
              //设置hashed凭证匹配器
              HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
           
              //设置md5加密
              credentialsMatcher.setHashAlgorithmName("md5");
           
              //设置散列次数
              credentialsMatcher.setHashIterations(1024);
              customerRealm.setCredentialsMatcher(credentialsMatcher);
              return customerRealm;
          }
      

6.7 授权实现

6.7.1 给用户授权的方式

public class CustomerRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("=================授权================");
        // 获取身份信息
        String principal = (String) principalCollection.getPrimaryPrincipal();
        // 根据主身份信息获取角色 和 权限信息
        if ("lyj".equals(principal)) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addRole("admin");

            simpleAuthorizationInfo.addStringPermission("user:*:*");

            return simpleAuthorizationInfo;
        }
        return null;
    }
}

6.7.2 页面资源授权

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--受限资源-->
<h1>系统主页</h1>
<a href="/user/logout">退出</a>
<ul>
    <shiro:hasAnyRoles name="admin, user">
        <li><a href="#">用户管理</a>
            <ul>
                <shiro:hasPermission name="user:add:*">
                    <li><a href="#">添加</a></li>
                </shiro:hasPermission>
                <li><a href="#">删除</a></li>
                <li><a href="#">修改</a></li>
                <li><a href="#">查询</a></li>
            </ul>
        </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>
</html>

6.7.3 代码判断用户是否具有权限

    @RequestMapping("save")
    public void save() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole("admin")) {
            System.out.println("成功");
        } else {
            System.out.println("失败");
        }
    }

6.7.4 注解判断用户是否具有权限

  • @RequiresRoles 基于角色判断权限
  • @RequiresPermissions 基于权限判断授权
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("order")
public class OrderController {

    @RequiresRoles(value={"admin","user"})//用来判断角色  同时具有 admin user
    @RequiresPermissions("user:update:01") //用来判断权限字符串
    @RequestMapping("save")
    public String save(){
        System.out.println("进入方法");
        return "redirect:/index.jsp";
    }
}

6.8 数据库持久化

img

6.8.1 数据库表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 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) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- 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) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
  `id` int(6) NOT NULL,
  `roleid` int(6) DEFAULT NULL,
  `permsid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- 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) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
  `id` int(6) NOT NULL,
  `userid` int(6) DEFAULT NULL,
  `roleid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = 1;

6.8.2 实体类

User

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;

    // 定义角色集合
    private List<Role> roles;

}

Role

@Data
public class Role {
    private Integer id;
    private String name;

    // 定义权限的集合
    private List<Perms> perms;
}

Perm

@Data
public class Perms {
    private Integer id;
    private String name;
    private String url;
}

6.8.3 创建dao方法

@Mapper
public interface UserDAO {
    void save(User user);

    User findByUserName(String username);

    //根据用户名查询所有角色
    User findRolesByUserName(String username);
    //根据角色id查询权限集合
    List<Perms> findPermsByRoleId(Integer id);
}

6.8.4 mapper实现

    <select id="findRolesByUserName" parameterType="String" resultMap="userMap">
        SELECT u.id uid,u.username,r.id,r.NAME rname
        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>

    <select id="findPermsByRoleId" parameterType="String" resultType="Perms">
        SELECT p.id,p.NAME,p.url,r.NAME
        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>

6.8.5 Service接口

    //根据用户名查询所有角色
    User findRolesByUserName(String username);
    //根据角色id查询权限集合
    List<Perms> findPermsByRoleId(Integer id);

6.8.6 Service实现

 @Override
    public List<Perms> findPermsByRoleId(Integer id) {
        return userDAO.findPermsByRoleId(id);
    }

    @Override
    public User findRolesByUserName(String username) {
        return userDAO.findRolesByUserName(username);
    }

6.8.7 修改自定义realm

注意:如果你创建了一个用户,并为这个用户授予了一个角色,但这个角色并未关联任何的 授权字符串,那么调用数据库获得的结果是 List<Perms> perms=null,此时 perms 已经被初始化,里面只有一个属性 null,使用判空的方法无法判别,此时继续遍历会报出空指针异常,此时应当添加判断条件 perms.get(0)!=null

package com.lyj.springboot_jsp_shiro.shiro;

import com.lyj.springboot_jsp_shiro.entity.Perms;
import com.lyj.springboot_jsp_shiro.entity.User;
import com.lyj.springboot_jsp_shiro.service.UserService;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public class CustomerRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("=================授权================");
        // 获取身份信息
        String principal = (String) principalCollection.getPrimaryPrincipal();

        User user = userService.findRolesByUserName(principal);

        // 根据主身份信息获取角色 和 权限信息
        if (!CollectionUtils.isEmpty(user.getRoles())) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            user.getRoles().forEach(role -> {
                simpleAuthorizationInfo.addRole(role.getName()); // 添加角色信息
                // 权限信息
                List<Perms> perms = userService.findPermsByRoleId(role.getId());
                System.out.println("perms: " + perms);
                if (!CollectionUtils.isEmpty(perms) && perms.get(0)!=null) {
                    perms.forEach(perms1 -> {
                        simpleAuthorizationInfo.addStringPermission(perms1.getName());
                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("=================认证================");
        String principal = (String) token.getPrincipal();
        if (principal == null) {
            return null;
        }
        User user = userService.findByUserName(principal);
        if (user!=null) {
            return new SimpleAuthenticationInfo(user.getUsername(),
                    user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()),this.getName());
        }
        return null;
    }
}

6.9 使用CacheManager

6.9.1 Cache作用

  • Cache 缓存: 计算机内存中的一段数据
  • 作用: 用来减轻DB的访问压力,从而提高系统的查询效率
  • 流程:img

6.9.2 shiro中默认EhCache实现缓存

  1. 引入依赖
<!--引入shiro和ehcache-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.5.3</version>
</dependency>
  1. 开启缓存
    // 3. 自定义realm
    @Bean
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        //设置hashed凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        //设置md5加密
        credentialsMatcher.setHashAlgorithmName("md5");

        //设置散列次数
        credentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(credentialsMatcher);


        // 开启缓存管理
        customerRealm.setCacheManager(new EhCacheManager());
        customerRealm.setCachingEnabled(true); // 开启全局缓存
        customerRealm.setAuthenticationCachingEnabled(true); // 开启认证缓存
        customerRealm.setAuthenticationCacheName("AuthenticationCache"); // 认证缓存的名字
        customerRealm.setAuthorizationCachingEnabled(true); // 开启授权缓存
        customerRealm.setAuthorizationCacheName("AuthorizationCache");// 授权缓存的名字

        return customerRealm;
    }

6.9.3 shiro中使用Redis作为缓存

  1. 引入依赖
<!--redis整合springboot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置 redis 连接
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
  1. 自定义 shiro 缓存管理器
// 自定义缓存管理器
public class RedisCacheManager implements CacheManager {
    //参数cacheName是认证或者是授权缓存的统一名称
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println(cacheName);
        return new RedisCache<K,V>(cacheName);
    }
}
  1. 自定义 redis 缓存的实现
package com.lut.shiro.cache;

import com.lut.utils.ApplicationContextUtils;
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缓存的实现
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);
        return (v) getRedisTemplate().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 value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

    @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);
    }

    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

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

解决方案: 需要自动salt实现序列化

  //自定义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.lut.shiro.salt.MyByteSource.BytesHelper()).getBytes(file);
  }
  
  public MyByteSource(InputStream stream) {
      this.bytes = (new com.lut.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 ||  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);
      }
  }


- 在realm中使用自定义salt
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  System.out.println("==========================");
  //根据身份信息
  String principal = (String) token.getPrincipal();
  //在工厂中获取service对象
  UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
  User user = userService.findByUserName(principal);
  if(!ObjectUtils.isEmpty(user)){
    return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), 
                                      new MyByteSource(user.getSalt()),this.getName());
  }
  return null;
}

6.10 常用标签

<!-- 验证当前用户是否为访客”,即未认证包含未记住的用户 -->
<p shiro:guest="">Please <a href="login.html">login</a></p>


<!-- 认证通过或已记住的用户 -->
<p shiro:user="">
    Welcome back John! Not John? Click <a href="login.html">here</a> to login.
</p>

<!-- 已认证通过的用户不包含已记住的用户这是与user标签的区别所在 -->
<p shiro:authenticated="">
    Hello, <span shiro:principal=""></span>, how are you today?
</p>
<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a>

<!-- 输出当前用户信息通常为登录帐号信息 -->
<p>Hello, <shiro:principal/>, how are you today?</p>


<!-- 未认证通过用户与authenticated标签相对应与guest标签的区别是该标签包含已记住用户 -->
<p shiro:notAuthenticated="">
    Please <a href="login.html">login</a> in order to update your credit card information.
</p>

<!-- 验证当前用户是否属于该角色 -->
<a shiro:hasRole="admin" href="admin.html">Administer the system</a><!-- 拥有该角色 -->

<!-- 与hasRole标签逻辑相反当用户不属于该角色时验证通过 -->
<p shiro:lacksRole="developer"><!-- 没有该角色 -->
    Sorry, you are not allowed to developer the system.
</p>

<!-- 验证当前用户是否属于以下所有角色 -->
<p shiro:hasAllRoles="developer, 2"><!-- 角色与判断 -->
    You are a developer and a admin.
</p>

<!-- 验证当前用户是否属于以下任意一个角色  -->
<p shiro:hasAnyRoles="admin, vip, developer,1"><!-- 角色或判断 -->
    You are a admin, vip, or developer.
</p>

<!--验证当前用户是否拥有指定权限  -->
<a shiro:hasPermission="userInfo:add" href="createUser.html">添加用户</a><!-- 拥有权限 -->

<!-- 与hasPermission标签逻辑相反当前用户没有制定权限时验证通过 -->
<p shiro:lacksPermission="userInfo:del"><!-- 没有权限 -->
    Sorry, you are not allowed to delete user accounts.
</p>

<!-- 验证当前用户是否拥有以下所有角色 -->
<p shiro:hasAllPermissions="userInfo:view, userInfo:add"><!-- 权限与判断 -->
    You can see or add users.
</p>

<!-- 验证当前用户是否拥有以下任意一个权限  -->
<p shiro:hasAnyPermissions="userInfo:view, userInfo:del"><!-- 权限或判断 -->
    You can see or delete users.
</p>
<a shiro:hasPermission="pp" href="createUser.html">Create a new User</a>
最近的文章

学习jwt

JWT学习1. 什么是JWTJSON Web Token (JWT) is an open standard ([RFC 7519](https://tools.ietf.org/html/rfc7519)) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be ...…

继续阅读
更早的文章

验证二叉搜索树

98. 验证二叉搜索树题目描述给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。有效 二叉搜索树定义如下: 节点的左子树只包含 小于 当前节点的数。 节点的右子树只包含 大于 当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。示例 1:输入:root = [2,1,3]输出:true实例2:输入:root = [5,1,4,null,null,3,6]输出:false解释:根节点的值是 5 ,但是右子节点的值是 4 。方法一:递归​ 递归有一个很重要的地...…

继续阅读