Apache Shiro(读作“sheeroh”,即日语“城”)是一个开源安全轻量级框架,提供身份验证、授权、密码学和会话管理。Shiro 框架直观、易用,同时也能提供健壮的安全性。
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。
引子
在学习之前,先了解几个名词,对于权限管理来说,最重要的就是(用户)认证和(用户)授权。
认证是为了确保你是个合法用户,常见的形式有账号+密码的方式,或者指纹、数字证书等。
授权就是用来控制(认证后)用户访问资源的,确保对应的用户只能访问指定的资源。
一般情况,当用户或者程序访问资源时,系统先判断该资源是否允许匿名访问,如果不允许就会检查该用户是否通过认证,没有就要求进行认证,也就是一般最常见的输入用户名和密码登陆。
认证通过后就会进入权限控制,就是检查当前的用户是否拥有权限操作该资源,如果没有就直接拒绝访问。
所以在设计的时候就有了这几个词:用户、权限、资源;为了方便一般会加入一个角色达到管理一堆权限的目的。
数据库设计中,一般就需要六张表:用户、权限、角色、资源、用户角色关系、角色权限关系。
用户与角色、角色与权限之间是多对多关系;权限与资源是多对一关系。因为权限都是针对资源来说的,资源在系统中都是固定的,所以一个资源(比如用户列表)对应多个权限(查看用户列表、修改用户列表)。
一般为了方便,会把资源和权限合并为一张表(资源名称、权限名称、资源地址)。
权限控制
关于权限的控制,主要可分为两类:
- 基于角色的访问控制
代码中是以角色为判断条件来控制的,当用户(角色)的权限需求变更时,只能修改对应的代码。
比如类似:if(user.hasRole("管理员")){....}
- 基于资源的访问控制
代码中是以权限为判断条件来控制的,当用户(角色)的权限需求变更时,只需要修改对应角色中的权限即可,不需要改变代码。
比如类似:if(user.hasPermission("查看用户列表")){......}
可以看出,第一种的扩展性很差,不利于系统维护,因为角色是针对人的,而人是活的(资源是死的);第二种只需要修改对应的角色权限列表即可,就是修改数据库的内容而已,完全可以通过 web 端应用做到,所以用的比较多。
权限管理的解决方案
一般情况也是分为两种:
- 粗颗粒权限管理
对资源类型的权限管理,比如菜单、用户信息等。
具体的例子有:管理员可以访问用户信息等页面,部门管理员可查看用户信息。 - 细颗粒权限管理
对资源实例的权限管理,就是资源类型的具体化,比如 XX 菜单、XX 用户信息。
具体的例子有:部门经理只能查看本部门的员工信息,用户只能查看自己的菜单。
然后下面就该谈实现了,对于粗颗粒来说是比较简单的,因为代码可以进行抽取,放在系统架构级别上统一处理,比如 SpringMVC 的拦截器就可以做到授权,基本上只需要判断下是否有权限就可以了。
但是对于细颗粒来说抽取就比较复杂了,在数据级别上是没有共性可言的,可以说是业务逻辑的一部分了(不单单是判断是否有权限那么简单了),在业务层去处理会比较简单,如果抽取到系统架构层面就非常麻烦,并且扩展性也很差,所以,一般情况下细颗粒的控制都是在业务层去实现。
下面来看具体的做法,一般情况下对于粗颗粒可以使用一些优秀的权限管理框架来做,比如 Shiro ,能够提高开发效率;如果不想用可以自己实现,方法一般用拦截器或者过滤器进行 url 的拦截(基于 url 拦截方式)。
如果自己实现的话,需要两个拦截器,一个负责用户认证的拦截(前面),一个负责用户权限(授权)的拦截。
可匿名访问的地址和公共地址一般是可配置的,也就是写在 prop 文件中,在写拦截器或者过滤器的时候读取这个文件来检查。
PS:自己实现的时候管理资源的 URL 是最头疼的,每一个页面里都有很多链接,都需要配置,确实麻烦;所以就有人想出来使用标识符来统一管理(数据库中增加一个标志字段)。
初识Shiro
最开始当然是先看看它能做什么,可以用一幅图来描述:
- Authentication:
身份认证 / 登录,验证用户是不是拥有相应的身份; - Authorization:
授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情
常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限; - Session Manager:
会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的; - Cryptography:
加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; - Web Support:
Web 支持,可以非常容易的集成到 Web 环境; - Caching:
缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率; - Concurrency:
shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; - Testing:
提供测试支持; - Run As:
允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; - Remember Me:
记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。
Shiro架构
同样,还是根据图来学习,这是官方提供的架构图:
- Subject:主体
可以看到主体可以是任何可以与应用交互的 “用户”;也就是说可以是人的操作也可以是程序的操作。 - SecurityManager:安全管理器
相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。 - Authenticator:认证器
负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了; - Authrizer:授权器
或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能; - Realm:域、领域
可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体(认证、授权相关数据);可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;在 realm 中存储授权和认证逻辑。
注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm; - SessionManager:会话管理器
主要用来管理 Session 的生命周期,Web 应用一般是用 Web 容器(比如 Tomcat)来管理。
Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器); - SessionDAO
通过 SessionDAO 管理 Session 数据,针对个性化的 Session 数据存储使用 SessionDAO。
DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能; - CacheManager:缓存管理器
来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
Shiro 只提供了缓存逻辑,还需要具体的缓存实现,比如和 ehcache 整合。 - Cryptography:密码模块
Shiro 提高了一些常见的加密组件用于如密码加密 / 解密、散列的。
认证入门程序
下面的入门代码是基于 SE 的,Realm 使用的是 ini 配置文件,就是说用户的认证信息都在 ini 配置文件里了:
1 | import org.apache.shiro.mgt; |
依赖什么的就不说了,太简单了,尤其是用了 Maven 后,然后还有 ini 的配置文件:
1 | [users] |
用 ini 而不用 properties 的一个原因就是 ini 中可以对数据进行分组。
下面来简单捋一下:
SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;Authenticator 才是真正的身份验证者,Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息。
这是稍微详细一点的执行流程(序号不是很对应):
- 首先通过 IniSecurityManagerFactory 并指定一个 ini 配置文件来创建一个 SecurityManager 工厂;
- 接着获取 SecurityManager 并绑定到 SecurityUtils,这是一个全局设置,设置一次即可;
- 通过 SecurityUtils 得到 Subject,其会自动绑定到当前线程;如果在 web 环境在请求结束时需要解除绑定;然后获取身份验证的 Token,如用户名 / 密码;
- 调用
subject.login()
方法进行登录,其会自动委托给SecurityManager.login()
方法进行登录; - SecurityManager 最终由 ModularRealmAuthenticator 进行认证(本例),ModularRealmAuthenticator 会调用 IniRealm 去配置文件查找用户信息。
如果查到用户信息就给 ModularRealmAuthenticator 返回用户信息。(本例的账号、密码)
如果查不到用户信息就给 ModularRealmAuthenticator 返回 null。 - ModularRealmAuthenticator 接收到 IniRealm 返回的 Authenticator 信息,如果是 null 就抛出 UnknownAccountException。
其他的可能抛出的 AuthenticationException 或其子类常见的如:
DisabledAccountException(禁用的帐号)
LockedAccountException(锁定的帐号)
UnknownAccountException(错误的帐号)
ExcessiveAttemptsException(登录失败次数过多)
IncorrectCredentialsException (错误的凭证)
ExpiredCredentialsException(过期的凭证)等 - (拓展)SecurityManager 接着会委托给 Authorizer(ModularRealmAuthorizer)进行授权,也就是执行 realm 中的授权方法进行查询权限。
- (拓展)权限信息返回给 ModularRealmAuthorizer 后会通过 PermissionResolver 把字符串转换成相应的 Permission 实例,然后进行对比,如果没有权限就抛出异常。
- 最后可以调用
subject.logout()
退出,其会自动委托给SecurityManager.logout()
方法退出。
对于页面的错误消息展示,最好使用如 “用户名 / 密码错误” 而不是 “用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库.
关于授权
授权的流程和认证差不多,只不过用的是 hasRole(判断是否具有某个角色) 而已,就不多说了。
授权的验证一般有三种形式,编程式(不推荐)、注解式、JSP/GSP 标签:
1 | // 编程式,测试时可以用 |
响应的在 ini 配置文件中也需要做出配置,主要是配角色:
1 | [users] |
角色的配置规则是:资源:操作:实例
,上面的例子中就是对 user 这个资源的所有实例进行 create 操作,写两段就意味着是 user:create:*
,所以说是可以使用通配符 *
的;多个规则用逗号分割。
下面是两个测试用例,一般分为两种,基于角色的授权和基于资源的授权;参考一下:
1 | // 基于资源的授权,根据资源标识符判断 |
isXXX 方法会返回布尔值,checkXXX 失败会抛出异常;篇幅限制,更多的内容去参考里的 wiki 看吧,因为项目中未使用,等用到了再进行补充。
自定义Realm
这是 Realm 的继承体系,自定义 Realm 可以直接继承自 Realm 这个顶级接口,也可以选择它的孩子;
一般,选择 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现),只需要实现验证和授权这两个方法逻辑就可以了。
顶级 Realm 接口定义的方法:
1 | //返回一个唯一的Realm名字 |
自定义 Realm 使用继承 AuthorizingRealm 的方式:
1 | public class MyRealm implements AuthorizingRealm { |
如果查到的密码和 token 里的密码不符调用方就会抛出 IncorrectCredentialsException;如果返回 null 就会抛出 UnknownAccountException。
最后别忘了配置下 ini 文件,把自定义的 Realm 写进去,要不然 Shiro 也不识别:
1 | [main] |
其他的代码和入门程序一样,就是把配置文件改改就可以了。
编码和加密
Shiro 还提供了一些关于常用的编码、加密。散列工具,使用也非常的简单,下面来看看简单的使用
编码&解码
Shiro 提供了 base64 和 16 进制字符串编码 / 解码的 API 支持,方便一些编码解码操作。Shiro 内部的一些数据的存储 / 表示都使用了 base64 和 16 进制字符串。
1 | // base64 操作 |
还有一个可能经常用到的类 CodecSupport,提供了 toBytes(str,”utf-8”) / toString(bytes,”utf-8”) 用于在 byte 数组 /String 之间转换。
散列
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐)防止暴力破解,盐有时是随机的(需要记录)。
1 | String str = "hello"; |
SimpleHash 可以指定散列算法,其内部使用了 Java 的 MessageDigest 实现。
为了方便使用,Shiro 提供了 HashService,默认提供了 DefaultHashService 实现。使用例子见参考。
加密&解密
Shiro 还提供对称式加密 / 解密算法的支持,如 AES、Blowfish 等;当前还没有提供对非对称加密 / 解密算法支持,未来版本可能提供。
下面是一个使用 AES 的例子:
1 | AesCipherService aesCipherService = new AesCipherService(); |
Shiro 还提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。
Shiro 默认提供了 PasswordService 实现 DefaultPasswordService;CredentialsMatcher 实现 PasswordMatcher 及 HashedCredentialsMatcher(更强大)。
使用
具体在 Shiro 中使用(也就是在 Realm 中)一般是先定义好 ini 文件,设置好使用什么,比如散列 md5,然后定义好散列几次
1 | [main] |
然后在自定义的 Realm 里返回身份信息的时候稍微改造一下:
1 | SimpleAuthenticationInfo ai = |
其他的和上面自定义 Realm 的代码一致,不需要更改。
这里的 pwd 和 salt 是从数据库获取的(假设存储介质是数据库),程序会拿着 token 中的密码和 salt 进行 md5 处理,然后和 pwd 进行对比。
PS:这里针对是的散列的验证,用于认证;上面说的是生成,可放在注册或者修改密码逻辑中。
与Spring整合
单独使用 Shiro 或者 SE 中用的情况其实不多,最多的还是 web 项目中用,自然就少不了要和 Spring 进行整合,首先准备工作当然是导入相应的依赖:
1 | <!-- shiro核心jar --> |
然后就需要在 web.xml 中配置过滤器了,毕竟权限的管理主要还是靠过滤器:
1 | <!-- shiro过滤器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 --> |
这里的关键是配 DelegatingFilterProxy,达到让 Spring 管理的 Bean 具有 Filter 的能力。
下面就是正式开始配置 Shiro 的配置了,可以建一个 spring-shiro.xml 文件,注意:一定要配置在 Spring 这个父容器,如果配置在 SpringMVC 子容器里是没用的,前面也提到过关于这父子容器的关系。
1 | <!-- Shiro 整合包里的 Web 过滤器,id 对应 web.xml 中指定的 targetBeanName --> |
内容比较多,需要注意的确实也不少,基本都是按最初没有使用 Spring 需要的那些对象来的,只要把那些对象搞定就 OK 了,比如 securityManager ,牵扯出了一系列的 Bean …..
使用了 authc 当用户没有认证时会跳转到指定页面,提交表单后凭证会传送给 FormAuthenticationFilter 进行验证(实际上最终会传给 Realm 进行查找凭证),如果没有找到或者凭证不正确会向 request 域填充异常信息(默认 key 为 shiroLoginFailure)。
然后可以在 controller 中从 request 取出这个异常信息判断是什么原因导致,值是异常的全路径,可以使用 xxx.class.getName().equals()
比较。
认证成功后 Controller 不需要处理,直接还是返回 login 页面即可,Shiro 会进行处理,默认是跳转到上一个页面。
在使用注解授权方式时,三层中都可以,但是推荐在 controller 中使用,比较直观,毕竟是入口;因为注解使用的是 AOP 代理的方式,所以还需要在 SpringMVC 的配置文件中开启 AOP 的支持:
1 | <!-- 开启aop,对类代理 --> |
配在 mvc 配置文件中的一个原因就是因为注解是加在 controller 上的,controller 的扫描是配在这的,所以放一起吧。
注解一般用 RequiresPermissions,而不用基于角色的。举几个例子:@RequiresPermissions(value="XXX")
or @RequiresPermissions("XXX")
or @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
拥有 user:a
或 user:a
权限。
缓存
shiro 中提供了认证信息(上面已经配过了,更高级的如果需要用到 Redis 之类的保存 Session,那就研究下 SessionDAO)和授权信息的缓存.
注意: shiro 默认关闭认证信息缓存, 但是对于授权信息的缓存默认是开启的.
由于 Shiro 只提供了缓存的处理逻辑,并没有实现具体的缓存逻辑(其实也有提供简单的实现),这里使用 ehcache 作为缓存的实现了,前面已经导入了相关的依赖,Spring 配置文件中也配好了,还缺一个 eh 缓存的配置文件 shiro-ehcache.xml:
1 | <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
参数就不解释了,以前也用过,在新版本中,无论用户正常退出还是非正常退出缓存都会自动清空。
但是,当管理员修改了用户的权限,但是该用户还没有退出,在默认情况下,修改的权限无法立即生效。需要手动进行编程实现:在权限修改后调用 realm 的 clearCache 方法清除缓存。
1 | //清除缓存 |
将上面的方法放在自定义的 Realm 中,在修改权限的 Service 中调用即可,但是我觉得这样只会清除当前用户的缓存,还有相关的一些代码贴在 github,等日后要仔细研究下,TODO。
clearCache 其同时调用 clearCachedAuthenticationInfo 和 clearCachedAuthorizationInfo,意为清空 AuthenticationInfo 和 AuthorizationInfo。
UserRealm 还提供了 clearAllCachedAuthorizationInfo、clearAllCachedAuthenticationInfo、clearAllCache,用于清空整个缓存。principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。
一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号
默认拦截器
Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的。
默认拦截器可以参考 org.apache.shiro.web.filter.mgt.DefaultFilter
中的枚举拦截器:
身份验证相关的
包名太长,所以省略前面相同的 org.apache.shiro.web.filter.authc
;说明栏中的括号里的内容是默认值。
默认拦截器名 | 拦截器类 | 说明 |
---|---|---|
authc | FormAuthenticationFilter | 基于表单的拦截器;如 /**=authc ,如果没有登录会跳到相应的登录页面登录;主要属性: usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp); successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure); |
authcBasic | BasicHttpAuthenticationFilter | Basic HTTP 身份验证拦截器 主要属性: applicationName:弹出登录框显示的信息(application); |
logout | LogoutFilter | 退出拦截器 主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 : /logout=logout |
user | UserFilter | 用户拦截器,用户已经身份验证或记住我登录的都可;示例 :/**=user |
anon | AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤; 示例 /static/**=anon |
另外还提供了一个 org.apache.shiro.web.filter.authz.HostFilter
,即主机拦截器,比如其提供了属性:
authorizedIps:已授权的 ip 地址
deniedIps:表示拒绝的 ip 地址;不过目前还没有完全实现,不可用。
这些默认的拦截器会自动注册,可以直接在 ini 配置文件中通过 拦截器名.属性
设置其属性.
授权相关的
包名太长,所以省略前面相同的 org.apache.shiro.web.filter.authz
;说明栏中的括号里的内容是默认值。
默认拦截器名 | 拦截器类 | 说明 |
---|---|---|
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色; 主要属性: loginUrl:登录页面地址(/login.jsp); unauthorizedUrl:未授权后重定向的地址;示例 : /admin/**=roles[admin] |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样; 示例 : /user/**=perms["user:create"] |
port | PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口; 示例 : /test= port[80] ,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样 |
rest | 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 | SslFilter | SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样; |
此外,还有一个 noSessionCreation(org.apache.shiro.web.filter.session.NoSessionCreationFilter)不创建会话拦截器,调用 subject.getSession(false)
不会有什么问题,但是如果 subject.getSession(true)
将抛出 DisabledSessionException 异常。
其他的 JSP 标签之类的看 wiki 吧,实在是太长了。
其他
需要验证验证码的就需要自定义 FormAuthenticationFilter 了,因为它是负责表单验证的,写一个类继承 FormAuthenticationFilter ,然后重写它的 onAccessDenied 方法,先从 Session 获取验证码和输入的比对,如果错误直接返回 true 终止执行,最好往 shiroLoginFailure 里加一下异常,便于 controller 的判断,通过则调用 super 的方法进行表单验证。
记得在配置文件里配一下自定义的验证器,上面其实已经配过了。
使用 Shiro 的记住我功能时需要把相关的 bean 设置为可序列化的,然后再在配置文件中配置 CookieRememberMeManager。
然后就可以使用 User 过滤器来指定那些 URL 是可以认证或者通过记住我就可以访问的。
也就是说 authc 拦截器即使使用了 记住我 也不会放行,user 可以。
参考
http://wiki.jikexueyuan.com/project/shiro/overview.html
https://www.ibm.com/developerworks/cn/java/j-lo-shiro/
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~