image

学习一门新技术的思路

是什么?

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

为什么要学?

与它相关的有哪些,需要另外了解的知识?

怎样去学?

文章主要内容:

  • 一、什么是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的功能特性(优势)

image

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通过角色关联用户,角色关联权限的方式间接赋予用户权限。如下图
image

有人会问为什么不直接给用户分配权限,还多此一举的增加角色这一环节呢?

其实是可以直接给用户分配权限,只是直接给用户分配权限,少了一层关系,扩展性弱了许多,适合那些用户数量、角色类型少的平台。

对于通常的系统,比如:存在多个用户拥有相同的权限,在分配的时候就要分别为这几个用户指定相同的权限,修改时也要为这几个用户的权限进行一一修改。有了角色后,我们只需要为该角色制定好权限后,将相同权限的用户都指定为同一个角色即可,便于权限管理。

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

小结:

RBAC 的优点主要在于易用和高效。给用户授权时只需要对角色授权,然后将相应的角色分配给用户即可;从技术角度讲,思路清晰且易于实现,且后期维护时只需要维护关系模型,显得简单而高效。

RBAC 的缺点主要有两个:一个是在进行较为复杂的权限校验时需要不断地遍历和递归,会造成一定的性能影响。另一个是缺少数据权限模型,基于 RBAC 来实现数据权限校验比较复杂和低效。

现在主流的权限管理系统设计大多还是基于RBAC模型的,只是根据不同的业务和设计方案,呈现不同的显示效果。

2、RBAC模型的分类

RBAC模型可以分为:RBAC0、RBAC1、RBAC2、RBAC3 四种。其中RBAC0是基础,也是最简单的,相当于底层逻辑,RBAC1、RBAC2、RBAC3都是以RBAC0为基础的升级。

一般情况下,使用RBAC0模型就可以满足常规的权限管理系统设计了。

RBAC0模型:

最简单的用户、角色、权限模型。是基础,定义了能构成 RBAC 权限控制系统的最小的集合。

RBAC0 由四部分构成:

  • 用户(User) 权限的使用主体
  • 角色(Role) 包含许可的集合
  • 会话(Session)绑定用户和角色关系映射的中间通道。而且用户必须通过会话才能给用户设置角色。
  • 许可(Pemission) 对特定资源的特定的访问许可。

image

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

image
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程序

根据官网:
image

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学习
image

image

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的架构(三大核心组件)

image

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 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

内部架构

image

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

Shiro中其他的一些概念:

  • Principals:一般指用户名等,唯一表明Subject身份也就是当前用户身份的东西;
  • Credentials:凭证,一般指密码,对当前登陆用户进行验证;

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

场景任务:

  • 用户访问注册页面,输入用户名密码,点击提交注册后,用shiro进行盐值MD5加密后保存到数据库——注册
  • 用户访问登录页面,进行登录时通过shiro进行用户认证——登录认证
  • 登录成功则显示当前登录的用户名(session管理)——登录认证
  • 进入首页,访问其他页面需要认证登录——页面拦截登录
  • 退出注销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

输入用户名和密码,点击提交进行注册

image
发现页面跳转到注册成功页面,查看数据库user表

image

加密注册成功!

登录认证:

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

输入用户名密码,点击登录

发现页面跳转到登录成功页面,并显示出了当前登录用户:纸飞机

image

页面登录拦截:

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页面。我们需要自定义跳转页面。

image

8、在ShiroFilterFactoryBean方法中添加shiroFilterFactoryBean.setLoginUrl

//修改到要跳转的login页面;
//前面我们已经创建好了login.html页面和跳转的Controller
shiroFilterFactoryBean.setLoginUrl("/login");

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

image

退出:

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未授权

image

当我们实现权限拦截后,shiro会自动跳转到未授权的页面,但我们没有这个页面,所以401

注意:ShiroFilterFactoryBean中完成授权配置后,此处访问add必须要先登录,未登录的情况下,授权的perms参数也会直接跳转到login,要求你先登录。

4、配置一个未授权的提示的页面,增加一个controller提示。然后在ShiroFilterFactoryBean中添加配置

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

5、再次启动访问测试。拦截成功。

image

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系列教材


谢谢您看完这篇技术文章

如果能对您有所帮助

那将是一件很美好的事情

保持好奇心的终身学习也是极棒的事

愿世界简单又多彩

转载请注明出处

​ ——纸飞机


纸飞机78
18 声望4 粉丝