🌽Shiro学习

Shiro

课程地址:

  【【编程不良人】2020最新版Shiro教程,整合SpringBoot项目实战教程】 https://www.bilibili.com/video/BV1uz4y197Zm/?p=6&share_source=copy_web&vd_source=045f1e1c5f0609a50870e499d44940af

GitHub:

  https://github.com/zhangzhixi0305/springboot-study-shiro

Gitee:

  https://gitee.com/zhang-zhixi/springboot-study-shiro

一、Shiro

1.1、权限管理

什么是权限管理

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

什么是身份认证:Authenticator

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

什么是授权:Authorizer

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

1.2、Shiro核心架构

Subject

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

SecurityManager

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

Authenticator

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

Authorizer

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

Realm

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

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

SessionManager

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

SessionDAO

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

CacheManager

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

Cryptography

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

1.3、Shiro中的认证 

1、创建maven项目,引入依赖

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

2、创建shiro配置文件并加入如下配置:shiro.ini

[users]
xiaochen=123
zhangsan=456

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

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

        //1.创建安全管理器对象
        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( "xiaochen1" ,  "123" );

        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、异常

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

1.3.1、自定义Realm

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

Shiro提供的Realm

自定义Realm实现AuthorizingRealm:源码

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

自定义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;

/**
 * @ClassName ConsumerRealm
 * @Author zhangzhixi
 * @Description
 * @Date 2023-03-12 22:17
 * @Version 1.0
 */
public class ConsumerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null ;
    }

    /**
     * 认证
     *
     * @param token 用户名密码token
     * @return 认证实例
     * @throws AuthenticationException 身份验证异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 通过token获取用户名
        String userName = token.getPrincipal().toString();
        // 这里模拟从数据库中验证用户,之后可能从JDBC、Mybatis取
        if ( "xiaochen" . equals (userName)) {
            // 参数1:返回数据库中正确的用户名 参数2:返回数据库中正确密码 参数3:提供当前realm的名字 this.getName();
            return new SimpleAuthenticationInfo(userName,  "1234" ,  this .getName());
        }
        return null ;
    }
}

测试  

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

        //创建securityManager
        DefaultSecurityManager defaultSecurityManager =  new DefaultSecurityManager();
        //设置自定义realm
        defaultSecurityManager.setRealm( new ConsumerRealm());
        //将安全工具类设置安全工具类
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        //创建token
        UsernamePasswordToken token =  new UsernamePasswordToken( "xiaochen" ,  "123" );
        //执行登录
        try {
            subject.login(token);
        }  catch (UnknownAccountException unknownAccountException) {
            unknownAccountException.printStackTrace();
            System. out .println( "用户名验证错误" );
        }  catch (IncorrectCredentialsException incorrectCredentialsException) {
            incorrectCredentialsException.printStackTrace();
            System. out .println( "密码验证错误" );
        }

    }
}

1.3.2、使用MD5+盐(Salt),实现认证

1、创建用户名和密码时候使用的加盐规则

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

/**
 * @ClassName ShiroMd5Utils
 * @Author zhangzhixi
 * @Description
 * @Date 2023-03-12 23:13
 * @Version 1.0
 */
public class ShiroMd5Utils {
    public static void main(String[] args) {
        // 使用用户名做盐值,为了让盐是动态的
        String userName =  "xiaochen" ;

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

        //使用MD5 + salt处理
        Md5Hash md5Hash1 =  new Md5Hash( "123" , userName);
        System. out .println(md5Hash1.toHex());

        //使用md5 + salt + hash散列
        Md5Hash md5Hash2 =  new Md5Hash( "123" , userName, 1024);
        System. out .println(md5Hash2.toHex());
    }
}

2、设置自定义:MD5+盐,的认证代码

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.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 principals) {
        return null ;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取身份信息
        String principal = token.getPrincipal().toString();

        //根据用户名查询数据库
        if ( "xiaochen" . equals (principal)) {
            // 参数1: 数据库用户名
            // 参数2:数据库md5+salt之后的密码
            // 参数3:注册时的随机盐,本例子中使用动态的,用户名作为随机盐
            // 参数4:realm的名字
            return new SimpleAuthenticationInfo(
                    principal,
                    "847b2f7a3705be61f6738b48bed195f8" ,
                    ByteSource.Util.bytes(principal),
                    this .getName());
        }
        return null ;
    }
}

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.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

