Apache Shiro
Apache Shiro 是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。
Apache Shiro Features
Shiro 把 Shiro 开发团队称为“应用程序的四大基石”——身份验证,授权,会话管理和加密作为其目标。
- Authentication:简称为"登录",这是一个证明用户是他们所说的他们是谁的行为
- Authorization:访问空值的过程,也就是"谁"去访问"什么";
- Session Management:管理用户特定的会话, 即使在非 Web 或 EJB 应用程序
- Cryptography:通过使用加密算法保持数据安全同时易于使用
开始一个应用程序
public static void main(String[] args){
//1.
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.
SecurityManager securityManager = factory.getInstance();
//3.
SecurityUtils.setSecurityManage(securityManger);
System.exit(0);
}
在几乎所有环境中,都可以通过下面的调用获取当前正在执行的用户:
Subject currentUser = SecurityUtils.getSubject();
subject指的是:"当前正在执行的用户的特定的安全视图",可以把Subject看成是shiro的"User"概念
subject获取会话
Session session = currentUser.getSession();
session.setAttribute("someKye","aValue");
为已知用户做检查
if(!currentUser.isAuthenticated()){
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr","vespa");
token.setRememberMe(true);
currentUser.login(token);
}
如果登陆失败则可以进行异常捕获
try{
currentUser.login(token);
}catch(UnknownAccountException u){
//username wasn't in the system ,show them an error message?
}catch(IncorrectCredentitalsException e){
//password didn't match ,try again?
}catch(LockedAccountException l){
//account for that username is locked -can't login.
}
获取登陆的角色
log.info("User ["+currentUser.getPrincipal()+"] logged in successfully.");
测试是否有特定的角色:
if(currentUser.hasRole("luoluo")){
log.info("May the luoluo be with you ");
}else{
log.info("hello ,mere mortal.");
}
判断是否有权限在一个确定类型实体上进行操作:
if(currentUser.isPermitted("lightsaber:weild")){
log.info("You may use a lightsaber ring. Use it wisely.");
}else{
log.info("Sorry,lightsaber rings are for schwartz masters only");
}
注销
currentUser.logout();
完整事例:
public class Tutorial{
private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);
public static void main(String[] args){
log.info("My First Apache Shiro Application");
//加载用户信息
Factory<SecurityManager> factory = new IniSecurityManagerFacotry("classpath:shiro:ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//get the currently executing user;
Subject currentUser = SecurityUtils.getSubject();
//Do some stuff with a session(no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey","aValue");
String value = (String)session.getAttribute("someKey");
if(value.equals("aValue")){
log.info("Retrieved the corrent value!["+value+"]");
}
//let's login the current user so we can check against and permissions;
if(!currentUser.isAuthenticated()){
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr","vespa");
token.setRememberMe(true);
try{
currentUser.login(token);
}catch(UnknownAccountException u){
log.info("There is no user with username of "+token.getPrincipal());
}catch(IncorrentCredentitalsException i){
log.info("Password for account "+token.getPrincipal()+" was incorrect!");
}catch(LockedAccountException l){
}
}
//sya who they are:
//print their identitfying principal(in this case,a username)
log.info("User["+currentUser.getPrincipal()+"]logged in successfully.");
//test a role ;
if(currentUser.hasRole("luoluo")){
log.info("May the luoluo be with you ");
}else{
log.info("hello ,mere mortal");
}
//test a typed permission(not instance-level)
if(currentUser.isPermitted("lightsaber:weild")){
log.info("you may use a lightsaber:weild");
}else{
log.info("Sorry , lightsaber rings are for schwartz masters only.");
}
//log_out!
currentUser.logout();
System.exit(0);
}
}
Apache Shiro Architecture
shiro的架构有3个主要的概念:Subject ,SecurityManager 和 Realms
- Subject:一个 Subject 可以是一个人,但它还可以代表第三方服务或其他类似的任何东西——基本上是当前正与软件进行交互的任何东西
- SecurityManager:是Shiro架构的心脏,并作为一种"保护伞"对象来协调内部的安全组件共同构成一个对象图
- Realms::Realms 担当 Shiro 和你的应用程序的安全数据之间的“桥梁”或“连接器”。当它实际上与安全相 关的数据如用来执行身份验证(登录)及授权(访问控制)的用户帐户交互时, Shiro 从一个或多个为应用程 序配置的 Realm 中寻找许多这样的东西。
Apache Shiro Configuration
首先需要创建一个SecurityManager
Realm realm = //instantiate or acquire a Realm instance.
SecurityManager securityManger = new DefaultSecurityManager(realm);
//Make the SecurityManager instance available to the entire application via static memory;
SecurityUtils.setSecurityManager(securityManager);
Authenticating Subjects(验证Subjects)
可以大致分为三步:
- 收集Subjects提交的Principals(身份)和Credentials(凭证)
- 提交Principals(身份)和Credentials(凭证)进行身份验证
- 如果提交成功,则允许访问,否则重新进行身份验证
Set1:
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
token.setRememberMe(true);
Step2:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
通过login方法,有效的体现了身份验证
Step3:处理成功或失败
如果验证成功,则调用subject.isAuthenticated()的调用将返回true
验证失败则运行AuthenticationException
try{
curretnUser.login(token);
}catch(UnknownAccountException uae){
}catch(IncorrectCredentialsException ice){
}catch(LockedAccountException eas){
}catch(ExcessiveAttemptsException eae){
}catch(AuthenticationException ae){
}
AuthenticationStrategy
当一个应用程序配置了两个或两个以上的 Realm 时,ModularRealmAuthenticator 依靠内部的 AuthenticationStrategy 组件来确定这些认证尝试的成功或失败条件
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这 4 次交互所需的任何必要的 状态将被作为方法参数):
- 在任何Realm被调用之前被询问
- 在一个单独的Realm的getAuthenticationInfo方法被调用之前立即被询问
- 在一个单独的Realm的getAuthenticationInfo方法被调用之后立即被询问
- 在所有Realm被调用之后被询问
Shiro 有 3 个具体的 AuthenticationStrategy 实现:
AtLeastOneSuccessfulStrategy:如果一个(或更多)Realm 验证成功,则整体的 尝试被认为是成功的。如果没有一个验证成功,则整体尝试失败。
FirstSuccessfulStrategy:只有第一个成功地验证的 Realm 返回的信息将被 使用。所有进一步的 Realm 将被忽略。如果没有 一个验证成功,则整体尝试失败。
AllSucessfulStrategy为了整体的尝试成功,所有配置的 Realm 必须验 证成功。如果没有一个验证成功,则整体尝试失败。
Authorization(授权)
控制谁有权限在应用程序中做什么
在 Shiro 中执行授权可以有 3 种方式:
- 编写代码——你可以在你的 Java 代码中用像 if 和 else 块的结构执行授权检查
- JDK 的注解——你可以添加授权注解给你的 Java 方法
- JSP/GSP 标签库——你可以控制基于角色和权限的 JSP 或者 GSP 页面输出
Role-Based Authorization(基于角色的授权)
Role checks
Subject currentUser = SecurityUtils.getSubject();
if(currentUser.hasRole("administrator")){
//show the admin button
}else{
//don't show the button?Grey it out?
}
Role Assertions(角色断言)
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is a bank teller and therefore allowed to open the account:
currentUser.checkRole("bankTeller");
openBankAccount();
通过使用 hasRole*方法的一个好处就是代码可以变得清洁,由于你不需要创建你自己的 AuthorizationException 如果 当前的 Subject 不符合预期条件
Permission-Based Authorization(基于权限的授权)
Permission Checks(权限检查)
1.基于对象
Permission printPermission = new PrinterPermission("laserjet4400h","print");
Subject currentUser = SecurityUtils.getSubject();
if(currentUser.isPermitted(printPermission)){
//show the Print button
}else{
//don't show the button?
}
2.基于字符串
Subject currentUser = SecurityUtils.getSubject();
if(currentUser.isPermitted("printer:print:laserjet4400n")){
//TODO
}else{
//TODO
}
基于字符串的权限是很有帮助的,由于你不必被迫实现一个接口,而且简单的字符串易于阅读。其缺点是,你不具 备类型安全,如果你需要更为复杂的行为将超出了字符串所能代表的范围,你就得实现你自己的基于权限接口的权 限对象。
Permission Assertions(权限断言)
Subject currentUser = SecurityUtils.getSubject();
Permission p = new AccountPersion("open");
currentUser.checkPermission(p);
openBankAccount();
或者,使用字符串权限:
Subject currentUser = SecurityUtils.getSubject();
currentUser.checkPermission("open");
openBankAccount();
Wildcard Permissions
为了使用易于处理且仍然可读的权限语句,Shiro 提供了强大而直观的语法,我们称之为 WildcardPermission。
一个极其简单的方法是授予用户"queryPrinter"权限。然后你可以检查用户是否具有 queryPrinter 权限通过调用:
subject.isPermitted("queryPrinter");
//上面的语法等同于
subject.isPermitted(new WildcardPermission("queryPrinter"));
Multiple Parts
通配符权限支持多层次或部件(parts)的概念
在该例中,第一部分是权限被操作的领域(打印机),第二部分是被执行的操作(查询)。上面其他的例子将被改 为:
printer:print
printer:manage
Multiple Vaules
printer:print,query
它能够赋予用户 print 和 query 打印机的能力。由于他们被授予了这两个操作,你可以通过调用下面的语句来判断用 户是否有能力查询打印机:
subject.isPermitted("print:query");//返回true
All Values
printer:query,print,manage
简单点:
printer:*
最后,在一个通配符权限字符串中的任何部分使用通配符 token 也是可以的。例如,如果你想对某个用户在所有领 域(不仅仅是打印机)授予"view"权限,你可以这样做:
*:view
Instance-Level Access Control
另一种常见的通配符权限用法是塑造实例级的访问控制列表。在这种情况下,你使用三个部件——第一个是域,第 二个是操作,第三个是被付诸实施的实例。
printer:query:lp7200
printer:print:epsoncolor
第一个定义了查询拥有 ID lp7200 的打印机的行为。第二条权限定义了打印到拥有 ID epsoncolor 的打印机的行为。
如果你授予这些权限给用户,那么他们能够在特定的实例上执行特定的行为。然后你可以在代码中做一个检查:
if(SecurityUtils.getSubject().isPermitted("printer:query:lp7200")){
//Return the current jobs on printer lp 7200
}
Missing Parts
printer:print
等价于
printer:print:*
printer
等价于
printer:*.*
Apache Shiro Realms
Realm 是一个能够访问应用程序特定的安全数据(如用户、角色及权限)的组件。
Handling supported AuthenticationTokens
若 Realm 支持一个提交的 AuthenticationToken,那么 Authenticator 将会调用该 Realm 的 getAuthenticationInfo(token) 方法。这有效地代表了一个与 Realm 的后备数据源的授权尝试。该方法按以下方法进行:
- 为主要的识别信息检查token
- 基于principal在数据源中寻找相吻合的账户数据
- 确保token支持的credentials匹配那些存储在数据源的
- 若credentitals匹配,返回一个封装了Shiro能够理解的账户数据格式的AuthenticationInfo实例
- 若credentials不匹配,则抛出AuthenticationException异常
Session Management
Apache Shiro 提供安全框架界独一无二的东西:一个完整的企业级 Session 解决方案,从最简单的命令行及智能手机 应用到最大的集群企业 Web 应用程序。
即使你在一个 Servlet 或 EJB 容器中部署你的应用程序,仍然有令人信服的理由来使用 Shiro 的 Session 支持而不是容 器的。下面是一个 Shiro 的 Session 支持的最可取的功能列表:
- pojo/j2sebased:Shiro 的一切(包括所有 Session 和 Session Management 方面)都是基于接口和 POJO 实现
- Easy Custom Session Storage:因为 Shiro 的 Session 对象是基于 POJO 的,会话数据可以很容易地存储在任意 数量的数据源。
- Container-Independent Clustering:Shiro 的会话可以很容易地聚集通过使用任何随手可用的网络缓存产品,像 Ehcache + Terracotta,Coherence,GigaSpaces, 等等。
- Heterogeneous Client Access:与 EJB 或 web 会话不同, Shiro 会话可以被各种客户端技术“共享”。
- Event Listeners:事件监听器允许你在会话生命周期监听生命周期事件。你可以侦听这些事件和对自定义应用 程序的行为作出反应——例如,更新用户记录当他们的会话过期时。
- Host Address Retention:Shiro Sessions 从会话发起地方保留 IP 地址或主机名。这允许你确定用户所在,并作 出相应的反应(通常是在 IP 分配确定的企业内部网络环境)。
- Inactivity/Expiration Support:由于不活动导致会话过期如预期的那样,但它们可以延续很久通过 touch()方法 来保持它们“活着”,如果你希望的话。
- Transparent Web Use:Shiro的网络支持
- Can be used for SSO:由于 Shiro 会话是基于 POJO 的,它们可以很容易地存储在任何数据源,而且它们可以跨 程序“共享”如果需要的话。
使用方式:
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey","someValue");
JSP Tag Library
引入:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
The guest tag
guest 标签将显示它包含的内容,仅当当前的 Subject 被认为是'guest'时。'guest'是指没有身份 ID 的任何 Subject。也 就是说,我们并不知道用户是谁
<shiro:guest>
Hi there ! Please<a href = "login.jsp">Login</a>or<a href="signup.jsp">Signup</a>today!
</shiro:guest>
The user Tag
user 标签将显示它包含的内容,仅当当前的 Subject 被认为是'user'时。'user'在上下文中被定义为一个已知身份 ID 的 Subject,或是成功通过身份验证及通过'RememberMe'服务的。请注意这个标签在语义上与 authenticated 标签是 不同的,authenticated 标签更为严格。
<shiro:user>
Welcome back luoluo ! NOt luoluo? Click<a href="login.jsp">here<a>to login.
</shiro:user>
The authenticated tag
仅仅只当当前用户在当前会话中成功地通过了身份验证 authenticated 标签才会显示包含的内容。它比'user'标签更 为严格。
<shiro:authenticated>
<a href="updateAccount.jsp">Update your contact information</a>
</shiro:authenticated>
The principal tag
principal 标签将会输出 Subject 的主体(标识属性)或主要的属性。
Hello,<shiro:principal />,how are you today?
等价于
Hello,<%=SecurityUtils.getSubject().getPrincipal().toString() %>,how are you today?
Typed principal
principal 标签默认情况下,假定该 principal 输出的是 subject.getPrincipal()的值。但若你想输出一个不是主要 principal 的值,而是属于另一个 Subject 的 principal collection,你可以通过类型来获取该 principal 并输出该值。
User ID:<principal type="java.lang.Integer" />
等价于
User ID:<%=SecurityUtils.getSubject().getPrincipals().oneByType(Integer.class).toString()%>
Principal property
但如果该 principal(是默认主要的 principal 或是上面的'typed' principal)是一个复杂的对象而不是一个简单的字符串, 而且你希望引用该 principal 上的一个属性该怎么办呢?你可以使用 property 属性来来表示 property 的名称来理解 (必须通过 JavaBeans 兼容的 getter 方法访问)。
Hello,<shiro:principal property="firstName" />,how are you today
等价于
Hello,<%=SecurityUtils.getSubject().getPrincipal().getFirstName().toString() %>,how are you today?
或者结合属性
Hello,<shiro:principal type="com.foo.User" property="firstName" /> ,how are you today?
等价于
Hello ,<%=SecurityUtils.getSubject().getPrincipals.oneByType(com.foo.User.class).getFirstName().toString() %>,how are you today?
The hasRole tag
<shiro:hasRole name="administrator">
<a href="admin.jsp">Administer the system</a>
</shiro:hasRole>
The hasAnyRole tag
<shiro:hasAnyRole name="developer,project manager , administrator">
You are either a developer,project manager,or administrater.
</shiro:hasAnyRole>
The hasPermission tag
hasPermission 标签将会显示它所包含的内容,仅当当前 Subject“拥有”(蕴含)特定的权限。也就是说,用户具 有特定的能力。
<shiro:hasPermission name="user:create">
<a href="createUser.jsp">Create a new User</a>
</shiro:hasPermission>
The lacksPermission tag
lacksPermission 标签将会显示它所包含的内容,仅当当前 Subject 没有拥有(蕴含)特定的权限。也就是说,用户没 有特定的能力。
<shiro:lacksPermission name="user:delete">
Sorry,you are not allowed to deleted user accounts;
</shiro:lacksPermission>
Understanding Subjects in Apache Shiro
一个 Shiro Subject 实例代表了一个单一应用程序用户的安全状态和操作
这些操作包括:
- authentication(login)
- authorization(access control)
- session access
- logout
The Currently Executing Subject
getSubject()方法调用一个独立的应用程序,该应用程序可以返回一个在应用程序特有位置上基于用户数据的 Subject, 在服务器环境中(如,Web 应用程序),它基于与当前线程或传入的请求相关的用户数据上获得 Subject。
Subject currentUser = SecurityUtils.getSubject();
可得的他们的 session:
Session session = currentUser.getSession();
session.setAttribute("someKey","aValue");
可以获取用户信息
log.info("User["+currentUser.getPrincipal()+"]logged in successfully");
可以测试是否有特定的角色
if(currentUser.hasRole("schwartz")){
log.info(".....");
}else{
log.info(".....");
}
可以判断否有权限
if(currentUser.isPermitted("...")){
}else{
}
可以注销
currentUser.logout();
Spring Framework
这里是在 Spring 应用程序中启用应用程序单例 SecurityManager 的最简单的方法:
web.xml
<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>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<description>Shrio的配置文件</description>
<!-- SecurityManager配置 -->
<!-- 配置Realm域 -->
<!-- 密码比较器 -->
<!-- 代理如何生成? 用工厂来生成Shiro的相关过滤器-->
<!-- 配置缓存:ehcache缓存 -->
<!-- 安全管理 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- Single realm app. If you have multiple realms, use the 'realms' property instead. -->
<property name="realm" ref="authRealm"/><!-- 引用自定义的realm -->
<!-- 缓存 -->
<property name="cacheManager" ref="shiroEhcacheManager"/>
</bean>
<!-- 自定义权限认证 -->
<bean id="authRealm" class="cn.itcast.jk.shiro.AuthRealm">
<property name="userService" ref="userService"/>
<!-- 自定义密码加密算法 -->
<property name="credentialsMatcher" ref="passwordMatcher"/>
</bean>
<!-- 设置密码加密策略 md5hash -->
<bean id="passwordMatcher" class="cn.itcast.jk.shiro.CustomCredentialsMatcher"/>
<!-- filter-name这个名字的值来自于web.xml中filter的名字 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!--登录页面 -->
<property name="loginUrl" value="/index.jsp"></property>
<!-- 登录成功后 -->
<property name="successUrl" value="/home.action"></property>
<property name="unauthorizedUrl" value="/index.jsp"></property>
<property name="filterChainDefinitions">
<!-- /**代表下面的多级目录也过滤 -->
<value>
/index.jsp* = anon
/home* = anon
/sysadmin/login/login.jsp* = anon
/sysadmin/login/logout.jsp* = anon
/login* = anon
/logout* = anon
/components/** = anon
/css/** = anon
/images/** = anon
/js/** = anon
/make/** = anon
/skin/** = anon
/stat/** = anon
/ufiles/** = anon
/validator/** = anon
/resource/** = anon
/sysadmin/deptAction_* = perms["部门管理"]
/** = authc
/*.* = authc
</value>
</property>
</bean>
<!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 生成代理,通过代理进行控制 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<!-- 安全管理器 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
如果需要设置注解,则要添加如下配置
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor" />
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>