乐趣区

关于java:Shiro入门学习

学习一门新技术的思路

是什么?

是用来干嘛的,利用在哪些地方?

为什么要学?

与它相干的有哪些,须要另外理解的常识?

怎么去学?

文章次要内容:

  • 一、什么是 Shiro——是什么?
  • 二、Shiro 的性能及具体个性(劣势)——是用来干嘛的利用在哪些地方?为什么要学?
  • 三、【拓展】RBAC(以角色为根底的访问控制)——须要另外理解的实践根底
  • 四、与其相干的 SpringSecurity 框架——与它相干的有哪些?
  • 五、第一个简略纯正的 Shiro 程序——怎么学?先会用跑起来再逐渐了解。干!
  • 六、Shiro 的架构(三大外围组件)
  • 七、Shiro 集成 SpringBoot 和数据库应用
  • 八、应用 Shiro 的坑
  • 九、总结

一、什么是 Shiro

官网文档:

http://shiro.apache.org/index.html

官网介绍:

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

  • Apache Shiro 是一个简略易用的Java 平安(权限)框架
  • Shiro 能够非常容易的开发出足够好的利用,其不仅能够用在 JavaSE 环境,也能够用在 JavaEE 环 境。(即能够不须要 Web 和 EJB 等容器反对)
  • 能够提供 认证、受权、加密、会话治理,Web 集成,缓存等。

二、Shiro 的性能个性(劣势)

Primary Concerns:

  • Authentication(认证):用户身份辨认,即一个用户是怎么的角色,是管理员或者普通用户?通常被称为用户“登录”
  • Authorization(受权):访问控制。比方某个用户是否具备某个删除操作的应用权限。
  • Session Management(会话治理):特定于用户的会话治理,即便在非 web 或 EJB 应用程序,可任意应用 Session API。能够响应认证、访问控制,或者 Session 生命周期中产生的事件。
  • Cryptography(加密):在对数据源应用加密算法加密的同时,保障易于应用。

Supporting Features:

  • Web Support(Web 反对):Shiro 的 Web 反对 API 有助于爱护 Web 应用程序。能够非常容易的集成到 Web 环境。
  • Caching(缓存):缓存是 Apache Shiro API 中的第一级,以确保安全操作放弃疾速和高效。比方用户登录后,其用户信息,领有的角色、权限不用每次去查,这样能够提高效率
  • Concurrency(并发性):Apache Shiro 反对多线程利用的并发验证。即,如在一个线程中开启另一个线程,能把权限主动的流传过来。
  • Testing(测试):存在测试反对,可帮忙编写单元测试和集成测试,并确保代码按预期失去保障。
  • Run As(运行形式):容许用户承当(伪装)另一个用户的身份 (如果容许) 的性能,有时在治理计划中很有用。
  • Remember Me:反对提供“Remember Me”服务,获取用户关联信息而无需登录
  • 可将一个或以上用户平安数据源数据组合成一个复合的用户“view”(视图)
  • 反对单点登录 (SSO) 性能

……

但,Shiro 不会去保护用户、保护权限,这些须要咱们本人去设计 / 提供,而后通过相应的接口注入给 Shiro

其中 Authentication(认证), Authorization(受权), Session Management(会话治理), Cryptography(加密) 被 Shiro 框架的开发团队称之为 利用平安的四大基石

长处(为什么要学?):

易于应用、全面、灵便、Web 反对、低耦合、被广泛支持宽泛应用(是 Apache 软件基金会的一部分)

公司要用、面试要问。。。

三、【拓展】RBAC(以角色为根底的访问控制)

1、什么是 RBAC

维基百科:以角色为根底的访问控制 Role-based access controlRBAC),是资讯平安畛域中,一种较新且广为应用的 访问控制机制,不间接赋予使用者权限,而是将权限赋予角色。

RBAC 通过角色关联用户,角色关联权限的形式 间接赋予用户权限。如下图

有人会问为什么不间接给用户调配权限,还多此一举的减少角色这一环节呢?

其实是能够间接给用户调配权限,只是 间接给用户调配权限 ,少了一层关系,扩展性弱了许多, 适宜那些用户数量、角色类型少的平台。

对于通常的零碎,比方:存在多个用户领有雷同的权限,在调配的时候就要别离为这几个用户指定雷同的权限,批改时也要为这几个用户的权限进行一一批改。有了角色后,咱们 只须要为该角色制订好权限后,将雷同权限的用户都指定为同一个角色即可,便于权限治理。

对于批量的用户权限调整,只需调整用户关联的角色权限,无需对每一个用户都进行权限调整,既大幅晋升权限调整的效率,又升高了漏调权限的概率。

小结:

RBAC 的 长处 次要在于易用和高效。给用户受权时只须要对角色受权,而后将相应的角色调配给用户即可;从技术角度讲,思路清晰且易于实现,且前期保护时只须要保护关系模型,显得简略而高效。