public class TestCustomerMd5RealmAuthenicator {

    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);
        // 4、设置凭据匹配器
        realm.setCredentialsMatcher(credentialsMatcher);

        defaultSecurityManager.setRealm(realm);
        // 5、将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);

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

        // 7、认证
        UsernamePasswordToken token =  new UsernamePasswordToken( "xiaochen" ,  "123" );
        try {
            subject.login(token);
            System. out .println( "登录成功" );
        }  catch (UnknownAccountException e) {
            e.printStackTrace();
            System. out .println( "用户名错误" );
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System. out .println( "密码错误" );
        }

    }
}

1.4、Shiro中的授权

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

1.4.1、关键对象

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

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

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

1.4.2、授权流程

1.4.3、授权方式

基于角色的访问控制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:*" )){   //资源类型
//对01用户进行修改
}

1.4.4、权限字符串

权限字符串的规则是:

  资源标识符:操作:资源实例标识符

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

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

1.4.5、自定义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;

import java.util.Arrays;

/**
 * 使用自定义realm 加入md5 + salt +hash
 */
public class CustomerMd5Realm extends AuthorizingRealm {


    /**
     * 授权
     * @param principals subject实体
     * @return 授权对象
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 1、获取当前身份信息
        String userName = principals.getPrimaryPrincipal().toString();

        // 2、获取权限对象
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 3、添加角色(从数据库中获取)
        authorizationInfo.addRoles(Arrays.asList("admin", "user"));
        // 4、添加权限:userName这个用户在user模块具有所有权限
        authorizationInfo.addStringPermissions(Arrays.asList("user:*:" + userName));
        return authorizationInfo;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取身份信息
        String principal = token.getPrincipal().toString();

        //根据用户名查询数据库
        if ("xiaochen".equals(principal)) {
            // 参数1: 数据库用户名
            // 参数2:数据库md5+salt之后的密码
            // 参数3:注册时的随机盐,本例子中使用动态的,用户名作为随机盐
            // 参数4:realm的名字
            return new SimpleAuthenticationInfo(
                    principal,
                    "847b2f7a3705be61f6738b48bed195f8",
                    ByteSource.Util.bytes(principal),
                    this.getName());
        }
        return null;
    }
}

1.4.6、测试授权

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.authz.AuthorizationException;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;

import java.util.Arrays;

public class TestCustomerMd5RealmAuthenicator {

    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);
        // 4、设置凭据匹配器
        realm.setCredentialsMatcher(credentialsMatcher);

        defaultSecurityManager.setRealm(realm);
        // 5、将安全管理器注入安全工具
        SecurityUtils.setSecurityManager(defaultSecurityManager);

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

        // 7、认证
        UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "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("************基于角色的控制************");
            // 基于角色的权限控制,是否有admin角色
            System.out.println(subject.hasRole("admin"));
            // 基于角色的权限控制,是否有其中的一种角色信息
            System.out.println(Arrays.toString(subject.hasRoles(Arrays.asList("admin", "super"))));
            // 基于角色的权限控制,是否同时具有提供的所有权限
            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));
            try {
                System.out.println("************基于权限的控制************");
                // 资源标识符:操作:资源类型,比如当前用户是否对user模块具有所有权限就可以这样写(user:*:xiaochen)
                System.out.println(subject.isPermitted("user:*:xiaochen"));
                // 分别具有哪些权限
                System.out.println(Arrays.toString(subject.isPermitted("user:*:xiaochen", "user:update:xiaochen")));
                // 同时具有哪些权限
                System.out.println(subject.isPermittedAll("user:*:xiaochen", "commodity:update:xiaochen"));
            } catch (AuthorizationException e) {
                e.printStackTrace();
                System.out.println("未经授权,无法访问");
            }
        }
    }
}

 1.5、常见过滤器

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

二、SpringBoot整合Shiro(JSP)

2.0、数据库表设计

在使用Apache Shiro框架时,可以设计以下几张表来处理用户权限相关的信息:

  1. 用户表(user)
  2. 角色表(role)
  3. 权限表(permission)
  4. 用户角色关联表(user_role)
  5. 角色权限关联表(role_permission)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 用户表
-- ----------------------------
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;

-- ----------------------------
-- 角色表
-- ----------------------------
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;
 
-- ----------------------------
-- 权限表
-- ----------------------------
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
  `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;
 


-- ----------------------------
-- 用户角色关联表
-- ----------------------------
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;

-- ----------------------------
-- 角色权限关联表
-- ----------------------------
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;
 

 
SET FOREIGN_KEY_CHECKS = 1;
-- 添加用户信息
INSERT INTO `shiro`.`t_user` (`id`, `username`, `password`, `salt`) VALUES (1, 'xiaochen', 'b81f9fd16871f518865c680de016ff5c', '%ut0ZmSH');
INSERT INTO `shiro`.`t_user` (`id`, `username`, `password`, `salt`) VALUES (2, 'zhangsan', 'a7c5d959a1a8ee1b983cd991bb32ad8e', '@MoM7X5G');
-- 添加角色信息
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (1, 'admin');
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (2, 'user');
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES (3, 'product');
-- 添加用户角色信息
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (1, 1, 1);
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (2, 2, 2);
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES (3, 2, 3);

2.1、环境配置

注意:

  启动项目需要在IDEA的配置上加上工作目录:$MODULE_WORKING_DIR$

 

2.1.1、引入依赖

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!--jsp依赖-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.servlet.jsp.jstl</groupId>
            <artifactId>jstl-api</artifactId>
            <version>1.2</version>
        </dependency>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2.1.2、配置项目

server.servlet.context-path=/
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

2.1.3、编写jsp页面

├─resources
│  │  application.properties
│  │  rebel.xml
│  │
│  ├─ static
│  └─templates
└─webapp
        index.jsp  -- 受限资源
        login.jsp -- 公共资源

2.1.4、自定义Realm

public class CustomerRealm extends AuthorizingRealm {
    //处理授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null ;
    }

    //处理认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
            AuthenticationException {
        return null ;
    }
}

2.1.5、配置Shiro

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @ClassName ShiroConfig
 * @Author zhangzhixi
 * @Description
 * @Date 2023-03-15 16:35
 * @Version 1.0
 */
@Configuration
public class ShiroConfig {

