转 Shiro笔记

Shiro笔记


1.权限的管理

1.1 什么是权限管理

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

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

1.2什么是身份认证

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

调用登录接口后,在CustomRealm的doGetAuthenticationInfo()方法中,通过用户名查询到数据库中的密码,与登录接口中传入的密码对比,相等则认证成功,返回Cookie。

1.3什么是访问授权

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

调用有权限注解的接口如/test,在CustomRealm的doGetAuthorizationInfo()方法中,通过用户名查询到数据库中的用户权限,与当前需要的权限对比,匹配则授权成功,可以继续访问。


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核心结构

image-20201109142559750

3.1 Subject

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

3.2 SecurityManager

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

Securitymanager是一个接口,继承了 Authenticator, Authorizer, Sessionmanager这三个接口。

3.3 Authenticator

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

3.4 Authorizer

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

3.5 Realm

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

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

3.6 SessionManager

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

3.7SessionDao

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

3.8 CacheManager

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

3.9 Cryptography

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


4.shiro中的认证

4.1 认证

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

4.2 shiro中认证的关键对象

  • Subject: 主题

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

  • Principal: 身份信息

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

  • credential:凭证信息

    是只有主题自己知道的安全信息,eg:密码,证书。

4.3 认证流程

image-20201109145155991

4.4 认证的开发

  1. 创建项目并引入依赖

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.5.3</version>
    </dependency>
    
  2. 引入shiro配置文件并加入如下配置(*.ini可以写比较复杂的配置格式)

    image-20201109182412444

  3. 使用ini配置文件进行测试

    public class TestAuthenticator {
        public static void main(String[] args) {
            //shiro-lang依赖slf4j,所以自动导入了
            //1.创建安全管理器对象 注意这个是apache中的不是lang中的
            DefaultSecurityManager securityManager = new DefaultSecurityManager();
    
            //2.给安全管理器设置realm
            securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    
            //3.SecurityUtils 全局安全工具类
            SecurityUtils.setSecurityManager(securityManager);
    
            //4.关键对象,subject 主体
            Subject subject = SecurityUtils.getSubject();
    
            //5,创建令牌
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
            try{
                System.out.println("认证状态:"+subject.isAuthenticated());
                subject.login(token); //用户认证
                System.out.println("认证状态:"+subject.isAuthenticated());
            }catch (UnknownAccountException e){
                 e.printStackTrace();
                System.out.println("认证失败,用户名不存在");
            }catch (IncorrectCredentialsException e){
                e.printStackTrace();
                System.out.println("认证信息无效,密码错误");
            }
        }
    }
    
  4. 方法剖析

    认证:

    1. 最终执行用户名比较 SimpleAccountRealm 在这个类中的方法doGetAuthenticationInfo完成了校验,之后的数据库校验也是在这个方法中完成的。
    2. 最终密码的校验是在AuthenticatingRealm这个类中的assertCredentialsMatch方法中完成的校验。这个是自动完成的。
  5. 总结:

    AuthenticatingRealm tken和info对比验证 assertCredentialsMatch方法
    AuthenticatingRealm 认证realm   doGetAuthenticationInfo  {这是一个抽象的方法}
    AuthorizingRealm    授权realm   doGetAuthorizationInfo {这是一个抽象的方法}
    

    关于token和info对比验证,是调用的SimpleCredentialsMatcher类中的doCredentialsMatch(token,info)这个方法,最后的比较是使用的equals(token,info).

    SimpleAccountRealm继承上边的两个类的两个抽象方法,并实现,所以再用数据库的时候,需要定义类继承SimpleAccountRealm这个类,然后覆盖对应的两个方法即可。

4.5 自定义realm

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

  1. shiro提供的Realm

    image-20201109201150605

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

    image-20201109201247776

    SimpleAccountRealm类的两个方法,一个是认证,一个是授权

     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            UsernamePasswordToken upToken = (UsernamePasswordToken)token;
            SimpleAccount account = this.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 = this.getUsername(principals);
            this.USERS_LOCK.readLock().lock();
    
            AuthorizationInfo var3;
            try {
                var3 = (AuthorizationInfo)this.users.get(username);
            } finally {
                this.USERS_LOCK.readLock().unlock();
            }
    
            return var3;
        }
    
  3. 自定义realm

    package com.englishcode.realm;
    
    /**
     * @author cyl
     * @version 1.0
     * @date 2020/11/9 20:20
     */
    
    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 {
            String principal = (String) authenticationToken.getPrincipal();  //这个是身份信息,就是用户名
            //可以根据身份信息使用jdbc 或者是 mybatis查询相关的数据库
    //        String credentials = (String) authenticationToken.getCredentials(); //这个是
            if ("zhangsan".equals(principal)){
                //这个对象包含的是数据库中查找到用户名和密码的信息,然后封装到下边的对象中返回
                //参数解释,第一个是princical(用户信息)就是用户名,第二个是credencials(证书就是密码)
                SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName());
                return simpleAuthenticationInfo;  //因为返回这个info之后,上层函数会调用其他的相关函数进行token和info的比较然后返回trueorfalse;
            }
            return null;
        }
    }
    
  4. 使用自定义的Realm认证

    package com.englishcode;
    
    /**
     * @author cyl
     * @version 1.0
     * @date 2020/11/9 20:30
     */
    
    import com.englishcode.realm.CustomerRealm;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.subject.Subject;
    
    /**
     * 使用自定义realm
     */
    public class TestCustomerAuthenticator {
        public static void main(String[] args) {
            //创建security manager
            DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    
            //设置自定义的realm
            defaultSecurityManager.setRealm(new CustomerRealm());
    
            //设置安全管理器
            SecurityUtils.setSecurityManager(defaultSecurityManager);
    
            //获取主题
            Subject subject = SecurityUtils.getSubject();
    
            //创建token
            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("密码错误");
            }
    
    
        }
    }
    

