springboot整合shiro理论到实战(由简入凡)

springboot整合shiro

  • 1. Shiro权限实战介绍
    • 1.1 Shiro权限实战介绍
    • 1.2 权限控制和初学JavaWeb处理访问权限控制
  • 2. 大话权限框架核心知识ACL和RBAC
    • 2.1 权限框架设计之ACL和RBAC
    • 2.2 主流权限框架介绍和技术选型
  • 3.Apache Shiro基础概念知识和架构
    • 3.1 Shiro核心知识之架构图交互和四大模块
    • 3.2 用户访问Shrio权限控制运行流程和常见概念
  • 4.Springboot2.x整合 Apache Shiro快速上手实战
    • 4.1 SpringBoot2.x整合Shiro权限认证项目搭建
    • 4.2 快速上手之Shiro认证和授权流程实操
    • 4.3 Shiro认证和授权流程和常用API梳理
  • 5. 详细讲解Apache Shiro realm实战
    • 5.1 Shiro安全数据来源之Realm讲解
    • 5.2 快速上手之Shiro内置IniRealm实操和权限验证api
    • 5.3 快速上手之Shiro内置JdbcRealm实操
    • 5.4 Apache Shiro 自定义Realm实战
    • 5.5 深入Shiro源码解读认证授权流程
  • 6. Shiro权限认证Web案例知识点
    • 6.1 Shiro内置的Filter过滤器
    • 6.2 Shiro的Filter配置路径
    • 6.3 Shiro 数据安全之数据加解密
    • 6.4 Shiro权限控制注解和编程方式
    • 6.5 Shiro 缓存模块
    • 6.6 Shiro Session模块
  • 7. Apache Shiro整合SpringBoot2.x综合案例实战
    • 7.1 Shiro整合SpringBoot2.x案例实战介绍
    • 7.2 基于RBAC权限控制实战之Mysql数据库设计
    • 7.3 SpringBoot2.x项目框架和依赖搭建
    • 7.4 案例实战之权限相关服务接口开发
    • 7.5 案例实战之用户角色权限多对多关联查询SQL
    • 7.6 案例实战自定义CustomRealm实战
    • 7.7 项目实战之ShiroFilterFactoryBean配置实战
    • 7.8 前后端分离自定义SessionManager验证
    • 7.9 API权限拦截验证实战
    • 7.10 加密处理
  • 8. 权限控制综合案例实战进阶
    • 8.1 实战进阶之自定义Shiro Filter过滤器
    • 8.2 实战进阶之自定义Shiro Filter过滤器
    • 8.3 性能提升之Redis整合CacheManager
    • 8.4 性能提升之Redis整合SessionManager
    • 8.5 ShiroConfig常用bean类配置
  • 9. 大话分布式应用的鉴权方式
    • 9.1 单体应用到分布式应用下的鉴权方式介绍
    • 9.2 分布式应用鉴权方式之Shiro整合SpringBoot下自定义SessionId

1. Shiro权限实战介绍

1.1 Shiro权限实战介绍

在阅读之前建议先去官网把一些基本概念看了
http://shiro.apache.org/get-started.html
springboot整合shiro理论到实战(由简入凡)_第1张图片

简介:为什么要学Shiro权限框架

  • 公司新项目需要用到、要么就是需要接收别人的代码、个人技术栈的成长
  • Springboot2.x/SpringMVC + Maven + jdk8 + IDEA/Eclipse
  • 学后水平:掌握Shiro在公司中实际的使用,包括明白里面的核心原理

1.2 权限控制和初学JavaWeb处理访问权限控制

简介:什么是权限控制,初学JavaWeb时处理流程

  • 什么是权限控制:
    • 忽略特别细的概念,比如权限能细分很多种,功能权限,数据权限,管理权限等
    • 理解两个概念:用户和资源,让指定的用户,只能操作指定的资源(CRUD)
  • 初学javaweb时怎么做
    • Filter接口中有一个doFilter方法,自己编写好业务Filter,并配置对哪个web资源进行拦截后
    • 如果访问的路径命中对应的Filter,则会执行doFilter()方法,然后判断是否有权限进行访问对应的资源
    • /api/user/info?id=1
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws Exception {
        HttpServletRequest httpRequest=(HttpServletRequest)request;
        HttpServletResponse httpResponse=(HttpServletResponse)response;
        
        HttpSession session=httpRequest.getSession();
        
        if(session.getAttribute("username")!=null){
            chain.doFilter(request, response);
        } else {
            httpResponse.sendRedirect(httpRequest.getContextPath()+"/login.jsp");
        }
        
    }

2. 大话权限框架核心知识ACL和RBAC

2.1 权限框架设计之ACL和RBAC

  • ACL: Access Control List 访问控制列表

    • 以前盛行的一种权限设计,它的核心在于用户直接和权限挂钩
    • 优点:简单易用,开发便捷
    • 缺点:用户和权限直接挂钩,导致在授予时的复杂性,比较分散,不便于管理
    • 例子:常见的文件系统权限设计, 直接给用户加权限
  • RBAC: Role Based Access Control

    • 基于角色的访问控制系统。权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限
    • 优点:简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来
    • 缺点:开发对比ACL相对复杂
    • 例子:基于RBAC模型的权限验证框架与应用 Apache Shiro、spring Security
  • BAT企业 ACL,一般是对报表系统,阿里的ODPS

  • 总结:不能过于复杂,规则过多,维护性和性能会下降, 更多分类 ABAC、PBAC等

2.2 主流权限框架介绍和技术选型

简介:介绍主流的权限框架 Apache Shiro、spring Security

  • 什么是 spring Security:官网基础介绍

    • 官网:https://spring.io/projects/spring-security

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoCDI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
一句话:Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架

什么是 Apache Shiro:官网基础介绍

  • https://github.com/apache/shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
一句话:Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能

两个优缺点,应该怎么选择

  • Apache ShiroSpring Security , 前者使用更简单

  • Shiro 功能强大、 简单、灵活, 不跟任何的框架或者容器绑定,可以独立运行

  • Spring SecuritySpring 体系支持比较好,脱离Spring体系则很难开发

  • SpringSecutiry 支持Oauth鉴权 https://spring.io/projects/spring-security-oauth,Shiro需要自己实现

  • 总结:两个框架没有谁超过谁,大体功能一致,新手一般先推荐Shiro,学习会容易点

3.Apache Shiro基础概念知识和架构

3.1 Shiro核心知识之架构图交互和四大模块

  • 直达Apache Shiro官网 http://shiro.apache.org/introduction.html
    图片来源于官网
    springboot整合shiro理论到实战(由简入凡)_第2张图片
    重点:记住下面这四个概念
  • 什么是身份认证
    • Authentication,身份认证,一般就是登录
  • 什么是授权
    • Authorization,给用户分配角色或者访问某些资源的权限
  • 什么是会话管理
    • Session Management, 用户的会话管理员,多数情况下是web session
  • 什么是加密
    • Cryptography, 数据加解密,比如密码加解密等

3.2 用户访问Shrio权限控制运行流程和常见概念

简介:讲解用户访问整合Shrio的系统,权限控制的运行流程和Shiro常见名称讲解

  • 直达官网 :http://shiro.apache.org/architecture.html

springboot整合shiro理论到实战(由简入凡)_第3张图片

重点:记住下面这七个概念

  • Subject
    • 我们把用户或者程序称为主题(如用户,第三方服务,cron作业),主题去访问系统或者资源,
      主题(subject)由主体(principal)和凭证(credential)构成,也就是用户名和密码。
  • SecurityManager
    • 安全管理器,Subject的认证和授权都要在安全管理器下进行
  • Authenticator
    • 认证器,主要负责Subject的认证
  • Realm
    • 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库; 通过realm获取认证授权相关信息
  • Authorizer
    • 授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
  • Cryptography
    • 加解密,Shiro的包含易于使用和理解的数据加解密方法,简化了很多复杂的api
  • Cache Manager
    • 缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能

更多资料导航:http://shiro.apache.org/reference.html

4.Springboot2.x整合 Apache Shiro快速上手实战

4.1 SpringBoot2.x整合Shiro权限认证项目搭建

简介:使用SpringBoot2.x整合Shiro权限认证

  • Maven + Jdk8 + Springboot 2.X + IDEA(Eclipse也可以)

步骤极少

  • 创建SpringBoot项目
 <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.1version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.4.0version>
        dependency>
        	<dependency>
			<groupId>org.apache.shirogroupId>
			<artifactId>shiro-springartifactId>
			<version>1.4.0version>
		dependency>

4.2 快速上手之Shiro认证和授权流程实操

简介:Shrio的认证和授权实操

认证:用户身份识别,俗称为用户“登录”
在测试包下创建测试类

package com.xdclass;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/20 10:14
 * @description:
 */
public class QuickStartTest {

    private DefaultSecurityManager securityManager = new DefaultSecurityManager();
    private SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before
    public void init() {

        //初始化数据源,相当于往数据库存两个用户
        simpleAccountRealm.addAccount("user","1234");
        simpleAccountRealm.addAccount("jack","123");
        //构建环境,把realm添加到shiro环境里
        securityManager.setRealm(simpleAccountRealm);
    }

    @Test
    public void testAuthentication() {
    //设置securityManager
        SecurityUtils.setSecurityManager(securityManager);
        //获取用户主体
        Subject subject = SecurityUtils.getSubject();
        //用户输入的用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("user","1234");
        //用户登录
        subject.login(token);
        System.out.println("身份认证结果:"+subject.isAuthenticated());
    }
}