    /**
     * shiro过滤器工厂
     * @param securityManager shiro安全管理器
     * @return 过滤器
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier( "securityManager" ) SecurityManager securityManager) {
        // 1、创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean =  new ShiroFilterFactoryBean();
        // 2、注入安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<string, string=""> authMap =  new HashMap<>();
        // 配置公共资源

        // 配置受限资源
        authMap.put( "/index.jsp" ,  "authc" );
        // 如果是受限资源,默认会重定向login.jsp
        //shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap);
        return shiroFilterFactoryBean;
    }

    /**
     * shiro的安全管理器
     * @param realm realm
     * @return shiro的安全管理器
     */
    @Bean( "securityManager" )
    public DefaultWebSecurityManager getSecurityManager(@Qualifier( "realm" ) Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager =  new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 创建自定义realm
     * @return 自定义Realm
     */
    @Bean( "realm" )
    public Realm getRealm() {
        return new CustomerRealm();
    }
}</string,>

2.1.6、测试  

2.2、认证与退出登录(不操作DB)

登录表单:login.jsp

<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
    <label>
        用户名:
        <input type="text" name="username">
    </label> <br/>
    <label>
        密码 :
        <input type="text" name="password">
    </label> <br>
    <input type="submit" value="登录">
</form>

受限页面:index.jsp

<h2>系统主页v1.0(受限页面)</h2>
<label>
    <a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
</label>
<ul>
    <li>用户管理</li>
    <li>商品管理</li>
    <li>订单管理</li>
    <li>物流管理</li>
</ul>
</body>
</html>

配置拦截路径:ShiroConfig

