1. 初识 Shiro
Apache Shiro
是一个强大易用的 Java 安全框架,提供了认证、授权、加密、会话管理、与 Web 集成、缓存等。
具体来说,满足对如下元素的支持:
- 用户,角色,权限(仅仅是操作权限,数据权限必须与业务需求紧密结合),资源(url)。
- 用户分配角色,角色定义权限。
- 访问授权时支持角色或者权限,并且支持多级的权限定义。
Shiro 作为一个完善的权限框架,可以应用在多种需要进行身份认证和访问授权的场景,例如:
独立应用
、web应用
、spring框架中集成
等。
2. Shiro 整体架构
在 shiro 架构中,有 3 个最主要的组件:Subject
,SecurityManager
,Realm
。
Subject
(如图上层部分):”操作用户(主体)”,本质上就是当前访问用户的抽象描述。SecurityManager
(如图中层部分):是 Shiro 架构中最核心的组件(控制器),通过它可以协调其他组件完成用户认证和授权。Authenticator
:认证器,协调一个或者多个 Realm,从 Realm 指定的数据源取得数据之后进行执行具体的认证。Authorizer
:授权器,用户访问控制授权,决定用户是否拥有执行指定操作的权限。Session Manager
:Session 管理器,Shiro 自己实现了一套 Session 管理机制。Session DAO
:实现了 Session 的操作,主要有增删改查。CacheManager
:缓存管理器,缓存角色数据和权限数据等。Pluggable Realms
:数据库与数据源之间的一个桥梁。Shiro 获取认证信息、权限数据、角色数据 通过 Realms 来获取。Cryptography
:是用来做加解密,能非常快捷的实现数据加密。
Realm
(如图下层部分):定义了访问数据的方式,用来连接不同的数据源,如:LDAP,关系数据库,配置文件等等。
3. Shiro 认证与授权
3.1 Shiro 认证
「创建SecurityManager
」>「主体提交请求」>「SecurityManager
调用Authenticator
去认证」>「Realm
验证」
- 操作用户(主体)提交请求到 Security Manager 调用 Authenticator 去认证,Authenticator 通过 Pluggable Realms 去获取认证信息,Pluggable Realms 是从下面的数据源(数据库)中去获取的认证信息,然后用通过 Pluggable Realms 从数据库中获取的认证信息和主体提交过来的认证数据做比对。
1 | /** |
3.2 Shiro 授权
shiro 访问授权有 3 种实现方式:**api
调用,java
注解,jsp
标签**。
- 通过 api 调用实现:「创建
SecurityManager
」>「主体授权」>「SecurityManager
调用Authorizer
授权」>「Realm
获取角色权限数据」- 大体上和认证操作一样,也是通过 Pluggable Realms 从下面的数据源(数据库)中去获取权限数据,角色数据。
1 | // 在执行访问授权验证之前,必须执行用户认证 |
- 在 spring 框架中可以通过 java 注解
1 |
|
- 在 JSP 页面中还可以直接使用 jsp 标签
1 | <!-- 使用shiro标签 --> |
3.3 Quickstart
- 新建一个
Maven
项目,pom
导入jar
包:shiro-all
、slf4j-api
、slf4j-log4j12
、log4j
; classpath
下新建shiro.ini
配置文件:
1 | # ----------------------------------------------------------------------------- |
- 启动运行 Quickstart
1 | public class Quickstart { |
4. 在 SpringMVC 框架中集成 Shiro
4.1 配置 Maven 依赖
1 | <!-- shiro配置 --> |
Shiro
使用了日志框架slf4j
,因此需要对应配置指定的日志实现组件,如:log4j
,logback
等。- 在此,以使用
log4j
为日志实现为例:
- 在此,以使用
1 | <!-- |
4.2 集成 Shiro
在Spring
框架中集成Shiro
,本质上是与Spring IoC
容器和Spring MVC
框架集成。
4.2.1 Shiro
与Spring IoC
容器集成
Spring IoC
容器提供了一个非常重要的功能,就是依赖注入,将Bean
的定义以及Bean
之间关系的耦合通过容器来处理。- 也就是说,在
Spring
中集成Shiro
时,Shiro
中的相应Bean
的定义以及他们的关系也需要通过Spring IoC
容器实现。 Shiro
提供了与Web
集成的支持,其通过一个ShiroFilter
入口来拦截需要安全控制的URL
,然后进行相应的控制。ShiroFilter
类是安全控制的入口点,其负责读取配置(如ini
配置文件),然后判断URL
是否需要登录/权限等工作。- [urls] 部分的配置,其格式是:
url = 拦截器[参数], 拦截器[参数]
- [urls] 部分的配置,其格式是:
shiro
中默认的过滤器:
默认拦截器名 | 拦截器类与说明(括号里的表示默认值) |
---|---|
身份验证相关 | |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如”/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure); |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性:applicationName:弹出登录框显示的信息(application); |
logout | org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例”/logout=logout” |
user | org.apache.shiro.web.filter.authc.UserFilter 用户拦截器,用户已经身份验证/记住我登录的都可;示例”/**=user” |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例”/static/**=anon” |
授权相关 | |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例”/admin/**=roles[admin]” |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例”/user/**=perms[“user:create”]” |
port | org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例”/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径/参数等都一样 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例”/users=rest[user]”,会自动拼出”user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll); |
ssl | org.apache.shiro.web.filter.authz.SslFilter SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样; |
其他 | |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果 subject.getSession(true)将抛出 DisabledSessionException 异常; |
URL
匹配模式:url 模式使用 Ant 风格模式- Ant 路径通配符支持
?
、*
、**
,注意通配符匹配不包括目录分隔符“/”: ?
:匹配一个字符,如/admin? 将匹配/admin1,但不匹配/admin 或/admin/;*
:匹配零个或多个字符串,如/admin 将匹配/admin、/admin123,但不匹配/admin/1;**
:匹配路径中的零个或多个路径,如/admin/** 将匹配/admin/a 或/admin/a/b
- Ant 路径通配符支持
URL
匹配顺序:URL 权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。如:- /bb/**=filter1
- /bb/aa=filter2
- /**=filter3
- 如果请求的 url 是“/bb/aa”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截,所以通配符一般写在靠后。
1 | <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> |
4.2.2 与Spring MVC
集成
- 跟在普通
Java Web
应用中使用Shiro
一样,集成Shiro
到Spring MVC
时,实际上就是通过在web.xml
中添加指定Filter
实现。配置如下:
1 | <!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml --> |
Spring
中集成Shiro
的原理就是:通过在web.xml
中配置的Shiro Filter
与Spring IoC
中定义的相应的Shiro Bean
定义建立关系,从而实现在Spring
框架集成Shiro
。
4.3 数据源配置
在Shiro
中,Realm
定义了访问数据的方式,用来连接不同的数据源,如:LDAP,关系数据库,配置文件等。
- 以
org.apache.shiro.realm.jdbc.JdbcRealm
为例,将用户信息存放在关系型数据库中。 - 在使用
JdbcRealm
时,必须要在关系型数据库中存在 3 张表,分别是users
表,存放认证用户基本信息,在该表中必须存在 2 个字段:username
,password
。roles_permissions
表,存放角色和权限定义,在该表中必须存在 2 个字段:role_name
,permission
。user_roles
表,存放用户角色对应关系,在该表中必须存在 2 个字段:username
,role_name
。
- 实际上,在更加复杂的应用场景下,通常需要扩展
JdbcRealm
。
4.4 认证
在Shiro
中,认证即执行用户登录,读取指定Realm
连接的数据源,以验证用户身份的有效性与合法性。
- 在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号。
- credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
- 最常见的 principals 和 credentials 组合就是用户名/密码了
- 身份认证流程:
- 首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,ShiroAPI 中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
- Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
- Realm:一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)
1 | Subject subject = SecurityUtils.getSubject(); |
4.5 授权
Shiro 作为权限框架,仅仅只能控制对资源的操作权限,并不能完成对数据权限的业务需求。
- 而对于 Java Web 环境下 Shiro 授权,包含两个方面的含义。
- 其一,对于前端来说,用户只能看到他对应访问权限的元素。
- 其二,当用户执行指定操作(即:访问某个 uri 资源)时,需要验证用户是否具备对应权限。
- 对于第一点,在 Java Web 环境下,通过 Shiro 提供的 JSP 标签实现。
- 对于第二点,与在非 Java Web 环境下一样,需要在后端调用 API 进行权限(或者角色)检验。
- 在 Spring 框架中集成 Shiro,还可以直接通过 Java 注解方式实现
Permissions
:- 规则:
资源标识符:操作:对象实例ID
,即对哪个资源的哪个实例可以进行什么操作.其默认支持通配符权限字符串,: 表示资源/操作/实例的分割;, 表示操作的分割,* 表示任意资源/操作/实例。如:user:edit:manager
- 也可以使用通配符来定义,如:
user:edit:*
、user:*:*
、user:*:manager
- 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:
user:edit
等价于user:edit:*
、user
等价于user:*:*
- 注意:通配符只能从字符串的结尾处省略部件,也就是说
user:edit
并不等价于user:*:edit
- 也可以使用通配符来定义,如:
- 规则:
- 授权流程:
- 首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
- Authorizer 是真正的授权者,如果调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
- 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
- Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。
ModularRealmAuthorizer
进行多 Realm 匹配流程:- 首先检查相应的 Realm 是否实现了实现了 Authorizer;
- 如果实现了 Authorizer,那么接着调用其相应的
isPermitted*/hasRole*
接口进行匹配; - 如果有一个 Realm 匹配那么将返回 true,否则返回 false。
4.5.1 Shiro 标签
<shiro:guest></shiro:guest>
:用户没有身份验证时显示相应信息,即游客访问信息<shiro:user></shiro:user>
:用户已经经过认证/记住我登录后显示相应的信息。<shiro:authenticated></shiro:authenticated>
:用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的<shiro:notAuthenticated></shiro:notAuthenticated>
标签:用户未进行身份验证,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。<shiro:pincipal></shiro:pincipal>
:显示用户身份信息,默认调用Subject.getPrincipal()
获取,即 Primary Principal。- **
<shiro:hasRole></shiro:hasRole>
**标签:如果当前 Subject 有角色将显示 body 体内容 <shiro:hasAnyRoles></shiro:hasAnyRoles>
标签:如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容<shiro:lacksRole></shiro:lacksRole>
:如果当前 Subject 没有角色将显示 body 体内容- **
<shiro:hasPermission></shiro:hasPermission>
**:如果当前 Subject 有权限将显示 body 体内容 <shiro:lacksPermission></shiro:lacksPermission>
:如果当前 Subject 没有权限将显示 body 体内容
1 | <!-- 在jsp页面中引入shiro标签库 --> |
4.5.2 调用 API 进行权限(或者角色)检验
1 | String roleAdmin = "admin"; |
4.5.3 Shiro 权限注解
@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。- 通过自定义拦截器可以扩展功能,例如:动态 url-角色/权限访问控制的实现、根据 Subject 身份信息获取用户信息绑定到 Request(即设置通用数据)、验证码验证、在线用户信息的保存等
1 |
|
4.6 Spring 集成 Shiro 注意事项
Spring 4.2.0 RELEASE
+
与Spring 4.1.9 RELEASE
**-
**版本,配置方式有所不同。- 虽然
shiro
的注解定义是在Class
级别的,但是实际验证只能支持方法级别:@RequiresAuthentication
、@RequiresPermissions
、@RequiresRoles
。
5. Shiro 会话管理
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。
5.1 会话相关的 API
- 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):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作
5.2 会话监听器
会话监听器(SessionListiner):会话监听器用于监听会话创建、过期及停止事件
5.3 SessionDao
- AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等
- CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager
- MemorySessionDAO 直接在内存中进行会话维护
- EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。
5.4 数据表
1 | create table sessions ( |
5.5 会话验证
- Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话
- 出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler
- Shiro 也提供了使用 Quartz 会话验证调度器:QuartzSessionValidationScheduler
6. Shiro 缓存
- CacheManagerAware 接口
- Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager。
- Realm 缓存 + Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现; + AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对 AuthenticationInfo 和 AuthorizationInfo 信息的缓
存。 - Session 缓存
- 如 SecurityManager 实现了 SessionSecurityManager,其会判断 SessionManager 是否实现了 acheManagerAware 接口,如果实现了会把 CacheManager 设置给它。
- SessionManager 也会判断相应的 SessionDAO(如继承自 CachingSessionDAO)是否实现了 CacheManagerAware,如果实现了会把 CacheManager 设置给它
- 设置了缓存的 SessionManager,查询时会先查缓存,如果找不到才查数据库。
- RememberMe
- Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
- 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的 Cookie 写到客户端并保存下来;
- 关闭浏览器再重新打开;会发现浏览器还是记住你的;
- 访问一般的网页服务器端还是知道你是谁,且能正常访问;
- 但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
- Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
- 认证和记住我
- subject.isAuthenticated() 表示用户进行了身份验证登录的,即使有 Subject.login 进行了登录;
- subject.isRemembered():表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的 cookie 被窃取)在访问的
- 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。
- 建议
- 访问一般网页:如个人在主页之类的,我们使用 user 拦截器即可,user 拦截器只要用户登录(isRemembered() || isAuthenticated())过即可访问成功;
- 访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。
- 实现
- 如果要自己做 RememeberMe,需要在登录之前这样创建 Token:UsernamePasswordToken(用户名,密码,是否记住我),且调用 UsernamePasswordToken 的:token.setRememberMe(true); 方法
参考文章: