標籤:rmi 選擇 nbsp 實現 login client mem 使用者 logo
Apache Shiro 是功能強大並且容易整合的開源許可權架構,它能夠完成認證、授權、加密、會話管理等功能。認證和授權為許可權控制的核心,簡單來說,“認證”就是證明你是誰? Web 應用程式一般做法通過表單提交使用者名稱及密碼達到認證目的。“授權”即是否允許已認證使用者訪問受保護資源。關於 Shiro 的一系列特徵及優點,很多文章已有列舉,這裡不再逐一贅述,本文重點介紹 Shiro 在 Web Application 中如何?驗證碼認證以及如何?單點登入。
使用者權限模型
在揭開 Shiro 面紗之前,我們需要認知使用者權限模型。本文所提到使用者權限模型,指的是用來表達使用者資訊及使用者權限資訊的資料模型。即能證明“你是誰?”、“你能訪問多少受保護資源?”。為實現一個較為靈活的使用者權限資料模型,通常把使用者資訊單獨用一個實體表示,使用者權限資訊用兩個實體表示。
- 使用者資訊用 LoginAccount 表示,最簡單的使用者資訊可能只包含使用者名稱 loginName 及密碼 password 兩個屬性。實際應用中可能會包含使用者是否被禁用,使用者資訊是否到期等資訊。
- 使用者權限資訊用 Role 與 Permission 表示,Role 與 Permission 之間構成多對多關係。Permission 可以理解為對一個資源的操作,Role 可以簡單理解為 Permission 的集合。
- 使用者資訊與 Role 之間構成多對多關係。表示同一個使用者可以擁有多個 Role,一個 Role 可以被多個使用者所擁有。
圖 1. 使用者權限模型
回頁首
認證與授權Shiro 認證與授權處理過程
- 被 Shiro 保護的資源,才會經過認證與授權過程。使用 Shiro 對 URL 進行保護可以參見“與 Spring 整合”章節。
- 使用者訪問受 Shiro 保護的 URL;例如 http://host/security/action.do。
- Shiro 首先檢查使用者是否已經通過認證,如果未通過認證檢查,則跳轉到登入頁面,否則進行授權檢查。認證過程需要通過 Realm 來擷取使用者及密碼資訊,通常情況我們實現 JDBC Realm,此時使用者認證所需要的資訊從資料庫擷取。如果使用了緩衝,除第一次外使用者資訊從緩衝擷取。
- 認證通過後接受 Shiro 授權檢查,授權檢查同樣需要通過 Realm 擷取使用者權限資訊。Shiro 需要的使用者權限資訊包括 Role 或 Permission,可以是其中任何一種或同時兩者,具體取決於受保護資源的配置。如果使用者權限資訊未包含 Shiro 需要的 Role 或 Permission,授權不通過。只有授權通過,才可以訪問受保護 URL 對應的資源,否則跳轉到“未經授權頁面”。
Shiro Realm
在 Shiro 認證與授權處理過程中,提及到 Realm。Realm 可以理解為讀取使用者資訊、角色及許可權的 DAO。由於大多 Web 應用程式使用了關聯式資料庫,因此實現 JDBC Realm 是常用的做法,後面會提到 CAS Realm,另一個 Realm 的實現。
清單 1. 實現自己的 JDBC Realm
public class MyShiroRealm extends AuthorizingRealm{ // 用於擷取使用者資訊及使用者權限資訊的業務介面 private BusinessManager businessManager; // 擷取授權資訊 protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { String username = (String) principals.fromRealm( getName()).iterator().next(); if( username != null ){ // 查詢使用者授權資訊 Collection<String> pers=businessManager.queryPermissions(username); if( pers != null && !pers.isEmpty() ){ SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for( String each:pers ) info.addStringPermissions( each ); return info; } } return null; } // 擷取認證資訊 protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken ) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; // 通過表單接收的使用者名稱 String username = token.getUsername(); if( username != null && !"".equals(username) ){ LoginAccount account = businessManager.get( username ); if( account != null ){ return new SimpleAuthenticationInfo( account.getLoginName(),account.getPassword(),getName() ); } } return null; } }
代碼說明:
- businessManager 表示從資料庫擷取使用者資訊及使用者權限資訊的業務類,實際情況中可能因使用者權限模型設計不同或持久化架構選擇不同,這裡沒給出範例程式碼。
- doGetAuthenticationInfo 方法,取使用者資訊。對照使用者權限模型來說,就是取 LoginAccount 實體。最終我們需要為 Shiro 提供 AuthenticationInfo 對象。
- doGetAuthorizationInfo 方法,擷取使用者權限資訊。代碼給出了擷取使用者 Permission 的樣本,擷取使用者 Role 的代碼類似。為 Shiro 提供的使用者權限資訊以 AuthorizationInfo 對象形式返回。
回頁首
為何對 Shiro 情有獨鐘
或許有人要問,我一直在使用 spring,應用程式的安全性群組件早已選擇了 Spring Security,為什麼還需要 Shiro ?當然,不可否認 Spring Security 也是一款優秀的安全控制組件。本文的初衷不是讓您必須選擇 Shiro 以及必須放棄 Spring Security,秉承客觀的態度,下面對兩者略微比較:
- 簡單性,Shiro 在使用上較 Spring Security 更簡單,更容易理解。
- 靈活性,Shiro 可運行在 Web、EJB、IoC、Google App Engine 等任何應用環境,卻不依賴這些環境。而 Spring Security 只能與 Spring 一起整合使用。
- 可插拔,Shiro 乾淨的 API 和設計模式使它可以方便地與許多的其它架構和應用進行整合。Shiro 可以與諸如 Spring、Grails、Wicket、Tapestry、Mule、Apache Camel、Vaadin 這類第三方架構無縫整合。Spring Security 在這方面就顯得有些捉衿見肘。
回頁首
與 Spring 整合
在 Java Web Application 開發中,Spring 得到了廣泛使用;與 EJB 相比較,可以說 Spring 是主流。Shiro 自身提供了與 Spring 的良好支援,在應用程式中整合 Spring 十分容易。
有了前面提到的使用者權限資料模型,並且實現了自己的 Realm,我們就可以開始整合 Shiro 為應用程式服務了。
Shiro 的安裝
Shiro 的安裝非常簡單,在 Shiro 官網下載 shiro-all-1.2.0.jar、shiro-cas-1.2.0.jar(單點登入需要),及 SLF4J 官網下載 Shiro 依賴的日誌組件 slf4j-api-1.6.1.jar。Spring 相關的 JAR 包這裡不作列舉。這些 JAR 包需要放置到 Web 工程 /WEB-INF/lib/ 目錄。至此,剩下的就是配置了。
配置過濾器
首先,配置過濾器讓請求資源經過 Shiro 的過濾處理,這與其它過濾器的使用類似。
清單 2. web.xml 配置
<filter> <filter-name>shiroFilter</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Spring 配置
接下來僅僅配置一系列由 Spring 容器管理的 Bean,整合大功告成。各個 Bean 的功能見代碼說明。
清單 3. Spring 配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.do"/> <property name="successUrl" value="/welcome.do"/> <property name="unauthorizedUrl" value="/403.do"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /=anon /login.do*=authc /logout.do*=anon # 許可權配置樣本 /security/account/view.do=authc,perms[SECURITY_ACCOUNT_VIEW] /** = authc </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myShiroRealm"/> </bean> <bean id="myShiroRealm" class="xxx.packagename.MyShiroRealm"> <!-- businessManager 用來實現使用者名稱密碼的查詢 --> <property name="businessManager" ref="businessManager"/> <property name="cacheManager" ref="shiroCacheManager"/> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManager" ref="cacheManager"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/>
代碼說明:
- shiroFilter 中 loginUrl 為登入頁面地址,successUrl 為登入成功頁面地址(如果首先訪問受保護 URL 登入成功,則跳轉到實際訪問頁面),unauthorizedUrl 認證未通過訪問的頁面(前面提到的“未經授權頁面”)。
- shiroFilter 中 filters 屬性,formAuthenticationFilter 配置為基於表單認證的過濾器。
- shiroFilter 中 filterChainDefinitions 屬性,anon 表示匿名訪問(不需要認證與授權),authc 表示需要認證,perms[SECURITY_ACCOUNT_VIEW] 表示使用者需要提供值為“SECURITY_ACCOUNT_VIEW”Permission 資訊。由此可見,串連地址配置為 authc 或 perms[XXX] 表示為受保護資源。
- securityManager 中 realm 屬性,配置為我們自己實現的 Realm。關於 Realm,參見前面“Shiro Realm”章節。
- myShiroRealm 為我們自己需要實現的 Realm 類,為了減小資料庫壓力,添加了緩衝機制。
- shiroCacheManager 是 Shiro 對緩衝架構 EhCache 的配置。
回頁首
實現驗證碼認證
驗證碼是有效防止暴力破解的一種手段,常用做法是在服務端產生一串隨機字串與目前使用者會話關聯(我們通常說的放入 Session),然後向終端使用者展現一張經過“擾亂”的圖片,只有當使用者輸入的內容與服務端產生的內容相同時才允許進行下一步操作。
產生驗證碼
作為示範,我們選擇開源的驗證碼組件 kaptcha。這樣,我們只需要簡單配置一個 Servlet,頁面通過 IMG 標籤就可以展現圖形驗證碼。
清單 4. web.xml 配置
<!-- captcha servlet--> <servlet> <servlet-name>kaptcha</servlet-name> <servlet-class> com.google.code.kaptcha.servlet.KaptchaServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>kaptcha</servlet-name> <url-pattern>/images/kaptcha.jpg</url-pattern> </servlet-mapping>
擴充 UsernamePasswordToken
Shiro 表單認證,頁面提交的使用者名稱密碼等資訊,用 UsernamePasswordToken 類來接收,很容易想到,要接收頁面驗證碼的輸入,我們需要擴充此類:
清單 5. CaptchaUsernamePasswordToken
public class CaptchaUsernamePasswordToken extends UsernamePasswordToken{ private String captcha; // 省略 getter 和 setter 方法 public CaptchaUsernamePasswordToken(String username, char[] password, boolean rememberMe, String host,String captcha) { super(username, password, rememberMe, host); this.captcha = captcha; } }擴充 FormAuthenticationFilter
接下來我們擴充 FormAuthenticationFilter 類,首先覆蓋 createToken 方法,以便擷取 CaptchaUsernamePasswordToken 執行個體;然後增加驗證碼校正方法 doCaptchaValidate;最後覆蓋 Shiro 的認證方法 executeLogin,在原表單認證邏輯處理之前進行驗證碼校正。
清單 6. CaptchaUsernamePasswordToken
public class CaptchaFormAuthenticationFilter extends FormAuthenticationFilter{ public static final String DEFAULT_CAPTCHA_PARAM = "captcha"; private String captchaParam = DEFAULT_CAPTCHA_PARAM; public String getCaptchaParam() { return captchaParam; } public void setCaptchaParam(String captchaParam) { this.captchaParam = captchaParam; } protected String getCaptcha(ServletRequest request) { return WebUtils.getCleanParam(request, getCaptchaParam()); } // 建立 Token protected CaptchaUsernamePasswordToken createToken( ServletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); String captcha = getCaptcha(request); boolean rememberMe = isRememberMe(request); String host = getHost(request); return new CaptchaUsernamePasswordToken( username, password, rememberMe, host,captcha); } // 驗證碼校正 protected void doCaptchaValidate( HttpServletRequest request ,CaptchaUsernamePasswordToken token ){ String captcha = (String)request.getSession().getAttribute( com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY); if( captcha!=null && !captcha.equalsIgnoreCase(token.getCaptcha()) ){ throw new IncorrectCaptchaException ("驗證碼錯誤!"); } } // 認證 protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { CaptchaUsernamePasswordToken token = createToken(request, response); try { doCaptchaValidate( (HttpServletRequest)request,token ); Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } }
代碼說明:
- 添加 captchaParam 變數,為的是頁面表單提交驗證碼的參數名可以進行靈活配置。
- doCaptchaValidate 方法中,驗證碼校正使用了架構 KAPTCHA 所提供的 API。
添加 IncorrectCaptchaException
前面驗證碼校正不通過,我們拋出一個異常 IncorrectCaptchaException,此類繼承 AuthenticationException,之所以需要擴充一個新的異常類,為的是在頁面能更精準顯示錯誤提示資訊。
清單 7. IncorrectCaptchaException
public class IncorrectCaptchaException extends AuthenticationException{ public IncorrectCaptchaException() { super(); } public IncorrectCaptchaException(String message, Throwable cause) { super(message, cause); } public IncorrectCaptchaException(String message) { super(message); } public IncorrectCaptchaException(Throwable cause) { super(cause); } }頁面展現驗證碼錯誤提示資訊清單 8. 頁面認證錯誤資訊展示
Object obj=request.getAttribute( org.apache.shiro.web.filter.authc.FormAuthenticationFilter .DEFAULT_ERROR_KEY_ATTRIBUTE_NAME); AuthenticationException authExp = (AuthenticationException)obj; if( authExp != null ){ String expMsg=""; if(authExp instanceof UnknownAccountException || authExp instanceof IncorrectCredentialsException){ expMsg="錯誤的使用者帳號或密碼!"; }else if( authExp instanceof IncorrectCaptchaException){ expMsg="驗證碼錯誤!"; }else{ expMsg="登入異常 :"+authExp.getMessage() ; } out.print("<div class=\"error\">"+expMsg+"</div>"); }
回頁首
實現單點登入
前面章節,我們認識了 Shiro 的認證與授權,並結合 Spring 作了整合實現。現實中,有這樣一個情境,我們擁有很多業務系統,按照前面的思路,如果訪問每個業務系統,都要進行認證,這樣是否有點難讓人授受。有沒有一種機制,讓我們只認證一次,就可以任意訪問目標系統呢?
上面的情境,就是我們常提到的單點登入 SSO。Shiro 從 1.2 版本開始對 CAS 進行支援,CAS 就是單點登入的一種實現。
Shiro CAS 認證流程
- 使用者首次訪問受保護的資源;例如 http://casclient/security/view.do
- 由於未通過認證,Shiro 首先把請求地址(http://casclient/security/view.do)緩衝起來。
- 然後跳轉到 CAS 伺服器進行登入認證,在 CAS 服務端認證完成後需要返回到請求的 CAS 用戶端,因此在請求時,必須在參數中添加返回地址 ( 在 Shiro 中名為 CAS Service)。 例如 http://casserver/login?service=http://casclient/shiro-cas
- 由 CAS 伺服器認證通過後,CAS 伺服器為返回地址添加 ticket。例如 http://casclient/shiro-cas?ticket=ST-4-BWMEnXfpxfVD2jrkVaLl-cas
- 接下來,Shiro 會校正 ticket 是否有效。由於 CAS 用戶端不提供直接認證,所以 Shiro 會向 CAS 服務端發起 ticket 校正檢查,只有服務端返回成功時,Shiro 才認為認證通過。
- 認證通過,進入授權檢查。Shiro 授權檢查與前面提到的相同。
- 最後授權檢查通過,使用者正常訪問到 http://casclient/security/view.do。
CAS Realm
Shiro 提供了一個名為 CasRealm 的類,與前面提到的 JDBC Realm 相似,該類同樣包括認證和授權兩部分功能。認證就是校正從 CAS 服務端返回的 ticket 是否有效;授權還是擷取使用者權限資訊。
實現單點登入功能,需要擴充 CasRealm 類。
清單 9. Shiro CAS Realm
public class MyCasRealm extends CasRealm{ // 擷取授權資訊 protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //... 與前面 MyShiroRealm 相同 } public String getCasServerUrlPrefix() { return "http://casserver/login"; } public String getCasService() { return "http://casclient/shiro-cas"; } 16 }
代碼說明:
- doGetAuthorizationInfo 擷取授權資訊與前面章節“實現自己的 JDBC Realm”相同。
- 認證功能由 Shiro 自身提供的 CasRealm 實現。
- getCasServerUrlPrefix 方法返回 CAS 伺服器位址,實際使用一般通過參數進行配置。
- getCasService 方法返回 CAS 用戶端處理地址,實際使用一般通過參數進行配置。
- 認證過程需 keystore,否則會出現異常。可以通過設定系統屬性的方式來指定,例如 System.setProperty("javax.net.ssl.trustStore","keystore-file");
CAS Spring 配置
實現單點登入的 Spring 配置與前面類似,不同之處參見代碼說明。
清單 10. Shiro CAS Spring 配置
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="http://casserver/login?service=http://casclient/shiro-cas"/> <property name="successUrl" value="/welcome.do"/> <property name="unauthorizedUrl" value="/403.do"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> <entry key="cas" value-ref="casFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /shiro-cas*=cas /logout.do*=anon /casticketerror.do*=anon # 許可權配置樣本 /security/account/view.do=authc,perms[SECURITY_ACCOUNT_VIEW] /** = authc </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myShiroRealm"/> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- CAS Realm --> <bean id="myShiroRealm" class="xxx.packagename.MyCasRealm"> <property name="cacheManager" ref="shiroCacheManager"/> </bean> <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManager" ref="cacheManager"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/> <!-- CAS Filter --> <bean id="casFilter" class="org.apache.shiro.cas.CasFilter"> <property name="failureUrl" value="casticketerror.do"/> </bean>
代碼說明:
- shiroFilter 中 loginUrl 屬性,為登入 CAS 服務端地址,參數 service 為服務端的返回地址。
- myShiroRealm 為上一節提到的 CAS Realm。
- casFilter 中 failureUrl 屬性,為 Ticket 校正不通過時展示的錯誤頁面。
回頁首
總結
至此,我們對 Shiro 有了較為深入的認識。Shiro 靈活,功能強大,幾乎能滿足我們實際應用中的各種情況,還等什麼呢?讓我開始使用 Shiro 為應用程式護航吧!
在 Web 項目中應用 Apache Shiro