    /**
     * shiro过滤器工厂
     * @param securityManager shiro安全管理器
     * @return 过滤器
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
        // 1、创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 2、注入安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, String> authMap = new HashMap<>();
        // 配置公共资源
        authMap.put("/login.jsp", "anon");
        // 配置受限资源
        authMap.put("/index.jsp", "authc");
        authMap.put("/", "authc");
        // 如果是受限资源,默认会重定向login.jsp
        //shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap);
        return shiroFilterFactoryBean;
    }

自定义Realm处理认证:ConsumerRealm

public class CustomerRealm extends AuthorizingRealm {
    //处理授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        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;
    }
}

web请求:UserController  

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


@Controller
@RequestMapping("user")
public class UserController {
    /**
     * 用来处理身份认证
     *
     * @param username 用户名
     * @param password 密码
     * @return 登录成功返回页面
     */
    @RequestMapping(value = "login",method = RequestMethod.POST)
    public String login(String username, String password) {
        //获取主体对象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username, password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误!");
        }
        return "redirect:/login.jsp";
    }

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

2.3、认证(MD5+Sale)(连接数据库,操作Mybatis)

1、注册页面

<%@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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>注册页面</h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
    <!--指定实际发送的请求方式-->
    <input type="hidden" name="_method" value="PUT">
    <label>
        用户名:
        <input type="text" name="username">
    </label> <br/>
    <label>
        密码 :
        <input type="text" name="password">
    </label> <br>
    <input type="submit" value="注册">
</form>

</body>
</html>

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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
    <label>
        用户名:
        <input type="text" name="username">
    </label> <br/>
    <label>
        密码 :
        <input type="text" name="password">
    </label> <br>
    <input type="submit" value="登录">    <a href="/register.jsp">没有账号?点我注册</a>
</form>

</body>
</html>

3、Controller

@Autowired
    private UserService userService;

    /**
     * 用来处理身份认证
     *
     * @return 登录成功返回页面
     */
    @RequestMapping(value = "login", method = RequestMethod.POST)
    public String login(User user) {
        //获取主体对象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误!");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误!");
        }
        return "redirect:/login.jsp";
    }

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


    /**
     * 用户注册
     * @param user 用户实体
     * @return 注册成功返回登录页面,注册失败返回注册页面
     */
    @PutMapping("/register")
    public String saveUser(User user) {
        int count = userService.saveUser(user);
        if (count == 1) {
            return "redirect:/login.jsp";
        }
        return "redirect:/register.jsp";
    }

    @RequestMapping("save")
    public String save() {
        //获取主体对象
        Subject subject = SecurityUtils.getSubject();
        // 基于角色权限控制
        if (subject.hasRole("admin")) {
            System.out.println("保存订单!");
        } else {
            System.out.println("无权访问!");
        }
        //基于权限字符串判断是否有权限访问
        subject.isPermitted("admin", "user");
        return "redirect:/index.jsp";
    }

4、mapper  

<insert id="saveUser" parameterType="com.zhixi.pojo.User" useGeneratedKeys="true">
    insert into shiro.t_user(username, password, salt)
    values (#{username}, #{password}, #{salt});
</insert>

5、service层,对密码字段进行md5+Sale+Hash

@Autowired
private UserMapper userMapper;
@Override
public int saveUser(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());
    return userMapper.saveUser(user);
}

6、自定义Realm处理登录

 /**
  * 处理认证
  * @param token 用户登录信息
  * @return 身份验证信息
  * @throws AuthenticationException 异常
  */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
     // 获取当前登录的用户名
     String principal = token.getPrincipal().toString();
     // 查询数据库
     User user = userService.selectUserByName(principal);
     return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
 }

7、Shiro配置:配置请求过滤以及自定义Realm使用到的加密算法以及散列次数

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @ClassName ShiroConfig
 * @Author zhangzhixi
 * @Description
 * @Date 2023-03-15 16:35
 * @Version 1.0
 */
@Configuration
public class ShiroConfig {