RBAC 的 毛病 次要有两个:一个是在进行较为简单的权限校验时须要一直地遍历和递归,会造成肯定的性能影响。另一个是短少数据权限模型,基于 RBAC 来实现数据权限校验比较复杂和低效。

当初 支流的权限管理系统设计大多还是基于 RBAC 模型的,只是依据不同的业务和设计方案,出现不同的显示成果。

2、RBAC 模型的分类

RBAC 模型能够分为:RBAC0、RBAC1、RBAC2、RBAC3 四种。其中 RBAC0 是根底,也是最简略的,相当于底层逻辑,RBAC1、RBAC2、RBAC3 都是以 RBAC0 为根底的降级。

个别状况下,应用 RBAC0 模型就能够满足惯例的权限管理系统设计了。

RBAC0 模型:

最简略的用户、角色、权限模型。是根底,定义了能形成 RBAC 权限控制系统的最小的汇合。

RBAC0 由四局部形成:

  • 用户(User)权限的应用主体
  • 角色(Role)蕴含许可的汇合
  • 会话(Session)绑定用户和角色关系映射的两头通道。而且用户必须通过会话能力给用户设置角色。
  • 许可(Pemission)对特定资源的特定的拜访许可。

RBAC0 对应的表构造:

RBAC0 外面又蕴含了 2 种(用户和角色的表关系):

  1. 用户和角色是多对一关系,即:一个用户只充当一种角色,一种角色能够有多个用户担当。
  2. 用户和角色是多对多关系,即:一个用户可同时充当多种角色,一种角色能够有多个用户担当。

那么,什么时候该应用多对一的权限体系,什么时候又该应用多对多的权限体系呢?

如果零碎性能比拟繁多,应用人员较少,岗位权限绝对清晰且确保不会呈现兼岗的状况,此时能够思考用多对一的权限体系。其余状况 尽量应用多对多的权限体系,保证系统的可扩展性。如:张三既是行政,也负责财务工作,那张三就同时领有行政和财务两个角色的权限。

角色与权限是多对多关系,用户与权限之间也是多对多关系,通过角色间接建设。

3 张根底表:用户、角色、权限

2 张两头表:建设用户与角色的多对多关系,角色与权限的多对多关系。

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
 
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS role;
DROP TABLE IF EXISTS permission;
DROP TABLE IF EXISTS user_role;
DROP TABLE IF EXISTS role_permission;

/* 用户表 */
CREATE TABLE `user` (
  id BIGINT AUTO_INCREMENT,
  NAME VARCHAR(100),
  PASSWORD VARCHAR(100),
  CONSTRAINT pk_users PRIMARY KEY(id)
) CHARSET=utf8 ENGINE=INNODB;

/* 角色表 */
CREATE TABLE role (
  id BIGINT AUTO_INCREMENT,
  NAME VARCHAR(100),
  CONSTRAINT pk_roles PRIMARY KEY(id)
) CHARSET=utf8 ENGINE=INNODB;

/* 权限表 */
CREATE TABLE permission (
  id BIGINT AUTO_INCREMENT,
  NAME VARCHAR(100),
  CONSTRAINT pk_permissions PRIMARY KEY(id)
) CHARSET=utf8 ENGINE=INNODB;

/* 用户角色(关系)表 */
CREATE TABLE user_role (
  uid BIGINT,
  rid BIGINT,
  CONSTRAINT pk_users_roles PRIMARY KEY(uid, rid)
) CHARSET=utf8 ENGINE=INNODB;

/* 角色权限(关系)表 */
CREATE TABLE role_permission (
  rid BIGINT,
  pid BIGINT,
  CONSTRAINT pk_roles_permissions PRIMARY KEY(rid, pid)
) CHARSET=utf8 ENGINE=INNODB;

其余模型这里不做过多深究介绍,其余模型的了解参考此篇文章:

http://www.woshipm.com/pd/1150093.html

3、权限(许可)

权限是 资源 的汇合。

这里的资源指的是软件中所有的内容,包含模块、菜单、页面、字段、操作性能(增删改查)等等。

具体的权限配置上,能够将权限分为:页面权限、操作权限和数据权限

页面权限:所有零碎都是由一个个的页面组成,页面再组成模块,用户是否能看到这个页面的菜单、是否能进入这个页面就称为页面权限。

操作权限:用户但凡在操作系统中的任何动作、交互都是操作权限,如增删改查等。

数据权限:个别业务管理系统,都有数据私密性的要求:哪些人能够看到哪些数据,不能够看到哪些数据。

四、与其相干的 SpringSecurity 框架

与 Shiro 相干的,可能就是 SpringSecurity 了

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


Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

是一个认证和访问控制(受权)框架,来爱护基于 Spring 的应用程序,专一于为 java 利用提认证和受权。

SpringSecurity 属于 Spring 全家桶的一部分,对于 Spring 我的项目来说,其实应用它是讨巧的。

