温馨提示:
-
看懂本文可能需要 -
会使用 spring -
配合相关视频进行学习 -
懂中文 :) -
代码块过长,可左右滑动哦 -
您的观看和点赞是对本公众号最大力的支持 ~~
本文章请配合相关视频进行学习 https://www.bilibili.com/video/BV1YW411M7S3
相关的源码在文章发布一两天内将会附上,公众号后台回复 shiro 即可
目录
ps: 目录有点长,不知道怎么换个好点的,只能先截图。。。。
简介
相关历史和具体简介自行百度
-
Apache Shiro 是 Java 的一个安全(权限)框架 -
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境 -
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等 -
下载:http://shiro.apache.org/
简介功能

-
Authentication:身份认证/登录,验证用户是不是拥有相应的身份; -
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用 户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户 对某个资源是否具有某个权限; -
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有 信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的; -
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; -
Web Support:Web 支持,可以非常容易的集成到Web 环境; -
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可 以提高效率; -
Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; -
Testing:提供测试支持; -
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; -
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
Shiro 架构
Shiro外部来看

-
Application Code:应用程序 -
Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定 是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫, 机器人等;与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者; -
SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色 -
Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户 进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource
Shiro内部来看

-
Subject:任何可以与应用交互的“用户”; -
SecurityManager :相当于SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进 行认证、授权、会话及缓存的管理。 -
Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证 策略(Authentication Strategy),即什么情况下算用户认证通过了; -
Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控 制着用户能访问应用中的哪些功能; -
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体 的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要 实现自己的 Realm; -
SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境 -
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能 -
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密。
HelloWorld
建立一个普通的maven工程
快速启动
(解读的内容在当前目录的 ./code/01-HelloWorld)
导入依赖
dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
<scope>test</scope> <!-- 注意这行要注释掉 -->
</dependency>
</dependencies>
找到shiro给我们提供的HelloWorld
,导入配置文件,导入 Quickstart.java
文件(注意文件编码)(这部分在源码中)

然后直接启动测试

解读这个java文件,在代码中
集成Spring
相关代码在 ./code/02-spring
加入Web模块
依赖增加
<dependencies>
<!--Jackson required包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.3</version>
</dependency>
</dependencies>
配置spring 并测试
然后加入shiro 的相关配置
-
web.xml中配置 Shiro 的 shiroFilter.
-
<!-- Shiro Filter is defined in the spring application context: -->
<!--
1. 配置 Shiro 的 shiroFilter.
2. DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和<filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id.
-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping> -
spring配置shiro
-
ShiroFilter 类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其 负责读取配置(如ini 配置文件),然后判断URL 是否需要登录/权限等工作。 -
DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和 <filter-name>
对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id. -
我们可以通过查看 org.springframework.web.filter.DelegatingFilterProxy
类的注释分析,或者在异常处打个断点查看 -
相关代码在 ./code/02-spring
ShiroFilter 的工作原理

部分细节
-
[urls] 部分的配置,其格式是:“url=拦截器[参数],拦截器[参数]” -
如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会 执行其配置的拦截器 -
anon(anonymous)拦截器表示匿名访问(即不需要登 录即可访问) -
authc (authentication)拦截器表示需要身份认证通过后 才能访问
shiro中默认的过滤器