    /**
     * shiro过滤器工厂
     * @param securityManager shiro安全管理器
     * @return 过滤器
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {
        // 1、创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 2、注入安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map<String, String> authMap = new HashMap<>();
        // 配置公共资源
        authMap.put("/login.jsp", "anon");
        authMap.put("/user/login", "anon");
        authMap.put("/register.jsp", "anon");
        authMap.put("/user/register", "anon");
        // 配置受限资源
        authMap.put("/**", "authc");
        authMap.put("/index.jsp", "authc");
        authMap.put("/", "authc");
        // 如果是受限资源,默认会重定向login.jsp
        //shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(authMap);
        return shiroFilterFactoryBean;
    }

    /**
     * shiro的安全管理器
     * @param realm realm
     * @return shiro的安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager getSecurityManager(@Qualifier("realm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

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

8、测试 

添加用户:

  ShiroConfig->register.jsp-->Controller->入库

用户登录:

  ShiroConfig-->login.jsp-->Controller-->自定义Realm处理认证

2.4、授权(不操作DB)

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

2.4.2、实现授权-自定义realm

/**
 * 处理授权
 * @param principals 权限数据
 * @return 返回的授权信息
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取登录用户名
    String userName = principals.getPrimaryPrincipal().toString();
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    if ("admin".equals(userName)) {
        // 添加角色
        simpleAuthorizationInfo.addRoles(Arrays.asList("admin", "user"));
        // 添加资源
        simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:*:*", "admin:*:*"));
    }
    return simpleAuthorizationInfo;
}

2.4.3、实现授权-页面授权方式

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h2>系统主页v1.0(受限页面)</h2>
<label>
    <a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
</label>
<shiro:hasAnyRoles name="user,admin">
    <li><a href="">用户管理(满足user/admin权限)</a>
        <ul>
            <shiro:hasPermission name="user:add:*">
                <li><a href="">添加</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>
<hr>

<shiro:hasRole name="admin">
<li><a href="">用户管理(满足admin权限)</a>
<ul>
    <li><a href="">商品管理</a></li>
    <li><a href="">订单管理</a></li>
    <li><a href="">物流管理</a></li>
</ul>
</shiro:hasRole>
</body>
</html>

登录后即可看到页面上面展示的列表数据。  

 2.4.4、实现授权-代码授权方式

@RequestMapping("save")
public String save(){
    //获取主体对象
    Subject subject = SecurityUtils.getSubject();
    // 基于角色权限控制
    if (subject.hasRole("admin")) {
        System.out.println("保存订单!");
    }else{
        System.out.println("无权访问!");
    }
    //基于权限字符串判断是否有权限访问
    subject.isPermitted("admin", "user");
    return "redirect:/index.jsp";
}

2.4.5、实现授权-注解授权方式

@RequiresRoles 用来基于角色进行授权

@RequiresPermissions 用来基于权限进行授权

// 用来判断角色:具有 admin/user
@RequiresRoles(value = {"admin", "user"}, logical = Logical.OR)
// 用来判断权限字符串
@RequiresPermissions("user:update:01")
@RequestMapping("saveToPermissions")
public String saveToPermissions() {
    return "redirect:/index.jsp";
}

2.5、授权-基于角色(连接数据库,操作Mybatis)

1、数据库表设计

2、实体

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private String salt;
    /**
     * 用户角色信息
     */
    private List<Role> roles;
}


@Data
public class Role {
    /**
     * 主键
     */
    private Integer id;
    /**
     * 角色名称
     */
    private String name;

}



@Data
public class Perms {
    private Integer id;
    /**
     * 权限字符串
     */
    private String name;

    /**
     * 权限url
     */
    private String url;

}

3、根据用户名查询角色信息  

/**
 * 根据用户名查询角色信息
 * @param username 用户名
 * @return 用户角色集合
 */
List<User> findRolesByUsernameRoles(String username);

4、mapper文件

<resultMap id="userRoleMap" type="com.zhixi.pojo.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <collection property="roles" javaType="list" ofType="com.zhixi.pojo.Role">
        <result column="roleid" property="id"/>
        <result column="rname" property="name"/>
    </collection>
</resultMap>
<select id="findRolesByUsernameRoles" resultMap="userRoleMap">
    SELECT t.id, t.username, ur.roleid, r.`name` rname
    FROM shiro.t_user t
             LEFT JOIN shiro.t_user_role ur ON t.id = ur.userid
             LEFT JOIN shiro.t_role r ON r.id = ur.roleid
    WHERE t.username = #{username}
</select>

5、修改自定义Realm中自定义授权代码 

import com.zhixi.pojo.User;
import com.zhixi.sevice.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Optional;