对于 Shiro 和 SpringSecurity 的比照,笔者在网上查问了下材料,并没发现有讲得很好的。

大多说的是因需应用,看应用场景,抉择应用。但在简略性上,还是优先选择 Shiro。本人对这两大框架的利用也并没有太多,因而也无奈讲清。在此只是提一嘴 SpringSecurity,感兴趣的可自行比照钻研。

五、第一个简略纯正的 Shiro 程序

依据官网:

1、新建一个一般的 Maven 我的项目

2、导入对应的 Pom 依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
</dependency>
<!-- Shiro uses SLF4J for logging.  We'll use the'simple' binding
             in this example app.  See http://www.slf4j.org for more info. -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

3、编写 Shiro 配置

https://github.com/apache/shiro/blob/master/samples/quickstart/src/main/resources/log4j.properties

log4j.properties:

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

shiro.ini:

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!";)), and role'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# 
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

4、编写 Quickstart.java

官网上有一个 10 分钟教程,它让咱们先看 Quickstart.java 学习

https://github.com/apache/shiro/blob/master/samples/quickstart/src/main/java/Quickstart.java

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Simple Quickstart application showing how to use Shiro's API.
 * 简略的疾速启动应用程序,演示如何应用 Shiro 的 API。* @since 0.9 RC2
 */
public class Quickstart {

    // 日志门面,默认是 commons-logging
    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityUtils.setSecurityManager(securityManager);
        