选中loginctrl+alt+B可以看到login方法的实现,可以看到调用的是
Subject subject = securityManager.login(this, token);
这个login再往深了看就是实现身份认证。
springboot整合shiro理论到实战(由简入凡)_第4张图片

运行测试方法,结果为true表示认证成功。
springboot整合shiro理论到实战(由简入凡)_第5张图片

springboot整合shiro理论到实战(由简入凡)_第6张图片
更细节的认证流程参考以下博客,看了直接懂
https://blog.csdn.net/yanluandai1985/article/details/79171389

4.3 Shiro认证和授权流程和常用API梳理

简介:讲解Shiro的授权实操和常用Api 梳理
创建新的测试类 QuickStartTest2

package com.xdclass;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/20 10:14
 * @description:
 */
public class QuickStartTest2 {
    private DefaultSecurityManager securityManager = new DefaultSecurityManager();
    private SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before
    public void init() {

        //初始化数据源
        simpleAccountRealm.addAccount("user","1234","root","admin");
        simpleAccountRealm.addAccount("jack","123","user");
        //构建环境
        securityManager.setRealm(simpleAccountRealm);
    }

    @Test
    public void testAuthentication() {
        SecurityUtils.setSecurityManager(securityManager);
        //用户主题(由principal(主体,即用户名)和credential(凭证,即密码)构成)
        Subject subject = SecurityUtils.getSubject();
        //模拟用户输入的用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("user","1234");
        //用户登录
        subject.login(token);
        System.out.println("身份认证结果:"+subject.isAuthenticated());
        System.out.println("主体是:"+subject.getPrincipal());
        System.out.println("是否有root角色"+subject.hasRole("root"));
        //用户退出登录
        subject.logout();
        System.out.println("身份认证结果:"+subject.isAuthenticated());
    }
}

springboot整合shiro理论到实战(由简入凡)_第7张图片
我们来看看hasRole具体怎么执行的

subject.hasRole("root") 

ctrl+alb+b点进去,可以看到调用的是DelegatingSubject类的抽象方法hasRole
DelegatingSubject
这个hasRole仍然是抽象的,再次进入hasRole,可以看到调用的AuthorizingSecurityManager类的hasRole方法依然是抽象的

在这里插入图片描述
在这个方法里又调用了hasRole方法,再次使用ctrl+alt+b,可以看到有三个实现方法

在这里插入图片描述
选那个呢,选ModularRealmAuthorizer类的,进去之后发现这个hasRole不是抽象的了,说明就是最终的实现方法了。

springboot整合shiro理论到实战(由简入凡)_第8张图片
怎么确定这个方法是属于ModularRealmAuthorizer类呢,可以在DelegatingSubject类的hasRole那里return哪行打个断点进行调试,开始dubug按键F7F8F7F8F8
在这里插入图片描述

springboot整合shiro理论到实战(由简入凡)_第9张图片
hasRole()有返回值,有这个角色返回true,没有返回false.
checkRole()没有返回值。也是用来判断有没有该角色。咋一看好像功能重复,其实它起到了断言的作用,如果用户没有该角色会抛出AuthorizationException,直接不往下走了,而使用hasRole()要达到没有某种角色就抛出异常停止运行需要额外的代码。

5. 详细讲解Apache Shiro realm实战

5.1 Shiro安全数据来源之Realm讲解

简介:讲解shiro默认自带的realm和常见使用方法

  • realm作用:ShiroRealm 获取安全数据
  • 默认自带的realmidea查看realm继承关系
    springboot整合shiro理论到实战(由简入凡)_第10张图片

有默认实现和自定义继承的realm

  • 两个概念

    • principal : 主体的标示,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等
    • credential:凭证, 一般就是密码
    • 所以一般我们说 principal + credential 就账号 + 密码
  • 开发中,往往是自定义realm , 即继承AuthorizingRealm
    为什么这么做,因为AuthorizingRealm支持授权,AuthenticatingRelam支持认证,这不就功能够了吗?
    所以,一般在真实的项目中,我们不会直接实现Realm接口,也不会直接继承最底层的功能贼复杂的IniRealm。我们一般的情况就是直接继承AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法:doGetAuthorizationInfodoGetAuthenticationInfo

5.2 快速上手之Shiro内置IniRealm实操和权限验证api

建议先看下ini配置,一看就懂的那种。IniRealmRealm的最底层实现类,它从后缀为ini或者其他的文件中读取配置,读取程序员设置的用户和角色等相关信息。但是不用设置SecurityManager,因为读取文件后就会有一个默认的。
ini配置文档:http://shiro.apache.org/configuration.html#Configuration-INIConfiguration
先看下上面这个。

下面开始实际案例
首先在test目录建立一个resources目录(要mark directory as test resources root,不然无法新建文件),里面建一个shiro.ini文件。

# 格式 name=password,role1,role2,..roleN
[users]
# user 'root' with password 'secret' and the 'admin' role,
jack = 456, user
# user 'guest' with the password 'guest' and the 'guest' role
xdclass = 123, root,admin

# 格式 role=permission1,permission2...permissionN   也可以用通配符
# 下面配置user的权限为所有video:find,video:buy,如果需要配置video全部操作crud 则 user = video:*
[roles]
user = video:find,video:buy
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *

这份文件设置了两个用户jack 密码是456,拥有user角色;xdclass密码是123,拥有rootadmin角色。video:find,video:buy表示角色user拥有这两个权限,具体命名去看上面的文档。
admin = *表示角色admin拥有所有权限

创建测试类QuickStartTest5_2

package com.xdclass;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/21 11:58
 * @description:
 */
public class QuickStartTest5_2 {
    @Test
    public void testAuthentication() {

        //创建SecurityManager工厂,通过ini配置文件读取
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //通过SecurityManager工厂获取SecurityManager实例
        SecurityManager securityManager = factory.getInstance();
        //通过SecurityUtils工具类将SecurityManager设置到运行环境中
        SecurityUtils.setSecurityManager(securityManager);
        //通过SecurityUtils工具类获取主题
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("jack","456");
        subject.login(token);

        System.out.println("身份认证结果:"+subject.isAuthenticated());
        String username = subject.getPrincipal().toString();
        System.out.println("主体是:"+subject.getPrincipal());
        System.out.println("用户 "+username+" 是否有root角色:"+subject.hasRole("root"));
        System.out.println("用户 "+username+" 是否有user角色:"+subject.hasRole("user"));
        System.out.println("用户 "+username+" 是否有video:find 权限:"+subject.isPermitted("video:find"));
        System.out.println("用户 "+username+" 是否有video:buy 权限:"+subject.isPermitted("video:buy"));
        System.out.println("用户 "+username+" 是否有video:find,video:buy 两个权限:"+subject.isPermittedAll("video:find","video:buy"));
        //用户退出登录
        subject.logout();
        System.out.println("身份认证结果:"+subject.isAuthenticated());

    }

    @Test
    public void testAuthentication2() {
        //创建SecurityManager工厂,通过ini配置文件读取
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //通过SecurityManager工厂获取SecurityManager实例
        SecurityManager securityManager = factory.getInstance();
        //通过SecurityUtils工具类将SecurityManager设置到运行环境中
        SecurityUtils.setSecurityManager(securityManager);
        //通过SecurityUtils工具类获取主题
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("xdclass","123");
        subject.login(token);

        System.out.println("身份认证结果:"+subject.isAuthenticated());
        String username = subject.getPrincipal().toString();
        System.out.println("主体是:"+subject.getPrincipal());
        System.out.println("用户 "+username+" 是否有admin角色:"+subject.hasRole("admin"));
        //在ini设置了admin=* 表示角色admin有所有权限
        System.out.println("用户 "+username+" 是否有video:find,video:buy 两个权限:"+subject.isPermittedAll("video:find","video:buy"));
        //用户退出登录
        subject.logout();
        System.out.println("身份认证结果:"+subject.isAuthenticated());

    }
}

第一个测试方法测试用户jack,第二个测试用户xdclass

5.3 快速上手之Shiro内置JdbcRealm实操

创建一个数据库,导入以下sql文件