@Component
public class CustomerRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    /**
     * 处理授权
     *
     * @param principals 权限数据
     * @return 返回的授权信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取登录用户名(从认证那里传过来的)
        User userLogin = (User)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 查询登录用户权限集合
        List<User> userList = userService.findRolesByUsernameRoles(userLogin.getUsername());
        Optional.ofNullable(userList).ifPresent(users -> users.forEach(user -> user.getRoles().forEach(role -> simpleAuthorizationInfo.addRole(role.getName()))));
        // 添加角色权限
        System.out.println(userList);
        return simpleAuthorizationInfo;
    }


    //处理认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("==========================");
        // 获取当前登录的用户名
        String principal = token.getPrincipal().toString();
        // 查询数据库
        User user = userService.selectUserByName(principal);
        return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
    }
}

6、受限页面:index.jsp

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h2>系统主页v1.0(受限页面)</h2>
<label>
    <a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
</label>
<%--获取当前登录的用户名--%>
<shiro:authenticated>
    <%--填写的是放在认证,SimpleAuthenticationInfo中的user数据--%>
    <h3>
        当前登录用户是:<shiro:principal property="username"/>
    </h3>
</shiro:authenticated>
<shiro:hasAnyRoles name="user,admin">
    <li><a href="">用户管理(满足user/admin权限)</a>
        <ul>
            <shiro:hasPermission name="user:add:*">
                <li><a href="">添加</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>
<hr>

<shiro:hasRole name="admin">
<li><a href="">用户管理(满足admin权限)</a>
    <ul>
        <li><a href="">商品管理</a></li>
        <li><a href="">订单管理</a></li>
        <li><a href="">物流管理</a></li>
    </ul>
    </shiro:hasRole>
</body>
</html>

7、测试  

2.6、授权-基于权限(连接数据库,操作Mybatis)

1、DB

-- 权限表数据
INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (1, 'user:*:*', NULL);
INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (2, 'product:*:01', NULL);
INSERT INTO `shiro`.`t_pers` (`id`, `name`, `url`) VALUES (3, 'order:*:*', NULL);
-- 基于角色的权限,一个角色可以有多个权限
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (1, 1, 1);
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (2, 1, 2);
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (3, 1, 3);
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (4, 2, 1);
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES (5, 3, 2);

 2、接口

 /**
  * 根据角色id查询权限集合
  *
  * @param id 角色ID
  * @return 角色的权限集合
  */
 List<Perms> findPermsByRoleId(Integer id);

3、 mapper

<resultMap id="roleToPerms" type="com.zhixi.pojo.Perms">
    <result column="pers_name" property="name"/>
    <result column="pers_url" property="url"/>
</resultMap>
<select id="findPermsByRoleId" resultMap="roleToPerms">
    SELECT role.id,
           role.name   as role_name,
           pers.`name` as pers_name,
           pers.url    as pers_url
    FROM shiro.t_role role
             LEFT JOIN shiro.t_role_perms rp ON role.id = rp.roleid
             LEFT JOIN shiro.t_pers pers ON rp.permsid = pers.id
    WHERE role.id = #{id}
</select>

4、自定义Realm-添加权限信息

/**
 * 处理授权
 *
 * @param principals 权限数据
 * @return 返回的授权信息
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取登录用户名(从认证那里传过来的)
    User userLogin = (User) principals.getPrimaryPrincipal();
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    // 查询登录用户权限集合
    List<User> userList = userService.findRolesByUsernameRoles(userLogin.getUsername());
    Optional.ofNullable(userList).
            ifPresent(users -> users.forEach(user -> {
                user.getRoles().forEach(role -> {
                            // 添加角色信息
                            simpleAuthorizationInfo.addRole(role.getName());
                            // 根据角色ID查询权限集合
                            List<Perms> permsByRoleId = userService.findPermsByRoleId(role.getId());
                            // 添加权限信息
                            Optional.ofNullable(permsByRoleId).ifPresent(perms -> perms.forEach(per -> simpleAuthorizationInfo.addStringPermission(per.getName())));
                        }
                );
            }));
    // 添加角色权限
    return simpleAuthorizationInfo;
}

5、受限页面:index.jsp

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h2>系统主页v1.0(受限页面)</h2>
<label>
    <a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
</label>
<%--获取当前登录的用户名--%>
<shiro:authenticated>
    <%--填写的是放在认证,SimpleAuthenticationInfo中的user数据--%>
    <h3>
        当前登录用户是:<shiro:principal property="username"/>
    </h3>
</shiro:authenticated>
<shiro:hasAnyRoles name="user,admin">
    <li><a href="">用户管理(满足user/admin权限)</a>
        <ul>
            <shiro:hasPermission name="user:add:*">
                <li><a href="">添加</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="order:find:*">
                <li><a href="">查询</a></li>
            </shiro:hasPermission>
        </ul>
    </li>
</shiro:hasAnyRoles>
<hr>

<shiro:hasRole name="admin">
<li><a href="">用户管理(满足admin权限)</a>
    <ul>
        <li><a href="">商品管理</a></li>
        <li><a href="">订单管理</a></li>
        <li><a href="">物流管理</a></li>
    </ul>
    </shiro:hasRole>
</body>
</html>

6、测试

 使用xiaochen登录,xioachen具有admin角色,admin角色具有:user:*:* product:*:01 order:*:*权限

 使用zhangsan登录,zhangsan具有user、product角色。

  user角色具有:user:*:* 

  product角色具有:product:*:01 权限

三、Shiro整合Cache

3.1、整合Ehcache

引入依赖

<!--Shiro整合Ehcache-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>

修改自定义Realm 

/**
 * 创建自定义realm
 * @return 自定义Realm
 */
