Shiro入门学习

image

学习一门新技术的思路html

是什么?vue

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

为何要学?mysql

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

怎样去学?ios

文章主要内容:

  • 1、什么是Shiro——是什么?
  • 2、Shiro的功能及具体特性(优点)——是用来干吗的应用在哪些地方?为何要学?
  • 3、【拓展】RBAC(以角色为基础的访问控制)——须要另外了解的理论基础
  • 4、与其相关的SpringSecurity框架——与它相关的有哪些?
  • 5、第一个简单纯粹的Shiro程序——怎么学?先会用跑起来再逐步理解。干!
  • 6、Shiro的架构(三大核心组件)
  • 7、Shiro集成SpringBoot和数据库使用
  • 8、使用Shiro的坑
  • 9、总结

1、什么是Shiro

官方文档:git

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

官方介绍:web

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集成,缓存等。

2、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软件基金会的一部分)

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

3、【拓展】RBAC(以角色为基础的访问控制)

一、什么是RBAC

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

RBAC经过角色关联用户,角色关联权限的方式间接赋予用户权限。以下图
image

有人会问为何不直接给用户分配权限,还画蛇添足的增长角色这一环节呢?

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

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

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

小结:

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

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

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

二、RBAC模型的分类

RBAC模型能够分为:RBAC0、RBAC一、RBAC二、RBAC3 四种。其中RBAC0是基础,也是最简单的,至关于底层逻辑,RBAC一、RBAC二、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

三、权限(许可)

权限是资源的集合。

这里的资源指的是软件中全部的内容,包括模块、菜单、页面、字段、操做功能(增删改查)等等。

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

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

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

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

4、与其相关的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,感兴趣的可自行对比研究。

5、第一个简单纯粹的Shiro程序

根据官网:
image

一、新建一个普通的Maven项目

二、导入对应的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>

三、编写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

四、编写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);
    }
}

五、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>做用域

六、加深理解

①类的描述

/**
* 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);

6、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:凭证,通常指密码,对当前登录用户进行验证;

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

场景任务:

  • 用户访问注册页面,输入用户名密码,点击提交注册后,用shiro进行盐值MD5加密后保存到数据库——注册
  • 用户访问登陆页面,进行登陆时经过shiro进行用户认证——登陆认证
  • 登陆成功则显示当前登陆的用户名(session管理)——登陆认证
  • 进入首页,访问其余页面须要认证登陆——页面拦截登陆
  • 退出注销session回到首页——退出
  • 只有某个用户具备访问某个页面的权限才能访问——受权

第一部分:简单环境搭建

一、建库建表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;

二、新建一个SpringBoot项目

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

三、导入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>

四、配置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

五、编写一个简单的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>

六、PageController用于页面跳转

register.html放在templates里面,直接访问不了,须要内部跳转访问

@Controller
public class PageController {

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

七、建立pojo,User类

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

八、建立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>

九、建立一个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相关的配置

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

二、自定义一个 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;
    }
}

三、编写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;
    }
}

第三部分:编写业务逻辑

注册:

一、编写注册的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;
    }
}

二、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>

三、建立registerSuccess.html注册成功页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册成功页面</title>
</head>
<body>
<h1>恭喜您,注册成功!</h1>
</body>
</html>

四、PageController中添加跳转

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

五、启动SpringBoot项目

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

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

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

image

加密注册成功!

登陆认证:

一、编写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;
    }

二、编写登陆的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 "帐号或密码错误";
        }
    }

三、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>

四、建立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>

五、PageController中添加跳转

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

六、启动SpringBoot项目

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

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

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

image

页面登陆拦截:

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

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

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

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

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

三、在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>

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

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

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

六、准备编写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;
    }

七、启动测试访问,发现点击add|update连接没法进入,已经被拦截,会自动给咱们跳转到login.jsp页面。咱们须要自定义跳转页面。

image

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

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

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

image

退出:

一、在登陆成功页面loginSuccess添加退出的连接

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

二、编写退出的Controller

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

受权:

简单Shiro的过滤器拦截请求

一、在登陆成功页面loginSuccess添加跳转到首页index.html的连接

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

二、在ShiroFilterFactoryBean中添加

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

三、启动测试,登陆访问add,发现出现401错误,Unauthorized未受权

image

当咱们实现权限拦截后,shiro会自动跳转到未受权的页面,但咱们没有这个页面,因此401

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

四、配置一个未受权的提示的页面,增长一个controller提示。而后在ShiroFilterFactoryBean中添加配置

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

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

image

六、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;
    }

而如何给某个用户具体受权,进行权限的操做等。本文不作细致的讲解,由于这须要结合项目中具体的业务逻辑来进行编写,而且须要进行数据库权限表等一系列的设计操做等,相较复杂。

8、使用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...

9、总结

本文只是Shiro的入门学习,适合于新手初探Shiro,有个大体的了解与应用。

主要讲了一些Shiro的基础知识,以及集成SpringBoot简单应用的流程和配置,而对于Shiro的一些源码等内容并无进行一个分析和讲解,可能会在以后的进阶文章中补充。

学习建议:

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

二、Shiro可定制化程度很高,在不少涉及到权限的项目中都有运用,可自行去码云或GitHub上寻找优质的(star数较高)的开源项目(如权限管理系统)来进行学习,看实际的项目中Shiro的应用。

三、本文文中涉及的代码。须要的话自行下载:连接:https://pan.baidu.com/s/1xicF...
提取码:quo0


参考:

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

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

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

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

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

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


谢谢您看完这篇技术文章

若是能对您有所帮助

那将是一件很美好的事情

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

愿世界简单又多彩

转载请注明出处

​ ——纸飞机

相关文章
相关标签/搜索