DROP TABLE IF EXISTS `roles_permissions`;
CREATE TABLE `roles_permissions` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) DEFAULT NULL,
  `permission` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_roles_permissions` (`role_name`,`permission`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `roles_permissions` WRITE;
/*!40000 ALTER TABLE `roles_permissions` DISABLE KEYS */;

INSERT INTO `roles_permissions` (`id`, `role_name`, `permission`)
VALUES
	(4,'admin','video:*'),
	(3,'role1','video:buy'),
	(2,'role1','video:find'),
	(5,'role2','*'),
	(1,'root','*');

/*!40000 ALTER TABLE `roles_permissions` ENABLE KEYS */;
UNLOCK TABLES;


DROP TABLE IF EXISTS `user_roles`;

CREATE TABLE `user_roles` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `role_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_roles` (`username`,`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_roles` WRITE;
/*!40000 ALTER TABLE `user_roles` DISABLE KEYS */;

INSERT INTO `user_roles` (`id`, `username`, `role_name`)
VALUES
	(1,'jack','role1'),
	(2,'jack','role2'),
	(4,'xdclass','admin'),
	(3,'xdclass','root');

/*!40000 ALTER TABLE `user_roles` ENABLE KEYS */;
UNLOCK TABLES;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `password_salt` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_users_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;

INSERT INTO `users` (`id`, `username`, `password`, `password_salt`)
VALUES
	(1,'jack','123',NULL),
	(2,'xdclass','456',NULL);

/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;

在test->resources目录下创建jdbcrealm.ini文件

#注意 文件格式必须为ini,编码为ANSI
#声明Realm,指定realm类型
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
#配置数据源
#dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource=com.alibaba.druid.pool.DruidDataSource
# mysql-connector-java 5 用的驱动url是com.mysql.jdbc.Driver,mysql-connector-java6以后用的是com.mysql.cj.jdbc.Driver
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
#避免安全警告
dataSource.url=jdbc:mysql://localhost:3306/xdclass_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
dataSource.username=root
dataSource.password=root
#指定数据源
jdbcRealm.dataSource=$dataSource

#开启查找权限, 默认是false,不会去查找角色对应的权限,坑!!!!!
jdbcRealm.permissionsLookupEnabled=true

#指定SecurityManager的Realms实现,设置realms,可以有多个,用逗号隔开
securityManager.realms=$jdbcRealm

新建测试类QuickStartTest5_3

package com.xdclass;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/21 11:58
 * @description:
 */
public class QuickStartTest5_3 {
    @Test
    public void testAuthentication() {

        //创建SecurityManager工厂,通过ini配置文件读取
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:jdbcrealm.ini");
        //通过SecurityManager工厂获取SecurityManager实例
        SecurityManager securityManager = factory.getInstance();
        //通过SecurityUtils工具类将SecurityManager设置到运行环境中
        SecurityUtils.setSecurityManager(securityManager);
        //通过SecurityUtils工具类获取主题
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        subject.login(token);
        System.out.println("身份认证结果:"+subject.isAuthenticated());
        System.out.println( "是否有role1角色:"+subject.hasRole("role1"));
        System.out.println( "是否有role2角色:"+subject.hasRole("role2"));
        System.out.println( "是否有video:find 权限:"+subject.isPermitted("video:find"));
        System.out.println(" 是否有video:buy 权限:"+subject.isPermitted("video:buy"));
    }
}

试着把ini里的jdbcRealm.permissionsLookupEnabled设置为false,发现jack没有对应的权限。

5.4 Apache Shiro 自定义Realm实战

简介:自定义Realm实战基础

  • 步骤:

    • 创建一个类 ,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm
    • 重写授权方法doGetAuthorizationInfo
    • 重写认证方法 doGetAuthenticationInfo
  • 方法:

    • 当用户登陆的时候会调用 doGetAuthenticationInfo
    • 进行权限校验的时候会调用: doGetAuthorizationInfo
  • 对象介绍

    • UsernamePasswordToken : 对应就是 shirotoken中有PrincipalCredential

      • UsernamePasswordToken-》HostAuthenticationToken-》AuthenticationToken
    • SimpleAuthorizationInfo:代表用户角色权限信息

    • SimpleAuthenticationInfo :代表该用户的认证信息

    继承AuthorizingRealm的自定义realm类

package com.xdclass;
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.realm.SimpleAccountRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.*;
/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/21 21:24
 * @description: 自定义realm
 */
public class CustomRealm extends AuthorizingRealm {

    //模拟数据库的用户数据(用户名密码)
    private final Map<String,String> userInfoMap = new HashMap<>();
    {
        userInfoMap.put("jack", "123");
        userInfoMap.put("xdclass", "456");
    }
    //模拟数据库的角色数据
    private   final Map<String, Set<String>> userHasRolesMap = new HashMap<>();
    {
        Set<String> set1 = new HashSet<>();
        set1.add("role1");
        set1.add("role2");
        Set<String> set2 = new HashSet<>();
        set2.add("root");
        set2.add("admin");
        userHasRolesMap.put("jack",set1);
        userHasRolesMap.put("xdclass",set2);
    }
    //模拟数据库的权限数据
    private  final Map<String, Set<String>> roleHasPermissionsMap = new HashMap<>();
    {
        Set<String> set3 = new HashSet<>();
        set3.add("video:look");
        set3.add("video:buy");
        Set<String> set4 = new HashSet<>();
        set4.add("video:delete");
        set4.add("video:query");
        Set<String> set5 = new HashSet<>();
        set5.add("video:delete");
        set5.add("video:query");
        set5.add("video:look");
        set5.add("video:buy");
        roleHasPermissionsMap.put("role1",set3);
        roleHasPermissionsMap.put("role2",set4);
        roleHasPermissionsMap.put("admin",set5);
    }
    /**
     * 权限校验时调用
     * @param principals
     * @return org.apache.shiro.authz.AuthorizationInfo
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        System.out.println("权限doGetAuthorizationInfo");
        //获取用户名
        String name = (String) principals.getPrimaryPrincipal();
        //通过用户名查询用户有哪些角色
        Set<String> userHasRoles = getRolesByUserNameFromDB(name);
        //通过用户名查询有哪些权限
        Set<String> userHasPermissions = getPermissionsByUserNameFromDB(name);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(userHasRoles);
        simpleAuthorizationInfo.setStringPermissions(userHasPermissions);
        return simpleAuthorizationInfo;
    }
    /**
     * 用户登录时调用,用于身份认证
     * @param token 包含用户输入的用户名和密码等信息
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("认证doGetAuthenticationInfo");
        //获取用户输入的用户名
        String name = token.getPrincipal().toString();
        //用户输入的用户名,模拟在数据查询有没有该用户对应的密码,没有则认证不通过,有则认证通过
        String password = getPassWordByUserNameFromDB(name);
        if (password == null || "".equals(password)) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name,password,this.getName());
        //认证通过则返回该该用户的认证信息,包含用户名密码和realm的名称
        return simpleAuthenticationInfo;
    }

    private String getPassWordByUserNameFromDB(String name) {
        return userInfoMap.get(name);
    }
    /**
     *  //模拟从数据库通过用户名查询有哪些权限
     * @param name
     * @return java.util.Set
     **/
    private Set<String> getPermissionsByUserNameFromDB(String name) {

        Set<String> userHasPermissions = new HashSet<>();
        //获取用户拥有的角色,遍历每个角色拥有的权限
        Set<String> roles = userHasRolesMap.get(name);
//        Set permissions = new HashSet<>();
        for (String role : roles) {
            //当某个角色暂时没有赋予权限时会报空指针,所以需要跳出本次循环
            if (roleHasPermissionsMap.get(role) == null)
                continue;
            Set<String> permissions = roleHasPermissionsMap.get(role);
           userHasPermissions.addAll(permissions);

        }
        return userHasPermissions;
    }
    /**
     *  //模拟从数据库通过用户名查询用户有哪些角色
     * @param name
     * @return java.util.Set
     **/
    private Set<String> getRolesByUserNameFromDB(String name) {
        return userHasRolesMap.get(name);
    }
}

测试类

package com.xdclass;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/21 11:58
 * @description: 测试自定义realm
 */
public class QuickStartTest5_4 {
    private DefaultSecurityManager securityManager = new DefaultSecurityManager();
    private CustomRealm customRealm = new CustomRealm();
    @Before
    public void init() {
        //设置realm
        securityManager.setRealm(customRealm);
        //构建环境
        SecurityUtils.setSecurityManager(securityManager);
    }
    //测试认证
    @Test
    public void testAuthentication() {
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //用户输入用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        subject.login(token);
        System.out.println("用户主体是:"+subject.getPrincipal());
        System.out.println("身份认证结果是:"+subject.isAuthenticated());
    }
    @Test
    public void testAuthorization() {
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //用户输入用户名和密码
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        subject.login(token);
        System.out.println("用户主体是:"+subject.getPrincipal());
        System.out.println("身份认证结果是:"+subject.isAuthenticated());
        String name = subject.getPrincipal().toString();
        //权限校验
        //checkRole通过就继续往下走,没通过就抛出异常
        subject.checkRole("role1");
        System.out.println(name+" 是否拥有拥有角色role1:"+subject.hasRole("role1"));
        //isPermitted只有当值为true才会返回值,为false不会返回值
        System.out.println(name+" 是否拥有拥有权限video:delete" +subject.isPermitted("video:delete"));
        subject.logout();
        Subject subject2 = SecurityUtils.getSubject();
        UsernamePasswordToken token1 = new UsernamePasswordToken("xdclass","456");
        subject2.login(token1);
        String name2 = subject2.getPrincipal().toString();
        System.out.println(name2+" 是否拥有拥有角色admin:"+subject2.hasRole("admin"));
        System.out.println(name2+" 是否拥有拥有权限video:look   "
                +subject2.isPermitted("video:look"));
    }
}

5.5 深入Shiro源码解读认证授权流程

简介:Shiro认证和授权流程的源码解读,和断点测试
认证流程解读:

subject.login(usernamePasswordToken);
	DelegatingSubject->login()
	DefaultSecurityManager->login()
	AuthenticatingSecurityManager->authenticate()
	AbstractAuthenticator->authenticate()
	ModularRealmAuthenticator->doAuthenticate()
	ModularRealmAuthenticator->doSingleRealmAuthentication()
	AuthenticatingRealm->getAuthenticationInfo()
	
	//补充:密码验证方法 AuthenticatingRealm-> assertCredentialsMatch()

授权流程解读:

subject.checkRole("admin")
	DelegatingSubject->checkRole()
	AuthorizingSecurityManager->checkRole()
	ModularRealmAuthorizer->checkRole()
	AuthorizingRealm->hasRole()
	AuthorizingRealm->doGetAuthorizationInfo()

6. Shiro权限认证Web案例知识点

6.1 Shiro内置的Filter过滤器

简介:讲解shiro内置的过滤器讲解

  • 核心过滤器类:DefaultFilter, 配置哪个路径对应哪个拦截器进行处理

  • authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter

    • 需要认证登录才能访问
      *userorg.apache.shiro.web.filter.authc.UserFilter
    • 用户拦截器,表示必须存在用户。
  • anonorg.apache.shiro.web.filter.authc.AnonymousFilter

    • 匿名拦截器,不需要登录即可访问的资源,匿名用户或游客,一般用于过滤静态资源。
  • rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter

    • 角色授权拦截器,验证用户是或否拥有角色。
    • 参数可写多个,表示某些角色才能通过,多个参数时写roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
  • permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

    • 权限授权拦截器,验证用户是否拥有权限
    • 参数可写多个,表示需要某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算可以
  • authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

    • httpBasic 身份验证拦截器。
  • logoutorg.apache.shiro.web.filter.authc.LogoutFilter

    • 退出拦截器,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
  • portorg.apache.shiro.web.filter.authz.PortFilter

    • 端口拦截器, 可通过的端口。
  • sslorg.apache.shiro.web.filter.authz.SslFilter

    • ssl拦截器,只有请求协议是https才能通过。

6.2 Shiro的Filter配置路径

简介:Filter配置路径

  • /admin/video /user /pub

  • 路径通配符支持 ?、*、**,注意通配符匹配不 包括目录分隔符“/”

  • 心 可以匹配所有,不加*可以进行前缀匹配,但多个冒号就需要多个 * 来匹配

URL权限采取第一次匹配优先的方式
? : 匹配一个字符,如 /user? , 匹配 /user3,但不匹配/user/;
* : 匹配零个或多个字符串,如 /add* ,匹配 /addtest,但不匹配 /user/1
** : 匹配路径中的零个或多个路径,如 /user/** 将匹 配 /user/xxx 或 /user/xxx/yyy

例子 /user/**=filter1 /user/add=filter2

请求 /user/add 命中的是filter1拦截器

6.3 Shiro 数据安全之数据加解密

简介: 数据安全核心知识,介绍常见的处理办法,Shiro 里的 CredentialsMatcher使用

  • 为啥要加解密

    • 明文数据容易泄露,比如密码明文存储,万一泄露则会造成严重后果
  • 什么是散列算法

    • 一般叫hash,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,适合存储密码,比如MD5
  • 什么是salt(盐) 667788——》aabbcc

    • 如果直接通过散列函数得到加密数据,容易被对应解密网站暴力破解,一般会在应用程序里面加特殊的自动进行处理,比如用户id,例子:加密数据 = MD5(明文密码+用户id), 破解难度会更大,也可以使用多重散列,比如多次md5
  • Shiro里面 CredentialsMatcher,用来验证密码是否正确,
    源码:AuthenticatingRealm -> assertCredentialsMatch()

一般会自定义验证规则
	@Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new 	HashedCredentialsMatcher();
        //散列算法,使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //散列的次数,比如散列两次,相当于 md5(md5("xxx"));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

6.4 Shiro权限控制注解和编程方式

简介:权限角色控制 @RequiresRoles, @RequiresPermissions等注解的使用和编程式控制

配置文件的方式

使用ShiroConfig
注解方式

  • @RequiresRoles(value={"admin", "editor"}, logical= Logical.AND)
    • 需要角色 admin 和 editor两个角色 AND表示两个同时成立
  • @RequiresPermissions (value={"user:add", "user:del"}, logical= Logical.OR)
    • 需要权限 user:add 或 user:del权限其中一个,OR是或的意思。
  • @RequiresAuthentication
    • 已经授过权,调用Subject.isAuthenticated()返回true
  • @RequiresUser
    • 身份验证或者通过记 住我登录的

编程方式

Subject subject = SecurityUtils.getSubject(); 
//基于角色判断
if(subject.hasRole(“admin”)) {
	//有角色,有权限
} else {
	//无角色,无权限
}
//或者权限判断
if(subject.isPermitted("/user/add")){
    //有权限
}else{
    //无权限
}

  • 常见API
    • subject.hasRole("xxx");
    • subject.isPermitted("xxx");
    • subject. isPermittedAll("xxxxx","yyyy");
    • subject.checkRole("xxx"); // 无返回值,可以认为内部使用断言的方式

6.5 Shiro 缓存模块

简介:缓存的作用和Shiro的缓存模块

  • 什么是shiro缓存

    • shiro中提供了对认证信息和授权信息的缓存。
      • 默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的(因为授权的数据量大)
  • AuthenticatingRealmAuthorizingRealm 分别提供了对AuthenticationInfoAuthorizationInfo 信息的缓存。

6.6 Shiro Session模块

简介:Shiro Session模块作用和SessionManager

  • 什么是会话session

    • 用户和程序直接的链接,程序可以根据session识别到哪个用户,和javaweb中的session类似
  • 什么是会话管理器SessionManager

    • 会话管理器管理所有subject的所有操作,是shiro的核心组件

    • 核心方法:

    • //开启一个session
      Session start(SessionContext context);
      //指定Key获取session
      Session getSession(SessionKey key)
      
    • shiro中的会话管理器有多个实现

  • SessionDao 会话存储/持久化

    • SessionDAO

      • AbstractSessionDAO
        • CachingSessionDAO
          • EnterpriseCacheSessionDAO
        • MemorySessionDAO
    • 核心方法

      //创建
      Serializable create(Session session);
      //获取
      Session readSession(Serializable sessionId) throws UnknownSessionException;
      //更新
      void update(Session session) 
      //删除,会话过期时会调用
      void delete(Session session);
      //获取活跃的session
      Collection<Session> getActiveSessions();
      
    • 会话存储有多个实现

    附属资料:

     RememberMe
      1Cookie 写到客户端并 保存
      2、 通过调用subject.login()前,设置 token.setRememberMe(true);
      3、 关闭浏览器再重新打开;会发现浏览器还是记住你的
      4、 注意点:
        - subject.isAuthenticated() 表示用户进行了身份验证登录的,即Subject.login 进行了登录
        - subject.isRemembered() 表示用户是通过RememberMe登录的
        - subject.isAuthenticated()==true,则 subject.isRemembered()==false, 两个互斥
        - 总结:特殊页面或者API调用才需要authc进行验证拦截,该拦截器会判断用户是否是通过 		subject.login()登录,安全性更高,其他非核心接口或者页面则通过user拦截器处理即可
    

7. Apache Shiro整合SpringBoot2.x综合案例实战

7.1 Shiro整合SpringBoot2.x案例实战介绍

简介:介绍Apache Shiro整合SpringBoot2.x综合实战和技术栈

  • 技术选型:前后端分离的权限检验 + SpringBoot2.x + Mysql + Mybatis + Shiro + Redis + IDEA + JDK8

7.2 基于RBAC权限控制实战之Mysql数据库设计

简介:设计案例实战数据库 用户-角色-权限 及关联表

  • 用户
  • 角色
  • 权限

springboot整合shiro理论到实战(由简入凡)_第11张图片
创建数据库xdclass_rbac_shiro,导入sql

DROP TABLE IF EXISTS `permission`;

CREATE TABLE `permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL COMMENT '名称',
  `url` varchar(128) DEFAULT NULL COMMENT '接口路径',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `permission` WRITE;
