主要内容:
1 SpringBoot整合Shiro安全框架;
2 Shiro主要学习内容总结;(执行流程、主要对象接口、注意事项等)
3 Redis实现对权限信息缓存;

! 温馨提示: 想要快速搭Shiro整合项目,请跳至SpringBoot整合Shiro

1 Shiro 介绍

https://baike.baidu.com/item/shiro/17753571

2 主要接口和对象

2.1 Subject 接口

此接口为简单理解为主体,外部应用想要访问我们的服务器时,如果我们使用了Shiro作为安全框架,那么我们的应用就需要与Subject进行交互,Subject会记录我们应用或者用户(可以简单理解为一个请求主体)的信息,它是一个接口,其中提供了认证授权的一些方法.(登录/登出的方法)
我的理解,Subject就相当于一个公司的"HR",她会将"面试者"(应用/用户)的信息和需求记录下来,交给下"领导层".并告诉领导面试者想要执行的操作(登录或者退出)

2.2 SecurityManager 接口

字面意思: 安全管理器;
SecurityManager是一个核心接口,他就是Subject的"领导层",SecurityManager他会对Subject进行管理,此接口可以完成Subject的认证和授权等操作.但其实他是继承了Authenticator(认证接口)和Authorizer(授权接口)和SessionManager(session管理器)三个接口,所以具体的认证和授权流程还是得这三个接口的具体实现来完成.

2.2.1 Authenticator

认证器:
Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,次实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,主要用于应用或者用户的认证.

2.2.2 Authorizer

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

2.2.3 Realm

字面意思: 区域/领域
这里的Realm有点特殊,他在SecurityManager接口中没有以任何形式体现出来,但是我们的认证器Authenticator 和授权器Authorizer,却需要Realm来完成具体认证或者授权数据的具体获取.
Realm是一个接口,其实现子类中有这样几个实现类尤为重要:
AuthenticatingRealm(专门用于认证的realm)、
AuthorizingRealm(专门用于授权的realm)、
SimpleAccountRealm(此realm是上面两个的子类)
如下是SecurityManager、Authenticator、 Authorizer 和这集合Realm之间的关系图:

springboot苹果账号密码登录 springboot认证登录_java

2.2.4 SessionManager

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

2.2.5 SessionDAO

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

2.2.6 CacheManager

CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能.
整合中我们会使用到两种类型的缓存方式都会用到CacheManager;

2.2.7 Cryptography

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

2.3 小结

以上介绍的Shiro接口和实现类,都是核心用到的,其实知道了那样几个借口和实现类干嘛用的我们对shiro执行的大致流程心里也有谱了;

Shiro官网贴出Shiro核心构造图解如下:

springboot苹果账号密码登录 springboot认证登录_redis_02

3 Shiro与Springboot的整合

主要内容:
1 Shiro的具体执行流程;
2 Shiro的整合思路
3 Shiro与SpringBoot的具体整合代码实现
4 具体实现中需要注意的事项

3.1 Shiro执行的大致流程

其实通过在Shiro的具体介绍中也能大概清楚了执行流程,这里以用户为请求为例来进行具体梳理依稀流程:
1) 当用户请求进来时,会携带具体的用户信息(用户名密码等);
2) 请求进来这时候就需要Subject对用户的具体信息进行封装到Token中(我们后期会用到UsernamePasswordToken 很见名知意吧~~~)
3) 这时候SecurityManager就会担起任务来管理我们的Subject来完成操作;
4) SecurityManager拿到后,通过认证器去执行认证流程,认证器的具体认证流程就需要我们使用认证有关的Realm来查询数据库完成数据对比来实现认证;
5) 当认证通过后,接下来就是具体的授权了,这时候,SecurityManager就会通过授权器来完成授权操作,这里校验用户权限的方式可能是多样的,也许是配置文件中已经配置,但大部分情况下也需要查询用户,角色和权限表来给用户来完成具体的授权;当然,授权这部分,授权器也会通过具体的授权Realm来完成授权操作;
6) 授权完成要我们就能根据权限来访问可见页面或者执行具体操作了

