Shiro源碼分析----受權流程

在一個用戶登陸後,即身份認證經過,只能證實該登陸身份是合法的,至於具體能訪問系統中的什麼資源,須要經過受權來控制。通常系統中都是經過用戶關聯角色、角色再關聯權限來實現判斷一個用戶是否有某資源的使用權限,Shiro也提供了相應的實現權限控制。java

Shiro中的權限控制也是經過Filter來實現的,在前面認證流程中講到,Shiro的DefaultFilterChainManager類會建立Filter鏈,鏈中包含了Shiro一些默認Filter,也能夠添加自定義Filter,並且這些Filter都有名字,Shiro會根據Filter配置爲每個配置的URL匹配符建立一個Filter鏈。web

protected FilterChainManager createFilterChainManager() {
    // 建立DefaultFilterChainManager
    DefaultFilterChainManager manager = new DefaultFilterChainManager();
    // 建立Shiro默認Filter,根據org.apache.shiro.web.filter.mgt.DefaultFilter建立
    Map<String, Filter> defaultFilters = manager.getFilters();
    //apply global settings if necessary:
    for (Filter filter : defaultFilters.values()) {
        // 設置相關Filter的loginUrl、successUrl、unauthorizedUrl屬性
        applyGlobalPropertiesIfNecessary(filter);
    }

    // 獲取在Spring配置文件中配置的Filter
    Map<String, Filter> filters = getFilters();
    if (!CollectionUtils.isEmpty(filters)) {
        for (Map.Entry<String, Filter> entry : filters.entrySet()) {
            String name = entry.getKey();
            Filter filter = entry.getValue();
            applyGlobalPropertiesIfNecessary(filter);
            if (filter instanceof Nameable) {
                ((Nameable) filter).setName(name);
            }
            // 將配置的Filter添加至鏈中,若是同名Filter已存在則覆蓋默認Filter
            manager.addFilter(name, filter, false);
        }
    }

    //build up the chains:
    Map<String, String> chains = getFilterChainDefinitionMap();
    if (!CollectionUtils.isEmpty(chains)) {
        for (Map.Entry<String, String> entry : chains.entrySet()) {
            String url = entry.getKey();
            String chainDefinition = entry.getValue();
            // 爲配置的每個URL匹配建立FilterChain定義,
            // 這樣當訪問一個URL的時候,一旦該URL配置上則就知道該URL須要應用上哪些Filter
            // 因爲URL匹配符會配置多個,因此以第一個匹配上的爲準,因此越具體的匹配符應該配置在前面,越寬泛的匹配符配置在後面
            manager.createChain(url, chainDefinition);
        }
    }

    return manager;
}

下面咱們看來都有哪些默認Filter,在DefaultFilterChainManager構造方法中調用addDefaultFilters方法:數據庫

protected void addDefaultFilters(boolean init) {
    for (DefaultFilter defaultFilter : DefaultFilter.values()) {
        addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
    }
}

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

    // 省略一些代碼...
}

DefaultFilter中的PermissionsAuthorizationFilterRolesAuthorizationFilter就是用於權限控制的Filter,名稱分別爲permsrolesPermissionsAuthorizationFilter用於判斷用戶訪問某URL時是否有相應權限,RolesAuthorizationFilter用於判斷用戶訪問某URL時是否有相應角色。apache

因爲Shiro中Filter繼承體系比較複雜,要想理解Shiro權限是如何控制的就必須先理解Filter的繼承體系,以及理解繼承體系中父類Filter的特色及做用。因爲Filter繼承體系龐大,下面只列出PermissionsAuthorizationFilterRolesAuthorizationFilter的繼承關係。數組

Shiro源碼分析----受權流程