4.6 使用MD5和Salt

  • MD5加密解释:一般用来加密 或者签名(就是校验和)

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

    eg: "123" ===> 十六进制的字符串(始终是一个十六进制32为长度的字符串)

public class TestShiroMD5 {
public static void main(String[] args) {
//错误的方法
Md5Hash md5Hash = new Md5Hash();
md5Hash.setBytes("123".getBytes()); //md5加密
System.out.println(md5Hash.toHex());

      //使用md5
      Md5Hash md5Hash1 = new Md5Hash("123456x0*7ps");  //使用对应的构造方法
      System.out.println(md5Hash1.toHex());

      //使用MD5+salt进行处理,默认的salt是加在source后边进行处理
      Md5Hash md5Hash2 = new Md5Hash("123456","x0*7ps");
      System.out.println(md5Hash2.toHex());

      //使用md5+salt+hash散列
      //第一个参数:原密码(加密字符串),第二个参数:盐(盐值),第三个参数:散列次数(也称加密次数)
      Md5Hash md5Hash3 = new Md5Hash("123456", "x0*7ps", 1024);
      System.out.println(md5Hash3.toHex());
  }

}


- 自定义Realm:

```java
public class CustomerRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String principal = (String) authenticationToken.getPrincipal();  //这个是身份信息,就是用户名
        //可以根据身份信息使用jdbc 或者是 mybatis查询相关的数据库
//        String credentials = (String) authenticationToken.getCredentials(); //这个是
        if ("zhangsan".equals(principal)){
            //这个对象包含的是数据库中查找到用户名和密码的信息,然后封装到下边的对象中返回
            //参数解释,第一个是princical(用户信息)就是用户名,第二个是credencials(证书就是密码),第三个就是realm的名字,这个是运行的时候生成的所以是this.getName
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName());
            return simpleAuthenticationInfo;  //因为返回这个info之后,上层函数会调用其他的相关函数进行token和info的比较然后返回trueorfalse;
        }
        return null;
    }
}
  • 测试

    public class TestCustomerMd5RealAuthenticator {
        public static void main(String[] args) {
            //创建安全管理器
            DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
            //设置自定义的realm{realm包括数据源和认证以及授权的代码}
            //注入realm
            CustomerMd5Realm realm = new CustomerMd5Realm();
            //设置realm使用hash凭证匹配器
            HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
            credentialsMatcher.setHashAlgorithmName("md5"); //指定加密的方法
            credentialsMatcher.setHashIterations(1024);    //设置散列次数
    
            realm.setCredentialsMatcher(credentialsMatcher);  //然后再使用equals
            defaultSecurityManager.setRealm(realm);
    
            //指定安全管理器
            SecurityUtils.setSecurityManager(defaultSecurityManager);
            //建立Token
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
            //获取subject
            Subject subject = SecurityUtils.getSubject();
            //subject登录
            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 授权流程

image-20201110105559554

5.4 授权方式

  • 基于角色的访问控制

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

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

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

      if(subject.ispermission("user:create:*")){	//资源类型
          //对所有的用户具有创建的权限
      }
      if(subject.ispermission("user:update:01")){  //资源实例
          //这个可以表示主体对user下边的01号具有修改权限
      }
      

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{
        //无权限
    }
    
  • 注解式

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

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

    对应的标签的含义:

    标签名称 标签条件(均是显示标签内容)
    <shiro:authenticated> 登录之后
    <shiro:notAuthenticated> 不在登录状态时
    <shiro:guest> 用户在没有RememberMe时
    <shiro:user> 用户在RememberMe时
    <shiro:hasAnyRoles name="abc,123" > 在有abc或者123角色时
    <shiro:hasRole name="abc"> 拥有角色abc
    <shiro:lacksRole name="abc"> 没有角色abc
    <shiro:hasPermission name="abc"> 拥有权限资源abc
    <shiro:lacksPermission name="abc"> 没有abc权限资源
    <shiro:principal> 显示用户身份名称
    <shiro:principal property="username"/> 显示用户身份中的属性值

5.7 开发授权

  1. realm中的实现

    public class CustomerMd5Realm extends AuthorizingRealm {
    
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
            System.out.println("身份信息是:"+primaryPrincipal);
            //根据身份信息 用户名 获取当前用户的角色信息以及权限信息 zhangsan 123456 
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    
            //将数据库中的查询角色信息复制给权限对象
            simpleAuthorizationInfo.addRole("admin");
            simpleAuthorizationInfo.addRole("user");
    
            //将数据库中的查询权限信息赋值给权限对象
            simpleAuthorizationInfo.addStringPermission("user:*:01");
            simpleAuthorizationInfo.addStringPermission("product:create");  //对所有商品具有创建的权限
            return simpleAuthorizationInfo;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            //获取身份信息
            String principal = (String) authenticationToken.getPrincipal();
    
            //根据用户名查询数据库
            //参数1:数据库用户名  参数二:用户名+随机盐之后的密码  参数三:注册时的随机盐  参数四:Realm名字
            if("zhangsan".equals(principal)){
                return new SimpleAuthenticationInfo(principal,
    //                    "5f9dcb70720b6f58a2b219015fad5c30",  //加密后的密文
                        "14b9f6ecccb925f15cf92c44ff9326ce",//经过1024次散列的值
                        ByteSource.Util.bytes("x0*7ps"),//随机盐
                        this.getName());
            }
            return null;
        }
    
  2. 测试

    public class TestCustomerMd5RealAuthenticator {
        public static void main(String[] args) {
            //创建安全管理器
            DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
            //设置自定义的realm{realm包括数据源和认证以及授权的代码}
            //注入realm
            CustomerMd5Realm realm = new CustomerMd5Realm();
            //设置realm使用hash凭证匹配器
            HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
            credentialsMatcher.setHashAlgorithmName("md5"); //指定加密的方法
            credentialsMatcher.setHashIterations(1024);    //设置散列次数
    
            realm.setCredentialsMatcher(credentialsMatcher);  //然后再使用equals
            defaultSecurityManager.setRealm(realm);
    
            //指定安全管理器
            SecurityUtils.setSecurityManager(defaultSecurityManager);
            //建立Token
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
            //获取subject
            Subject subject =  SecurityUtils.getSubject();
            //subject登录
            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()){ //认证之后进行授权
                //1.基于角色的权限控制
    //            System.out.println(subject.hasRole("admin"));//输出是否有这个权限
    
                //2.基于多角色的权限控制
    //            System.out.println(subject.hasAllRoles(Arrays.asList("admin","user")));
    
                //是否具有其中一个角色
    //            boolean[] booleans =  subject.hasRoles(Arrays.asList("admin","super","user"));
    
                //3.基于资源的权限控制
    //            for (boolean a:booleans) {
    //                System.out.println(a);
    //            }
    
                //4.基于权限字符串的访问控制 资源标识符:操作:资源类型
    //            System.out.println(subject.isPermitted("user:update:01"));
                //查询是否对所有的商品具有修改的权限
    //            System.out.println("权限:"+subject.isPermitted("product:create"));
    
    //            System.out.println("权限:"+subject.isPermitted("product:create:02"));
    
                //分别具有哪些权限
                boolean[] permitted = subject.isPermitted("user:*:01", "order:*:01");
    
    //            for (boolean b:permitted) {
    //                System.out.println(b);
    //            }
    
    
    
                //同时具有哪些权限
                boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create");
                System.out.println(permittedAll);
    
            }
        }
    }
    

6.整合springboot项目

6.1 shiro整合springboot框架

image-20201110173927685

上图中那个Request是访问受限资源的时候才经过Filter,访问公共资源的时候不需要经过ShiroFilter。

6.2 创建springboot项目

image-20201110190328679

6.3 引入Shiro依赖

<!--引入shiro整合springboot的依赖-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

6.4 配置shiro环境

  1. 创建配置类

    image-20201110202123886

  2. 配置shiroFilterFactoryBean

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        //创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    
        //配置系统受限资源
        //配置系统的公共资源
        Map<String,String> map = new HashMap<String,String>();
        map.put("/index.jsp","authc"); //请求这个资源需要认证和授权,其实这个authc是一个filter过滤器
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    
        //这是在不写的情况下,默认的认证界面是login.jsp
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        return shiroFilterFactoryBean;
    }
    
  3. 配置WebSecurityManager

    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){ //这个报错是因为Realm有多个实现类,Realm是接口,不知道返回的时候返回哪个
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    
  4. 创建自定义realm

    //创建自定义realm
    public class CustomerRealm extends AuthorizingRealm {
        //处理授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            return null;
        }
        //处理认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            return null;
        }
    }
    
  5. 配置自定义的realm

    //3.配置自定义的realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }
    
  6. 加入权限控制

    • 修改shiroFilterFactoryBean的配置

      image-20201110202913221

    • @Bean
      public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
          //创建shiro的filter
          ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
          //给filter设置安全管理器
          shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
          //配置系统受限资源
          Map<String,String> map = new HashMap<String,String>();
          map.put("/**","authc"); //请求这个资源需要认证和授权,其实这个authc是一个filter过滤器
          shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
          //配置系统的公共资源
          //这是在不写的情况下,默认的认证界面是login.jsp
          shiroFilterFactoryBean.setLoginUrl("/login.jsp");
          return shiroFilterFactoryBean;
      }
      
  7. 重启项目查看

    因为项目中已经创建了login.jsp,所以,访问index.jsp的时候会重定向到login.jsp的页面,如果是在thymeleaf中,因为没有login.jsp,只有html,所以可以设置重定向的地址 shiroFilterFactoryBean.setLoginUrl("重定向的位置");

    JM59oFGZ2f

6.5 常见过滤器

  • image-20201110212720587

6.6 实现登录

  1. UserController中的login方法

    @RequestMapping(value = "login")
    public String login(String username,String password){
        System.out.println("fuck you ");
        //获取主题对象
        Subject subject = SecurityUtils.getSubject();  //只要在shiroConfig中创建了安全管理器,就会自动注入到SecurityUtils中
        try {
            subject.login(new UsernamePasswordToken(username,password));
            System.out.println("登陆成功");
            return "index";
        }catch(UnknownAccountException e){
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "login";
    }
    
  2. 登陆对应的前端页面

    <%@page contentType="text/html; UTF_8" pageEncoding="UTF-8" isELIgnored="false" %>
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>登录</title>
    </head>
    <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>
    </html>
    
  3. 登陆页面登录

    RiqGvhuKQJ

6.7 退出认证

  1. 退出的前台页面

    <a href="${pageContext.request.contextPath}/user/logout">退出</a>
    
  2. 退出对应的controller

    @RequestMapping("logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }
    
  3. 操作

    9lDMOyIcTf

6.8 MD5、Salt的认证实现

  1. 开发数据库注册

    1. 开发注册页面

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

      image-20201111142329295

    2. 创建数据表

      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 AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
      
      SET FOREIGN_KEY_CHECKS = 1;
      

      image-20201111142611738

    3. 项目引入依赖

      <!--MySQL驱动依赖-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.38</version>
      </dependency>
      <!--mybatis相关依赖-->
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>2.1.3</version>
      </dependency>
      <!--阿里的druid依赖,数据库连接池-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.23</version>
      </dependency>
      
    4. 配置application.properties配置文件

      #配置数据库(数据源)相关配置
      #配置数据源的类型
      spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
      spring.datasource.driver-class-name=com.mysql.jdbc.Driver
      #url后边要配置上相应的时区类型
      spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false
      spring.datasource.username=root
      spring.datasource.password=chaiyinlei
      
      
      #mybatis相关配置
      #指明mybatis对应的实体包
      mybatis.type-aliases-package=com.englishcode.springboot_jsp_shiro.entity
      mybatis.mapper-locations=classpath:com/englishcode/mapper/*.xml
      
    5. 创建entity

      @Data     //自动生成getter和setter
      @Accessors(chain = true)  //setter方法返回当前对象
      @AllArgsConstructor   //有参构造,全部的参数
      @NoArgsConstructor    //无参构造
      public class User {
          private Integer id;
          private String username;
          private String password;
          private String salt;
      }
      
    6. 创建DAO接口

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

      <?xml version="1.0" encoding="utf-8" ?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
      <mapper namespace="com.englishcode.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>
      
    8. 开发service接口

      public interface UserService {
          //注册用户
          void register(User user);
      }
      
    9. 创建salt(随机盐)的工具类

      public class SaltUtils {
          public static String getSalt(int n){  //产生n个长度的随机盐
              char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&".toCharArray();
              //String 字符串的拼接,每次拼接都生成对象,则对系统的性能产生影响,当内存中的对象多了之后,JVN的GC就会开始工作,性能就降低。
              //StringBuilder:单线程操作大量数据,长度可变
              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();
          }
          public static void main(String[] args) {
              System.out.println(getSalt(4));
          }
      }
      
    10. 开发service实现类

      @Override
      public void register(User user) {
          //处理业务 调用dao
      
          //生成8位的随机盐
          String salt = SaltUtils.getSalt(8);
          //随机盐保存到user对象中,便于之后保存到数据库
          user.setSalt(salt);
          //明文的密码进行md5+salt和hash散列
          Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt,1024);
          user.setPassword(md5Hash.toHex());
          userDAO.save(user);
      }
      
    11. 开发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";
          }
      }
      
    12. 启动项目注册之后数据库中信息

      image-20201111144103340

  2. 开发数据库认证

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

      <select id="findByUserName" parameterType="java.lang.String" resultType="com.englishcode.springboot_jsp_shiro.entity.User">
          select id,username,password,salt from t_user where username=#{username}
      </select>
      
    2. 开发service接口

      //通过名字查找查找
      User findByUserName(String username);
      
    3. 开发对应的service接口的实现类

      @Override
      public User findByUserName(String username) {
          User user = userDAO.findByUserName(username);
          return user;
      }
      
    4. 开发在工厂中获取bean的工具类

      @Component
      public class ApplicationContextUtils implements ApplicationContextAware {
      
          private static ApplicationContext context;  //applicationcontext就是bean工厂,所以用工具类获取对应的bean实例
      
          @Override   //set方法;是把创建好的工厂对象通过参数的方法回传给我们
          public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
              this.context = applicationContext;
          }
      
          //根据bean的名字获取工厂中指定的bean
          public static Object getBean(String beanName) {
              Object bean = context.getBean(beanName);
              return bean;
          }
      }
      
    5. 修改自定义realm,获得userService的bean

      public class CustomerRealm extends AuthorizingRealm {
      
          //处理授权
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              return null;
          }
          //处理认证
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
              System.out.println("CustomerRealm");
              String principal = (String) authenticationToken.getPrincipal();
      
              //在工厂中获取service对象
              UserService userServiceImpl = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
              User user = userServiceImpl.findByUserName(principal); //从数据库中查询
              if(!ObjectUtils.isEmpty(user)){
                  //第三个参数注意了,随机盐应该使用ByteSource的工具类把对应的String转换为字节
                  return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
              }
              return null;
          }
      }
      
    6. 修改ShiroConfig使用凭证匹配器以及hash散列

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

6.9 授权实现

  1. 前端页面加如shiro控制

    <h1>主页V1.0</h1>
    <ul>
        <shiro:hasAnyRoles name="user,admin">  <%--这个是可以设置多个角色访问的,只要有其中一个权限就可以访问--%>
            <li><a href="">用户管理</a>
                <ul>
                    <shiro:hasPermission name="user:add:*">
                        <li><a href="${pageContext.request.contextPath}/order/">添加</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:delete:*">
                        <li><a href="">删除</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:update:*">
                        <li><a href="">修改</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:find:*">
                        <li><a href="">查询</a></li>
                    </shiro:hasPermission>
                </ul>
            </li>
        </shiro:hasAnyRoles>
    
        <shiro:hasRole name="admin">  <%--这个设置是只有指定用户权限的才能看到--%>
            <li><a href="index.jsp">主数据库管理</a></li>
            <li><a href="">人事管理</a></li>
        </shiro:hasRole>
    </ul>
    
  2. 自定义realm中加入授权

        //处理授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //获取主身份信息
            String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
    
            //根据主身份信息获取角色 和 权限信息
            if("abc".equals(primaryPrincipal)){
                SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                simpleAuthorizationInfo.addRole("admin");
                simpleAuthorizationInfo.addRole("user");
    //            simpleAuthorizationInfo.addRole("guest");
                simpleAuthorizationInfo.addStringPermission("user:update:01");   //只有更新
                simpleAuthorizationInfo.addStringPermission("user:find:*");     //只有查询
                return simpleAuthorizationInfo;
            }
          return null;
        }
    
  3. 方法调用授权

    • @RequiresRoles:基于角色进行授权
    • @RequiresPermissions基于权限进行授权

    @RequiresPermissions多权限是分两种的,这里要注意

    1. 必须全部符合(默认不写或者在后面添加logical = Logical.AND)
      @RequiresPermissions(value={“stuMan:find_record_list”,“tea:find_record_list”})
      上面这种情况是默认当前对象必须同时全部拥有指定权限
    2. 符合其中一个即可(logical = Logical.OR)
      @RequiresPermissions(value={“stuMan:find_record_list”,“tea:find_record_list”},logical=Logical.OR)
      上面这种情况则是只要有其中一个权限即可访问
    @RequestMapping("/save")                  //注意这个
    @RequiresRoles(value={"admin","user"})  //同时具有以上的角色才可以访问
    @RequiresPermissions("user:update:01")   //具有user下的01用户具有修改权限的时候才可进入
    public String  save(){
      //通过代码的形式查看是否具有保存的权限
         //获取主题对象
      Subject subject = SecurityUtils.getSubject();
      if(subject.hasRole("admin")){
          System.out.println("保存订单");
      }else{
          System.out.println("无权访问");
      }
      //添加之后返回主页
      return "redirect:/index.jsp";
    }
    
  4. 授权数据持久化

    • 库表结构

    image-20201112083647125

    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) 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 AUTO_INCREMENT = 1 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,
      `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
    -- ----------------------------
    DROP TABLE IF EXISTS `t_user`;
    CREATE TABLE `t_user`  (
      `id` int(6) NOT NULL AUTO_INCREMENT COMMENT '设置为自增',
      `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 AUTO_INCREMENT = 3 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,
      `userid` int(6) NULL DEFAULT NULL,
      `roleid` int(6) 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;
    
  5. @Mapper
    public interface UserDAO {
        void save(User user);
        User findByUserName(String username);
        //根据用户名查询所有角色
        User findRolesByUserName(String name);
    
        //根据角色的id查询权限集合的方法
        List<Perms> findPermsByRoleId(int id);
    }
    
  6. 对应的mapper的信息

    <!--根据role的id查询权限集合-->
    <select id="findPermsByRoleId" parameterType="java.lang.Integer" resultType="com.englishcode.springboot_jsp_shiro.entity.Perms">
        SELECT p.id,p.`name`,p.url 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>
    
  7. service添加获取权限列表的接口

    //根据角色id查询权限集合
    List<Perms> findPermsByRoleId(Integer id);
    
  8. service接口的实现类

        @Override
        public List<Perms> findPermsByRoleId(Integer id) {
            List<Perms> permsList = userDAO.findPermsByRoleId(id);
    //        if(!CollectionUtils.isEmpty(permsList)){
    //            System.out.println(permsList);
    //        }
            return permsList;
        }
    
  9. 给授权中添加角色和权限信息

    //处理授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //获取主身份信息
            String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
            //根据主身份信息获取角色 和 权限信息
            UserService userService = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
            User user = userService.findRolesByUserName(primaryPrincipal);
            List<Role> rolesList = user.getRoles();
            //授权角色信息
            if(!CollectionUtils.isEmpty(rolesList)){
                SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                rolesList.forEach(role -> {
                    simpleAuthorizationInfo.addRole(role.getName());
                    //权限信息
                    List<Perms> permsList = userService.findPermsByRoleId(role.getId());
    
                    if(!CollectionUtils.isEmpty(permsList)){
    //                    System.out.println("====================");
                        permsList.forEach(perms -> {
                            simpleAuthorizationInfo.addStringPermission(perms.getName());
    //                        System.out.println(perms.getName());
                        });
                    }
                });
    
                return simpleAuthorizationInfo;
            }
          return null;
        }
    
  10. 进行测试

9JIXv8LAfe

6.10使用cacheManager

  1. Cache的作用

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

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

    • 流程

      image-20201112220842065

  2. 使用shiro中默认的EnCache实现缓存

    实现的是本地缓存,不是分布式缓存,redis是分布式缓存。

    1. 引入依赖

      <!--shiro和ehcache的整合-->
      <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-ehcache</artifactId>
          <version>1.5.3</version>
      </dependency>
      
    2. 开启缓存(在shiroconfig中引入)

      //开启缓存管理,因为默认的cachemanager是没有打开的
      customerRealm.setCacheManager(new EhCacheManager());  //设置缓存管理器
      customerRealm.setCachingEnabled(true);   //设置全局开启缓存管理
      customerRealm.setAuthenticationCachingEnabled(true);  //开启认证缓存管理
      customerRealm.setAuthenticationCacheName("authenticationCache");  //默认的名字是realm.authentication
      customerRealm.setAuthorizationCachingEnabled(true);   //开启授权缓存管理
      customerRealm.setAuthorizationCacheName("authoraizationCache");
      return customerRealm;
      
    3. 启动进行测试

      除了第一次登录的时候访问数据,之后再刷新都没有访问数据库。

      0zSeq3M1vL

  3. shiro中使用Redis作为缓存实现

    1. 引入redis依赖

      <!--springboot整合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
      #设置连接的redis的第几号库
      spring.redis.database=0
      
    3. 启动redis的库

      windows上可以把redis注册到服务不用每次都启动,

      linux上可以使用命令启动然后通过命令设置开机自启动

    4. 开发redisCacheManager

      public class RedisCacheManager implements CacheManager {//继承缓存管理器
      
          @Override
          public <K, V> Cache<K, V> getCache(String s) throws CacheException {
              System.out.println(s);
              return new RedisCache<K, V>(s);
          }
      }
      

      并在ShiroConfig中指定缓存管理器是RedisCacheManager

        @Bean
          public Realm getRealm(){
              CustomerRealm customerRealm = new CustomerRealm();
              //建立校验匹配器
              HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
              //设置加密算法为md5
              credentialsMatcher.setHashAlgorithmName("MD5");
              //设置散列次数
              credentialsMatcher.setHashIterations(1024);
              //修改凭证校验匹配器
              customerRealm.setCredentialsMatcher(credentialsMatcher);
      
              //开启缓存管理,因为默认的cachemanager是没有打开的
      //      customerRealm.setCacheManager(new EhCacheManager());  //设置缓存管理器为shiro默认的缓存管理
              customerRealm.setCacheManager(new RedisCacheManager());  //设置缓存管理器为redis管理器
              customerRealm.setCachingEnabled(true);   //设置全局开启缓存管理
              customerRealm.setAuthenticationCachingEnabled(true);  //开启认证缓存管理
              customerRealm.setAuthenticationCacheName("authenticationCache");  //默认的名字是realm.authentication
              customerRealm.setAuthorizationCachingEnabled(true);   //开启授权缓存管理
              customerRealm.setAuthorizationCacheName("authoraizationCache");
      
              return customerRealm;
          }
      
    5. 开发RedisCache的实现

      RedisCache中所用知识点总结:
      edisTemplate.opsForValue();//操作字符串
      redisTemplate.opsForHash();//操作hash
      redisTemplate.opsForList();//操作list
      redisTemplate.opsForSet();//操作set
      redisTemplate.opsForZSet();//操作有序set
      redisTemplate.opsForValue().set(k.toString(),v);
      
      public class RedisCache<K,V> implements Cache<K,V>{
          private String cacheName;
      
          public RedisCache() {
          }
      
          public RedisCache(String name) {
              this.cacheName = name;
          }
      
          @Override
          public V get(K k) throws CacheException {
              System.out.println("get key:"+k);
              return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString()); //如果使用这个的时候是相当于Map<KEY,Map<key,value>>
          }
      
          @Override
          public V put(K k, V v) throws CacheException {
              System.out.println("key:"+k);
              System.out.println("value:"+v);
      //      redisTemplate是对对象进行友好操作的就是默认的序列化是,而第一个参数是String,所以是修改序列化方式为String序列化
              getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);//如果使用这个的时候是相当于Map<KEY,Map<key,value>>
              return null;
          }
      
          @Override
          public V remove(K k) throws CacheException {
              //当对应的values中只有一条数据的时候,remove就相当于clear,清空,当有多条数据的时候,只会删除其中 的cacheName和k对应的其中一条数据
              return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
          }
      
          @Override
          public void clear() throws CacheException {
              getRedisTemplate().delete(this.cacheName);   //清除对应的cachename
          }
      
          @Override
          public int size() {
              return getRedisTemplate().opsForHash().size(this.cacheName).intValue(); //因为size默认返回的是龙类型的,所以要转成int类型
          }
      
          @Override
          public Set<K> keys() {
              return getRedisTemplate().opsForHash().keys(this.cacheName); //获取所有的keys
          }
      
          @Override
          public Collection<V> values() {
              return getRedisTemplate().opsForHash().values(this.cacheName);//获取cache对应的所有的calues
          }
      
          private RedisTemplate getRedisTemplate(){
      
              //因为RedisTemplate默认的是JdkSerializationRedisSerializer进行序列化,所以才要用stringRedisSerializer修改key的序列化方式
              RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate"); //因为配置完依赖和配置文件,就会自动创建一个RedisTemplate文件
              RedisSerializer stringRedisSerializer = new StringRedisSerializer();  //设置成一个字符串的序列化文件
              //当上边使用hash的时候,因为Map<Key1,Map<key2,value>>
              redisTemplate.setKeySerializer(stringRedisSerializer);//设置序列化的格式为字符串的序列化,,这个是设置
              redisTemplate.setHashKeySerializer(stringRedisSerializer);
              return redisTemplate;
          }
      }
      
    6. 修改自定义的realm中的对应的验证中对盐的处理信息.

      因为以前是用的是ByteSource.Util.bytes(salt),但是在盐存储到redis中序列化的时候会报错,对盐的修饰需要继承ByteSource和Serializable

      自定义的realm中的验证部分做的修改后的函数如下:

      //处理认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
          System.out.println("调用doGetAuthenticationInfo这个函数");
          String principal = (String) authenticationToken.getPrincipal();
          //在工厂中获取service对象
          UserService userServiceImpl = (UserService) ApplicationContextUtils.getBean("userServiceImpl");
          User user = userServiceImpl.findByUserName(principal); //从数据库中查询
          if(!ObjectUtils.isEmpty(user)){
              //第三个参数注意了,随机盐应该使用ByteSource的工具类把对应的String转换为字节
              // 获取用户的盐值
              //ByteSource salt = ByteSource.Util.bytes(user.getSalt());  //旧代码会抛出NotSerializableException:org异常,替换成下面代码就可以了
              //MySimpleByteSource salt = new MySimpleByteSource(user.getSalt());
              return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), new ShiroByteSource(user.getSalt()),this.getName());
          }
          return null;
      }
      //ShiroByteSource  这个类是自己创建继承ByteSource 和 Serializable的类
      

      对应的ShiroByteSource如下:

      /**
       * 方法一:自己实现Serializable 和 ByteSource
       */
      public class ShiroByteSource  implements ByteSource, Serializable {
          private static final long serialVersionUID = -6814382603612799610L;
          private volatile byte[] bytes;
          private String cachedHex;
          private String cachedBase64;
      
          public ShiroByteSource() {   //这个不能少
      //        super();
      
          }
      
          public ShiroByteSource(String string) {
      //        super();
              this.bytes = CodecSupport.toBytes(string);
          }
      
          public void setBytes(byte[] bytes) {
              this.bytes = bytes;
          }
      
          @Override
          public byte[] getBytes() {
              return this.bytes;
          }
      
          @Override
          public String toHex() {
              if ( this.cachedHex == null ) {
                  this.cachedHex = Hex.encodeToString(getBytes());
              }
              return this.cachedHex;
          }
      
          @Override
          public String toBase64() {
              if ( this.cachedBase64 == null ) {
                  this.cachedBase64 = Base64.encodeToString(getBytes());
              }
              return this.cachedBase64;
          }
      
          @Override
          public boolean isEmpty() {
              return this.bytes == null || this.bytes.length == 0;
          }
      
          @Override
          public String toString() {
              return toBase64();
          }
      
          @Override
          public int hashCode() {
              if (this.bytes == null || this.bytes.length == 0) {
                  return 0;
              }
              return Arrays.hashCode(this.bytes);
          }
      
          @Override
          public boolean equals(Object o) {
              if (o == this) {
                  return true;
              }
              if (o instanceof ByteSource) {
                  ByteSource bs = (ByteSource) o;
                  return Arrays.equals(getBytes(), bs.getBytes());
              }
              return false;
          }
      
          public static ByteSource of(String string) {
              return new ShiroByteSource(string);
          }
      }
      
  4. 加入验证码验证:

    1. 开发页面加入验证码:

      <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="text" name="code"> <img src="${pageContext.request.contextPath}/user/getImage" alt="图片失效后替代的文字">  <br>
          <input type="submit" value="登录">
      </form>
      
    2. 验证码方法对应的controller文件

      /**
       * 验证码方法
       */
      @RequestMapping("/getImage")
      public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
          //生成验证码
          String code = VerifyCodeUtils.generateVerifyCode(4);
          //验证码放入session中
          session.setAttribute("code",code);   //便于登录页面获取后验证
          //验证码存入图片
          ServletOutputStream os = response.getOutputStream();
          response.setContentType("image/png");
          VerifyCodeUtils.outputImage(220,60,os,code);
      }
      
    3. 把/user/getImage 加入到公共资源中,不让filter(滤器)进行拦截

      map.put("/user/getImage","anon");  //设置验证吗图片是可以访问的
      
    4. 生成验证码的字符串和返回给前端图片的函数

      package com.englishcode.springboot_jsp_shiro.utils;
      
      /**
       * @author cyl
       * @version 1.0
       * @date 2020/11/15 16:34
       */
      import java.awt.Color;
      import java.awt.Font;
      import java.awt.Graphics;
      import java.awt.Graphics2D;
      import java.awt.RenderingHints;
      import java.awt.geom.AffineTransform;
      import java.awt.image.BufferedImage;
      import java.io.File;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.OutputStream;
      import java.util.Arrays;
      import java.util.Random;
      import javax.imageio.ImageIO;
      /**
       * 生成验证码图片的工具类
       *
       */
      public class VerifyCodeUtils {
      
      
          // 使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
          public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
          private static Random random = new Random();
      
      
          /**
           * 使用系统默认字符源生成验证码
           *
           * @param verifySize 验证码长度
           * @return
           */
          public static String generateVerifyCode(int verifySize) {
              return generateVerifyCode(verifySize, VERIFY_CODES);
          }
      
      
          /**
           * 使用指定源生成验证码
           *
           * @param verifySize 验证码长度
           * @param sources 验证码字符源
           * @return
           */
          public static String generateVerifyCode(int verifySize, String sources) {
              if (sources == null || sources.length() == 0) {
                  sources = VERIFY_CODES;
              }
              int codesLen = sources.length();
              Random rand = new Random(System.currentTimeMillis());
              StringBuilder verifyCode = new StringBuilder(verifySize);
              for (int i = 0; i < verifySize; i++) {
                  verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
              }
              return verifyCode.toString();
          }
      
      
          /**
           * 生成随机验证码文件,并返回验证码值
           *
           * @param w
           * @param h
           * @param outputFile
           * @param verifySize
           * @return
           * @throws IOException
           */
          public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
              String verifyCode = generateVerifyCode(verifySize);
              outputImage(w, h, outputFile, verifyCode);
              return verifyCode;
          }
      
      
          /**
           * 输出随机验证码图片流,并返回验证码值
           *
           * @param w
           * @param h
           * @param os
           * @param verifySize
           * @return
           * @throws IOException
           */
          public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
              String verifyCode = generateVerifyCode(verifySize);
              outputImage(w, h, os, verifyCode);
              return verifyCode;
          }
      
      
          /**
           * 生成指定验证码图像文件
           *
           * @param w
           * @param h
           * @param outputFile
           * @param code
           * @throws IOException
           */
          public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
              if (outputFile == null) {
                  return;
              }
              File dir = outputFile.getParentFile();
              if (!dir.exists()) {
                  dir.mkdirs();
              }
              try {
                  outputFile.createNewFile();
                  FileOutputStream fos = new FileOutputStream(outputFile);
                  outputImage(w, h, fos, code);
                  fos.close();
              } catch (IOException e) {
                  throw e;
              }
          }
      
      
          /**
           * 输出指定验证码图片流
           *
           * @param w
           * @param h
           * @param os
           * @param code
           * @throws IOException
           */
          public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
              int verifySize = code.length();
              BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
              Random rand = new Random();
              Graphics2D g2 = image.createGraphics();
              g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
              Color[] colors = new Color[5];
              Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA,
                      Color.ORANGE, Color.PINK, Color.YELLOW };
              float[] fractions = new float[colors.length];
              for (int i = 0; i < colors.length; i++) {
                  colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
                  fractions[i] = rand.nextFloat();
              }
              Arrays.sort(fractions);
      
              g2.setColor(Color.GRAY);// 设置边框色
              g2.fillRect(0, 0, w, h);
      
              Color c = getRandColor(200, 250);
              g2.setColor(c);// 设置背景色
              g2.fillRect(0, 2, w, h - 4);
      
              // 绘制干扰线
              Random random = new Random();
              g2.setColor(getRandColor(160, 200));// 设置线条的颜色
              for (int i = 0; i < 20; i++) {
                  int x = random.nextInt(w - 1);
                  int y = random.nextInt(h - 1);
                  int xl = random.nextInt(6) + 1;
                  int yl = random.nextInt(12) + 1;
                  g2.drawLine(x, y, x + xl + 40, y + yl + 20);
              }
      
              // 添加噪点
              float yawpRate = 0.05f;// 噪声率
              int area = (int) (yawpRate * w * h);
              for (int i = 0; i < area; i++) {
                  int x = random.nextInt(w);
                  int y = random.nextInt(h);
                  int rgb = getRandomIntColor();
                  image.setRGB(x, y, rgb);
              }
      
              shear(g2, w, h, c);// 使图片扭曲
      
              g2.setColor(getRandColor(100, 160));
              int fontSize = h - 4;
              Font font = new Font("Algerian", Font.ITALIC, fontSize);
              g2.setFont(font);
              char[] chars = code.toCharArray();
              for (int i = 0; i < verifySize; i++) {
                  AffineTransform affine = new AffineTransform();
                  affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1),
                          (w / verifySize) * i + fontSize / 2, h / 2);
                  g2.setTransform(affine);
                  g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
              }
      
              g2.dispose();
              ImageIO.write(image, "jpg", os);
          }
      
      
          private static Color getRandColor(int fc, int bc) {
              if (fc > 255)
                  fc = 255;
              if (bc > 255)
                  bc = 255;
              int r = fc + random.nextInt(bc - fc);
              int g = fc + random.nextInt(bc - fc);
              int b = fc + random.nextInt(bc - fc);
              return new Color(r, g, b);
          }
      
      
          private static int getRandomIntColor() {
              int[] rgb = getRandomRgb();
              int color = 0;
              for (int c : rgb) {
                  color = color << 8;
                  color = color | c;
              }
              return color;
          }
      
      
          private static int[] getRandomRgb() {
              int[] rgb = new int[3];
              for (int i = 0; i < 3; i++) {
                  rgb[i] = random.nextInt(255);
              }
              return rgb;
          }
      
      
          private static void shear(Graphics g, int w1, int h1, Color color) {
              shearX(g, w1, h1, color);
              shearY(g, w1, h1, color);
          }
      
      
          private static void shearX(Graphics g, int w1, int h1, Color color) {
              int period = random.nextInt(2);
      
              boolean borderGap = true;
              int frames = 1;
              int phase = random.nextInt(2);
      
              for (int i = 0; i < h1; i++) {
                  double d = (double) (period >> 1)
                          * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
                  g.copyArea(0, i, w1, 1, (int) d, 0);
                  if (borderGap) {
                      g.setColor(color);
                      g.drawLine((int) d, i, 0, i);
                      g.drawLine((int) d + w1, i, w1, i);
                  }
              }
          }
      
      
          private static void shearY(Graphics g, int w1, int h1, Color color) {
              int period = random.nextInt(40) + 10; // 50;
      
              boolean borderGap = true;
              int frames = 20;
              int phase = 7;
              for (int i = 0; i < w1; i++) {
                  double d = (double) (period >> 1)
                          * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
                  g.copyArea(i, 0, 1, h1, 0, (int) d);
                  if (borderGap) {
                      g.setColor(color);
                      g.drawLine(i, (int) d, i, 0);
                      g.drawLine(i, (int) d + h1, i, h1);
                  }
              }
          }
      
      
          //生成一张验证码图片,并保存到项目的verifyCodeImg文件夹下
          @SuppressWarnings("finally")
          public static String createOneCodeImage(){
              String imgName = "";
              try {
                  File dir = new File("./verifyCodeImg");
                  int w = 95, h = 50;
                  String verifyCode = generateVerifyCode(4);
                  File file = new File(dir, verifyCode + ".jpg");
                  outputImage(w, h, file, verifyCode);
                  imgName = verifyCode;
              } catch (IOException e) {
                  imgName = "";
                  e.printStackTrace();
              }finally{
                  return imgName;
              }
          }
      
      
          public static void main(String[] args) throws IOException {
              String codeImage = VerifyCodeUtils.createOneCodeImage();
              System.out.println(codeImage);
          }
      
      }
      
    5. 操作页面

      image-20201115184229160

    6. 修改cotroller中login中的认证流程

      @RequestMapping(value = "/login")
      public String login(String username,String password,String code,HttpSession session){
      
          System.out.println("Login____");
          //比较验证码
          String imageCode = (String) session.getAttribute("code");
          //获取主题对象
          Subject subject = SecurityUtils.getSubject();  //只要在shiroConfig中创建了安全管理器,就会自动注入到SecurityUtils中
          try {
              //比较验证码
              if (code.equals(imageCode)) {
                  subject.login(new UsernamePasswordToken(username, password));
                  System.out.println("登陆成功");
                  return "redirect:/index.jsp";
              }else{
                  throw new RuntimeException("兄弟,登陆页面的验证码错误");
              }
          }catch(UnknownAccountException e){
              e.printStackTrace();
              System.out.println("用户名错误");
          }catch (IncorrectCredentialsException e){
              e.printStackTrace();
              System.out.println("密码错误");
          }catch (Exception e){
              e.printStackTrace();
              System.out.println(e.getMessage());
          }
          return "redirect:/login.jsp";
      }
      

7.Shiro整合springboo之thymeleaf的权限控制

1. 引入扩展依赖

<!--引入springboot集成Thymeleaf的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.页面引入命名空间

xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

3. 常见权限空hi标签的使用

image-20201115213304846

image-20201115213513765

4.加入Shiro的方言配置

  • 页面标签不起作用一定记住加入方言处理

    image-20201115213713660

posted @ 2022-03-10 17:17  托马斯布莱克  阅读(29)  评论(0编辑  收藏  举报