        // Now that a simple Shiro environment is set up, let's see what you can do:
        // get the currently executing user:
        // 取得以后执行用户(重要!!)Subject currentUser = SecurityUtils.getSubject();
        // Do some stuff with a Session (no need for a web or EJB container!!!)
        // 不同于 HttpSession,不须要 Web 或 EJB 的容器反对
        Session session = currentUser.getSession();
        // 存值取值
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        // 以后用户身份验证
        if (!currentUser.isAuthenticated()) {
            // 创立标记,其中用户名和明码是读取 shiro.ini 配置文件中的
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            token.setRememberMe(true);
            try {
                // 执行登录操作!!!源码中看不到,但就是这个操作!currentUser.login(token);
            } catch (UnknownAccountException uae) {// 未知用户异样(用户不存在)log.info("There is no user with username of" + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {// 明码不正确
                log.info("Password for account" + token.getPrincipal() + "was incorrect!");
            } catch (LockedAccountException lae) {// 如明码输出谬误次数过多,锁账户
                log.info("The account for username" + token.getPrincipal() + "is locked." +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {// 大异样,相似 java 中的 Exception
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {log.info("May the Schwartz be with you!");
        } else {log.info("Hello, mere mortal.");
        }

        // 粗粒度
        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        // 细粒度
        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {log.info("You are permitted to'drive'the winnebago with license plate (id)'eagle5'." +
                    "Here are the keys - have fun!");
        } else {log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        // 登记
        //all done - log out!
        currentUser.logout();

        // 完结零碎
        System.exit(0);
    }
}

5、main 办法启动测试

运行后果:

打印了一堆默认的日志音讯

2020-09-19 13:09:07,652 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 
2020-09-19 13:09:07,733 INFO [Quickstart] - Retrieved the correct value! [aValue] 
2020-09-19 13:09:07,734 INFO [Quickstart] - User [lonestarr] logged in successfully. 
2020-09-19 13:09:07,734 INFO [Quickstart] - May the Schwartz be with you! 
2020-09-19 13:09:07,734 INFO [Quickstart] - You may use a lightsaber ring.  Use it wisely. 
2020-09-19 13:09:07,735 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  Here are the keys - have fun! 
  • 若启动报错,Pom 中尝试导入后
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.2</version>
</dependency>
  • 若启动后,什么都没打印,尝试 Pom 依赖中删掉所有的 <scope>runtime</scope> 作用域

6、加深了解

①类的形容

/**
* Simple Quickstart application showing how to use Shiro's API. 
* 简略的疾速启动应用程序,演示如何应用 Shiro 的 API。*/

②通过工厂模式创立 SecurityManager 的实例对象

// 读取类门路下的 shiro.ini 文件
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
Factory<SecurityManager> factory = new
IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 曾经建设了一个简略的 Shiro 环境

③获取以后的 Subject

// get the currently executing user: 获取以后正在执行的用户
Subject currentUser = SecurityUtils.getSubject();

④session 的操作

// 用会话做一些事件(不须要 web 或 EJB 容器!!!)
// Do some stuff with a Session (no need for a web or EJB container!!!)
// 存值取值
Session session = currentUser.getSession(); // 取得 session
session.setAttribute("someKey", "aValue"); // 设置 Session 的值!String value = (String) session.getAttribute("someKey"); // 从 session 中获取
值
if (value.equals("aValue")) { // 判断 session 中是否存在这个值!log.info("==Retrieved the correct value! [" + value + "]");
}

⑤用户认证

// 测试以后的用户是否曾经被认证,即是否曾经登录!// let's login the current user so we can check against roles and
permissions:
if (!currentUser.isAuthenticated()) {// isAuthenticated(); 是否认证
    // 将用户名和明码封装为 UsernamePasswordToken;UsernamePasswordToken token = new UsernamePasswordToken("lonestarr",
                                                            "vespa");
    token.setRememberMe(true); // 记住我性能
    try {currentUser.login(token); // 执行登录,能够登录胜利的!} catch (UnknownAccountException uae) { 
        // 如果没有指定的用户,则 UnknownAccountException 异样
        log.info("There is no user with username of" +
                     token.getPrincipal());
    } catch (IncorrectCredentialsException ice) { // 明码不正确的异样!log.info("Password for account" + token.getPrincipal() + " was
                 incorrect!");
    } catch (LockedAccountException lae) { // 用户被锁定的异样
        log.info("The account for username" + token.getPrincipal() + "is locked."              +"Please contact your administrator to unlock it.");
    }
    // ... catch more exceptions here (maybe custom ones specific toyour application?
    catch (AuthenticationException ae) { // 认证异样,下面的异样都是它的子类
    //unexpected condition? error?
    }
}
   //             
   log.info("User [" + currentUser.getPrincipal() + "] logged insuccessfully.");

⑥角色查看

//test a role:
// 是否存在某一个角色
if (currentUser.hasRole("schwartz")) {log.info("May the Schwartz be with you!");
} else {log.info("Hello, mere mortal.");
}

⑦权限查看,粗粒度

// 测试用户是否具备某一个权限,行为
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {log.info("You may use a lightsaber ring. Use it wisely.");
} else {log.info("Sorry, lightsaber rings are for schwartz masters only.");
}

⑧权限查看,细粒度

// 测试用户是否具备某一个权限,行为,比下面更加的具体!//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
    log.info("You are permitted to'drive' the winnebago with license
             plate (id) 'eagle5'. "+"Here are the keys - have fun!");
} else {log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

⑨登记操作

// 执行登记操作!//all done - log out!
currentUser.logout();

⑩退出零碎

System.exit(0);

六、Shiro 的架构(三大外围组件)

Shiro 架构蕴含三个次要的理念:Subject、SecurityManagerRealm

  • Subject:代表以后用户,Subject 能够是一个人,但也能够是第三方服务、守护过程帐户、时钟守护工作或者其它以后和软件交互的任何事件(网络爬虫、机器人等)。Subject 与利用代码间接交互,也就是说 Shiro 的对外 API 外围就是 Subject。SecurityManageer 才是理论的执行者。
  • SecurityManager:平安管理器,治理所有 Subject,SecurityManager 是 Shiro 架构的外围,负责与 Shiro 的其余组件进行交互,它相当于 SpringMVC 的 DispatcherServlet 的角色。
  • Realms:域,但这不容易了解。它 用于进行权限信息的验证,须要咱们本人实现 (能够用 JDBC 实现,也能够是内存实现)。SecurityManager 要验证用户身份,那么它须要从 Realm 获取相应的用户进行比拟,来确定用户的身份是否非法;也须要从 Realm 失去用户相应的角色、权限,进行验证用户的操作是否可能进行。所以Realm 实质上是一个特定的平安 DAO:它封装与数据源连贯的细节,失去 Shiro 所需的相干的数据。 在配置 Shiro 的时候,你必须指定至多一个 Realm 来实现认证(authentication)和 / 或受权(authorization)。

咱们须要实现 Realms 的 Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是受权访问控制,用于对用户进行的操作受权,证实该用户是否容许进行以后操作,如拜访某个链接,某个资源文件等。

外部架构

  • Authenticator:负责 Subject 认证,是一个扩大点,能够自定义实现;能够应用认证策略
    (Authentication Strategy),即什么状况下算用户认证通过了
  • Authorizer:受权器,即拜访控制器,用来决定主体是否有权限进行相应的操作;即管制着用户能
    拜访利用中的那些性能
  • SessionManager:治理 Session 生命周期的组件,而 Shiro 并不仅仅能够用在 Web 环境,也能够用
    在一般的 JavaSE 环境中
  • CacheManager:缓存控制器,来治理如用户,角色,权限等缓存的;因为这些数据基本上很少改
    变,放到缓存中后能够进步拜访的性能
  • Cryptography:明码模块,Shiro 进步了一些常见的加密组件用于明码加密,解密等。

Shiro 中其余的一些概念:

  • Principals:个别指用户名等,惟一表明 Subject 身份也就是以后用户身份的货色;
  • Credentials:凭证,个别指明码,对以后登陆用户进行验证;

七、Shiro 集成 SpringBoot 和数据库应用

场景工作:

  • 用户拜访注册页面,输出用户名明码,点击提交注册后,用 shiro 进行盐值 MD5加密 后保留到数据库——注册
  • 用户拜访登录页面,进行登录时通过 shiro 进行用户 认证——登录认证
  • 登录胜利则显示以后登录的用户名(session 治理)——登录认证
  • 进入首页,拜访其余页面须要认证登录——页面拦挡登录

第一局部:简略环境搭建

1、建库建表 sql

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
/* 用户表 */
/*salt 是盐,用来和 shiro 联合的时候,加密用的 */
CREATE TABLE `user`(id INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(255)DEFAULT NULL,
  `password` VARCHAR(255)DEFAULT NULL,
  salt VARCHAR(255)DEFAULT NULL,
  PRIMARY KEY(id)
)ENGINE=INNODB DEFAULT CHARSET=utf8;

2、新建一个 SpringBoot 我的项目

勾选 lombok、SpringWeb、thymeleaf、JDBC API 以及 MySQL Driver

3、导入 Pom 依赖,shiro 两种形式

第一种:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

第二种:

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.6.0</version>
</dependency>

其中第二种会报错,在开端第九局部会具体阐明呈现了什么谬误以及怎么解决

mybatis 依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

4、配置 application.yml 文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    username: root
    password: admin
    
  thymeleaf:
    cache: false
    encoding: utf-8
    mode: HTML5
    servlet:
      content-type: text/html

mybatis:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.cqy.shiro.pojo
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

server:
  servlet:
    context-path: /shiro

5、编写一个简略的 register.html 注册页面(应用 Vue)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title> 注册 </title>
    <script src="js/vue/2.5.16/vue.min.js"></script>
    <script src="js/axios/0.17.1/axios.min.js"></script>
<!--    <script src="js/jquery/2.0.0/jquery.min.js"></script>-->
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<script>
    $(function () {
        var data4 = {
            uri: 'submitregister',
            user: {
                name: '',
                password:''
            }
        };
        var vue = new Vue({
            el: '#work',
            data: data4,
            mounted: function(){},
            methods: {submit: function () {
                    var url = this.uri;
                    axios.post(url,this.user).then(function (response) {// 临时不写,前面写了 controller 后再编写});
                }
            }
        });
    });
</script>

<div id="work">
    <h3> 注册 </h3>
    用户名:<input type="text" v-model="user.name"><br>
    明码:<input type="password" v-model="user.password"><br>
    <button type="button" @click="submit"> 提交 </button>
</div>
</body>
</html>

6、PageController 用于页面跳转

register.html 放在 templates 外面,间接拜访不了,须要外部跳转拜访

@Controller
public class PageController {@GetMapping("/register")
    public String register(){return "register";}
}

7、创立 pojo,User 类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String password;
    private String salt;
}

8、创立 UserMapper 和 UserMapper.xml

这里为了便捷,间接应用 Class 不再进行 dao、service 层的编写

提供两个办法

@Repository
public interface UserMapper {
    // 依据 name 查问用户
    public User queryUserByName(String name);
    // 增加用户
    public int addUser(User user);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cqy.shiro.mapper.UserMapper">

    <select id="queryUserByName" resultType="user">
        select * from `user` where `name`=#{name}
    </select>

    <insert id="addUser" parameterType="user">
        insert into `user` values (#{id},#{name},#{password},#{salt})
    </insert>

</mapper>

9、创立一个 login.html 登录页面用于登录

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title> 登录 </title>
    <script src="js/vue/2.5.16/vue.min.js"></script>
    <script src="js/axios/0.17.1/axios.min.js"></script>
    <!--    <script src="js/jquery/2.0.0/jquery.min.js"></script>-->
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<script>
    $(function () {
        var data4 = {
            uri: 'submitlogin',
            user: {
                name: '',
                password:''
            }
        };
        var vue = new Vue({
            el: '#work',
            data: data4,
            methods: {submit: function () {
                    var url = this.uri;
                    axios.post(url,this.user).then(function (response) {// 临时不写,前面写了 controller 后再编写});
                }
            }
        });
    });
</script>
<div id="work">
    <h3> 登录 </h3>
    用户名:<input type="text" v-model="user.name"><br>
    明码:<input type="password" v-model="user.password"><br>
    <button type="button" @click="submit"> 登录 </button>
</div>
</body>
</html>

第二局部:进行 Shiro 相干的配置

1、导入 Shiro 的 Pom 依赖(文上)

2、自定义一个 Realm 的类

用来编写一些认证与受权的逻辑

// 自定义 Realm
public class UserRealm extends AuthorizingRealm {
    
    // 执行受权逻辑
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 执行机会:在用户进行登录,认证身份时执行
        System.out.println("====>>>> 执行了受权逻辑 PrincipalCollection");
        return null;
    }

    // 执行认证逻辑
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 执行机会:在用户登录后,每拜访一次那些须要权限能力拜访的页面时执行
        System.out.println("====>>>> 执行了认证逻辑 AuthenticationToken");
        // 临时为空,前面依据业务逻辑编写认证代码
        return null;
    }
}

3、编写 Shiro 配置类,config 包下


@Configuration
public class ShiroConfig {

    // 创立 ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置平安管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        return shiroFilterFactoryBean;
    }

    // 创立 DefaultWebSecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联 Realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

     // 创立 Hash 凭证匹配器
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //md5 默认就是加密 1 次
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(1);
        return hashedCredentialsMatcher;
    }
    
    // 创立 Realm 对象,须要自定义
    @Bean
    public UserRealm getUserRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher){UserRealm userRealm = new UserRealm();
        // 关联凭证匹配器!!!userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return userRealm;
    }
}

第三局部:编写业务逻辑

注册:

1、编写注册的 Controller

@RestController
public class ShiroController {

    @Autowired
    UserMapper userMapper;

    @PostMapping("/submitregister")
    public Object submitRegister(@RequestBody User user){String name = user.getName();
        String password = user.getPassword();
        // 依据 name 从数据库中查问对应的 user
        if (null!=userMapper.queryUserByName(name)){return "用户名曾经被应用,请从新输出";}
        // 通过随机形式创立盐,并且加密算法采纳 md5
        String salt = new SecureRandomNumberGenerator().nextBytes().toString();
        // 加密次数,默认加密 1 次
        int times = 1;
        String algorithmName = "md5";
        String encodePassword = new SimpleHash(algorithmName,password,salt,times).toString();
        // 盐如果失落了,无奈验证明码是否正确,因而会增加到数据库保留
        user.setSalt(salt);
        user.setPassword(encodePassword);
        // 增加到数据库
        userMapper.addUser(user);
        return 0;
    }
}

2、register.html 批改

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title> 注册 </title>
    <script src="js/vue/2.5.16/vue.min.js"></script>
    <script src="js/axios/0.17.1/axios.min.js"></script>
<!--    <script src="js/jquery/2.0.0/jquery.min.js"></script>-->
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<script>
    $(function () {
        var data4 = {
            uri: 'register',
            user: {
                name: '',
                password:''
            }
        };
        var vue = new Vue({
            el: '#work',
            data: data4,
            mounted: function(){},
            methods: {submit: function () {
                    var url = this.uri;
                    axios.post(url,this.user).then(function (response) {
                        var result = response.data;
                        // 如果注册胜利,跳转到注册胜利页面
                        if (0===result){location.href="registerSuccess";}
                    });
                }
            }
        });
    });
</script>

<div id="work">
    <h3> 注册 </h3>
    用户名:<input type="text" v-model="user.name"><br>
    明码:<input type="password" v-model="user.password"><br>
    <button type="button" @click="submit"> 提交 </button>
</div>
</body>
</html>

3、创立 registerSuccess.html 注册胜利页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 注册胜利页面 </title>
</head>
<body>
<h1> 祝贺您,注册胜利!</h1>
</body>
</html>

4、PageController 中增加跳转

@GetMapping("/registerSuccess")
public String registerSuccess(){return "registerSuccess";}

5、启动 SpringBoot 我的项目

拜访 http://localhost:8080/shiro/register

输出用户名和明码,点击提交进行注册


发现页面跳转到注册胜利页面,查看数据库 user 表

加密注册胜利!

登录认证:

1、编写 Realm 里的认证逻辑

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 将 AuthenticationToken 转成 UsernamePasswordToken
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        // 获取账号名
        String username = token.getUsername();
        // 依据 name 获取用户对象从而拿到明码和盐值
        User user = userMapper.queryUserByName(username);
        // 获取用户明码(数据库中加密后的),须要通过 user 对象来拿
        String passwordInDB = user.getPassword();
        // 拿到盐
        String salt = user.getSalt();
        // 认证信息里寄存账号密码,getName()是以后 Realm 的继承办法,通常返回以后类名:UserRealm
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, passwordInDB, ByteSource.Util.bytes(salt), getName());
        return simpleAuthenticationInfo;
    }

2、编写登录的 Controller,在 ShiroController 中

@PostMapping("/submitlogin")
    public Object submitLogin(@RequestBody User userParam){String name = userParam.getName();
        String password = userParam.getPassword();
        // 应用 Shiro 形式获取以后用户
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(name, password);
        // 执行登录,与 Realm 中查问的数据库中用户的信息进行比拟
        try {subject.login(token);
            // 依据用户名
            User user = userMapper.queryUserByName(name);
            subject.getSession().setAttribute("user",user);
            return 0;
        } catch (AuthenticationException e) {return "账号或明码谬误";}
    }

3、login.html 批改

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title> 登录 </title>
    <script src="js/vue/2.5.16/vue.min.js"></script>
    <script src="js/axios/0.17.1/axios.min.js"></script>
    <!--    <script src="js/jquery/2.0.0/jquery.min.js"></script>-->
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<script>
    $(function () {
        var data4 = {
            uri: 'submitlogin',
            user: {
                name: '',
                password:''
            }
        };
        var vue = new Vue({
            el: '#work',
            data: data4,
            methods: {submit: function () {
                    var url = this.uri;
                    axios.post(url,this.user).then(function (response) {
                        var result = response.data;
                        // 如果返回 0,阐明认证胜利,即跳转到登陆胜利页面
                        if (0===result){location.href="loginSuccess";}
                    });
                }
            }
        });
    });
</script>
<div id="work">
    <h3> 登录 </h3>
    用户名:<input type="text" v-model="user.name"><br>
    明码:<input type="password" v-model="user.password"><br>
    <button type="button" @click="submit"> 登录 </button>
</div>
</body>
</html>

4、创立 loginSuccess.html 登录胜利页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> 登录胜利页面 </title>
</head>
<body>
<h1> 祝贺您登录胜利!</h1>
<span> 以后用户是:<a th:text="${session.user.name}"></a>
</span>
</body>
</html>

5、PageController 中增加跳转

@GetMapping("/loginSuccess")
public String loginSuccess(){return "loginSuccess";}

6、启动 SpringBoot 我的项目

拜访 http://localhost:8080/shiro/login

输出用户名明码,点击登录

发现页面跳转到登录胜利页面,并显示出了以后登录用户:纸飞机

页面登录拦挡:

1、在 templates 目录下新建一个 user 目录,编写两个页面 add.html、update.html

<body>
<h1>add</h1>
</body>
<body>
<h1>update</h1>
</body>

2、编写对应跳转到两个页面的 Controller

@GetMapping("/user/add")
public String toAdd(){return "user/add";}

@GetMapping("/user/update")
public String toUpdate(){return "user/update";}

3、在 templates 目录下创立一个首页 index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> 首页 </title>
</head>
<body>
    <a href="login"> 点击进入登录页面 </a>
    <br>
    <br>
    <a th:href="@{/user/add}">add</a> | 
    <a th:href="@{/user/update}">update</a>
</body>
</html>

4、跳转到首页 index.html 的 Controller

@GetMapping({"/","/index"})
public String toIndex(){return "index";}

5、运行测试页面跳转是否 OK(能够间接进入 add 或 update 页面)

6、筹备编写 Shiro 的内置过滤器(设置过滤规定)

    // 创立 ShiroFilterFactoryBean
    @Bean(name ="shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置平安管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 批改到要跳转的 login 页面;shiroFilterFactoryBean.setLoginUrl("/login");
        /*
         * 罕用的有如下过滤器:* anon:无需认证就能够拜访
         * authc:必须认证才能够拜访
         * user:如果应用了记住我性能就能够间接拜访
         * perms: 领有某个资源权限才能够拜访
         * role:领有某个角色权限才能够拜访
         */
        // 留神此处应用的是 LinkedHashMap,是有程序的,shiro 会按从上到下的程序匹配验证,匹配了就不再持续验证
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add","authc");
        filterMap.put("/user/update","authc");
        // 能够间接使通配符
        //filterMap.put("/user/*","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

7、启动测试拜访,发现点击 add|update 链接无奈进入,曾经被拦挡,会主动给咱们跳转到 login.jsp 页面。咱们须要自定义跳转页面。

8、在 ShiroFilterFactoryBean 办法中增加 shiroFilterFactoryBean.setLoginUrl

// 批改到要跳转的 login 页面;// 后面咱们曾经创立好了 login.html 页面和跳转的 Controller
shiroFilterFactoryBean.setLoginUrl("/login");

9、再次启动测试,发现点击 add|update 链接会间接跳转到 login.html 登录页面让你进行登录

退出:

1、在登录胜利页面 loginSuccess 增加退出的链接

<a href="logout"> 退出 </a>

2、编写退出的 Controller

// 退出登录,跳转到首页
@GetMapping("/logout")
public String loginOut() {Subject subject = SecurityUtils.getSubject();
    if(subject.isAuthenticated())
        subject.logout();// 会主动让 session 生效
    return "redirect:index";
}

受权:

简略 Shiro 的过滤器拦挡申请

1、在登录胜利页面 loginSuccess 增加跳转到首页 index.html 的链接

<a href="index"> 点击跳转到首页 </a>

2、在 ShiroFilterFactoryBean 中增加

// 受权拦挡,须要放在认证的下面。某个用户有 add 的权限才能够拜访。filterMap.put("/user/add","perms[user:add]");

3、启动测试,登录拜访 add,发现呈现 401 谬误,Unauthorized 未受权

当咱们实现权限拦挡后,shiro 会主动跳转到未受权的页面,但咱们没有这个页面,所以 401

留神:ShiroFilterFactoryBean 中实现受权配置后,此处拜访 add 必须要先登录,未登录的状况下,受权的 perms 参数也会间接跳转到 login,要求你先登录。

4、配置一个未受权的提醒的页面,减少一个 controller 提醒。而后在 ShiroFilterFactoryBean 中增加配置

@RequestMapping("/noauth")
@ResponseBody
public String noAuth(){return "未经受权不能拜访此页面";}
shiroFilterFactoryBean.setUnauthorizedUrl("/noauth"); 

5、再次启动拜访测试。拦挡胜利。

6、ShiroFilterFactoryBean 中残缺代码

    // 创立 ShiroFilterFactoryBean
    @Bean(name ="shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置平安管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        /*
         * 罕用的有如下过滤器:* anon:无需认证就能够拜访
         * authc:必须认证才能够拜访
         * user:如果应用了记住我性能就能够间接拜访
         * perms: 领有某个资源权限才能够拜访
         * role:领有某个角色权限才能够拜访
         */
        // 留神此处应用的是 LinkedHashMap,是有程序的,shiro 会按从上到下的程序匹配验证,匹配了就不再持续验证
        // 所以下面的 url 要刻薄,宽松的 url 要放在上面,尤其是 "/**" 要放到最上面,如果放后面的话其后的验证规定就没作用了。Map<String, String> filterMap = new LinkedHashMap<>();
        // 受权拦挡,须要放在认证的下面。某个用户有 add 的权限才能够拜访。// 受权,失常状况下,没有受权会跳转到未受权页面
        filterMap.put("/user/update","perms[user:update]");
        filterMap.put("/user/add","perms[user:add]");

        // 认证
//        filterMap.put("/user/add","authc");
//        filterMap.put("/user/update","authc");

        // 批改到要跳转的 login 页面;shiroFilterFactoryBean.setLoginUrl("/login");
        // 跳转到未受权的申请页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

而如何给某个用户具体受权,进行权限的操作等。本文不做粗疏的解说,因为这须要联合我的项目中具体的业务逻辑来进行编写,并且须要进行数据库权限表等一系列的设计操作等,相较简单。

八、应用 Shiro 的坑

在文章集成 SpringBoot 局部,说了 Shiro 的导入 Pom 依赖的两种形式。其中第二种可能会报错。

报错内容一:

可能会呈现启动时无奈找到 shiroFilterFactoryBean 的 bean

***************************
APPLICATION FAILED TO START
***************************

Description:

Method filterShiroFilterRegistrationBean in org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration required a bean named 'shiroFilterFactoryBean' that could not be found.


Action:

Consider defining a bean named 'shiroFilterFactoryBean' in your configuration.

解决:如果在 Shiro 配置类中的 ShiroFilterFactoryBean 的办法名没有命名为 shiroFilterFactoryBean,须要手动在 @Bean 上增加上名字,否则无奈辨认注入。如下

@Bean(name = "shiroFilterFactoryBean")

报错内容二:

我的项目无奈启动,报没有 authorizer 的 bean 的谬误: No bean named 'authorizer' available

解决:本人在配置类中配置

@Configuration
public class ShiroConfig {

@Bean
public Authorizer authorizer(){return new ModularRealmAuthorizer();
    }
}

具体参考这篇文章:https://www.cnblogs.com/insan…

九、总结

本文只是 Shiro 的入门学习,适宜于老手初探 Shiro,有个大抵的理解与利用。

次要讲了一些 Shiro 的基础知识,以及集成 SpringBoot 简略利用的流程和配置,而对于 Shiro 的一些源码等内容并没有进行一个剖析和解说,可能会在之后的进阶文章中补充。

学习倡议:

1、Shiro 的一些配置类和简略业务逻辑代码是死的,大可不必花很多精力去死记那些单词又长的代码,在初学之后应总结成本人的环境模板,用时即取即可,多将精力放在 Shiro 的整个认证受权的运行过程上,理解是怎么进行的。

2、Shiro 可定制化水平很高,在很多波及到权限的我的项目中都有使用,可自行去码云或 GitHub 上寻找优质的(star 数较高)的开源我的项目(如权限管理系统)来进行学习,看理论的我的项目中 Shiro 的利用。

3、本文文中波及的代码。需要的话自行下载:链接:https://pan.baidu.com/s/1xicF…
提取码:quo0


参考:

1、http://shiro.apache.org/index… Shiro 官网

2、https://www.bilibili.com/vide… 遇见狂神说,【狂神说 Java】SpringBoot 整合 Shiro 框架

3、https://zhuanlan.zhihu.com/p/… 我没有三颗心脏,Shiro 平安框架【疾速入门】就这一篇!公众号 wmyskxz

4、https://juejin.im/post/684490… 码农小胖哥,Spring Security 实战干货:RBAC 权限管制概念的了解

5、http://www.woshipm.com/pd/115… 珣玗琪,RBAC 模型:基于用户 - 角色 - 权限管制的一些思考

6、https://how2j.cn/k/shiro/shir… How2j Shiro 系列教材


谢谢您看完这篇技术文章

如果能对您有所帮忙

那将是一件很美妙的事件

放弃好奇心的一生学习也是极棒的事

愿世界简略又多彩

转载请注明出处

​ ——纸飞机

退出移动版