下面對繼承關係中一些重要的Filter做簡要說明,具體的Filter詳細分析容後續再講。app

  1. NameableFilter:爲Filter添加名稱jsp

  2. OncePerRequestFilter:保證Filter在鏈中只被執行一次ide

  3. AdviceFilter源碼分析

    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
               throws ServletException, IOException {
    
    Exception exception = null;
    try {
        // 前置處理,若是返回false則再也不執行鏈中的後續Filter
        boolean continueChain = preHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
        }
    
        if (continueChain) {
            // 繼續執行鏈中的後續Filter
            executeChain(request, response, chain);
        }
    
        // 後置處理
        postHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Successfully invoked postHandle method");
        }
    
    } catch (Exception e) {
        exception = e;
    } finally {
        cleanup(request, response, exception);
    }
    }
  4. PathMatchingFilter:基於路徑匹配的Filter,重寫preHandle方法post

    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    // appliedPaths存儲該Filter須要被應用的URL路徑,例若有這樣一個配置:/user_add.jsp = perms["user:add"]
    // 那麼在PermissionsAuthorizationFilter.appliedPaths中就有一條key爲/user_add.jsp, value爲[user:add]數組
    // value爲數組而不是字符串的緣由是權限能夠有多個
    
    // 若是appliedPaths爲空則直接繼續執行Filter鏈
    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
        if (log.isTraceEnabled()) {
            log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
        }
        return true;
    }
    
    for (String path : this.appliedPaths.keySet()) {
        // 若是匹配,則根據onPreHandle方法的返回值來肯定是否繼續執行Filter鏈
        if (pathsMatch(path, request)) {
            log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
            Object config = this.appliedPaths.get(path);
            return isFilterChainContinued(request, response, path, config);
        }
    }
    
    //no path matched, allow the request to go through:
    return true;
    }
    @SuppressWarnings({"JavaDoc"})
    private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
                                       String path, Object pathConfig) throws Exception {
    // 若是該Filter可用
    if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
        // 省略一些代碼...
        return onPreHandle(request, response, pathConfig);
    }
    
    // 省略一些代碼...
    return true;
    }
  5. AccessControlFilter:實現onPreHandle方法

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

    根據isAccessAllowed方法的返回值來肯定是否繼續執行Filter鏈,若是不執行Filter鏈,則還會執行onAccessDenied方法。

  6. AuthorizationFilter:實現了onAccessDenied方法

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
    
    Subject subject = getSubject(request, response);
    // 若是沒有登陸則重定向至登陸頁面
    if (subject.getPrincipal() == null) {
        saveRequestAndRedirectToLogin(request, response);
    } else {
        // 重定向至未受權頁面
        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 {
            // 若是未受權頁面未配置則發送401狀態碼
            WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
    return false;
    }
  7. PermissionsAuthorizationFilter:實現了isAccessAllowed方法

    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
    
    Subject subject = getSubject(request, response);
    // 訪問時須要的權限
    String[] perms = (String[]) mappedValue;
    // 調用Subject對象的isPermitted或isPermittedAll來判斷是否有權限
    boolean isPermitted = true;
    if (perms != null && perms.length > 0) {
        if (perms.length == 1) {
            if (!subject.isPermitted(perms[0])) {
                isPermitted = false;
            }
        } else {
            if (!subject.isPermittedAll(perms)) {
                isPermitted = false;
            }
        }
    }
    
    return isPermitted;
    }public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
    
    Subject subject = getSubject(request, response);
    // 訪問時須要的權限
    String[] perms = (String[]) mappedValue;
    // 調用Subject對象的isPermitted或isPermittedAll來判斷是否有權限
    boolean isPermitted = true;
    if (perms != null && perms.length > 0) {
        if (perms.length == 1) {
            if (!subject.isPermitted(perms[0])) {
                isPermitted = false;
            }
        } else {
            if (!subject.isPermittedAll(perms)) {
                isPermitted = false;
            }
        }
    }
    
    return isPermitted;
    }

    isPermittedisPermittedAll最終都委託給了ModularRealmAuthorizer.isPermittedModularRealmAuthorizer.isPermittedAll方法,至於爲何爲會委託給ModularRealmAuthorizer請參看:Shiro源碼分析----登陸流程

    下面以ModularRealmAuthorizer.isPermitted爲例,分析一下是如何進行權限判斷的:

    public boolean isPermitted(PrincipalCollection principals, String permission) {
    assertRealmsConfigured();
    for (Realm realm : getRealms()) {
        if (!(realm instanceof Authorizer)) continue;
        // 調用Realm的isPermitted方法
        if (((Authorizer) realm).isPermitted(principals, permission)) {
            return true;
        }
    }
    return false;
    }

    在使用Shiro時,Realm對象包含了認證與受權信息,在實際應用時,通常都是存儲在數據庫中的,且Realm通常都會自定義實現。實現自定義的Realm時,通常繼承自org.apache.shiro.realm.AuthorizingRealm類,下面是一個例子:

    public class UserRealm extends AuthorizingRealm {
    
       private UserService userService;
    
       public void setUserService(UserService userService) {
           this.userService = userService;
       }
    
    // 從數據庫中獲取權限信息
       @Override
       protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
           String username = (String)principals.getPrimaryPrincipal();
    
           SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 從數據庫中查詢當前用戶所擁有的角色
           authorizationInfo.setRoles(userService.findRoles(username));
        // 從數據庫中查詢當前用戶所擁有的權限
           authorizationInfo.setStringPermissions(userService.findPermissions(username));
    
           return authorizationInfo;
       }
    
    // 從數據庫中獲取認證信息
       @Override
       protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
           String username = (String)token.getPrincipal();
    
           User user = userService.findByUsername(username);
    
           if(user == null) {
               throw new UnknownAccountException();//沒找到賬號
           }
    
           if(Boolean.TRUE.equals(user.getLocked())) {
               throw new LockedAccountException(); //賬號鎖定
           }
    
           //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,若是以爲人家的很差能夠自定義實現
           SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                   user.getUsername(), //用戶名
                   user.getPassword(), //密碼
                   ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
                   getName()  //realm name
           );
           return authenticationInfo;
       }
    
    }

    AuthorizingRealm.isPermetted方法中就是根據用戶所擁有的權限與訪問時須要的權限進行匹配,若是有權限則繼續執行Filter鏈,反之則重定向至配置的未受權頁面。
    理解了PermissionsAuthorizationFilter的判斷邏輯,那麼RolesAuthorizationFilter的判斷邏輯就很容易理解了,由於其流程是同樣的,只是RolesAuthorizationFilter是基於用戶角色進行判斷的。

  8. RolesAuthorizationFilter:實現了isAccessAllowed方法:

    @SuppressWarnings({"unchecked"})
    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);
    // 委託給Subject.hasAllRoles方法
    return subject.hasAllRoles(roles);
    }

    同理,hasAllRoles方法,最終都委託給了ModularRealmAuthorizer.hasAllRoles方法

    public boolean hasAllRoles(PrincipalCollection principals, Collection<String> roleIdentifiers) {
    assertRealmsConfigured();
    for (String roleIdentifier : roleIdentifiers) {
        if (!hasRole(principals, roleIdentifier)) {
            return false;
        }
    }
    return true;
    }
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
    assertRealmsConfigured();
    for (Realm realm : getRealms()) {
        if (!(realm instanceof Authorizer)) continue;
        if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
            return true;
        }
    }
    return false;
    }

    AuthorizingRealm.hasAllRoles方法中就是根據用戶所擁有的角色與訪問時須要的角色進行匹配,若是有角色則繼續執行Filter鏈,反之則重定向至配置的未受權頁面。

至此,Shiro受權流程分析完畢,若有錯誤之處,敬請指正。
-------------------------------- END -------------------------------

及時獲取更多精彩文章,請關注公衆號《Java精講》。

相關文章
相關標籤/搜索