目錄[-]java
Apache Shiro 是功能強大而且容易集成的開源權限框架,它可以完成認證、受權、加密、會話管理等功能。認證和受權爲權限控制的核心,簡單來講,「認證」就是證實你是誰? Web 應用程序通常作法經過表單提交用戶名及密碼達到認證目的。「受權」便是否容許已認證用戶訪問受保護資源。關於 Shiro 的一系列特徵及優勢,不少文章已有列舉,這裏再也不逐一贅述,本文重點介紹 Shiro 在 Web Application 中如何實現驗證碼認證以及如何實現單點登陸。web
在揭開 Shiro 面紗以前,咱們須要認知用戶權限模型。本文所提到用戶權限模型,指的是用來表達用戶信息及用戶權限信息的數據模型。即能證實「你是誰?」、「你能訪問多少受保護資源?」。爲實現一個較爲靈活的用戶權限數據模型,一般把用戶信息單獨用一個實體表示,用戶權限信息用兩個實體表示。spring
回頁首數據庫
在 Shiro 認證與受權處理過程當中,說起到 Realm。Realm 能夠理解爲讀取用戶信息、角色及權限的 DAO。因爲大多 Web 應用程序使用了關係數據庫,所以實現 JDBC Realm 是經常使用的作法,後面會提到 CAS Realm,另外一個 Realm 的實現。apache
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; } }
代碼說明:設計模式
回頁首api
或許有人要問,我一直在使用 Spring,應用程序的安全組件早已選擇了 Spring Security,爲何還須要 Shiro ?固然,不能否認 Spring Security 也是一款優秀的安全控制組件。本文的初衷不是讓您必須選擇 Shiro 以及必須放棄 Spring Security,秉承客觀的態度,下面對二者略微比較:緩存
回頁首安全
在 Java Web Application 開發中,Spring 獲得了普遍使用;與 EJB 相比較,能夠說 Spring 是主流。Shiro 自身提供了與 Spring 的良好支持,在應用程序中集成 Spring 十分容易。服務器
有了前面提到的用戶權限數據模型,而且實現了本身的 Realm,咱們就能夠開始集成 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 的過濾處理,這與其它過濾器的使用相似。
<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 容器管理的 Bean,集成大功告成。各個 Bean 的功能見代碼說明。
<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"/>
代碼說明:
驗證碼是有效防止暴力破解的一種手段,經常使用作法是在服務端產生一串隨機字符串與當前用戶會話關聯(咱們一般說的放入 Session),而後向終端用戶展示一張通過「擾亂」的圖片,只有當用戶輸入的內容與服務端產生的內容相同時才容許進行下一步操做。
做爲演示,咱們選擇開源的驗證碼組件 kaptcha。這樣,咱們只須要簡單配置一個 Servlet,頁面經過 IMG 標籤就能夠展示圖形驗證碼。
<!-- 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>
Shiro 表單認證,頁面提交的用戶名密碼等信息,用 UsernamePasswordToken 類來接收,很容易想到,要接收頁面驗證碼的輸入,咱們須要擴展此類:
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 類,首先覆蓋 createToken 方法,以便獲取 CaptchaUsernamePasswordToken 實例;而後增長驗證碼校驗方法 doCaptchaValidate;最後覆蓋 Shiro 的認證方法 executeLogin,在原表單認證邏輯處理以前進行驗證碼校驗。
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); } } }
代碼說明:
前面驗證碼校驗不經過,咱們拋出一個異常 IncorrectCaptchaException,此類繼承 AuthenticationException,之因此須要擴展一個新的異常類,爲的是在頁面能更精準顯示錯誤提示信息。
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); } }
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 提供了一個名爲 CasRealm 的類,與前面提到的 JDBC Realm 類似,該類一樣包括認證和受權兩部分功能。認證就是校驗從 CAS 服務端返回的 ticket 是否有效;受權仍是獲取用戶權限信息。
實現單點登陸功能,須要擴展 CasRealm 類。
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 }
代碼說明:
實現單點登陸的 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>
代碼說明:
至此,咱們對 Shiro 有了較爲深刻的認識。Shiro 靈活,功能強大,幾乎能知足咱們實際應用中的各類狀況,還等什麼呢?讓我開始使用 Shiro 爲應用程序護航吧!