@Bean("realm")
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;
}

测试

  在第一次加载的时候会加载权限认证授权等数据,后序不会加载。

3.2、整合Redis

1、导入SpringBoot整合Redis依赖

<!--redis整合springboot-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置application.properties

##################Redis配置#############
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=2

3、SpringBoot获取Bean工具类

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

/**
 * @author zhixi
 */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

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


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

4、创建Redis缓存管理器

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

/**
 * @author zhixi
 */
public class RedisCacheManager implements CacheManager {

    /**
     * 缓存
     *
     * @param cacheName 认证或者是授权缓存的统一名称
     * @param <K> k
     * @param <V> v
     * @return 自定义CacheManager
     * @throws CacheException 异常
     */

    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println(cacheName);
        return new RedisCache<K, V>(cacheName);
    }
}

5、Redis缓存实现类

/**
 * 自定义redis缓存的实现
 *
 * @param <k> k
 * @param <v> v
 */
@SuppressWarnings("all")
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 {
        v v = (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
        return v;
    }

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

        /*设置key的序列化方式*/
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

6、设置Shiro盐的序列化方式

  此处需要在认证的时候设置盐的序列化方式,不能够使用之前的ByteSource.Util了,否则会出现登录(序列化)正常,第二次登录(反序列化)失败的错误

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

/**
 * 解决:
 *  shiro 使用缓存时出现:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource
 *  no valid constructor
 *  序列化后,无法反序列化的问题(https://blog.csdn.net/qq_15090197/article/details/108743271)
 */
public class MySimpleByteSource implements ByteSource, Serializable {
    private static final long serialVersionUID = 1L;

    private  byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MySimpleByteSource(){
    }

    public MySimpleByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MySimpleByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MySimpleByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MySimpleByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MySimpleByteSource(File file) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
    }

    public MySimpleByteSource(InputStream stream) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public 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(this.getBytes());
        }
        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if(this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }
    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0? Arrays.hashCode(this.bytes):0;
    }

    @Override
    public boolean equals(Object o) {
        if(o == this) {
            return true;
        } else if(o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }

}

7、设置自定义Realm的管理器为redis

8、测试  

 四、SpringBoot整合验证码功能(非shiro)

1、验证码工具类

import javax.imageio.ImageIO;
import java.awt.*;
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;

/**
 * @创建人 cx
 * @创建时间 2018/11/27 17:36
 * @描述 验证码生成
 */
public class VerifyCodeUtils {

    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static final 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) (0)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            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);
            g.setColor(color);
            g.drawLine(i, (int) d, i, 0);
            g.drawLine(i, (int) d + h1, i, h1);

        }
    }
}
View Code

2、添加验证码请求以及修改登录请求

/**
 * 用来处理身份认证
 *
 * @return 登录成功返回页面
 */
@RequestMapping(value = "login", method = RequestMethod.POST)
public String login(User user, String code, HttpSession session) {
    //比较验证码
    String codes = session.getAttribute("code").toString();
    try {
        if (codes.equalsIgnoreCase(code)) {
            //获取主体对象
            Subject subject = SecurityUtils.getSubject();
            subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
            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("验证码错误");
    }
    return "redirect:/login.jsp";
}

3、登录页面添加验证码标签

<%@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, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
    <label>
        用户名:
        <input type="text" name="username">
    </label> <br/>
    <label>
        密码 :
        <input type="text" name="password">
    </label> <br>
    <label>
        输入验证码:<input type="text" name="code">
        <img src="${pageContext.request.contextPath}/user/getImage" alt="验证码"/>
    </label><br>

    <input type="submit" value="登录">    <a href="/register.jsp">没有账号?点我注册</a>
</form>

</body>
</html>

4、Shiro放行验证码请求

5、测试

五、Shiro整合Thymeleaf

 https://github.com/zhangzhixi0305/springboot-study-shiro/tree/master/shiro-04-thymeleaf

 

posted @ 2023-03-12 20:55  Java小白的搬砖路  阅读(80)  评论(1编辑  收藏  举报