Apache Shiro 是功能強大而且容易集成的開源權限框架,它可以完成認證、受權、加密、會話管理等功能。認證和受權爲權限控制的核心,簡單來講,「認證」就是證實你是誰? Web 應用程序通常作法經過表單提交用戶名及密碼達到認證目的。「受權」便是否容許已認證用戶訪問受保護資源。關於 Shiro 的一系列特徵及優勢,不少文章已有列舉,這裏再也不逐一贅述,本文重點介紹 Shiro 在 Web Application 中如何實現驗證碼認證以及如何實現單點登陸。java
用戶權限模型
在揭開 Shiro 面紗以前,咱們須要認知用戶權限模型。本文所提到用戶權限模型,指的是用來表達用戶信息及用戶權限信息的數據模型。即能證實「你是誰?」、「你能訪問多少受保護資源?」。爲實現一個較爲靈活的用戶權限數據模型,一般把用戶信息單獨用一個實體表示,用戶權限信息用兩個實體表示。web
- 用戶信息用 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 的實現。spring
清單 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,秉承客觀的態度,下面對二者略微比較:apache
- 簡單性,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 爲應用程序服務了。api
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"; } }
代碼說明:
- 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 爲應用程序護航吧!