Apache Shiro
Apache Shiro 是一個強大而靈活的開源安全框架,它干凈利落地處理身份認證暴拄,授權噩翠,企業(yè)會話管理和加密思喊。
Apache Shiro Features
Shiro 把 Shiro 開發(fā)團隊稱為“應用程序的四大基石”——身份驗證,授權望浩,會話管理和加密作為其目標辖所。
- 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);
}
在幾乎所有環(huán)境中,都可以通過下面的調用獲取當前正在執(zhí)行的用戶:
Subject currentUser = SecurityUtils.getSubject();
subject指的是:"當前正在執(zhí)行的用戶的特定的安全視圖",可以把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
![](/Users/luozhiyun/javawork/notes/static/Shiro%E6%9E%B6%E6%9E%84.png)
- Subject:一個 Subject 可以是一個人磨德,但它還可以代表第三方服務或其他類似的任何東西——基本上是當前正與軟件進行交互的任何東西
- SecurityManager:是Shiro架構的心臟,并作為一種"保護傘"對象來協調內部的安全組件共同構成一個對象圖
- Realms::Realms 擔當 Shiro 和你的應用程序的安全數據之間的“橋梁”或“連接器”缘回。當它實際上與安全相 關的數據如用來執(zhí)行身份驗證(登錄)及授權(訪問控制)的用戶帳戶交互時, Shiro 從一個或多個為應用程 序配置的 Realm 中尋找許多這樣的東西典挑。
Apache Shiro Configuration
首先需要創(chuàng)建一個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 是一個無狀態(tài)的組件,它在身份驗證嘗試中被詢問 4 次(這 4 次交互所需的任何必要的 狀態(tài)將被作為方法參數):
- 在任何Realm被調用之前被詢問
- 在一個單獨的Realm的getAuthenticationInfo方法被調用之前立即被詢問
- 在一個單獨的Realm的getAuthenticationInfo方法被調用之后立即被詢問
- 在所有Realm被調用之后被詢問
Shiro 有 3 個具體的 AuthenticationStrategy 實現:
AtLeastOneSuccessfulStrategy:如果一個(或更多)Realm 驗證成功您觉,則整體的 嘗試被認為是成功的拙寡。如果沒有一個驗證成功,則整體嘗試失敗顾犹。
FirstSuccessfulStrategy:只有第一個成功地驗證的 Realm 返回的信息將被 使用倒庵。所有進一步的 Realm 將被忽略。如果沒有 一個驗證成功炫刷,則整體嘗試失敗。
AllSucessfulStrategy為了整體的嘗試成功郁妈,所有配置的 Realm 必須驗 證成功浑玛。如果沒有一個驗證成功,則整體嘗試失敗噩咪。
Authorization(授權)
控制誰有權限在應用程序中做什么
在 Shiro 中執(zhí)行授權可以有 3 種方式:
- 編寫代碼——你可以在你的 Java 代碼中用像 if 和 else 塊的結構執(zhí)行授權檢查
- 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*方法的一個好處就是代碼可以變得清潔顾彰,由于你不需要創(chuàng)建你自己的 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)的概念
在該例中建丧,第一部分是權限被操作的領域(打印機),第二部分是被執(zhí)行的操作(查詢)波势。上面其他的例子將被改 為:
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 的打印機的行為者疤。
如果你授予這些權限給用戶福澡,那么他們能夠在特定的實例上執(zhí)行特定的行為。然后你可以在代碼中做一個檢查:
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 提供安全框架界獨一無二的東西:一個完整的企業(yè)級 Session 解決方案辣辫,從最簡單的命令行及智能手機 應用到最大的集群企業(yè) 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:事件監(jiān)聽器允許你在會話生命周期監(jiān)聽生命周期事件。你可以偵聽這些事件和對自定義應用 程序的行為作出反應——例如俯邓,更新用戶記錄當他們的會話過期時骡楼。
- Host Address Retention:Shiro Sessions 從會話發(fā)起地方保留 IP 地址或主機名。這允許你確定用戶所在稽鞭,并作 出相應的反應(通常是在 IP 分配確定的企業(yè)內部網絡環(huán)境)鸟整。
- Inactivity/Expiration Support:由于不活動導致會話過期如預期的那樣,但它們可以延續(xù)很久通過 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 實例代表了一個單一應用程序用戶的安全狀態(tài)和操作
這些操作包括:
- authentication(login)
- authorization(access control)
- session access
- logout
The Currently Executing Subject
getSubject()方法調用一個獨立的應用程序病苗,該應用程序可以返回一個在應用程序特有位置上基于用戶數據的 Subject, 在服務器環(huán)境中(如症汹,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執(zhí)行 -->
<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>