补充:
通过上面流程可以很容易猜到:
1 查询数据库中用户信息和权限信息的操作就需要在Realm中来完成;查询操作就需要我们定义Service来查询,所以我们不能使用默认的Realm来操作,我们需要自定义Realm来加入我们自己的逻辑,和需求;
2 查询出来数据的缓存也需要在Realm中完成(因为数据是在Realm中查询出来的)
所以我们需要在Realm中配置开启缓存,让Shiro来完成数据的缓存
3 具体使用Redis来缓存还是使用Shiro自带的缓存来事项就看我们怎么配置了,反正都需要使用到Shiro的CacheManager (如使用Redis就需要自定缓存管理器时实现此接口)
4 从介绍SecurityManager可以看到SimpleAccountRealm是AuthenticatingRealm(专门用于认证的realm)、AuthorizingRealm(专门用于授权的realm)的子类,所以我们在自定义Realm时就可以继承SimpleAccountRealm 来覆盖其中认证和授权相关方法即可完成从数据库中查询数据完成认证授权的先关操作.

3.2 Shiro与SpringBoot的整合思路

1 导入相关依赖和插件(SpringBoot的依赖就不用说了,最起码我们目前知道的Redis依赖、Shiro依赖、数据库相关依赖(mysql驱动程序包,连接池等)、Mybatis依赖等等)
2 创建数据库: 用户,角色,权限一般这三张表不会少的;
3 yml中配置: 一般需要配置端口号、应用名、数据源、连接池(c3p0,druid等)、Redis等
4 准备工作结束,接下来就是具体的代码实现和实现过程中注意事项了

3.3 具体整合实现代码(只板书主要流程代码)

如果使用的是Maven使用的依赖如下:(有些是我后期加上的,为了在整合日志等)
pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>show.mrkay</groupId>
    <artifactId>boot-shiro-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!--java版本,编译版本-->
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <!--SpringBoot父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
    </parent>

    <dependencies>
        <!--springboot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.7.0</version>
        </dependency>
        <!--使用shiro默认的缓存来缓存用户和权限信息-->
        <!-- <dependency>
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-ehcache</artifactId>
             <version>1.7.0</version>
         </dependency>-->

        <!--使用redis作为缓存:来缓存数据用相关信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--引入jsp解析依赖-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!--thymeleaf 用于HTML适用shiro标标签-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

        <!--数据库相关-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <!--日志-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
        </dependency>
        <!--也可以使用log4j-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </dependency>

        <!--lombok插件简化开发-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

数据库sql如下:

/*
SQLyog Enterprise v12.09 (64 bit)
MySQL - 5.5.40 : Database - shiro
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`shiro` /*!40100 DEFAULT CHARACTER SET utf8 */;

/*Table structure for table `t_perms` */

DROP TABLE IF EXISTS `t_perms`;

CREATE TABLE `t_perms` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(80) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `t_perms` */

insert  into `t_perms`(`id`,`name`,`url`) values (1,'user:*:*',''),(2,'product:*:01',NULL),(3,'order:*:*',NULL);

/*Table structure for table `t_role` */

DROP TABLE IF EXISTS `t_role`;

CREATE TABLE `t_role` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `t_role` */

insert  into `t_role`(`id`,`name`) values (1,'admin'),(2,'user'),(3,'product');

/*Table structure for table `t_role_perms` */

DROP TABLE IF EXISTS `t_role_perms`;

