1. 程式人生 > >shiro認證授權原始碼分析

shiro認證授權原始碼分析

shiro內建了許多過濾器用來控制認證授權

anon : org.apache.shiro.web.filter.authc.AnonymousFilter

authc : org.apache.shiro.web.filter.authc.FormAuthenticationFilter

authcBasic : org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

perms : org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

port : org.apache.shiro.web.filter.authz.PortFilter

rest : org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

roles : org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

ssl : org.apache.shiro.web.filter.authz.SslFilter

user : org.apache.shiro.web.filter.authc.UserFilter

其中認證是否已登入FormAuthenticationFilter

認證是否授權(角色、許可權)RolesAuthorizationFilter,PermissionsAuthorizationFilter

(以下以RolesAuthorizationFilter為例

核心都繼承自AccessControlFilter

   

左邊是認證登入,右邊是認證授權。注意紅框位置FormAuthenticationFilter 和 RolesAuthorizationFilter 都通過了過渡類繼承自AccessControlFilter

簡單介紹下這個類的幾個方法

onPreHandle:判斷認證是否通過,以及後續處理上程式碼:

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws
 Exception {
   return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

isAccessAllowed:判斷認證是否通過(FormAuthenticationFilter中是認證是否已登入,RolesAuthorizationFilter是認證是否已授權

onAccessDenied:認證失敗的後續處理

isLoginRequest:判斷是否是登入請求,登入地址在配置檔案中已配:

<!--未登入狀態下訪問authc則進入loginUrl配置的路徑-->
 <property name="loginUrl" value="/login"></property>
 <!--登入成功後跳轉的預設地址-->
 <property name="successUrl" value="/main"></property>
 <!--如果您請求的資源不再您的許可權範圍,則跳轉到/400請求地址 -->
 <property name="unauthorizedUrl" value="400"></property>

saveRequest:儲存當前請求

redirectToLogin:重定向到登入頁面

saveRequestAndRedirectToLogin:儲存當前請求並重定向到登入頁面

認證登入流程按照繼承關係:

AccessControlFilter

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
       return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
   }

AuthenticatingFilter(判斷是否認證通過,將呼叫父類的isAccessAllowed)

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
   return super.isAccessAllowed(request, response, mappedValue) ||
           (!isLoginRequest(request, response) && isPermissive(mappedValue));
}

AuthenticationFilter(在這裡真正判斷是否已登入)

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
   Subject subject = getSubject(request, response);
   return subject.isAuthenticated();
}

登入的話,直接跳轉,如果未登入:

FormAuthenticationFilter 

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
   if (isLoginRequest(request, response)) {
       if (isLoginSubmission(request, response)) {
           if (log.isTraceEnabled()) {
               log.trace("Login submission detected.  Attempting to execute login.");
           }
           return executeLogin(request, response);
       } else {
           if (log.isTraceEnabled()) {
               log.trace("Login page view.");
           }
           //allow them to see the login page ;)
           return true;
       }
   } else {
       if (log.isTraceEnabled()) {
           log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +
                   "Authentication url [" + getLoginUrl() + "]");
       }

       saveRequestAndRedirectToLogin(request, response);
       return false;
   }
} 首先判斷是否是登入請求,如果是get形式登入頁面跳轉繼續過濾器鏈,如果是post表單提交執行executeLogin方法,

將走realm中的doGetAuthenticationInfo方法。

ps:執行executeLogin方法過程中會生成AuthenticationToken,具體程式碼:

protected AuthenticationToken createToken(String username, String password,
                                         ServletRequest request, ServletResponse response) {
   boolean rememberMe = isRememberMe(request);
   String host = getHost(request);
   return createToken(username, password, rememberMe, host);
} 預設傳username,password,所以前臺頁面name屬性要一一對應。

登入成功或失敗會走onLoginSuccess或onLoginFailure

如果不是登入請求則儲存當前請求跳轉至登入頁面,如果想封裝ajax請求的shiro處理,可自定義自己的FormAuthenticationFilter

認證授權流程按照繼承關係

AccessControlFilter

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
       return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
   } RolesAuthorizationFilter(認證是否有許可權,shiro預設是需要所有角色才認證通過的) public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

      Subject subject = getSubject(request, response);
      String[] rolesArray = (String[]) mappedValue;

      if (rolesArray == null || rolesArray.length == 0) {
          //no roles specified, so nothing to check - allow access.
          return true;
      }

      Set<String> roles = CollectionUtils.asSet(rolesArray);
      return subject.hasAllRoles(roles);
  }

AuthorizationFilter(認證角色未通過後的處理)

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {

      Subject subject = getSubject(request, response);
      // If the subject isn't identified, redirect to login URL
      if (subject.getPrincipal() == null) {
          saveRequestAndRedirectToLogin(request, response);
      } else {
          // If subject is known but not authorized, redirect to the unauthorized URL if there is one
          // If no unauthorized URL is specified, just return an unauthorized HTTP status code
          String unauthorizedUrl = getUnauthorizedUrl();
          //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
          if (StringUtils.hasText(unauthorizedUrl)) {
              WebUtils.issueRedirect(request, response, unauthorizedUrl);
          } else {
              WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
          }
      }
      return false;
  }

如果未登入,儲存當前請求跳轉至登入頁面,否則判斷是否有配置認證許可權未通過須要跳轉的請求,如果配置了則跳轉,否則丟擲錯誤碼SC_UNAUTHORIZED(401),可在web.xml中捕獲該錯誤碼自己定義要跳轉的頁面。

這是我理解的shiro認證授權策略,其中認證授權前後所要做的處理都可以通過繼承相應類重現方法來達到自定義效果,無需在contrller層寫多餘的業務邏輯程式碼,shiro的這些介面和相關配置檔案都可以幫你搞定!

最後需要在shiro配置檔案中配置

01 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
02         <property name="securityManager" ref="securityManager"/>
03         <!--未登入狀態下訪問authc則進入loginUrl配置的路徑-->
04         <property name="loginUrl" value="/login"></property>
05         <!--登入成功後跳轉的預設地址-->
06         <property name="successUrl" value="/main"></property>
07          <!--如果您請求的資源不再您的許可權範圍,則跳轉到/400請求地址 -->
08         <property name="unauthorizedUrl" value="400"></property>
09         <property name="filters">
10             <map>
11                 <entry key="authc">
12                     <bean class="com.luming.shiro.MyFormAuthenticationFilter"/>
13                 </entry>
14                 <entry key="roles">
15                     <bean class="com.luming.shiro.MyRolesAuthorizationFilter"/>
16                 </entry>
17             </map>
18         </property>

19 </bean>

MyFormAuthenticationFilter繼承自FormAuthenticationFilter

MyRolesAuthorizationFilter繼承自RolesAuthorizationFilter

最後基於自身專案的shiro自定義認證授權還須要結合自定義的realm:shiro認證授權

因為JdbcRealm的doGetAuthenticationInfo定義瞭如何對使用者提交的資料進行認證,doGetAuthorizationInfo定義了授予使用者何種許可權,

FormAuthenticationFilter和RolesAuthorizationFilter主要是負責認證及授權的前後須要做的一些自定義操作,比如IP過濾等