URL 匹配模式
-
url 模式使用 Ant 风格模式 -
Ant 路径通配符支持 ?、 * 、 **,注意通配符匹配不 包括目录分隔符“/”:** -
?:匹配一个字符 ,如 /admin? 将匹配 /admin1,但不 匹配 /admin 或 /admin/; -
*:匹配零个或多个字符串 ,如 /admin 将匹配 /admin、 /admin123,但不匹配 /admin/1; -
**:匹配路径中的零个或多个路径 ,如 /admin/将匹 配 /admin/a 或 /admin/a/b
URL 匹配顺序
URL 权限采取第一次匹配优先的方式,即从头开始 使用第一个匹配的 url 模式对应的拦截器链
-
/bb/**=filter1 -
/**/bb/aa=filter2 -
/**=filter3 -
如果请求的url是“/bb/aa”,因为按照声明顺序进行匹 配,那么将使用 filter1 进行拦截。
认证
直接点就是登陆
认证的过程:
-
获取当前的 Subject. 调用 SecurityUtils.getSubject(); -
测试当前的用户是否已经被认证. 即是否已经登录. 调用 Subject 的 isAuthenticated() -
若没有被认证, 则把用户名和密码封装为 UsernamePasswordToken 对象 -
创建一个表单页面 -
把请求提交到 SpringMVC 的 Handler -
获取用户名和密码. -
执行登录: 调用 Subject 的 login(AuthenticationToken) 方法. -
自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给 Shiro. -
实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类(如果仅仅需要认证,可以继承这个类,实现 doGetAuthenticationInfo(AuthenticationToken) 方法) -
步骤 -
把 AuthenticationToken 转型为 UsernamePasswordToken -
从 UsernamePasswordToken 中获取 username -
调用数据库的方法,从数据库中查询 username 对应的用户记录(这里就不查询直接给出了) -
如果用户不存在,则可以抛出 UnknownAccountException -
根据用户信息的情况,决定是否需要抛出其他的 AuthenticationException -
根据用户的情况,来构建 AuthenticationInfo 对象返回,这个实现类通常使用 SimpleAuthenticationInfo -
由 shiro 完成对密码的比对.
直接看代码
Subject 的 login(AuthenticationToken) 分析







密码加密
密码比对分析
比对密码肯定要调用 getPassword方法,因此可以在这个方法上打断点

往上找

密码的比对是通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对
MD5加密方式
但是实际上,我们存储密码是不可能通过明文存储,那么对于加密是怎么比对的呢,其实就是用到了 CredentialsMatcher 类
这个 CredentialsMatcher 有多个实现类,其中包括 MD5

接下来我们需要更换一下我们的 Realm 为 MD5的 Realm
<bean id="jdbcRealm" class="com.wu.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
</bean>
</property>
</bean>
之后就会自动使用 MD5 把前台传过来的数据进行加密
注意数据库中存储的密码还是不变,由于我们是直接给出密码,因此我们需要提前将密码用相同的条件进行加密
image-20210102231114554
那怎么自动呢?同样开始调试(getPassword
方法的断点还在)

可以看到自动加密的,现在要看加密过程,由于加密后是 tokenHashedcredentials
变量,所以直接看hashProvidedCredentials
方法

继续点入

实际上就是这行的作用,其中我们可以看到加密的次数,默认是一次,密码有些常见,因此我们可以考虑反复加密,及生成的密码再加密多次,安全性更高
反复加密
所以我们还可以指定一下加密次数
<bean id="jdbcRealm" class="com.wu.shiro.realms.ShiroRealm">
.....
<property name="hashIterations" value="1024"/>
</bean>
盐值加密
但是还有一个问题,就是同一个明文,可以得到同样的密码,可以采用暴力破解的方式进行破解,还是不安全,因此考虑加点佐料,即加盐
需要修改第六步的代码,将盐加进去
// 。。。。
// 加盐
// 6. 根据用户的情况,来构建 AuthenticationInfo 对象返回,这个实现类通常使用 SimpleAuthenticationInfo
// 以下信息是从数据库中获取的
Object principal = username; // 认证实体的信息
Object credentials = null; // 密码
if ("admin".equals(username)) {
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
} else if ("user".equals(username)) {
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
ByteSource credentialsSalt = ByteSource.Util.bytes(username); // 盐值
String realmName = getName(); // 当前 realm 对象的 name ,直接调用父类的即可
return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
同样不链接数据库,因此需要明确给出真实密码
public static void main(String[] args) {
String algorithmName = "MD5";
Object source = "123456";
Object adminSalt = ByteSource.Util.bytes("admin");
Object userSalt = ByteSource.Util.bytes("user");
int hashIterations = 1024;
// 下面这行对象是通过调试源码看出来的
SimpleHash adminSaltHash = new SimpleHash(algorithmName, source, adminSalt, hashIterations);
SimpleHash userSaltHash = new SimpleHash(algorithmName, source, userSalt, hashIterations);
System.out.println("admin:" + adminSaltHash);
System.out.println("user:" + userSaltHash);
}
// admin:038bdaf98f2037b31f1e75b5b4c9b26e
// user:098d2c478e9c11555ce2823231e02ec1
多Realm验证
如果我们现在有多个数据库(MySQL,Oracle),并且采用不同的加密方式,这时候就需要进行多 Realm 验证
先来查看一下多 Realm 是怎么进行验证的
同样登录方法进入

进入到管理器的实现



找到父级的实现


在这个方法中,会获取到所有 Realm 然后判断是多个还是一个,进而执行对应的方法

这个方法所在的类是ModularRealmAuthenticator
,所以我们给这个类的 realms
属性配置多个 Realm,然后稍微的往上看,这个类是AuthenticatingSecurityManager
的一个属性,因此还需要将这个类配置给它的authenticator
属性

具体实现代码
<beans>
...
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
</bean>
...
<!-- 配置多个 Realm -->
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator" id="authenticator">
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="com.wu.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
<bean id="secondRealm" class="com.wu.shiro.realms.SecondRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
</beans>
认证策略
有两个 Realm 怎么样才算认证通过呢,这就需要我们的认证策略(AuthenticationStrategy)
AuthenticationStrategy 实际上是一个接口,有三个实现:
-
FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略 -
AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息 -
AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了
ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略
那怎么修改认证策略呢?
同样 debug 方式查看,以相同的方式进入到这个方法

由于是多 Realm 认证,因此看doMultiRealnAuthentication (realms, authenticationToken);
方法

然后打断点,调试,发现strategy
变量就是我们的认证策略

同样,这个变量是通过getAuthenticationstrategy ()
方法得到,我们可以修改authenticationstrategy
属性所在的类即可,this指针指向ModularRealmAuthenticator
所以在 bean中配置一下即可
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator" id="authenticator">
。。。
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
</property>
</bean>
将Reamls配置给securityManage
但是由于接下来授权的时候需要从securityManage中读 Realm,所以我们考虑将Reamls配置给securityManage
还记得之前配置的情况吗
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms" ref="jdbcRealm"/>
</bean>
之前这里我们直接把单个 Realm直接配置到securityManager中,也是能成功运行的,其实securityManager还有一个 realms 属性,所以我们也可以把之前的多 Realm 的配置配到 securityManager 中
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<!-- 换个地方 -->
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
<!-- 把ModularRealmAuthenticator中配置的多 Realm 删掉 -->
</bean>
运行测试,实际上是可以的,但是我们在 debug 的时候发现了新的问题,就是这个属性并没有调用到!下面是调试的过程(还是同样断点的地方)


可以看到他还是调用了同样的方法,但是他有值,要知道我们并没有配置,所以只有一种可能,那就是在初始化的时候 securityManager
会把多Realms设置给了ModularRealmAuthenticator
,要设置,那么肯定要调用set方法,所以我们可以在ModularRealmAuthenticator
的set方法上面打上断点

然后重新 debug方式运行,之后程序确实卡在了这一行,我们往上找,找到了一个方法

可以看到在父类中,通过这样代码,将Realm设置给了ModularRealmAuthenticator
(先判断authorizer
属性是否是ModularRealmAuthenticator
类型,如果是就将 Realms 设置进去,因为我们在 xml 文件中给securityManager
配置的确实是这种类型,所以进入了这个判断)
更换认证策略
最后,我们修改一下我们的验证方式
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator" id="authenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
</property>
</bean>
重新运行测试
授权
注意要先将多Realm配置给securityManage(看 多Realm验证 — 将Reamls配置给securityManage 部分)
基本概念
授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作 等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限 (Permission)、角色(Role)。
-
主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源 -
资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些 数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。 -
权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户 有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用 户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许 -
Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限, 即实例级别的) -
角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有 一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等 都是角色,不同的角色拥有一组不同的权限
授权方式
Shiro 支持三种方式的授权:
-
编程式:通过写if/else 授权代码块完成 -
注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常 -
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成

后面解释
默认拦截器
身份验证相关的

授权相关的

其他

授权使用
加页面,加配置,具体看代码
<property name="filterChainDefinitions">
<!-- 除了 /login.jsp 可以匿名访问,其他都是需要登录,因此 login.jsp 的配置是必要的 -->
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
<!-- 配置用户访问权限 -->
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
启动测试,user.jsp是没有权限访问的,admin.jsp同理(因为我们没有配置具体权限,只是配置了认证)
接下来我们配置一下具体的权限
-
授权需要继承 AuthorizingRealm
类, 并实现其doGetAuthorizationInfo
方法 -
AuthorizingRealm
类继承自AuthenticatingRealm
, 但没有实现AuthenticatingRealm
中的doGetAuthenticationInfo
, 所以认证和授权只需要继承AuthorizingRealm
就可以了. 同时实现他的两个抽象方法.

改写 ShiroRealm
类,让他继承 AuthorizingRealm
,然后编写doGetAuthorizationInfo
方法
-
从 principalCollection 中来获取登录用户的信息 -
利用登录的用户的信息来给予当前用户的角色或权限(可能需要查询数据库) -
创建 AuthorizationInfo 的实现类 SimpleAuthorizationInfo,并设置 reles 属性 -
返回这个对象
// 授权会被 shiro 回调的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1. 从 principalCollection 中来获取登录用户的信息
Object principal = principalCollection.getPrimaryPrincipal();
// 2. 利用登录的用户的信息来给予当前用户的角色或权限(可能需要查询数据库)
Set<String> roles = new HashSet<>();
roles.add("user");
if ("admin".equals(principal)) { // 如果是管理员,加多一个身份
roles.add("admin");
}
// 3. 创建 AuthorizationInfo 的实现类 SimpleAuthorizationInfo,并设置 roles 属性
// 4. 返回这个对象
return new SimpleAuthorizationInfo(roles);
}
然后运行测试,admin能访问所有页面,而user只能访问 user.jsp
授权源码分析

单 Realm
他是怎么进行授权的呢,点进方法

继续点进 hasRole 方法

可以看到有多个类,但是我们使用的是 单 Realm,所以我们点第一个

第一个是单 Realm,第二个会调用
this.authorizer.hasRole
方法,而authorizer
其实就是ModularRealmAuthorizer
,也就是第三,也就是说第二第三个其实没多大区别,都是多 Realm,下面会说idea的继承看不太清楚这里贴上 eclipse的
image-20210103215200993
点进 getAuthorizationInfo
方法

往下找到doGetAuthorizationlnfo
点入

这时候就进入到AuthorizingRealm
类的doGetAuthorizationInfo
方法,查看继承体系,可以看到我们写的类把这个抽象方法实现了

多 Realm
同样步骤,但是 hasRole
方法需要进入到第三个

继续点刚刚的 hasRole
方法,发现又回到了刚刚的接口

感觉有点像调用链(递归)??
可以发现,只要有一个 Realm 授权通过,就会return true
PrincipalCollection类
形参里面有一个变量,类型是PrincipalCollection
,我们看一下他的继承体系图

实际是第一个类型

我们看看这个get方法的调用过程

jsp授权 – shiro标签
由于 jsp 在前后端分离开发中不常见,所以这部分会忽略较多
需要导入一个 shiro的标签库<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
idea会自动导入
简单举例
修改 list.jsp
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h2>List Page</h2>
Welcome:<shiro:principal/>
<shiro:hasRole name="admin">
<br><br>
<a href="admin.jsp">admin page</a>
</shiro:hasRole>
<shiro:hasRole name="user">
<br><br>
<a href="user.jsp">user page</a>
</shiro:hasRole>
<br><br>
<a href="shiro/logout">logout</a>
</body>
</html>
注解授权 – 权限注解
-
@RequiresAuthentication:表示当前Subject已经通过login 进行了身份验证;即 Subject. isAuthenticated() 返回 true -
@RequiresUser:表示当前 Subject 已经身份验证或者通过记 住我登录的 -
@RequiresGuest:表示当前Subject没有身份验证或通过记住 我登录过,即是游客身份 -
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND):表示当前 Subject 需要角色 admin 和user -
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR):表示当前 Subject 需要权限 user:a 或 user:b。
编写一个Service然后加入bean中,并注入到controller,然后稍微修改一下 list.jsp
service

controller

list.jsp

不知道是不是版本的原因,还需要创建一个默认建议自动代理创建器加入到容器中,才能保证这个注解生效
https://blog.csdn.net/qq_35981283/article/details/78631924 Shiro @RequiresRoles注解不生效解决方案及相应设置
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="com.wu.shiro.service.ShiroServiceImpl" id="shiroService"/>
注意:如果Service加上事务注解,它变成了代理,这时权限注解就需要标注在Controller层,不能让Service变成代理的代理,否则会发生类型转化异常
权限从数据库中获取
我们发现这一块直接卸载配置文件中肯定不太好,一般来说这些玩意保存在数据库中,以便动态更新

这个内容我们是配置在 filterChainDefinitions
属性之上,我们点击去对应的set方法查看细节

通过最后一行设置进入,继续点进

发现他最终放入到这个类的这个属性之上,我们打一个断点运行,查看这个具体是什么类型

可以看到这个属性是 LinkHashMap,因此我们可以考虑将我们通过数据库查询出来的权限设置进这里
所以我们可以把内容配置到一个工厂类中
工厂类
public class FilterChainDefinitionMapBuilder {
public Map<String, String> buildFilterChainDefinitionMap() {
Map<String, String> map = new LinkedHashMap<>();
// 假设下方是查询数据库得到的数据
map.put("login.jsp", "anon");
map.put("shiro/login", "anon");
map.put("shiro/logout", "logout");
map.put("user.jsp", "roles[user]");
map.put("admin.jsp", "roles[admin]");
map.put("/**", "authc");
return map;
}
}
配置文件
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
。。。
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>
</bean>
<!-- 配置一个 bean,这个 bean 实际上是一个 map,通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder"
factory-method="buildFilterChainDefinitionMap"/>
<bean class="com.wu.shiro.factory.FilterChainDefinitionMapBuilder"
id="filterChainDefinitionMapBuilder"/>
会话管理
概述
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容 器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境 都可以使用,提供了会话管理、会话事件监听、会话存储/ 持久化、容器无关的集群、失效/过期支持、对Web 的透明 支持、SSO 单点登录的支持等特性
会话相关的 API
这个玩意的使用和 web 中的 Session的使用基本一致
-
Subject.getSession():即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建 一个;Subject.getSession(false),如果当前没有创建 Session 则返回 null -
session.getId():获取当前会话的唯一标识 -
session.getHost():获取当前Subject的主机地址 -
session.getTimeout() & session.setTimeout(毫秒):获取/设置当 前Session的过期时间 -
session.getStartTimestamp() & session.getLastAccessTime():获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定 期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每 次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。 -
session.touch() & session.stop():更新会话最后访问时 间及销毁会话;当Subject.logout()时会自动调用 stop 方法 来销毁会话。如果在web中,调用 HttpSession. invalidate() 也会自动调用Shiro Session.stop 方法进行销毁Shiro 的会 话 -
session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):设置/获取/删除会话属 性;在整个会话范围内都可以对这些属性进行操作
会话监听器
会话监听器用于监听会话创建、过期及停止事件

对比一下我们的 HttpSessionListener

web层还是建议使用原生的HTTPSession,但是这个我们没办法从service去访问httpsession因为controller是顶层的api,但是有了shiro就可以
使用举例
controller层
@RequestMapping("testShiroAnnotation")
public String testShiroAnnotation(HttpSession session) {
// 注意这个session是 HttpSession
session.setAttribute("key", "value123456");
shiroService.testMethod();
return "redirect:/list.jsp";
}
service层
@RequiresRoles({"admin"})
public void testMethod() {
System.out.println("testMethod, time: " + new Date());
// 获取Session,取得数据并打印输出
// 注意这个 session 是 shiro 的
System.out.println("Service SessionVal: " +
SecurityUtils.getSubject().getSession().getAttribute("key"));
}
运行测试

SessionDao
这个类可以把session写到数据库中,之后进行增删改查操作,然后对session进行管理,这部分用的不是很多
继承体系

-
AbstractSessionDAO 提供了 SessionDAO 的基础实现, 如生成会话ID等 -
CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager -
MemorySessionDAO 直接在内存中进行会话维护 -
EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。开发的时候可以直接继承这个类
查看一下 EnterpriseCacheSessionDAO 的基本API
这个类有基本的增删改查方法

简单使用(这里我报错了。。)
我们就以EnterpriseCacheSessionDAO
为例进行使用,我们看看 doCreate方法

其中第一行需要传入一个 SessionId,进入看看

发现,如果不传入,他会报异常,所以这个 ID必须要有,我们看看这个 ID的类型

看看他的继承体系

这里我们以 JavaUuidSessionIdGenerator
作为 id,所以我们在配置 bean 的时候,需要将这个类配置给 ID 所在的类
bean
<!-- Session Id 生成器 -->
<bean class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" id="sessionIdGenerator"/>
<!-- Session Dao 继承 EnterpriseCacheSessionDAO -->
<bean class="com.wu.shiro.dao.MySessionDao" id="sessionDao">
<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
<!-- 会话管理器 -->
<bean class="org.apache.shiro.session.mgt.DefaultSessionManager" id="sessionManager">
<property name="globalSessionTimeout" value="1800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionDAO" ref="sessionDao"/>
</bean>
具体细节就先忽略,以为我报错了
会话验证
需要开多一个线程,会造成一定的性能问题,用的不多
-
Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话 -
出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler -
Shiro 也提供了使用Quartz会话验证调度器:QuartzSessionValidationScheduler
缓存
CacheManagerAware 接口
Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager
Realm 缓存
Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现
AuthenticatingRealm 及 AuthorizingRealm 也分别提 供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓 存
我们看一下我们配置的 Realm

看看这个继承的方法



这个接口会自动注入 我们的 cacheManage



如果要去除缓存,可以把上面的 cacaheManage和 securityManager中的CacheManager属性 删了,或者可以利用下面这个属性

启动调试,如果每次都能进入对应的调试方法(Realm中的验证方法),就说明缓存删除成功
我们配置的缓存配置就是在这个文件中

Session 缓存
如 SecurityManager 实现了 SessionSecurityManager, 其会判断 SessionManager 是否实现了 CacheManagerAware 接口,如果实现了会把 CacheManager 设置给它
SessionManager 也会判断相应的 SessionDAO(如继承 自CachingSessionDAO)是否实现了 CacheManagerAware,如果实现了会把 CacheManager 设置给它
设置了缓存的 SessionManager,查询时会先查缓存,如 果找不到才查数据库
到后面,可以使用Redis作为 session缓存
RememberMe
Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝 等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问,基本流程如下
-
首先在登录页面选中 RememberMe 然后登录成功;如果是 浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并 保存下来 -
关闭浏览器再重新打开;会发现浏览器还是记住你的 -
访问一般的网页服务器端还是知道你是谁,且能正常访问 -
但是比如我们访问淘宝时,如果要查看我的订单或进行支付 时,此时还是需要再进行身份认证的,以确保当前用户还是你

如果把这行注释掉,可以发现刷新就需要重新登录了
认证和记住我
-
subject.isAuthenticated() 表示用户进行了身份验证登录的, 即使有 Subject.login 进行了登录 -
subject.isRemembered():表示用户是通过记住我登录的, 此时可能并不是真正的你(如你的朋友使用你的电脑,或者 你的cookie 被窃取)在访问的
两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样
建议
访问一般网页:如个人在主页之类的,我们使用user 拦截 器即可,user 拦截器只要用户登录 (isRemembered() || isAuthenticated())过即可访问成功
访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如 果是才放行,否则会跳转到登录页面叫你重新登录
简单使用
还记得前面的几张图嘛

修改配置

然后启动,打开浏览器访问,用admin登录,发现 admin.jsp user.jsp 是可以访问
然后关闭浏览器,重新打开,访问 list.jsp,发现可以直接访问,因为记住了 但是 admin.jsp user.jsp 需要重新登录才可以访问
设置记住我的时间
这里我们打一个断点运行

找到DefaultWebSecurityManager
,查看变量信息,找到this.rememberMeManager.cookie.maxAge
发现,有默认时间,31536000秒

所以我们可以修改这个变量信息

重新运行,登录后关闭浏览器,10秒后再打开试试,因为十秒内是被记住的,正常是需要重新登录的
end
您的观看和点赞是对本公众号最大力的支持 ~~
原文始发于微信公众号(一个调皮的bug):shiro 入门
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/43178.html