/*!40000 ALTER TABLE `permission` DISABLE KEYS */;

INSERT INTO `permission` (`id`, `name`, `url`)
VALUES
	(1,'video_update','/api/video/update'),
	(2,'video_delete','/api/video/delete'),
	(3,'video_add','/api/video/add'),
	(4,'order_list','/api/order/list'),
	(5,'user_list','/api/user/list');

/*!40000 ALTER TABLE `permission` ENABLE KEYS */;
UNLOCK TABLES;

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL COMMENT '名称',
  `description` varchar(64) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;

INSERT INTO `role` (`id`, `name`, `description`)
VALUES
	(1,'admin','普通管理员'),
	(2,'root','超级管理员'),
	(3,'editor','审核人员');

/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;

DROP TABLE IF EXISTS `role_permission`;

CREATE TABLE `role_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) DEFAULT NULL,
  `permission_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role_permission` WRITE;
/*!40000 ALTER TABLE `role_permission` DISABLE KEYS */;

INSERT INTO `role_permission` (`id`, `role_id`, `permission_id`)
VALUES
	(1,3,1),
	(2,3,2),
	(3,3,3),
	(4,2,1),
	(5,2,2),
	(6,2,3),
	(7,2,4);

/*!40000 ALTER TABLE `role_permission` ENABLE KEYS */;
UNLOCK TABLES;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(128) DEFAULT NULL COMMENT '用户名',
  `password` varchar(256) DEFAULT NULL COMMENT '密码',
  `create_time` datetime DEFAULT NULL,
  `salt` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;

INSERT INTO `user` (`id`, `username`, `password`, `create_time`, `salt`)
VALUES
	(1,'二当家小D','123456',NULL,NULL),
	(2,'大当家','123456789',NULL,NULL),
	(3,'jack','123',NULL,NULL);

/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `remarks` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;

INSERT INTO `user_role` (`id`, `role_id`, `user_id`, `remarks`)
VALUES
	(1,3,1,'二当家小D是editor'),
	(2,1,3,'jack是admin'),
	(3,2,3,'jack是root'),
	(4,3,3,'jack是editor'),
	(5,1,2,'大当家是admin');

/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;

7.3 SpringBoot2.x项目框架和依赖搭建

简介:使用springboot+mybatis+shiro搭建项目基础框架
创建项目rbac_shiro,添加wb,mysql,mybatis,druid,shiro依赖
springboot整合shiro理论到实战(由简入凡)_第12张图片

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.0version>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
 		
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.1version>
        dependency>

        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.4.0version>
        dependency>
    dependencies>

7.4 案例实战之权限相关服务接口开发

简介:开发用户-角色-权限 相关Service和Dao层

  • 数据库配置
 #============数据库相关配置================
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/xdclass_rbac_shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.username =root
spring.datasource.password =root
#使用阿里巴巴druid数据源,默认使用自带的
#spring.datasource.type =com.alibaba.druid.pool.DruidDataSource
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true

7.5 案例实战之用户角色权限多对多关联查询SQL

简介:开发用户-角色-权限 多对多关联查询SQL

  • 第一步 查询用户对应的角色映射关系

    select * from user u 
    left join user_role ur on u.id=ur.user_id
    where  u.id=3
    
  • 第二步 查询用户对应的角色信息

    select * from user u 
    left join user_role ur on u.id=ur.user_id
    left join role r on ur.role_id = r.id
    where  u.id=3
    
  • 第三步 查询角色和权限的关系

    select * from user u 
    left join user_role ur on u.id=ur.user_id
    left join role r on ur.role_id = r.id
    left join role_permission rp on r.id=rp.role_id
    where  u.id=1
    
  • 第四步 查询角色对应的权限信息(某个用户具备的角色和权限集合)

    select * from user u 
    left join user_role ur on u.id=ur.user_id
    left join role r on ur.role_id = r.id
    left join role_permission rp on r.id=rp.role_id
    left join permission p on rp.permission_id=p.id
    where  u.id=1
    

    首先在项目中创建如下目录
    springboot整合shiro理论到实战(由简入凡)_第13张图片
    controller层代码如下:
    UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    
    /**
     * 根据用户id查询用户简单信息
     *
     * @param id
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/findByUserId")
    public User getUserByUserId(@RequestParam("userId") int id) {

        return userService.findUserSimpleInfoById(id);
    }

    /**
     * 根据用户名查询用户简单信息
     *
     * @param username
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/findByUsername")
    public User getUserByUsername(@RequestParam("username") String username) {

        return userService.findUserSimpleInfoByUsername(username);
    }

    /**
     * 根据用户名和密码查询用户简单信息
     *
     * @param username
     * @param password
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/find")
    public User getUserByUsernameAndPassword(@RequestParam("username") String username, @RequestParam("password") String password) {

        return userService.findByUsernameAndPassword(username, password);
    }


    /**
     * 根据用户id查询用户角色(包含角色的权限)
     *
     * @param id
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/findRoleListByUserId")
    public List<Role> getUserInfoByUserId(@RequestParam("userId") int id) {

        return userService.findRoleListByUserId(id);
    }

    /**
     * 根据用户名查询用户角色(包含角色的权限)
     *
     * @param username
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/findRoleListByUsername")
    public List<Role> getUserInfoByUsername(@RequestParam("username") String username) {

        return userService.findRoleListByUsername(username);
    }

    /**
     * 根据用户ID查询用户全部信息(包括角色和角色有的权限)
     * @param userId
     * @return net.xdclass.rbac_shiro.domain.User
     **/
    @RequestMapping("/findUserAllInfoByUserId")
    public User findUserAllInfoByUserId(@RequestParam("userId") int userId){

        return userService.findUserAllInfoByUserId(userId);
    }

  /**
   * 根据用户名查询用户全部信息(包括角色和角色有的权限)
   * @param username
   * @return net.xdclass.rbac_shiro.domain.User
   **/
    @RequestMapping("/findUserAllInfoByUsername")
    public User findUserAllInfoByUsername(@RequestParam("username") String username){

       return userService.findUserAllInfoByUsername(username);
    }
}

RoleController.java

@RestController
@RequestMapping("/role")
public class RoleController {

    @Autowired
    private RoleService roleService;

    /** 根据角色Id查询权限
     * @param roleId
     * @return java.util.List
     **/
    @RequestMapping("/findPermissionById")
    public List<Permission> getPermission(@RequestParam("roleId") int roleId){
        return roleService.findPermissionListByRoleId(roleId);
    }

    /** 根据角色名查询权限
     * @param roleName
     * @return java.util.List
     **/
    @RequestMapping("/findPermissionByName")
    public List<Permission> getPermission(@RequestParam("roleName") String roleName){
        return roleService.findPermissionListByRoleName(roleName);
    }
}

dao层代码如下
UserMapper.java

public interface UserMapper {

    /**
     * 根据用户id查询用户简单信息
     */
    @Select("select * from user where id=#{id} ")
    User findById(@Param("id") int id);

    /**
     * 根据用户名查询用户简单信息
     */
    @Select("select * from user where username =#{username}  ")
    User findByUsername(@Param("username") String username);

    /**
     * 根据用户名和密码查询用户简单信息
     */
    @Select("select * from user where username =#{username} and password=#{pwd} ")
    User findByUsernameAndPassword(@Param("username") String name, @Param("pwd") String password);

    /**
     * 根据用户ID查询用户拥有的角色(包括角色有的权限)
     */
    @Select("Select r.id id,r.name name,r.description description from  user_role ur left join role r on ur.role_id=r.id where ur.user_id=#{userId} ")
    @Results(value = {
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "name", column = "name"),
            @Result(property = "description", column = "description"),
            @Result(property = "permissionList", column = "id", many = @Many(
                    select = "net.xdclass.rbac_shiro.dao.RoleMapper.findPermissionListByRoleId",
                    fetchType = FetchType.DEFAULT))})
    List<Role> findRoleListByUserId(@Param("userId") int userId);

}

RoleMapper.java

public interface RoleMapper {

    /**
     * 根据角色id查询权限
     */
    @Select("select p.id,p.name,p.url from role_permission rp  left join permission p on rp.permission_id=p.id where rp.role_id=#{roleId} ")
    List<Permission> findPermissionListByRoleId(@Param("roleId") int roleId);

    /**
     * 根据角色名查询权限
     */
    @Select("select p.id,p.name,p.url from role r left join role_permission rp on r.id=rp.role_id left join" +
            " permission p on rp.permission_id=p.id where r.name=#{roleName} ")
    List<Permission> findPermissionListByRoleName(@Param("roleName") String roleName);
}

domain层代码如下(只写属性)
User.java

    private int id;
    private String username;
    private String password;
    private Date createTime;
    private String salt;
    private List<Role> roleList;

Role.java

    private int id;
    private String name;
    private String description;
    private List<Permission> permissionList;

UserRole.java

    private int id;
    private int userId;
    private int roleId;

Permission.java

    private int id;
    private String name;
    private String url;

RolePermission.java

    private int id;
   private int roleId;
   private int permissionId;

service及impl层层代码如下
Uservice.java

public interface UserService {

    /**
     * 根据用户id查询用户简单信息
     */
    User findUserSimpleInfoById(int id);

    /**
     * 根据用户名查询用户简单信息
     */
    User findUserSimpleInfoByUsername(String name);

    /**
     * 根据用户名和密码查询用户简单信息
     */
    User findByUsernameAndPassword(String name, String password);

    /**
     * 根据用户ID查询用户拥有的角色(包括角色拥有的权限)
     */
    List<Role> findRoleListByUserId(int userId);

    /**
     * 根据用户名查询用户拥有的角色(包括角色拥有的权限)
     */
    List<Role> findRoleListByUsername(String name);


    /**
     * 根据用户ID查询用户全部信息(包括角色和角色有的权限)
     */
     User findUserAllInfoByUserId(int userId);


    /**
     * 根据用户名查询用户全部信息(包括角色和角色有的权限)
     */
    User findUserAllInfoByUsername(String name);

}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据用户id查询用户简单信息
     */
    @Override
    public User findUserSimpleInfoById(int id) {
        return userMapper.findById(id);
    }

    /**
     * 根据用户名查询用户简单信息
     */
    @Override
    public User findUserSimpleInfoByUsername(String name) {
        return userMapper.findByUsername(name);
    }

    /**
     * 根据用户名和密码查询用户简单信息
     */
    @Override
    public User findByUsernameAndPassword(String name, String password) {
        return userMapper.findByUsernameAndPassword(name, password);
    }

    /**
     * 根据用户ID查询用户拥有的角色(包括角色拥有的权限)
     */
    @Override
    public List<Role> findRoleListByUserId( int userId){
        return userMapper.findRoleListByUserId(userId);
    }

    /**
     * 根据用户名查询用户拥有的角色(包括角色拥有的权限)
     */
    @Override
    public List<Role> findRoleListByUsername(String name){

        int id = userMapper.findByUsername(name).getId();
        return userMapper.findRoleListByUserId(id);
    }

    /**
     * 根据用户ID查询用户全部信息(包括角色和角色有的权限)
     */
    @Override
    public User findUserAllInfoByUserId( int userId){

        User user = userMapper.findById(userId);
        List<Role> roleList = userMapper.findRoleListByUserId(userId);
        user.setRoleList(roleList);
        return user;
    }

    /**
     * 根据用户名查询用户全部信息(包括角色和角色有的权限)
     */
    @Override
    public User findUserAllInfoByUsername(String name){

        User user = userMapper.findByUsername(name);
        List<Role> roleList = userMapper.findRoleListByUserId(user.getId());
        user.setRoleList(roleList);
        return user;
    }
}

RoleService.java

public interface RoleService {

    /**
     * 根据角色id查询权限
     */
    List<Permission> findPermissionListByRoleId(int roleId);

    /**
     * 根据角色名查询权限
     */
    List<Permission> findPermissionListByRoleName( String roleName);
}

RoleServiceImpl.java

@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    private RoleMapper roleMapper;

    /**
     * 根据角色id查询权限
     */
    @Override
    public List<Permission> findPermissionListByRoleId(int roleId) {

        return roleMapper.findPermissionListByRoleId(roleId);
    }
    /**
     * 根据角色名查询权限
     */
    @Override
    public List<Permission> findPermissionListByRoleName(String roleName) {
        return roleMapper.findPermissionListByRoleName(roleName);
    }
}

7.6 案例实战自定义CustomRealm实战

简介:自定义CustomRealm开发实战

  • 继承 AuthorizingRealm
  • 重写 doGetAuthorizationInfo
  • 重写 doGetAuthenticationInfo

config包建一个CustomRealm

package net.xdclass.rbac_shiro.config;

import net.xdclass.rbac_shiro.domain.Permission;
import net.xdclass.rbac_shiro.domain.Role;
import net.xdclass.rbac_shiro.domain.User;
import net.xdclass.rbac_shiro.service.UserService;
import org.apache.shiro.authc.*;
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.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/24 12:10
 * @description: 自定义realm
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private  UserService userService;
    /**
     * 授权,权限校验时使用
     * @param principals
     * @return org.apache.shiro.authz.AuthorizationInfo
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        System.out.println("授权doGetAuthorizationInfo");
        //获取用户名
        String username = (String) principals.getPrimaryPrincipal();

        List<String> stringRoleList = new ArrayList<>();
        List<String> stringPermissionList = new ArrayList<>();
        //通过用户名查询用户拥有的角色
        User user = userService.findUserAllInfoByUsername(username);
        //获取用户角色列表
        List<Role> roleList = user.getRoleList();
        for (Role role : roleList) {
            stringRoleList.add(role.getName());
            List<Permission> permissionList = role.getPermissionList();
            for (Permission permission : permissionList) {
                if (permission != null) {
                    stringPermissionList.add(permission.getName());
                }
            }
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(stringRoleList);
        simpleAuthorizationInfo.addStringPermissions(stringPermissionList);
        return simpleAuthorizationInfo;
    }

    /**
     * 身份认证,登录时调用
     * @param token 含有用户输入的用户名和密码
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证doGetAuthenticationInfo");
        //从token中获取用户名
        String username = (String) token.getPrincipal();
        //从数据库中取密码
        User user = userService.findUserAllInfoByUsername(username);
        String password = user.getPassword();
        if (password == null || "".equals(password)){
//            return null;
            //如果根据用户输入的用户名,去数据库中没有查询到相关的密码
            throw new UnknownAccountException();
        }

        /**
         * 返回一个从数据库中查出来的的认证信息。用户名为username,密码为从数据库查出对应用户名的password。
         * 封装成当前返回值
         * 接下来shiro框架做的事情就很简单了。
         * 它会拿你的输入的token与当前返回的SimpleAuthenticationInfo对比一下
         * 看看是不是一样,如果用户的帐号密码(即token)与数据库中查出来的数据一样,那么本次登录成功
         * 否则就是你密码输入错误*/
        return new SimpleAuthenticationInfo(username,password,this.getName());
    }
}

7.7 项目实战之ShiroFilterFactoryBean配置实战

简介:讲解ShiroFilterFactoryBean配置实战

shiro大体上的配置就是一个配置类里面有5个bean(如果不是前后端分离则不用设置sessionManager)
1.ShiroFilterFactoryBean 2.SecurityManager 3.CustomSessionManager 4.CustomRealm 5.hashedCredentialsMatcher
也就是说在项目中使用shiro,主要是写好这五个bean。

  • 配置流程和思路从上而下
  • ShiroFilterFactoryBean->设置SecurityManager
  • SecurityManager->设置CustomSessionManagerCustomRealm
  • CustomRealm->设置hashedCredentialsMatcher
    CustomRealmrealm的自定义实现,CustomSessionManagerSessionManager的自定义实现
    ============================
  • SessionManager
    • DefaultSessionManager: 默认实现,常用于javase
    • ServletContainerSessionManager: web环境
    • DefaultWebSessionManager:常用于自定义实现

首先在config下创建SessionManager的自定义实现类CustomSessionManager,继承自DefaultWebSessionManager,具体实现下节写
config包创建配置类ShiroConfig,这个类就是shiro的关键
ShiroConfig代码如下

package net.xdclass.rbac_shiro.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/24 18:01
 * @description: realm和sessionManager都需要自定义实现
 * shiro配置
 * shiroFilterFactoryBean里面-》设置
 *      SecurityManager-里面》设置
 *          CustomSessionManager和
 *          CustomRealm里面-》设置
 *              hashedCredentialsMatcher
 */
@Configuration
public class ShiroConfig {

    /**
     * spring容器中注入securityManager
     * securityManager里面设置CustomSessionManager和CustomRealm
     * @param
     * @return org.apache.shiro.mgt.SecurityManager
     **/
    @Bean
    public SecurityManager securityManager() {
        //web环境下用SecurityManager的web环境下实现类
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //设置sessionManager,非前后端分离下不用设置
        defaultWebSecurityManager.setSessionManager(sessionManager());
         //设置realm,推荐放到后面(某些情况下可能不生效)
        defaultWebSecurityManager.setRealm(customRealm());
        return defaultWebSecurityManager;
    }

    /**
     * spring容器中注入自定义realm
     * @param
     * @return net.xdclass.rbac_shiro.config.CustomRealm
     **/
    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        //设置hashedCredentialsMatcher
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }

    /**
     * spring容器中注入HashedCredentialsMatcher,密码加解密规则
     * @param
     * @return org.apache.shiro.authc.credential.HashedCredentialsMatcher
     **/
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //设置散列算法
        credentialsMatcher.setHashAlgorithmName("md5");
        //设置hash次数
        credentialsMatcher.setHashIterations(2);
        return credentialsMatcher;
    }
    /**
     * spring容器中注入自定义sessionManager
     * @param
     * @return org.apache.shiro.session.mgt.SessionManager
     **/
    @Bean
    public SessionManager sessionManager() {
        return new CustomSessionManager();
    }
    /**
     * filter工厂,设置过滤条件和跳转条件
     * @param securityManager
     * @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
     **/
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //将securityManager设置到环境中(必须设置)
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //需要登录的接口,如果访问某个接口,需要登录却没有登录,则调用此接口(非前后端分离则跳转页面)
        //如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/pub/need_login");

        //登录成功,跳转url,如果前后端分离,则没这个调用
        shiroFilterFactoryBean.setSuccessUrl("");

        //没有权限,未授权会调用此方法,先验证登录-》再验证是否有权限
        shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");

        //拦截路径,坑一:部分路径无法进行拦截,时有时无,因为有的人用hashMap
        //hashMap无序,配置的过滤器需要按照顺序进行拦截,所以用LinkHashedMap
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //value来源于DefaultFilter
        //退出过滤器
        filterChainDefinitionMap.put("/logout","logout");
        //匿名可以访问,也就是游客模式
        filterChainDefinitionMap.put("/pub/**","anon");
        //登录用户才可以访问(即通过认证才能访问/autc开头的路径)
        filterChainDefinitionMap.put("/authc/**","authc");
        //管理员角色才可以访问
        filterChainDefinitionMap.put("/admin/**","roles[admin]");
        
	//默认是同时具有admin和root角色才可以访问(可以自定义过滤器实现只有其中任意一个角色就可以访问,具体请看8.1)
        //filterChainDefinitionMap.put("/admin/**","roles[admin,root]");
        
        //有编辑权限才可以访问
        filterChainDefinitionMap.put("/video/update","perms[video_update]");
        //坑二:过滤链是顺序执行,从上而下,一般/** 放到最后面,如果用hashMap,有可能将/**放到第一个
        //那么变成了任何路径都得要"authc"filter
        filterChainDefinitionMap.put("/**","authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }
}

7.8 前后端分离自定义SessionManager验证

简介:讲解前后端分离情况下自定义SessionManager

自定义sessionManager的实现,在config包下建CustomSessionManager类,并修改ShiroConfig类的sessionManager()方法
修改后如下:

@Bean
    public SessionManager sessionManager() {
        CustomSessionManager customSessionManager = new CustomSessionManager();
        //设置session的过期时间,单位毫秒
        customSessionManager.setGlobalSessionTimeout(30000);
        return customSessionManager;
    }

CustomSessionManager类代码如下:

package net.xdclass.rbac_shiro.config;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/24 21:26
 * @description:
 * 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,
 * 在前后端分离的项目中(也可在移动APP项目使用),非前后端分离就不用设置sessionManager,因为sessionId可以从cookie中取
 * 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。
 */
public class CustomSessionManager extends DefaultWebSessionManager {

    //自定义请求头,key 为"token",value为会话的sessionId
    private static final String AUTHORIZATION = "token";

    public CustomSessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

        //获取请求头的sessionId
        String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);

        if (sessionId != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);

            //标记sessionId有效
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        } else {
            //如果没有携带id参数则按照父类的方式在cookie进行获取
            return super.getSessionId(request, response);
        }
    }
}

7.9 API权限拦截验证实战

**简介: 讲解使用filterChainDefinitionMap控制api访问角色权限 **

localhost:8080/pub/login

localhost:8080/authc/video/play_record

localhost:8080/admin/video/order

localhost:8080/video/update

创建6个类
创建JsonData类用于结果集,创建UserQuery用于包装用户名和密码的实体类,
PubController OrderController AdminController OrderController

具体哪些路径被设置了什么过滤器,参考上面的ShiroConfig类

PubController用于测试anon过滤器,即匿名过滤器任何人都可以访问包括游客,测试路径有:
localhost:8080/pub/need_login
此路径一是用于访问需要登录的接口时会调用,二可以直接调用因为是匿名过滤器
localhost:8080/pub/not_permit
此路径一是用于访问需要某些角色或者权限的接口时会调用,二可以直接调用因为是匿名过滤器
localhost:8080/pub/index首页
localhost:8080/pub/login登录接口
OrderController用于测试authc过滤器,只有登录用户可以访问
AdminController用于测试roles过滤器,只有具有某些角色的用户才可以访问
OrderController用于测试perms过滤器,只有具有某些权限的用户才可以访问

6个类的代码如下
JsonData.java

package net.xdclass.rbac_shiro.domain;
import java.io.Serializable;
/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/25 17:59
 * @description: 结果集
 */
public class JsonData implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer code; // 状态码 0 表示成功,1表示处理中,-1表示失败
    private Object data; // 数据
    private String msg;// 描述
    public JsonData() {
    }
    public JsonData(Integer code, Object data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }
    // 成功,传入数据
    public static JsonData buildSuccess() {
        return new JsonData(0, null, null);
    }
    // 成功,传入数据
    public static JsonData buildSuccess(Object data) {
        return new JsonData(0, data, null);
    }
    // 失败,传入描述信息
    public static JsonData buildError(String msg) {
        return new JsonData(-1, null, msg);
    }
    // 失败,传入描述信息,状态码
    public static JsonData buildError(String msg, Integer code) {
        return new JsonData(code, null, msg);
    }
    // 成功,传入数据,及描述信息
    public static JsonData buildSuccess(Object data, String msg) {
        return new JsonData(0, data, msg);
    }
    // 成功,传入数据,及状态码
    public static JsonData buildSuccess(Object data, int code) {
        return new JsonData(code, data, null);
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    @Override
    public String toString() {
        return "JsonData [code=" + code + ", data=" + data + ", msg=" + msg
                + "]";
    }
}

UserQuery.java

package net.xdclass.rbac_shiro.domain;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/25 21:01
 * @description:
 */
public class UserQuery {
    private String name;
    private String password;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

PubController,java

package net.xdclass.rbac_shiro.controller;

import net.xdclass.rbac_shiro.domain.JsonData;
import net.xdclass.rbac_shiro.domain.UserQuery;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/25 18:01
 * @description: 测试anon过滤器,以/pub开头的路径任何人都能访问包括游客
 */
@RestController
@RequestMapping("/pub")
public class PubController {

    //当游客访问需要登录的接口时会调用下面方法
    @RequestMapping("/need_login")
    public JsonData needLogin() {

        return JsonData.buildSuccess("温馨提示:请使用对应的账号登录", -2);
    }
    //当用户没有权限访问某些接口时而去访问会调用下面方法
    @RequestMapping("/not_permit")
    public JsonData notPermit() {

        return JsonData.buildSuccess("温馨提示:拒绝访问,没权限", -3);
    }

    //首页
    @RequestMapping("/index")
    public JsonData index() {

        List<String> videoList = new ArrayList<>();
        videoList.add("Mysql零基础入门到实战 数据库教程");
        videoList.add("Redis高并发高可用集群百万级秒杀实战");
        videoList.add("Zookeeper+Dubbo视频教程 微服务分布式教程");
        videoList.add("2019年新版本RocketMQ4.X教程消息队列教程");
        videoList.add("微服务SpringCloud+Docker入门到高级实战");
        return JsonData.buildSuccess(videoList);
    }

    /**
     * 登录接口
     *
     * @param userQuery 前端传过来的用户名和密码 json
     * @param request
     * @param response
     * @return net.xdclass.rbac_shiro.domain.JsonData
     **/
    @PostMapping("/login")
    public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response) {

        Map<String, Object> info = new HashMap<>();
        Subject subject = SecurityUtils.getSubject();
        try {
            UsernamePasswordToken token = new UsernamePasswordToken(userQuery.getName(), userQuery.getPassword());
            subject.login(token);
            info.put("msg", "登录成功");
            info.put("session_id", subject.getSession().getId());
            return JsonData.buildSuccess(info);
        } catch (Exception e) {
            e.printStackTrace();
            return JsonData.buildError("账号或密码错误");
        }
    }
}

OrderController.java

package net.xdclass.rbac_shiro.controller;

import net.xdclass.rbac_shiro.domain.JsonData;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/25 21:43
 * @description: 测试authc过滤器,用户播放记录,当用户登录后可以正常访问以/authc开头的接口,未登录则提示需要登录
 */
@RestController
@RequestMapping("/authc")
public class OrderController {

    @RequestMapping("/video/play_record")
    public JsonData findMyPlayRecord() {

        Map<String, String> recordMap = new HashMap<>();
        recordMap.put("SpringBoot入门到高级实战", "第8章第1集");
        recordMap.put("Cloud微服务入门到高级实战", "第4章第10集");
        recordMap.put("分布式缓存Redis", "第10章第3集");
        return JsonData.buildSuccess(recordMap);
    }
}

AdminController.java

package net.xdclass.rbac_shiro.controller;

import net.xdclass.rbac_shiro.domain.JsonData;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/25 22:08
 * @description: 测试roles过滤器,具有admin角色的用户才能访问下面的接口(以/admin开头的接口)
 */
@RestController
@RequestMapping("/admin")
public class AdminController {

    @RequestMapping("/video/order")
    public JsonData findOrder() {

        Map<String, String> recordMap = new HashMap<>();
        recordMap.put("SpringBoot入门到高级实战", "300元");
        recordMap.put("Cloud微服务入门到高级实战", "877元");
        recordMap.put("分布式缓存Redis", "990元");
        return JsonData.buildSuccess(recordMap);
    }
}

OrderController.java

package net.xdclass.rbac_shiro.controller;

import net.xdclass.rbac_shiro.domain.JsonData;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/26 11:54
 * @description: 测试perms过滤器,具有perms[video_update]权限的用户才能访问/video/update接口
 */
@RestController
@RequestMapping("/video")
public class VideoController {

    @RequestMapping("/update")
    public JsonData videoUpdate() {
        return JsonData.buildSuccess("更新视频成功");
    }
}

使用postman进行测试,在测试之前需要将ShiroConfig类中的设置加密注释掉,因为数据库存的密码没有加密,以后加密。
springboot整合shiro理论到实战(由简入凡)_第14张图片
接着启动项目,目前还没有吧登录,
1.直接在路径中输入localhost:8080/authc/video/play_record
springboot整合shiro理论到实战(由简入凡)_第15张图片
可以看到需要登录
2.我们进行登录(注意前面ShiroConfig类设置了session的有效期为20秒)
如下图所示
springboot整合shiro理论到实战(由简入凡)_第16张图片
接着把session_id的值复制,访问上一个接口,要往header里添加key-value,注意时间只有2秒这个sessionid,或者可以去ShiroConfig类里sessionManager方法修改sessionid有效期。
3.如下图所示,添加名为token值为刚刚登录获取的session_id
springboot整合shiro理论到实战(由简入凡)_第17张图片
4.然后测试需要admin角色的路径
去数据库查看,可以看到用户jack具有admin角色
依然是先登录,用户名和密码为jack 123
然后如下图所示,同样需要用到token
springboot整合shiro理论到实战(由简入凡)_第18张图片
接着测试perms过滤器,ShiroConfig类中写了filterChainDefinitionMap.put("/video/update","perms[video_update]");
也就是说访问/video/update路径需要具有video_update权限
测试方法依旧,先登录,拿着token去访问路径
如下图所示:
springboot整合shiro理论到实战(由简入凡)_第19张图片

7.10 加密处理

简介:加密处理
取消掉ShiroConfig类的customRealm的注释
依次将数据库中用户的密码放到下面的测试类运行,将得到的加密密码依次替换数据库的明文密码(记的保存明文密码),然后在postman进行测试登录(请求体里还是写明文密码如123)

    @Test
    public void testMD5(){
		//加密算法
        String algoName = "md5";
        String password = "123";
        //算法名,明文密码,盐,md5次数
        Object object = new SimpleHash(algoName,password,null,2);
        System.out.println(object);
    }

springboot整合shiro理论到实战(由简入凡)_第20张图片

8. 权限控制综合案例实战进阶

8.1 实战进阶之自定义Shiro Filter过滤器

简介:权限控制综合案例 自定义Shiro Filter过滤器

  • 背景知识:

    • /admin/order= roles[“admin, root”] ,表示 /admin/order 这个接口需要用户同时具备 admin 与 root 角色才可访问, 相当于hasAllRoles() 这个判断方法
  • 我们的需求:

    • 订单信息,可以由角色 普通管理员 admin 或者 超级管理员 root 查看

    • 只要用户具备其中一个角色即可
      首先进行未自定义的过滤器的测试,通过数据库可以得知,
      springboot整合shiro理论到实战(由简入凡)_第21张图片
      **大当家只有admin权限,修改ShiroConfig类的方法
      springboot整合shiro理论到实战(由简入凡)_第22张图片
      修改处为红线所示,此时访问admin开头的路径需要同时具备admin和root角色
      postman登录
      springboot整合shiro理论到实战(由简入凡)_第23张图片

    并访问接口localhost:8080/admin/video/order**
    springboot整合shiro理论到实战(由简入凡)_第24张图片
    可以看到访问不了

因为在ShiroConfig类的shiroFactoryBean方法如果设置以下代码
filterChainDefinitionMap.put("/admin/**","roles[admin,root]");
表示如果要访问/admin开头的路径要同时具备admin和root角色

我们如果需要达到这种需求:访问/admin路径只需要具备admin或者root角色
那就需要自定义过滤器
既然要自定义先看它的默认实现
springboot整合shiro理论到实战(由简入凡)_第25张图片
点开RolesAuthorizationFilter类,可以看到实现roles过滤器的方法
springboot整合shiro理论到实战(由简入凡)_第26张图片
可以看到最后用的是hasAllRoles方法
我们自定义过滤器同样继承AuthorizationFilter

  • 自定义Filter 在config包建一个类
    // mappedValue -》roles[admin,root]
    public class CustomRolesOrAuthorizationFilter extends AuthorizationFilter {  
      
        @Override  
         @SuppressWarnings({"unchecked"})
        protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {  
            
            Subject subject = getSubject(req, resp);
    
            //获取当前访问路径所需要的角色集合  
            String[] rolesArray = (String[]) mappedValue;  
      
      		//没有角色限制,有权限访问  
            if (rolesArray == null || rolesArray.length == 0) { 
                return true;  
            }  
    
            //当前subject是rolesArray中的任何一个,则有权限访问  
            for (int i = 0; i < rolesArray.length; i++) {  
                if (subject.hasRole(rolesArray[i])) { 
                    return true;  
                }  
            }  
      
            return false;  
        }  
    }
    

然后修改ShiroConfig类的shiroFactoryBean 方法修改后的代码如下修改的地方如红线所示
springboot整合shiro理论到实战(由简入凡)_第27张图片

Filter类是javax.servlet.Filter;

8.2 实战进阶之自定义Shiro Filter过滤器

简介:自定义Shiro Filter过滤器验证

上节自定义了过滤器roleOrFilter,现在进行测试看能不能达到需求

登录(post)
localhost:8080/pub/login

springboot整合shiro理论到实战(由简入凡)_第28张图片

管理员查看后台信息(get)
localhost:8080/admin/video/order

springboot整合shiro理论到实战(由简入凡)_第29张图片
可以看出访问成功

8.3 性能提升之Redis整合CacheManager

简介:讲解使用Redis整合CacheManager

  • 使用原因?

    • 授权的时候每次都去查询数据库,对于频繁访问的接口,性能和响应速度比较慢,所以使用缓存
  • 步骤

    • 加依赖
  
       <dependency>
  			<groupId>org.crazycakegroupId>
  			<artifactId>shiro-redisartifactId>
  			<version>3.1.0version>
  		dependency>
  • 配置bean

ShiroConfig类中添加以下方法

  /** 配置redisManager
     * @param
     * @return org.crazycake.shiro.RedisManager
     **/
    public RedisManager getRedisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);
        return redisManager;
    }
  /**
     * 配置具体cache实现类
     * @param
     * @return org.crazycake.shiro.RedisCacheManager
     **/
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(getRedisManager());
        return redisCacheManager;
    }

然后在securityManager方法里设置自定义cacheManager

springboot整合shiro理论到实战(由简入凡)_第30张图片

  • 安装redis (如何不会使用redis,则参考网上的博客文章)

    • 建议本地安装,默认是不能外网访问的,然后不是守护进程方式
    • https://www.cnblogs.com/it-cen/p/4295984.html

启动redis服务端和项目
然后进行登录并访问以下接口localhost:8080/admin/video/order
springboot整合shiro理论到实战(由简入凡)_第31张图片
可以看到报了个错

  • 错误如下

    class java.lang.String must has getter for field: authCacheKey or id\nWe need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal. For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.\nDefault value is authCacheKey or id, that means your principal object has a method called \"getAuthCacheKey()\" or \"getId()\""
    

因为用了redis,而rediskey-value类型,而我们没有指定key,所以需要进行改造

  • 改造原有的逻辑,修改缓存的唯一key

    • doGetAuthorizationInfo 方法
      原有:
      	String username = (String)principals.getPrimaryPrincipal();
      	User user = userService.findUserAllInfoByUsername(username);
      
      改为
      
      	User newUser = (User)principals.getPrimaryPrincipal();
          User user = userService.findUserAllInfoByUsername(newUser.getUsername());
      
      doGetAuthenticationInfo方法
      原有:
      return new SimpleAuthenticationInfo(username, user.getPassword(), this.getClass().getName());
      
      改为
      return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
      

这样redis就会将userid作为它的key
再次进行登录并访问刚刚的接口两次,可以看到控制台只打印一次输出,说明redis起作用了
springboot整合shiro理论到实战(由简入凡)_第32张图片
启动redis客户端,输入keys * 可以看到key
在这里插入图片描述

cacheManager方法里添加以下代码

//设置缓存过期时间,单位是秒,20s,
  redisCacheManager.setExpire(20);

8.4 性能提升之Redis整合SessionManager

简介:使用Redis整合SessionManager,管理Session会话

  • 为啥session也要持久化?

    • 开发者重启应用,用户无感知,但是可以继续以原先的状态继续访问
    • 例如我们在登录状态访问淘宝,突然淘宝后台重启应用,我们需要保持登录状态
  • 怎么持久化?

    shiroConfig类中添加自定义sessionDao

/**
     * 自定义sessionDao
     * @param
     * @return org.apache.shiro.session.mgt.eis.SessionDAO
     **/
    public SessionDAO sessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(getRedisManager());
        return redisSessionDAO;
    }

sessionManager里添加代码来配置session持久化
springboot整合shiro理论到实战(由简入凡)_第33张图片
然后启动redis服务端和项目,进行登录
在这里插入图片描述
会提示没有序列化,然后User,Role,Permission类都实现Serializable接口
注意点:

  • DO对象需要实现序列化接口 Serializable

  • logout接口和以前一样调用,请求logout后会删除redis里面的对应的key,即删除对应的token

在实现序列化启动redis服务端和项目,进行登录,然后访问admin开头的接口,然后重启项目,再次访问admin开头的接口发现可以访问的到,说明session持久化成功

8.5 ShiroConfig常用bean类配置

简介:讲解ShiroConfig常用bean 介绍
在一些博客搜索springboot整合shiro经常会看到下面代码
ShiroConfig类将下面三个bean加进去

  • LifecycleBeanPostProcessor

    • 作用:管理shiro一些bean的生命周期 即bean初始化 与销毁
/**
     * 管理shiro一些bean的生命周期 即bean初始化 与销毁
     * @param
     * @return org.apache.shiro.spring.LifecycleBeanPostProcessor
     **/
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
  • AuthorizationAttributeSourceAdvisor

    • 作用:加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
 /**
     * 加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
     * @param
     * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     **/
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
  • DefaultAdvisorAutoProxyCreator

    • 作用: 用来扫描上下文寻找所有的Advistor(通知器), 将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
/**
     * 用来扫描上下文寻找所有的Advistor(通知器), 将符合条件的Advisor应用到切入点的Bean中,
     * 需要在LifecycleBeanPostProcessor创建后才可以创建
     * @param
     * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
     **/
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

9. 大话分布式应用的鉴权方式

9.1 单体应用到分布式应用下的鉴权方式介绍

**简介:介绍单体应用到分布式应用下的鉴权方式 **

电商项目:商品服务,支付服务,用户服务

  • 分布式session
  • UUID
    *JWT
    • https://www.cnblogs.com/cjsblog/p/9277677.html
  • Oauth2.0
    • https://www.cnblogs.com/flashsun/p/7424071.html

9.2 分布式应用鉴权方式之Shiro整合SpringBoot下自定义SessionId

简介:基于原先项目,实现自定义sessionid

  • Shiro 默认的sessionid生成类 类名 SessionIdGenerator有两个实现类
    在这里插入图片描述
    springboot整合shiro理论到实战(由简入凡)_第34张图片

  • 创建一个类,实现 SessionIdGenerator 接口的方法(参照其实现类源码)

import java.io.Serializable;
import java.util.UUID;

/**
 * @author : wly
 * @version : 1.0
 * @date : 2021/11/28 20:43
 * @description: 自定义sessionid生成器
 */
public class CustomSeessionIdGenerator implements SessionIdGenerator {
    @Override
    public Serializable generateId(Session session) {
         return "xdclass"+UUID.randomUUID().toString().replace("-","");
    }
}

然后在ShiroConfig类的sessionDAO方法里设置自定义sessionid生成器
springboot整合shiro理论到实战(由简入凡)_第35张图片
未设置自定义sessionid生成器时sessionid长这样,

37ab859d-5e8c-492c-b949-e7e1a4f5c4bc

自定义之后长这样
springboot整合shiro理论到实战(由简入凡)_第36张图片

下面验证多节点下sessionid是否有效,也就是分布式sessionid
首先如下图所示允许项目并行运行
springboot整合shiro理论到实战(由简入凡)_第37张图片
接着在application.properties里设置server.port为8080,8081,8082,设置一次启动一次项目
在这里插入图片描述
然后用8080端口进行登录,8081,8082端口查询数据,可以看到已经成功,说明sessionid在分布式环境也可以使用成功
springboot整合shiro理论到实战(由简入凡)_第38张图片

  • 没有100%可靠的算法,暴力破解,穷举

    • 限制时间内ip登录错误次数
    • 增加图形验证码,不能过于简单,常用的OCR可以识别验证码
  • 建议:微服务里面,特别是对C端用户的应用,不要做过于复杂的权限校验,特别是影响性能这块

你可能感兴趣的:(springboot,shiro,java,spring,boot,shiro)