CREATE TABLE `t_role_perms` (
  `id` int(6) NOT NULL,
  `roleid` int(6) DEFAULT NULL,
  `permsid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `t_role_perms` */

insert  into `t_role_perms`(`id`,`roleid`,`permsid`) values (1,1,1),(2,1,2),(3,2,1),(4,3,2),(5,1,3);

/*Table structure for table `t_user` */

DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `username` varchar(40) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

/*Data for the table `t_user` */

insert  into `t_user`(`id`,`username`,`password`,`salt`) values (1,'xiaochen','24dce55acdcb3b6363c7eacd24e98cb7','28qr0xu%'),(2,'zhangsan','ca9f1c951ce2bfb5669f3723780487ff','IWd1)#or'),(4,'mrkay','da8a464db04099b5549594a1cc3c8e38','iqhEGj');

/*Table structure for table `t_user_role` */

DROP TABLE IF EXISTS `t_user_role`;

CREATE TABLE `t_user_role` (
  `id` int(6) NOT NULL,
  `userid` int(6) DEFAULT NULL,
  `roleid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `t_user_role` */

insert  into `t_user_role`(`id`,`userid`,`roleid`) values (1,1,1),(2,2,2),(3,2,3),(4,4,2),(5,4,3),(6,4,1);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

application.yml中配置如下:

server:
  port: 9999
#  thymleaf
spring:
  thymeleaf:
    prefix: classpath:/static/
    suffix: .html
  # 数据源
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
    username: root
    password: ****
    druid:
      db-type:com.alibaba.druid.pool.DruidDataSource
  # 配置redis连接
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
# mybatis
mybatis:
  mapper-locations: classpath:show/mrkay/dao/*.xml

创建ShiroConfig.java用于配置我们的SecurityManager、Realm、ShiroFilterFactoryBean(过滤放行拦截路径等)等等

package show.mrkay.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import show.mrkay.shiro.cache.RedisCacheManager;
import show.mrkay.shiro.relms.Md5CustumRealm;

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


/**
 * @ClassName: ShiroConfig
 * @description: Shiro配置类, 用于将shiro用到的安全管理器, 过滤器, realm交给IOC容器
 * @Author: MrKay
 * @Date: 2020/12/8
 */
@Configuration
public class ShiroConfig {
    /**
     * @MethodName: ShiroFilterFactoryBean
     * @Params: [securityManager]
     * @return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
     * @Description: ShiroFilterFactoryBean 用于过滤URL请求路径
     * @Author: MrKay
     * @Date: 2020/12/8
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        //放行访问路径
        map.put("/login.html", "anon");
        map.put("/user/login", "anon");
        map.put("/user/register", "anon");
        map.put("/user/registerview", "anon");
        map.put("/user/getImage", "anon");

        //放行静态资源
        map.put("/js/**", "anon");
        map.put("/css/**", "anon");
        map.put("/img/**", "anon");

        //拦截
        map.put("/**", "authc");

        //默认登录请求路径
        shiroFilterFactoryBean.setLoginUrl("/user/loginview");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }


    /**
     * @MethodName: DefaultSecurityManager
     * @Params: [realm]
     * @return: org.apache.shiro.mgt.DefaultSecurityManager
     * @Description: 注入安全管理器
     * @Author: MrKay
     * @Date: 2020/12/8
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        defaultSecurityManager.setRealm(realm);
        return defaultSecurityManager;
    }

    /**
     * @MethodName: Realm
     * @Params: []
     * @return: org.apache.shiro.realm.Realm
     * @Description: 注入自定义realm
     * @Author: MrKay
     * @Date: 2020/12/8
     */
    @Bean
    public Realm getRealm() {
        Md5CustumRealm md5CustumRealm = new Md5CustumRealm();
        //修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //设置使用MD5加密
        credentialsMatcher.setHashAlgorithmName("MD5");
        //设置散列次数
        credentialsMatcher.setHashIterations(1024);
        md5CustumRealm.setCredentialsMatcher(credentialsMatcher);

        //设置缓存管理器(shiro默认自带缓存管理器)
        // md5CustumRealm.setCacheManager(new EhCacheManager());

        //使用redis自定义缓存管理器来缓存我们的数据
        md5CustumRealm.setCacheManager(new RedisCacheManager());
        md5CustumRealm.setCachingEnabled(true);//开启全局缓存
        // 开启认证缓存
        md5CustumRealm.setAuthenticationCachingEnabled(true);
        md5CustumRealm.setAuthenticationCacheName("authenticationCache");
        //开启授权缓存
        md5CustumRealm.setAuthorizationCachingEnabled(true);
        md5CustumRealm.setAuthorizationCacheName("authorizationCache");

        return md5CustumRealm;
    }

    /**
     * @MethodName: getShiroDialect
     * @Params: []
     * @return: at.pollux.thymeleaf.shiro.dialect.ShiroDialect
     * @Description: 为了时Shiro标签在html中生效需要使用shiro的方言才能使thymeleaf标签生效
     * @Author: MrKay
     * @Date: 2020/12/13
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect getShiroDialect() {
        return new ShiroDialect();
    }

}

注意:上面文件中使用的工具类和自定义Redis缓存等,我会在后面附上代码这里不再进行板书;

自定义Realm

package show.mrkay.shiro.relms;

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.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import show.mrkay.entity.Perms;
import show.mrkay.entity.Role;
import show.mrkay.entity.User;
import show.mrkay.service.RoleService;
import show.mrkay.service.UserService;
import show.mrkay.shiro.salt.CustumByteSource;
import show.mrkay.utils.ApplicationContextUtils;

/**
 * @ClassName: CustumRealm
 * @description: 自定义Realm来来完成对用户用户信息的认证和授权(MD5加密)
 * @Author: MrKay
 * @Date: 2020/12/6
 */
public class Md5CustumRealm extends SimpleAccountRealm {
    /**
     * @MethodName: doGetAuthorizationInfo
     * @Params: [principals]
     * @return: org.apache.shiro.authz.AuthorizationInfo
     * @Description: 授权
     * @Author: MrKay
     * @Date: 2020/12/6
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("==========START==================授权开始================START============");
        //获取身份信息
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        //获取userService
        UserService userService = (UserService) ApplicationContextUtils.getObjectBName("userService");
        RoleService roleService = (RoleService) ApplicationContextUtils.getObjectBName("roleService");
        User rolesUser = userService.findRolesByUserName(primaryPrincipal);
        //授权
        if (!CollectionUtils.isEmpty(rolesUser.getRoles())) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            for (Role role : rolesUser.getRoles()) {
                //授权角色
                simpleAuthorizationInfo.addRole(role.getName());

                Role permsRole = roleService.findPermsByRoleId(role.getId());
                if (!CollectionUtils.isEmpty(permsRole.getPerms())) {
                    for (Perms perm : permsRole.getPerms()) {
                        //授权权限资源
                        simpleAuthorizationInfo.addStringPermission(perm.getName());
                    }
                }
            }
            System.out.println("==========END==================授权结束================END============");
            return simpleAuthorizationInfo;
        }
        return null;
    }

    /**
     * @MethodName: doGetAuthenticationInfo
     * @Params: [token]
     * @return: org.apache.shiro.authc.AuthenticationInfo
     * @Description: 认证
     * @Author: MrKay
     * @Date: 2020/12/6
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("==========START==================认证开始================START============");
        //获取token中用户输入当然凭证
        String principal = (String) token.getPrincipal();
        //获取UserService实例对象
        UserService userService = (UserService) ApplicationContextUtils.getObjectBName("userService");
        User findUser = userService.findByUserName(principal);
        if (!ObjectUtils.isEmpty(findUser)) {
            System.out.println("==========END==================认证结束================END============");
            return new SimpleAuthenticationInfo(findUser.getUsername(), findUser.getPassword(), new CustumByteSource(findUser.getSalt()), this.getName());
        }
        return null;
    }
}

同样:上面代码中使用到的工具类的会在文末附上代码地址;

自定义RedisManager

package show.mrkay.shiro.cache;

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

/**
 * @ClassName: RedisCacheManager
 * @description: Redis缓存管理器, 用于缓存用户信息和权限信息,这里将我们自己的实现的redis缓存管理器
 * @Author: MrKay
 * @Date: 2020/12/17
 */
public class RedisCacheManager implements CacheManager {
    /**
     * @MethodName: getCache
     * @Params: [cacheName] 认证或者授权缓存的统一名称(AuthenticationInfo,AuthorizationInfo)
     * @return: org.apache.shiro.cache.Cache<K, V>
     * @Description:
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("================" + cacheName + "======================");
        return new RedisCache<K, V>(cacheName);
    }
}

RedisCache.java

package show.mrkay.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import show.mrkay.utils.ApplicationContextUtils;

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

/**
 * @ClassName: RedisCache
 * @description: 自定义redis缓存Cache
 * @Author: MrKay
 * @Date: 2020/12/17
 */
public class RedisCache<Key, Value> implements Cache<Key, Value> {
    //创建成员变量
    private String cacheName;

    public RedisCache() {
    }

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

    public String getCacheName() {
        return cacheName;
    }

    public void setCacheName(String cacheName) {
        this.cacheName = cacheName;
    }

    /**
     * @MethodName: get
     * @Params: [key]
     * @return: Value
     * @Description: 获取缓存
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public Value get(Key key) throws CacheException {
        //从redis
        System.out.println("======START============获取缓存{cacheName:" + this.cacheName + ",键:" + key + "}=====================START========");
        RedisTemplate redisTemplate = getRedisTemplate();
        Value value = (Value) redisTemplate.opsForHash().get(this.cacheName, key.toString());
        System.out.println("======END============获取缓存{cacheName:" + this.cacheName + ",键:" + key + "}=====================END========");
        return value;
    }

    /**
     * @MethodName: put
     * @Params: [key, value]
     * @return: Value
     * @Description: 存储缓存
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public Value put(Key key, Value value) throws CacheException {
        System.out.println("======START============设置缓存{cacheName:" + this.cacheName + ",键:" + key + ",值:" + value + "}=====================START========");
        getRedisTemplate().opsForHash().put(this.cacheName, key.toString(), value);
        System.out.println("======END============设置缓存{cacheName:" + this.cacheName + ",键:" + key + ",值:" + value + "}=====================END========");
        return null;
    }

    /**
     * @MethodName: remove
     * @Params: [key]
     * @return: Value
     * @Description: 删除缓存
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public Value remove(Key key) throws CacheException {
        System.out.println("======START============删除指定缓存{cacheName:" + this.cacheName + ",键:" + key + "}=====================START========");
        getRedisTemplate().opsForHash().delete(this.cacheName, key.toString());
        System.out.println("======END============删除指定缓存{cacheName:" + this.cacheName + ",键:" + key + "}=====================END========");
        return null;
    }

    /**
     * @MethodName: clear
     * @Params: []
     * @return: void
     * @Description: 清除缓存, 这里的情书是将所有缓存清除而不是某一个缓存
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public void clear() throws CacheException {
        System.out.println("======START============清空所有缓存{cacheName:" + this.cacheName + "}=====================START========");
        getRedisTemplate().delete(this.cacheName);
        System.out.println("======END============清空所有缓存{cacheName:" + this.cacheName + "}=====================END========");
    }

    /**
     * @MethodName: size
     * @Params: []
     * @return: int
     * @Description: 返回缓存数量大小
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    /**
     * @MethodName: keys
     * @Params: []
     * @return: java.util.Set<Key>
     * @Description: 返回缓存的key的去重集合
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public Set<Key> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    /**
     * @MethodName: values
     * @Params: []
     * @return: java.util.Collection<Value>
     * @Description: 返回所有值的集合
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    @Override
    public Collection<Value> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    /**
     * @MethodName: getRedisTemplate
     * @Params: []
     * @return: org.springframework.data.redis.core.RedisTemplate
     * @Description: 获取redisTemplate用于操作redis;
     * @Author: MrKay
     * @Date: 2020/12/19
     */
    private RedisTemplate getRedisTemplate() {
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getObjectBName("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

主要Controller:

package show.mrkay.controller.sys;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @ClassName: IndexController
 * @description: 首页Controller
 * @Author: MrKay
 * @Date: 2020/12/12
 */
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}
package show.mrkay.controller.sys;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import show.mrkay.entity.User;
import show.mrkay.service.UserService;
import show.mrkay.utils.VerifyCodeUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @ClassName: UserController
 * @description: 用户controller
 * @Author: MrKay
 * @Date: 2020/12/12
 */
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * @MethodName: register
     * @Params: []
     * @return: java.lang.String
     * @Description: 注册页面
     * @Author: MrKay
     * @Date: 2020/12/12
     */
    @RequestMapping("/registerview")
    public String register() {
        return "register";
    }

    /**
     * @MethodName: loginview
     * @Params: []
     * @return: java.lang.String
     * @Description: 登录页面
     * @Author: MrKay
     * @Date: 2020/12/13
     */
    @RequestMapping("/loginview")
    public String loginview() {
        return "login";
    }


    /**
     * @MethodName: getImage
     * @Params: [session, request, response]
     * @return: void
     * @Description: 获取验验证码请求路径
     * @Author: MrKay
     * @Date: 2020/12/12
     */
    @RequestMapping("/getImage")
    public void getImage(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
        session.setAttribute("code", verifyCode);
        ServletOutputStream sos = response.getOutputStream();
        response.setContentType("image/png");
        VerifyCodeUtils.outputImage(100, 30, sos, verifyCode);
    }

    /**
     * @MethodName: register
     * @Params: []
     * @return: java.lang.String
     * @Description: 具体的注册操作, 注册成功跳转到登录页面, 注册失败跳转到注册 页面
     * @Author: MrKay
     * @Date: 2020/12/12
     */
    @RequestMapping("/register")
    public String register(User user) {
        try {
            //注册成功后跳转到登录页面
            userService.register(user);
            return "redirect:/user/loginview";
        } catch (Exception e) {
            e.printStackTrace();
            return "redirect:/user/registerview";
        }
    }

    @RequestMapping("/login")
    public String login(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password, @RequestParam String code, HttpSession session) {
        //从session中获取验证码
        String varifyCode = (String) session.getAttribute("code");
        try {
            if (varifyCode.equalsIgnoreCase(code)) {
                //执行登录的验证
                Subject subject = SecurityUtils.getSubject();
                subject.login(new UsernamePasswordToken(username, password));
                //登录成功重定向到index页面
                return "redirect:/index";
            } 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();
        }
        return "redirect:/user/loginview";
    }

    /**
     * @MethodName: logout
     * @Params: []
     * @return: java.lang.String
     * @Description: 退出登录
     * @Author: MrKay
     * @Date: 2020/12/12
     */
    @RequestMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/user/loginview";
    }


}

登录,注册,首页html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width,mvcUser-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>用户登录</title>
    <link rel="stylesheet" th:href="@{/css/style2.0.css}">
    <style type="text/css">
        ul li {
            font-size: 30px;
            color: #2ec0f6;
        }

        .tyg-div {
            z-index: -1000;
            float: left;
            position: absolute;
            left: 5%;
            top: 20%;
        }

        .tyg-p {
            font-size: 14px;
            font-family: 'microsoft yahei';
            position: absolute;
            top: 135px;
            left: 60px;
        }

        .tyg-div-denglv {
            z-index: 1000;
            float: right;
            position: absolute;
            right: 3%;
            top: 10%;
        }

        .tyg-div-form {
            background-color: #23305a;
            width: 300px;
            height: auto;
            margin: 120px auto 0 auto;
            color: #2ec0f6;
        }

        .tyg-div-form form {
            padding: 10px;
        }

        .tyg-div-form form input[type="text"] {
            width: 270px;
            height: 30px;
            margin: 25px 10px 0px 0px;
        }

        .tyg-div-form form button {
            cursor: pointer;
            width: 270px;
            height: 44px;
            margin-top: 25px;
            padding: 0;
            background: #2ec0f6;
            -moz-border-radius: 6px;
            -webkit-border-radius: 6px;
            border-radius: 6px;
            border: 1px solid #2ec0f6;
            -moz-box-shadow: 0 15px 30px 0 rgba(255, 255, 255, .25) inset,
            0 2px 7px 0 rgba(0, 0, 0, .2);
            -webkit-box-shadow: 0 15px 30px 0 rgba(255, 255, 255, .25) inset,
            0 2px 7px 0 rgba(0, 0, 0, .2);
            box-shadow: 0 15px 30px 0 rgba(255, 255, 255, .25) inset,
            0 2px 7px 0 rgba(0, 0, 0, .2);
            font-family: 'PT Sans', Helvetica, Arial, sans-serif;
            font-size: 14px;
            font-weight: 700;
            color: #fff;
            text-shadow: 0 1px 2px rgba(0, 0, 0, .1);
            -o-transition: all .2s;
            -moz-transition: all .2s;
            -webkit-transition: all .2s;
            -ms-transition: all .2s;
        }
    </style>

<body>
<div class="tyg-div">
    <ul>
        <li>让</li>
        <li>
            <div style="margin-left:20px;">数</div>
        </li>
        <li>
            <div style="margin-left:40px;">据</div>
        </li>
        <li>
            <div style="margin-left:60px;">改</div>
        </li>
        <li>
            <div style="margin-left:80px;">变</div>
        </li>
        <li>
            <div style="margin-left:100px;">生</div>
        </li>
        <li>
            <div style="margin-left:120px;">活</div>
        </li>
    </ul>
</div>
<div id="contPar" class="contPar">
    <div id="page1" style="z-index:1;">
        <div class="title0">SHIRO-DEMO</div>
        <div class="title1">自强 民主 文明 和谐</div>
        <div class="imgGroug">
            <ul>
                <img alt="" class="img0 png" th:src="@{/img/page1_0.png}">
                <img alt="" class="img1 png" th:src="@{/img/page1_1.png}">
                <img alt="" class="img2 png" th:src="@{/img/page1_2.png}">
            </ul>
        </div>
        <img alt="" class="img3 png" th:src="@{/img/page1_3.jpg}">
    </div>
</div>
<div class="tyg-div-denglv">
    <div class="tyg-div-form">
        <form th:action="@{/user/login}" method="post">
            <h2>登录</h2>
            <p class="tyg-p">欢迎登录</p>
            <div style="margin:5px 0px;">
                <input type="text" name="username" placeholder="请输入账号..."/>
            </div>
            <div style="margin:5px 0px;">
                <input type="text" name="password" placeholder="请输入密码..."/>
            </div>
            <div style="margin:5px 0px;">
                <input type="text" name="code" style="width:150px;" placeholder="请输入验证码..."/>
                <img th:src="@{/user/getImage}" style="vertical-align:bottom;" alt="验证码"/>
            </div>
            <button type="submit">登<span style="width:20px;"></span>录</button>
            <a th:href="@{/user/registerview}">注册</a>
        </form>
    </div>
</div>


<script th:src="@{/js/jquery-1.8.0.min.js}"></script>
<script th:src="@{/js/com.js}"></script>
</body>
</html>
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, mvcUser-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>用户注册</title>
</head>
<body>

    <h1>用户注册</h1>


    <form th:action="@{/user/register}" method="post">
        用户名:<input type="text" name="username" > <br/>
        密码  : <input type="text" name="password"> <br>
        <input type="submit" value="立即注册">
    </form>

</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

    <h1>欢迎访问主页</h1><br>

    <!--获取身份信息-->
    <span shiro:principal=""></span><br>

    <!--认证处理-->
    <span shiro:authenticated="">认证通过展示内容</span><br>

    <span shiro:notAuthenticated="">没有认证展示的内容</span><br>

    <!--授权角色-->
    <span shiro:hasRole="admin">admin角色用户能看到此模块</span><br>

    <!--资源授权-->
    <span shiro:hasPermission="product:*:01">具有product操作权限的用户可看到</span><br>


</body>
</html>

4 注意事项

代码板书是按照流程将主要代码做了板书,其实注意事项在代码的注解中也很详细:
这里主要强调以下几点:
1 尽量不要使用Lombok插件,因为这样会增大代码的耦合性,在实际开发中如果你使用了Lombok插件,别人使用你的代码可能也需要安装Lombok插件才能正常运行.具体原因可以谷歌或者百度(和jdk版本等都有很大关系)
2 如果实体使用了实现了序列化接口,那么我们在Ream中设置盐的时候就必须自定义CustumByteSource并实现序列化接口;
3 我们在ShiroConfig.java中使用了MD5加密和散列来保障数据的安全,散列的次数越多,安全性越高

5 代码地址

https://gitee.com/mrkay0313/boot-shiro-template.git

6 说明

项目中还使用了sl4j 和logback来完成日志的记录;具体可以百度:
这里其实不推荐使用这种方式,我推荐使用将日志记录到数据库中:
参看我以前文章可自行实现: