Shiro - web應用

先不談Spring,首先試着用最簡易的方式將Shiro集成到web應用。 即便用一些Servlet ContextListener、Filter、ini這些簡單的配置完成與web應用的集成。
web.xml: html

?
1
2
3
4
5
6
7
8
9
10
11
<listener>
  <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<context-param>
  <param-name>shiroEnvironmentClass</param-name>
  <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
</context-param>
<context-param>
  <param-name>shiroConfigLocations</param-name>
  <param-value>classpath:shiro_web.ini</param-value>
</context-param>

上面的配置中我註冊了一個Listener——org.apache.shiro.web.env.EnvironmentLoaderListener。 web

該類的意義主要是爲了實現ServletContextListener,將WebEnvironment隨着ServletContext事件進行建立和銷燬。 
對WebEnvironment的處理邏輯所有在其父類——EnvironmentLoader中。 
WebEnvironment的類關係圖: 


若是想獲取WebEnvironment則能夠試試如下方法:
WebUtils.getRequiredWebEnvironment(servletContext);
上面的配置中用到了兩個參數(事實上EnvironmentLoader也只有這兩個參數)。
數據庫

  • shiroEnvironmentClass
  • shiroConfigLocations
shiroEnvironmentClass 用於指定使用的WebEnvironment實現類,缺省值爲org.apache.shiro.web.env.IniWebEnvironment。IniWebEnvironment根據設置的.ini配置文件的路徑建立ini實例,若是沒法得到.ini配置文件則拋出ConfigurationException。 
固然,若是有須要(好比換個配置格式、解析方法什麼的...),咱們也能夠本身實現一個WebEnvirontment,並經過shiroEnvironmentClass屬性來進行註冊。 
而  shiroConfigLocations 則是指定.ini配置文件的路徑的參數。若是沒有進行手動指定,他會嘗試在如下兩個路徑中尋找: 
public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";  
public static final String DEFAULT_INI_RESOURCE_PATH = "classpath:shiro.ini";  
順便記錄,IniWebEnvironment查找.ini配置時使用ResourceUtils,見: 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
privateIni convertPathToIni(String path,booleanrequired) {
 
  //TODO - this logic is ugly - it'd be ideal if we had a Resource API to polymorphically encaspulate this behavior
 
  Ini ini =null;
 
  if(StringUtils.hasText(path)) {
    InputStream is =null;
 
    //SHIRO-178: Check for servlet context resource and not only resource paths:
    if(!ResourceUtils.hasResourcePrefix(path)) {
      is = getServletContextResourceStream(path);
    }else{
      try{
        is = ResourceUtils.getInputStreamForPath(path);
      }catch(IOException e) {
        if(required) {
          thrownewConfigurationException(e);
        }else{
          if(log.isDebugEnabled()) {
            log.debug("Unable to load optional path '"+ path +"'.", e);
          }
        }
      }
    }
    if(is !=null) {
      ini =newIni();
      ini.load(is);
    }else{
      if(required) {
        thrownewConfigurationException("Unable to load resource path '"+ path +"'");
      }
    }
  }
 
  returnini;
}

該方法首先調用ResourceUtils.hasResourcePrefix(path)檢查路徑前綴是否符合如下三種之一: apache

?
1
2
3
publicstaticfinalString CLASSPATH_PREFIX ="classpath:";
publicstaticfinalString URL_PREFIX ="url:";
publicstaticfinalString FILE_PREFIX ="file:";

若是不符合這三種前綴則在Servlet Context進行查找。 安全

若是符合三種前綴之一,則調用ResourceUtils.getInputStreamForPath(path),根據path及其不一樣的前綴以不一樣的方式獲取輸入流。 
  1. 對於classpath,調用ClassUtils.getResourceAsStream(path);,經過ClassLoader實例調用getResourceAsStream(name);
  2. 對於url,則是返回url.openStream();
  3. 對於file,返回new FileInputStream(path);
繼續配置web.xml,此次添加一個Filter:

?
1
2
3
4
5
6
7
8
9
10
11
12
<filter>  
  <filter-name>ShiroFilter</filter-name>    
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> 
</filter>
<filter-mapping>  
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>    
  <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>      
    <dispatcher>INCLUDE</dispatcher>  
      <dispatcher>ERROR</dispatcher>
  </filter-mapping>

這是基於當前的WebEnvironment實例配置的Filter,即單獨存在沒什麼意義。 cookie

ShiroFilter用WebEnvironment實例對全部被過濾的請求進行安全處理。 
Shiro提供的一些Filter實現: 
 


暫且不論AdviceFilter,咱們使用的ShiroFilter在AbstractShiroFilter下。
其中IniShiroFilter從1.2開始已deprecated了,但這個東西用起來仍是有點意思的,只不過沒什麼意義。
IniShiroFilter不須要同時配置EnvironmentLoaderListener,也就是說這裏面沒有WebEnvironment對象,他自己就是一個簡易的Environment。
有意思的地方就是這點,他能夠把.ini中的配置直接寫到web.xml,好比這樣: session

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<filter>
  <filter-name>ShiroFilter</filter-name>
  <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
  <init-param>
    <param-name>config</param-name>
    <param-value>        
    [urls]          
    /main/logout = logout          
    /main/loginPage = anon            
    /** = user            
    [main]          
    user.loginUrl = /main/login          
    authc.successUrl = /main/welcome        
    myRealm=pac.king.common.security.realm.MainRealm  
    securityManager.realms=$myRealm    
    </param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>ShiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>FORWARD</dispatcher>
  <dispatcher>INCLUDE</dispatcher>
  <dispatcher>ERROR</dispatcher>
</filter-mapping>

Shiro也建議用戶們不要這樣配置,對此他們給出了幾個理由: app

  • 安全配置可能會常常變化,而咱們不想老是修改web.xml。
  • 安全配置可能會愈來愈龐大,這會影響web.xml的可讀性。
  • 咱們儘可能保證安全配置不會散落在各個地方。

不管如何,這取決於用戶和項目。
另外說說web應用相關的ini配置。
以前幾篇中用過[main]、[users]、[roles]等片斷,在web應用中咱們能夠試試[urls]。
[urls]也是Shiro的一大賣點(文檔提供人說根本沒見過其餘web framework也能作到這點)。
就是爲每一個URL配置專有的filter chain!!
URL_Ant_Path_Expression = Path_Specific_Filter_Chain[optional_config]
左側使用Ant風格的表達式描述URL;
右側則是用逗號分隔的過濾器鏈;
最後的optional_config則是一些附加屬性,好比描述對用戶資源有刪除操做的權限perms["user:delete"]。
配置[urls],官網上的例子: dom

?
1
2
3
4
5
6
7
[urls]
/index.html = anon
/user/create = anon
/user/** = authc
/admin/** = authc, roles[administrator]
/rest/** = authc, rest
/remoting/rpc/** = authc, perms["remote:invoke"]

URL是相對路徑,即便部署的時候換了個域名也沒有問題。
注意!URL配置的順序對filter chain是有影響的!他是FIRST MATCH WINS。
好比下面的例子中,第二行配置就不會生效。
/user/** = authc
/user/list = anon
默認的Filter,好比anon,authc,users等等,他們是由哪些類來實現的?

Filter Name Class jsp

  1. anon                      org.apache.shiro.web.filter.authc.AnonymousFilter
  2. authc                     org.apache.shiro.web.filter.authc.FormAuthenticationFilter
  3. authcBasic              org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
  4. logout                    org.apache.shiro.web.filter.authc.LogoutFilter
  5. noSessionCreation   org.apache.shiro.web.filter.session.NoSessionCreationFilter
  6. perms                     org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
  7. port                       org.apache.shiro.web.filter.authz.PortFilter
  8. rest                        org.apache.shiro.web.filter.authz.HttpMethodPermissionFilterv
  9. roles                      org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
  10. ssl                         org.apache.shiro.web.filter.authz.SslFilter
  11. user                       org.apache.shiro.web.filter.authc.UserFilter
應用啓動時,將默認Filter所有加載。 
默認Filter的定義見enum類DefaultFilter。 
pool of Filters則定義在DefaultFilterChainManager中,用LinkedHashMap維護。DefaultFilterChainManager在constructor中調用void addDefaultFilters(boolean init)將Filters放入Map中。 
隨着應用作得愈來愈大,這些URL會變得愈來愈難以管理。 
固然,咱們也能夠把這些URL放在數據庫裏管理,但老是有個別的URL須要特殊配置Filter。 
隨着開發、測試、生產環境的切換,這些filters也須要能夠進行啓用/禁用。 
總不把filter能一個個刪掉再一個個寫回去... 
見OncePerRequestFilter有個field: 
private boolean enabled = true;  
並且全部的default filters都繼承了OncePerRequestFilter!! 


因而我能夠直接在ini文件中直接進行啓用/禁用,好比這樣:
user.enabled=false
固然,咱們也能夠試着自定義一個Filter(好比根據判斷具體的請求或者路徑,動態將全部filter啓用/禁用),並在[main]註冊。
另外,上面的類關係圖中AccessControlFilter有一個field爲loginUrl,其默認值爲:
public static final String DEFAULT_LOGIN_URL = "/login.jsp";
咱們常用的filter中的authc(FormAuthenticationFilter)中存在如下屬性:

?
1
2
3
4
5
6
7
publicstaticfinalString DEFAULT_USERNAME_PARAM ="username";
publicstaticfinalString DEFAULT_PASSWORD_PARAM ="password";
publicstaticfinalString DEFAULT_REMEMBER_ME_PARAM ="rememberMe";
 
privateString usernameParam = DEFAULT_USERNAME_PARAM;
privateString passwordParam = DEFAULT_PASSWORD_PARAM;
privateString rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;
咱們能夠在表單中使用這些屬性,讓其進行認證+remember me。 

固然,這些值也是能夠改變的,好比: 
?
1
[main]authc.loginUrl = /main/loginauthc.usernameParam = userNameauthc.passwordParam = pwdauthc.rememberMeParam = rememberCookie
說到remember me,其實現是有RememberMeManager提供,默認實現是基於Cookie的。 

好比DefaultWebSecurityManager的constructor中將CookieRememberMeManager設爲默認(field定義於其父類DefaultSecurityManager):

?
1
2
3
4
5
6
7
8
publicDefaultWebSecurityManager() {
  super();
  ((DefaultSubjectDAO)this.subjectDAO).setSessionStorageEvaluator(newDefaultWebSessionStorageEvaluator());
  this.sessionMode = HTTP_SESSION_MODE;
  setSubjectFactory(newDefaultWebSubjectFactory());
  setRememberMeManager(newCookieRememberMeManager());
  setSessionManager(newServletContainerSessionManager());
}

看起來不錯,那我就一步步detect看看RememberMeManager是怎麼manage的。
用戶登陸時咱們調用Subject.login(token) 以DelegaingSubject爲例,第一步直接將驗證工做委託給securityManager。
工做中一步步進行委託,securityManager -> authenticator -> realm...
驗證經過後將AuthenticationInfo結果返回到securityManager,securityManager將結果傳遞給RememberMeManager,委託rememberMe的工做。
參考AbstractRememberMeManager中的method:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicvoidonSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
  //always clear any previous identity:
  forgetIdentity(subject);
 
  //now save the new identity:
  if(isRememberMe(token)) {
    rememberIdentity(subject, token, info);
  }else{
    if(log.isDebugEnabled()) {
      log.debug("AuthenticationToken did not indicate RememberMe is requested.  "+
          "RememberMe functionality will not be executed for corresponding account.");
    }
  }
}

第一步:先將Cookie移除,Shiro默認使用的Cookie是本身的SimpleCookie,調用其removeFrom方法將Cookie"移除"。

第二步:檢查token是不是RememberMeAuthenticationToken的實例並是否設置了rememberMe=true。 
第三步:進行rememberMe的具體工做,這個工做由AbstractRememberMeManager的子類進行。 
以CookieRememberMeManager爲例: 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protectedvoidrememberSerializedIdentity(Subject subject,byte[] serialized) {
 
  if(!WebUtils.isHttp(subject)) {
    if(log.isDebugEnabled()) {
      String msg ="Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet "+
          "request and response in order to set the rememberMe cookie. Returning immediately and "+
          "ignoring rememberMe operation.";
      log.debug(msg);
    }
    return;
  }
 
 
  HttpServletRequest request = WebUtils.getHttpRequest(subject);
  HttpServletResponse response = WebUtils.getHttpResponse(subject);
 
  //base 64 encode it and store as a cookie:
  String base64 = Base64.encodeToString(serialized);
 
  Cookie template = getCookie();//the class attribute is really a template for the outgoing cookies
  Cookie cookie =newSimpleCookie(template);
  cookie.setValue(base64);
  cookie.saveTo(request, response);
}

代碼很是簡單,接着轉到SimpleCookie:



?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
publicvoidsaveTo(HttpServletRequest request, HttpServletResponse response) {
 
  String name = getName();
  String value = getValue();
  String comment = getComment();
  String domain = getDomain();
  String path = calculatePath(request);
  intmaxAge = getMaxAge();
  intversion = getVersion();
  booleansecure = isSecure();
  booleanhttpOnly = isHttpOnly();
 
  addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
}
 
privatevoidaddCookieHeader(HttpServletResponse response, String name, String value, String comment,
               String domain, String path,intmaxAge,intversion,
               booleansecure,booleanhttpOnly) {
 
  String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
  response.addHeader(COOKIE_HEADER_NAME, headerValue);
 
  if(log.isDebugEnabled()) {
    log.debug("Added HttpServletResponse Cookie [{}]", headerValue);
  }
}

但畢竟不少人不喜歡cookie...咱們也能夠本身去實現RememberMeManager,並進行註冊(仍然是注入到securityManger):

rememberMeManager = com.my.impl.RememberMeManager  
securityManager.rememberMeManager = $rememberMeManager  
咱們使用的UsernamePasswordToken繼承的RememberMeAuthenticationToken提供rememberMe特性。 
boolean isRememberMe();  
好比咱們能夠Realm的驗證方法中這樣使用: 
UsernamePasswordToken uToken = (UsernamePasswordToken)token;  
uToken.setRememberMe(true);
相關文章
相關標籤/搜索