Shiro源碼分析 - 授權流程

注意,該授權流程指的是,在xml中配置filter映射時,指定了某個url需要的角色和權限。如果是用註解授權的話,該流程不合適,後面會有一篇針對註解授權的文章對註解授權流程解讀。

==========================================================

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

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

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中的PermissionsAuthorizationFilter與RolesAuthorizationFilter就是用於權限控制的Filter,名稱分別爲perms與roles。PermissionsAuthorizationFilter用於判斷用戶訪問某URL時是否有相應權限,RolesAuthorizationFilter用於判斷用戶訪問某URL時是否有相應角色。


    由於Shiro中Filter繼承體系比較複雜,要想理解Shiro權限是如何控制的就必須先理解Filter的繼承體系,以及理解繼承體系中父類Filter的特點及作用。由於Filter繼承體系龐大,下面只列出PermissionsAuthorizationFilter與RolesAuthorizationFilter的繼承關係。

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

1.NameableFilter:爲Filter添加名稱
2.OncePerRequestFilter:保證Filter在鏈中只被執行一次
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方法

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;  
}  

isPermitted和isPermittedAll最終都委託給了ModularRealmAuthorizer.isPermitted與ModularRealmAuthorizer.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鏈,反之則重定向至配置的未授權頁面。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章