Shiro原理解析(三)--再談過濾器

import.發表於2019-03-27

前言

前面兩篇文章主要講了SpringShiroFilter的初始化以及doFilter方法。總結一下:初始化的主要操作是根據配置構建所有url對應的過濾鏈,doFilter()方法將url對應的過濾鏈新增到javaEE原生的的過濾器中。

本篇文章的內容

本篇文章主要解析具體的Filter是如何處理鑑權的(即如何判斷某個使用者是否有許可權訪問該url)。本篇文章一PermissionsAuthorizationFilter為例(shiro預設的攔截器有9個,包括roles,anno,perms等等)

正文

首先給出PermissionsAuthorizationFilter的類結構,該類只有一個isAccessAllowed方法

Shiro原理解析(三)--再談過濾器

public class PermissionsAuthorizationFilter extends AuthorizationFilter {
  //TODO - complete JavaDoc

    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

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

        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;
    }
}
複製程式碼

第一步,既然是過濾器,我們當然還是一步一步往上尋找doFilter()方法,最終還是在OncePerRequestFilter()方法中找到

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
            filterChain.doFilter(request, response);
        } else //noinspection deprecation
            if (/* added in 1.2: */ !isEnabled(request, response) ||
                /* retain backwards compatibility: */ shouldNotFilter(request) ) {
            log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",
                    getName());
            filterChain.doFilter(request, response);
        } else {
            // Do invoke this filter...
            log.trace("Filter '{}' not yet executed.  Executing now.", getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

            try {
                doFilterInternal(request, response, filterChain);
            } finally {
                // Once the request has finished, we're done and we don't
                // need to mark as 'already filtered' any more.
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }


  public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        Exception exception = null;

        try {

            boolean continueChain = preHandle(request, response);
            if (log.isTraceEnabled()) {
                log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
            }

            if (continueChain) {
                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);
        }
    }

複製程式碼

這裡鑑權的核心在於doFilterInternal方法中的boolean continueChain = preHandle(request, response),如果鑑權通過則返回true,並且呼叫executeChain()呼叫下一個過濾器。(這裡我找了好一番時間才發現鑑權邏輯躲在preHandle裡面,抓狂,正常理解不怎麼都應該在executeChain裡面嘛。暗暗吐槽)
下面讓我們來一層一層的剝開preHandle的外衣。

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        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()) {
            // If the path does match, then pass on to the subclass implementation for specific checks
            //(first match 'wins'):
            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;
    }
複製程式碼

第一步檢查appliedPaths是否為空,這裡可能有小夥伴忘記appliedPaths裡面存的是什麼了,還是得祭出第一篇文章的dubug圖。

Shiro原理解析(三)--再談過濾器
第二步開始遍歷appliedPaths的key,與request裡的uri進行匹配。匹配的邏輯與第二篇文章uri匹配過濾鏈的邏輯一致,都是交給了預設的AntPathMatcher。

for (String path : this.appliedPaths.keySet()) {
            // If the path does match, then pass on to the subclass implementation for specific checks
            //(first match 'wins'):
            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);
            }
        }
複製程式碼

這裡得到了許可權配置的config,這裡的config物件通過debug圖可看到是一個String陣列,這都是在第一篇文章Filter初始化時完成的,其實裡面就是我們配置的許可權,例如perms[user:add,user:delete]。第三步呼叫isFilterChainContinued()進行鑑權

private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
                                           String path, Object pathConfig) throws Exception {

        if (isEnabled(request, response, path, pathConfig)) { 
            return onPreHandle(request, response, pathConfig);
        }
        return true;
    }
複製程式碼

點開onPreHandle,注意這裡是個邏輯表示式,如果前面isAccessAllowed()返回true,後面的OnAccessDenied()方法將不會執行。(短路原則)

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

最終呼叫到我們PermissionsAuthorizationFilter的isAccessAllowed方法。getSubject得到使用者,將鑑權的邏輯委派給Subject。(Subject在SpringShiroFilter中建立了,所以這裡可以直接獲取到,如何建立以及維護的在後面的文章講解)

 public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

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

        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;
    }
複製程式碼

這裡首先判斷有沒有principals(憑證,登入時放進去的,許可權是通過憑證獲取的,也是識別使用者的唯一標識,一般為使用者名稱)。如果有憑證則將鑑權委託給securityManager。(shiro官網也提及到securityManager是shiro的核心,到後面會發現所有的登入,鑑權,session管理都是由它掌控的)

public boolean isPermittedAll(String... permissions) {
        return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
    }
複製程式碼

這裡稍微看一下securityManager的類圖結構,方便以後章節的講解。authenticator處理登入,authorizer處理鑑權,sessionManager處理sesison管理

Shiro原理解析(三)--再談過濾器

本篇文章涉及到的是authorizer,回頭看到我們的spring-shiro.xml配置檔案,我配置的是一個多認證器,併為他設定了認證策略(一個成功為成功)。

Shiro原理解析(三)--再談過濾器
點進ModularRealmAuthenticator的isPermitted方法,第一步獲取到我們所有的realms,並逐個呼叫isPermitted()方法,一個通過則鑑權通過

  public boolean isPermittedAll(PrincipalCollection principals, String... permissions) {
        assertRealmsConfigured();
        if (permissions != null && permissions.length > 0) {
            for (String perm : permissions) {
                if (!isPermitted(principals, perm)) {
                    return false;
                }
            }
        }
        return true;
    }
複製程式碼

 public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        assertRealmsConfigured();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            if (((Authorizer) realm).isPermitted(principals, permission)) {
                return true;
            }
        }
        return false;
    }
複製程式碼
public boolean isPermitted(PrincipalCollection principals, String permission) {
        Permission p = getPermissionResolver().resolvePermission(permission);
        return isPermitted(principals, p);
    }
複製程式碼

這裡將許可權字串構建成了一個permission物件,這個permission物件可以執行萬用字元式的許可權比對。然後開始獲取AuthorizationInfo(授權資訊)

 public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        AuthorizationInfo info = getAuthorizationInfo(principals);
        return isPermitted(permission, info);
    }
複製程式碼

注意這個getAuthorizationInfo先從快取中獲取(所以shiro是支援授權快取的),如果快取為空才呼叫doGetAuthorizationInfo獲取授權。

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

        if (principals == null) {
            return null;
        }

        AuthorizationInfo info = null;

        if (log.isTraceEnabled()) {
            log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
        }

        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
        if (cache != null) {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
            }
            Object key = getAuthorizationCacheKey(principals);
            info = cache.get(key);
            if (log.isTraceEnabled()) {
                if (info == null) {
                    log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
                } else {
                    log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
                }
            }
        }


        if (info == null) {
            // Call template method if the info was not found in a cache
            info = doGetAuthorizationInfo(principals);
            // If the info is not null and the cache has been created, then cache the authorization info.
            if (info != null && cache != null) {
                if (log.isTraceEnabled()) {
                    log.trace("Caching authorization info for principals: [" + principals + "].");
                }
                Object key = getAuthorizationCacheKey(principals);
                cache.put(key, info);
            }
        }

        return info;
    }
    
複製程式碼

doGetAuthorizationInfo最終會呼叫到我們的配置的Realms類的,實現我們自己的授權邏輯

 /**
     * 授權,會在需要驗證許可權的時候被shiro呼叫,如果開啟了快取則只會第一次驗證的時候被呼叫.
     *
     * @param principals
     * @return AuthorizationInfo
     * @author liuruojing
     * @since ${PROJECT_NAME} 0.1.0
     */
    @Override
    protected final AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {

        String username = (String) principals.getPrimaryPrincipal();
        User  user = userMapper.selectByUsername(username);
        Set<String> roles = new HashSet<>();  //角色集合
        Set<String> permissions = new HashSet<>(); //許可權集合
        //根據userId查出所擁有的roleId
        List<Long> roleIds = userRoleMapper.selectByUserId(user.getUserId());
        Iterator<Long> iterator = roleIds.iterator();
        while (iterator.hasNext()) {
            Long roleId = iterator.next();
            Role role = roleMapper.selectByPrimaryKey(roleId);
            //將角色放入角色集合
            roles.add(role.getRoleName());
            //根據角色查出許可權Id
            List<Long> permissionIds = rolePermissionMapper.selectByRoleId(roleId);
            Iterator<Long> pIterator = permissionIds.iterator();
            while (pIterator.hasNext()) {
                Long permissionId = (Long) pIterator.next();
                Permission permission = permissionMapper.selectByPrimaryKey(permissionId);
                //將所有許可權放入許可權集合
                permissions.add(permission.getPermissionName());
            }
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles); //放入角色
        info.setStringPermissions(permissions); //放入許可權
        return info;
    }
}
複製程式碼

現在我們知道了訪問url需要的許可權,同時也查詢出了此使用者具有的許可權,我們只需要進行比對兩者便可以知道使用者是否有許可權訪問此url了。將獲取的AuthorizationInfo解析成Permission物件集合,逐個呼叫Permission物件的implies方法進行比對。如果比對成功則返回true執行下一個所需許可權的比對。

//changed visibility from private to protected for SHIRO-332
    protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
        Collection<Permission> perms = getPermissions(info);
        if (perms != null && !perms.isEmpty()) {
            for (Permission perm : perms) {
                if (perm.implies(permission)) {
                    return true;
                }
            }
        }
        return false;
    }
複製程式碼

最後所有許可權比對完後會回到上面的onpreHandle(),如果isAccessAllowed鑑權通過則返回true,否則呼叫onAccessDenied方法設定重定向到unloginUrl或者unAuthorizedUrl,這兩個url是我們在xml中配置的

 public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }
複製程式碼
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;
    }
複製程式碼

相關文章