shiro source code - role and permission verification filter principle

Sometimes we will perform permission authentication on the interface. Next, we will study its authentication principle. That is to study role-based authentication and authority code based authentication.

1. Front configuration

1. shiro configuration

        /**
         *  Path - > filter name 1 [parameter 1, parameter 2, parameter 3...], filter name 2 [parameter 1, parameter 2...]
         * Custom configuration (path in front, specific filter name and parameters in the back, multiple filter parameters are separated by commas, and multiple filter parameters are also separated by commas))
         * Some filters do not require parameters, such as anon, authc and shiro. When parsing, an array is parsed as [name, null] by default
         */
        FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // Test address
        FILTER_CHAIN_DEFINITION_MAP.put("/login2", "anon"); // Login address
        FILTER_CHAIN_DEFINITION_MAP.put("/login3", "anon"); // Login address
        FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[system administrator,User administrator],perms[user:manager:*]");
        FILTER_CHAIN_DEFINITION_MAP.put("/dept/**", "perms[dept:manage:*]");
        FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // All resources need to be validated

two   com.zd.bx.config.shiro.CustomRealm custom realm

package com.zd.bx.config.shiro;

import com.beust.jcommander.internal.Lists;
import com.zd.bx.bean.user.User;
import com.zd.bx.utils.permission.PermissionUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(CustomRealm.class);

    /**
     * authentication 
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // getPrimaryPrincipal What you get is doGetAuthenticationInfo Method last saved user object
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        if (primaryPrincipal == null) {
            return null;
        }

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User currentUser = (User) primaryPrincipal;
        // Add role
        authorizationInfo.addRoles(Lists.newArrayList("administrators"));
        // add permission
        authorizationInfo.addStringPermissions(Lists.newArrayList("user:manage:*", "dept:manage:*"));

        log.debug("authorizationInfo roles: {}, permissions: {}", authorizationInfo.getRoles(),
                authorizationInfo.getStringPermissions());
        return authorizationInfo;
    }

    /**
     * authentication
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
            throws AuthenticationException {

        if (authenticationToken == null || !(authenticationToken instanceof UsernamePasswordToken)) {
            return null;
        }

        User user = new User();
        user.setPassword("111222");
        return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        log.info("token: {}", token);
        return token != null && UsernamePasswordToken.class.isAssignableFrom(token.getClass());
    }
}

2. Role based verification principle

1. Access address:   http://localhost:8081/user/test

two   The entry is SpringShiroFilter, and the request reaches org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter  

3. Then go to: org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal, create Subject, bind to ThreadLocal object, and construct filterchain (the filter that Shiro environment needs to pass through)

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

        if (t != null) {
            if (t instanceof ServletException) {
                throw (ServletException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }

  4. Arrival   org.apache.shiro.web.servlet.AbstractShiroFilter#executeChain

    protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
            throws IOException, ServletException {
        FilterChain chain = getExecutionChain(request, response, origChain);
        chain.doFilter(request, response);
    }

1>   org.apache.shiro.web.servlet.AbstractShiroFilter#getExecutionChain get filter chain

    protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
        FilterChain chain = origChain;

        FilterChainResolver resolver = getFilterChainResolver();
        if (resolver == null) {
            log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
            return origChain;
        }

        FilterChain resolved = resolver.getChain(request, response, origChain);
        if (resolved != null) {
            log.trace("Resolved a configured FilterChain for the current request.");
            chain = resolved;
        } else {
            log.trace("No FilterChain configured for the current request.  Using the default.");
        }

        return chain;
    }
  • call   org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }

        String requestURI = getPathWithinApplication(request);

        // in spring web, the requestURI "/resource/menus" ---- "resource/menus/" bose can access the resource
        // but the pathPattern match "/resource/menus" can not match "resource/menus/"
        // user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
        if(requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
                && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
            requestURI = requestURI.substring(0, requestURI.length() - 1);
        }


        //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
        //as the chain name for the FilterChainManager's requirements
        for (String pathPattern : filterChainManager.getChainNames()) {
            if (pathPattern != null && !DEFAULT_PATH_SEPARATOR.equals(pathPattern)
                    && pathPattern.endsWith(DEFAULT_PATH_SEPARATOR)) {
                pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
            }

            // If the path does match, then pass on to the subclass implementation for specific checks:
            if (pathMatches(pathPattern, requestURI)) {
                if (log.isTraceEnabled()) {
                    log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + Encode.forHtml(requestURI) + "].  " +
                            "Utilizing corresponding filter chain...");
                }
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }

        return null;
    }

Here, regular matching is performed according to the path. If the conditions are met, it is called   org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#proxy generate proxy FilterChain.

    public FilterChain proxy(FilterChain original, String chainName) {
        NamedFilterList configured = getChain(chainName);
        if (configured == null) {
            String msg = "There is no configured chain under the name/key [" + chainName + "].";
            throw new IllegalArgumentException(msg);
        }
        return configured.proxy(original);
    }

Finally, the generated agent FilterChain is as follows:

2> Call org.apache.shiro.web.servlet.ProxiedFilterChain#doFilter to start executing the proxy FilterChain logic

    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.filters == null || this.filters.size() == this.index) {
            //we've reached the end of the wrapped chain, so invoke the original one:
            if (log.isTraceEnabled()) {
                log.trace("Invoking original filter chain.");
            }
            this.orig.doFilter(request, response);
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Invoking wrapped filter at index [" + this.index + "]");
            }
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }

5. Permission filter starts filtering:

In template mode, you will call:   org.apache.shiro.web.servlet.AdviceFilter#doFilterInternal

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

preHandle(request, response); Call to   org.apache.shiro.web.filter.PathMatchingFilter#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;
    }

Here, first perform path matching, and then this.appliedPaths.get(path); Obtain relevant configuration information as follows:

  Then call   org.apache.shiro.web.filter.PathMatchingFilter#isFilterChainContinued

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

        if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
            if (log.isTraceEnabled()) {
                log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  " +
                        "Delegating to subclass implementation for 'onPreHandle' check.",
                        new Object[]{getName(), path, pathConfig});
            }
            //The filter is enabled for this specific request, so delegate to subclass implementations
            //so they can decide if the request should continue through the chain or not:
            return onPreHandle(request, response, pathConfig);
        }

        if (log.isTraceEnabled()) {
            log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  " +
                    "The next element in the FilterChain will be called immediately.",
                    new Object[]{getName(), path, pathConfig});
        }
        //This filter is disabled for this specific request,
        //return 'true' immediately to indicate that the filter will not process the request
        //and let the request/response to continue through the filter chain:
        return true;
    }

Continue to call down to org.apache.shiro.web.filter.AccessControlFilter#onPreHandle:

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

Here, as before, there are two logics: if the isAccessAllowed request allows direct return, the next filter chain will be called; Returning true means to continue to execute the following filter chain, and returning false means not to execute the following filter chain (it seems that it is a little ambiguous with the method name...).  

(1)   org.apache.shiro.web.filter.authz.RolesAuthorizationFilter#isAccessAllowed judge whether the request is allowed and start permission verification:

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

Parameter interpretation:

mappedValue is the role name of the resolved configuration. The above passed is:   [system administrator, user administrator]

Code interpretation:

After verification, it is called:   org.apache.shiro.subject.support.DelegatingSubject#hasAllRoles request forwarded to subject

    public boolean hasAllRoles(Collection<String> roleIdentifiers) {
        return hasPrincipals() && securityManager.hasAllRoles(getPrincipals(), roleIdentifiers);
    }

1>   org.apache.shiro.subject.support.DelegatingSubject#hasPrincipals actually determines whether to authenticate. After authentication, the authentication information will be stored here

    protected boolean hasPrincipals() {
        return !isEmpty(getPrincipals());
    }

Here is a simple authentication, that is, the authentication information is stored in the session, and then study the second step below.

  2>   org.apache.shiro.mgt.AuthorizingSecurityManager#hasAllRoles determines whether there is permission, and the request is transferred to the authorizer

    public boolean hasAllRoles(PrincipalCollection principals, Collection<String> roleIdentifiers) {
        return this.authorizer.hasAllRoles(principals, roleIdentifiers);
    }

Then the request is called to:   org.apache.shiro.authz.ModularRealmAuthorizer#hasAllRoles

    public boolean hasAllRoles(PrincipalCollection principals, Collection<String> roleIdentifiers) {
        assertRealmsConfigured();
        for (String roleIdentifier : roleIdentifiers) {
            if (!hasRole(principals, roleIdentifier)) {
                return false;
            }
        }
        return true;
    }

    protected void assertRealmsConfigured() throws IllegalStateException {
        Collection<Realm> realms = getRealms();
        if (realms == null || realms.isEmpty()) {
            String msg = "Configuration error:  No realms have been configured!  One or more realms must be " +
                    "present to execute an authorization operation.";
            throw new IllegalStateException(msg);
        }
    }

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

You can see that all roles are traversed, and then hasRole is called for individual judgement. The hasrole obtains the realm internally, and then requests the realm to determine whether there is a role. Request arrival: org.apache.shiro.realm.authorizingream#hasrole (org.apache.shiro.subject.principalcollection, Java. Lang.string)

    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        AuthorizationInfo info = getAuthorizationInfo(principal);
        return hasRole(roleIdentifier, info);
    }

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

    protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
        return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
    }

Here you can see its logic, call    getAuthorizationInfo obtains its authorization information, and then determines whether it contains the specified role from the authorization role information.

getAuthorizationInfo obtains authorization information in the same logic as obtaining authentication information. First get it from the cache, and then call realm to obtain it in real time. There is no cache here, so call directly   com.zd.bx.config.shiro.CustomRealm#doGetAuthorizationInfo real-time access.

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // getPrimaryPrincipal What you get is doGetAuthenticationInfo Method last saved user object
        Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
        if (primaryPrincipal == null) {
            return null;
        }

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User currentUser = (User) primaryPrincipal;
        // Add role
        authorizationInfo.addRoles(PermissionUtils.listUserRolenames(currentUser));
        // add permission
        authorizationInfo.addStringPermissions(PermissionUtils.listUserPermissionCodes(currentUser));

        log.debug("authorizationInfo roles: {}, permissions: {}", authorizationInfo.getRoles(),
                authorizationInfo.getStringPermissions());
        return authorizationInfo;
    }

You can see that this method is to call your own realm to obtain authorization and obtain an authorizationinfo object. Then take the authorizationinfo object and call org.apache.shiro.realm.AuthorizingRealm#hasRole(java.lang.String, org.apache.shiro.authz.AuthorizationInfo) to judge whether there is a role. This method returns the authorization information authorizationinfo after judging whether it is non empty, role collection and whether it contains the specified role.

(2) call back after false is returned.   org.apache.shiro.web.filter.AccessControlFilter#onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse, java.lang.Object)   onAccessDenied: if this method returns true, the next chain can be executed; If false is returned, execute the chain step.

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return onAccessDenied(request, response);
    }

Continue request call to   org.apache.shiro.web.filter.authz.AuthorizationFilter#onAccessDenied:

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

If it is judged that it is not authenticated, it will be redirected to the login address; If it is after authentication, the permission is insufficient. If there is an unauthorized page, redirect to the unauthorized page; If there is no unauthorized page, call WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); Send unauthorized information. Then false is returned. If false is returned, the next step will be followed.

Call to:   org.apache.catalina.connector.ResponseFacade#sendError(int) send error response code

    public void sendError(int sc) throws IOException {
        if (this.isCommitted()) {
            throw new IllegalStateException(sm.getString("coyoteResponse.sendError.ise"));
        } else {
            this.response.setAppCommitted(true);
            this.response.sendError(sc);
        }
    }

The above is the role-based authentication process. Both take org.apache.shiro.web.filter.AccessControlFilter#onPreHandle as the entry. There are two methods:   isAccessAllowed determines whether execution is allowed. If it returns true, the subsequent filter execution will continue. If it returns false, onaccessdenied will continue to be called. onAccessDenied   Some logic after rejection processing is processed in. Returning true indicates that execution can continue, and returning false indicates that subsequent filters will not be executed.

3. Principle of authority verification

1. Access connection  :   http://localhost:8081/dept/test

2. The previous steps are the same as the role-based verification. View at org.apache.shiro.web.filter.AccessControlFilter#onPreHandle under the breakpoint:

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

Parameters:   mappedValue passes   ["dept:manage:*"]

1> Isaccessallowed method:   org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter#isAccessAllowed

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

It can be understood here that the process is transferred to the Subject as above,

(1) Call org.apache.shiro.subject.support.DelegatingSubject#isPermitted(java.lang.String)

    public boolean isPermitted(String permission) {
        return hasPrincipals() && securityManager.isPermitted(getPrincipals(), permission);
    }

First verify the authentication, and then forward it to the security manager.

(2) org.apache.shiro.mgt.AuthorizingSecurityManager#isPermitted(org.apache.shiro.subject.PrincipalCollection, java.lang.String)

    public boolean isPermitted(PrincipalCollection principals, String permissionString) {
        return this.authorizer.isPermitted(principals, permissionString);
    }

Continue to pass on to the authorizer

(3) org.apache.shiro.authz.ModularRealmAuthorizer#isPermitted(org.apache.shiro.subject.PrincipalCollection, java.lang.String)

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

You can see that the realm is obtained, traversed and authenticated.

(4) org.apache.shiro.realm.AuthorizingRealm#isPermitted(org.apache.shiro.subject.PrincipalCollection, java.lang.String)

    public boolean isPermitted(PrincipalCollection principals, String permission) {
        Permission p = getPermissionResolver().resolvePermission(permission);
        return isPermitted(principals, p);
    }

Here, the Permission code is parsed into a Permission object.   org.apache.shiro.authz.permission.WildcardPermissionResolver#resolvePermission:

    public Permission resolvePermission(String permissionString) {
        return new WildcardPermission(permissionString, caseSensitive);
    }

org.apache.shiro.authz.permission.WildcardPermission#WildcardPermission(java.lang.String, boolean):

    /*--------------------------------------------
    |             C O N S T A N T S             |
    ============================================*/
    protected static final String WILDCARD_TOKEN = "*";
    protected static final String PART_DIVIDER_TOKEN = ":";
    protected static final String SUBPART_DIVIDER_TOKEN = ",";
    protected static final boolean DEFAULT_CASE_SENSITIVE = false;

    public WildcardPermission(String wildcardString, boolean caseSensitive) {
        setParts(wildcardString, caseSensitive);
    }

    protected void setParts(String wildcardString, boolean caseSensitive) {
        wildcardString = StringUtils.clean(wildcardString);

        if (wildcardString == null || wildcardString.isEmpty()) {
            throw new IllegalArgumentException("Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.");
        }

        if (!caseSensitive) {
            wildcardString = wildcardString.toLowerCase();
        }

        List<String> parts = CollectionUtils.asList(wildcardString.split(PART_DIVIDER_TOKEN));

        this.parts = new ArrayList<Set<String>>();
        for (String part : parts) {
            Set<String> subparts = CollectionUtils.asSet(part.split(SUBPART_DIVIDER_TOKEN));

            if (subparts.isEmpty()) {
                throw new IllegalArgumentException("Wildcard string cannot contain parts with only dividers. Make sure permission strings are properly formatted.");
            }
            this.parts.add(subparts);
        }

        if (this.parts.isEmpty()) {
            throw new IllegalArgumentException("Wildcard string cannot contain only dividers. Make sure permission strings are properly formatted.");
        }
    }

It can be seen that it is cut according to the transmitted permission code according to ":", and then divided according to ",". Finally, it is placed inside the parts collection.

(5) Continue Calling: org.apache.shiro.realm.authorizingream#ispermitted (org.apache.shiro.subject.principalcollection, org.apache.shiro.authz.permission)

    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        AuthorizationInfo info = getAuthorizationInfo(principals);
        return isPermitted(permission, info);
    }

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

    //visibility changed from private to protected per SHIRO-332
    protected Collection<Permission> getPermissions(AuthorizationInfo info) {
        Set<Permission> permissions = new HashSet<Permission>();

        if (info != null) {
            Collection<Permission> perms = info.getObjectPermissions();
            if (!CollectionUtils.isEmpty(perms)) {
                permissions.addAll(perms);
            }
            perms = resolvePermissions(info.getStringPermissions());
            if (!CollectionUtils.isEmpty(perms)) {
                permissions.addAll(perms);
            }

            perms = resolveRolePermissions(info.getRoles());
            if (!CollectionUtils.isEmpty(perms)) {
                permissions.addAll(perms);
            }
        }

        if (permissions.isEmpty()) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(permissions);
        }
    }

    private Collection<Permission> resolvePermissions(Collection<String> stringPerms) {
        Collection<Permission> perms = Collections.emptySet();
        PermissionResolver resolver = getPermissionResolver();
        if (resolver != null && !CollectionUtils.isEmpty(stringPerms)) {
            perms = new LinkedHashSet<Permission>(stringPerms.size());
            for (String strPermission : stringPerms) {
                if (StringUtils.clean(strPermission) != null) {
                    Permission permission = resolver.resolvePermission(strPermission);
                    perms.add(permission);
                }
            }
        }
        return perms;
    }

getAuthorizationInfo is the same as the role verification above. To obtain authorization information, the function of realm will be called   doGetAuthorizationInfo   () method. Then, the permission permission code collection and permission code object are obtained from the parameters returned by this method, and then constructed into a collection < Permission >, representing the permissions of the current user. Then traverse the permissions of the current user and call org.apache.shiro.authz.permission.WildcardPermission#implies to determine whether the permission codes match:

    public boolean implies(Permission p) {
        // By default only supports comparisons with other WildcardPermissions
        if (!(p instanceof WildcardPermission)) {
            return false;
        }

        WildcardPermission wp = (WildcardPermission) p;

        List<Set<String>> otherParts = wp.getParts();

        int i = 0;
        for (Set<String> otherPart : otherParts) {
            // If this permission has less parts than the other permission, everything after the number of parts contained
            // in this permission is automatically implied, so return true
            if (getParts().size() - 1 < i) {
                return true;
            } else {
                Set<String> part = getParts().get(i);
                if (!part.contains(WILDCARD_TOKEN) && !part.containsAll(otherPart)) {
                    return false;
                }
                i++;
            }
        }

        // If this permission has more parts than the other parts, only imply it if all of the other parts are wildcards
        for (; i < getParts().size(); i++) {
            Set<String> part = getParts().get(i);
            if (!part.contains(WILDCARD_TOKEN)) {
                return false;
            }
        }

        return true;
    }

You can see that the matching rule is to match each permission part after the permission code is split. If * represents any permission, you can match the next part.

  • Getparts(). Size() - 1 < I means that the part of the current permission code is less than the part to be matched, then true is returned.

For example:   user:manage ->user:manage:1.   True is returned in this case. After comparing user and manage, i becomes 2,   getParts().size() - 1   It's 1. That is, if there is no comparable part left in the current Permisson(user:manage), true is returned.

  • If the current comparison does not contain *, it is considered as equivalent matching. The two parts must be the same. If they are different, false is returned. Otherwise, if it is considered the same, continue the comparison of the next part.
  • Continuing to judge is to deal with the situation that the permission code of the current object is greater than that of the compared object, such as:    user:manage:* ->user:manage   . The extra part can only contain *, otherwise false will be returned.

That is, true can be returned in the following cases: (the permission code owned by the current user is in the front, followed by the permission to be verified)

user:manage ->user:manage:1:2:....

user:manage:1 -> user:manage:1

user:manage:*:* ->user:manage

 

2>   The onAccessDenied method is the same as the role-based verification logic above.

4. Principle of permission verification based on annotation

We can also verify methods based on annotations. This general idea is based on AOP implementation.

1. Start:

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

2. Test code

    @RequiresPermissions(value = {"test"})
    @GetMapping("/test")
    public String test() {
        return "test";
    }

3. Test

Access error  

org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [test]
    at org.apache.shiro.authz.ModularRealmAuthorizer.checkPermission(ModularRealmAuthorizer.java:323) ~[shiro-core-1.5.3.jar:1.5.3]
    at org.apache.shiro.mgt.AuthorizingSecurityManager.checkPermission(AuthorizingSecurityManager.java:137) ~[shiro-core-1.5.3.jar:1.5.3]
    at org.apache.shiro.subject.support.DelegatingSubject.checkPermission(DelegatingSubject.java:209) ~[shiro-core-1.. . . 

4. Principle

Based on   PointcutAdvisor   Interface implements AOP.

1. Class diagram of authorizationattributesourceadvisor

  2. Source code:

package org.apache.shiro.spring.security.interceptor;

import org.apache.shiro.authz.annotation.*;
import org.apache.shiro.mgt.SecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.core.annotation.AnnotationUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;


/**
 * TODO - complete JavaDoc
 *
 * @since 0.1
 */
@SuppressWarnings({"unchecked"})
public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);

    private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    RequiresPermissions.class, RequiresRoles.class,
                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
            };

    protected SecurityManager securityManager = null;

    /**
     * Create a new AuthorizationAttributeSourceAdvisor.
     */
    public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }

    public SecurityManager getSecurityManager() {
        return securityManager;
    }

    public void setSecurityManager(org.apache.shiro.mgt.SecurityManager securityManager) {
        this.securityManager = securityManager;
    }

    /**
     * Returns <tt>true</tt> if the method or the class has any Shiro annotations, false otherwise.
     * The annotations inspected are:
     * <ul>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresAuthentication RequiresAuthentication}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresUser RequiresUser}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresGuest RequiresGuest}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresRoles RequiresRoles}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresPermissions RequiresPermissions}</li>
     * </ul>
     *
     * @param method      the method to check for a Shiro annotation
     * @param targetClass the class potentially declaring Shiro annotations
     * @return <tt>true</tt> if the method has a Shiro annotation, false otherwise.
     * @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, Class)
     */
    public boolean matches(Method method, Class targetClass) {
        Method m = method;

        if ( isAuthzAnnotationPresent(m) ) {
            return true;
        }

        //The 'method' parameter could be from an interface that doesn't have the annotation.
        //Check to see if the implementation has it.
        if ( targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
            } catch (NoSuchMethodException ignored) {
                //default return value is false.  If we can't find the method, then obviously
                //there is no annotation, so just use the default return value.
            }
        }

        return false;
    }

    private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {
        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
            Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);
            if ( a != null ) {
                return true;
            }
        }
        return false;
    }

    private boolean isAuthzAnnotationPresent(Method method) {
        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
            Annotation a = AnnotationUtils.findAnnotation(method, annClass);
            if ( a != null ) {
                return true;
            }
        }
        return false;
    }

}

3. Interpretation:

(1) To determine whether the match is:   org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor#matches method. In fact, it is used to judge whether org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor #authz is carried_ ANNOTATION_ Relevant annotations specified by classes.

(2)   Org.apache.shiro.spring.security.interceptor.aolianceannotationsauthorizing methodinterceptor #aolianceannotationsauthorizing methodinterceptor is the processing class of methodinterceptor:

package org.apache.shiro.spring.security.interceptor;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.shiro.aop.AnnotationResolver;
import org.apache.shiro.authz.aop.*;
import org.apache.shiro.spring.aop.SpringAnnotationResolver;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * Allows Shiro Annotations to work in any <a href="http://aopalliance.sourceforge.net/">AOP Alliance</a>
 * specific implementation environment (for example, Spring).
 *
 * @since 0.2
 */
public class AopAllianceAnnotationsAuthorizingMethodInterceptor
        extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {

    public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
        List<AuthorizingAnnotationMethodInterceptor> interceptors =
                new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);

        //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
        //raw JDK resolution process.
        AnnotationResolver resolver = new SpringAnnotationResolver();
        //we can re-use the same resolver instance - it does not retain state:
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));

        setMethodInterceptors(interceptors);
    }
    /**
     * Creates a {@link MethodInvocation MethodInvocation} that wraps an
     * {@link org.aopalliance.intercept.MethodInvocation org.aopalliance.intercept.MethodInvocation} instance,
     * enabling Shiro Annotations in <a href="http://aopalliance.sourceforge.net/">AOP Alliance</a> environments
     * (Spring, etc).
     *
     * @param implSpecificMethodInvocation AOP Alliance {@link org.aopalliance.intercept.MethodInvocation MethodInvocation}
     * @return a Shiro {@link MethodInvocation MethodInvocation} instance that wraps the AOP Alliance instance.
     */
    protected org.apache.shiro.aop.MethodInvocation createMethodInvocation(Object implSpecificMethodInvocation) {
        final MethodInvocation mi = (MethodInvocation) implSpecificMethodInvocation;

        return new org.apache.shiro.aop.MethodInvocation() {
            public Method getMethod() {
                return mi.getMethod();
            }

            public Object[] getArguments() {
                return mi.getArguments();
            }

            public String toString() {
                return "Method invocation [" + mi.getMethod() + "]";
            }

            public Object proceed() throws Throwable {
                return mi.proceed();
            }

            public Object getThis() {
                return mi.getThis();
            }
        };
    }

    /**
     * Simply casts the method argument to an
     * {@link org.aopalliance.intercept.MethodInvocation org.aopalliance.intercept.MethodInvocation} and then
     * calls <code>methodInvocation.{@link org.aopalliance.intercept.MethodInvocation#proceed proceed}()</code>
     *
     * @param aopAllianceMethodInvocation the {@link org.aopalliance.intercept.MethodInvocation org.aopalliance.intercept.MethodInvocation}
     * @return the {@link org.aopalliance.intercept.MethodInvocation#proceed() org.aopalliance.intercept.MethodInvocation.proceed()} method call result.
     * @throws Throwable if the underlying AOP Alliance <code>proceed()</code> call throws a <code>Throwable</code>.
     */
    protected Object continueInvocation(Object aopAllianceMethodInvocation) throws Throwable {
        MethodInvocation mi = (MethodInvocation) aopAllianceMethodInvocation;
        return mi.proceed();
    }

    /**
     * Creates a Shiro {@link MethodInvocation MethodInvocation} instance and then immediately calls
     * {@link org.apache.shiro.authz.aop.AuthorizingMethodInterceptor#invoke super.invoke}.
     *
     * @param methodInvocation the AOP Alliance-specific <code>methodInvocation</code> instance.
     * @return the return value from invoking the method invocation.
     * @throws Throwable if the underlying AOP Alliance method invocation throws a <code>Throwable</code>.
     */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
        return super.invoke(mi);
    }
}

methodInterceptors maintain five related handlers.

org.apache.shiro.authz.aop.AuthorizingMethodInterceptor:

public abstract class AuthorizingMethodInterceptor extends MethodInterceptorSupport {

    /**
     * Invokes the specified method (<code>methodInvocation.{@link org.apache.shiro.aop.MethodInvocation#proceed proceed}()</code>
     * if authorization is allowed by first
     * calling {@link #assertAuthorized(org.apache.shiro.aop.MethodInvocation) assertAuthorized}.
     */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        assertAuthorized(methodInvocation);
        return methodInvocation.proceed();
    }

    /**
     * Asserts that the specified MethodInvocation is allowed to continue by performing any necessary authorization
     * (access control) checks first.
     * @param methodInvocation the <code>MethodInvocation</code> to invoke.
     * @throws AuthorizationException if the <code>methodInvocation</code> should not be allowed to continue/execute.
     */
    protected abstract void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException;

}

When invoking AOP, there is assertauthorized logic. The logic of this method is to call the five Handler above and call it.   The supports method determines whether to support or not, and then invoke s its assertAuthorized method.

(3) Take org.apache.shiro.authz.aop.RoleAnnotationMethodInterceptor#RoleAnnotationMethodInterceptor() role verification as an example

public class RoleAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {

    /**
     * Default no-argument constructor that ensures this interceptor looks for
     * {@link RequiresRoles RequiresRoles} annotations in a method declaration.
     */
    public RoleAnnotationMethodInterceptor() {
        super( new RoleAnnotationHandler() );
    }

    /**
     * @param resolver
     * @since 1.1
     */
    public RoleAnnotationMethodInterceptor(AnnotationResolver resolver) {
        super(new RoleAnnotationHandler(), resolver);
    }
}

org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor source code is as follows:

public abstract class AuthorizingAnnotationMethodInterceptor extends AnnotationMethodInterceptor
{
    
    /**
     * Constructor that ensures the internal <code>handler</code> is set which will be used to perform the
     * authorization assertion checks when a supported annotation is encountered.
     * @param handler the internal <code>handler</code> used to perform authorization assertion checks when a 
     * supported annotation is encountered.
     */
    public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler ) {
        super(handler);
    }

    /**
     *
     * @param handler
     * @param resolver
     * @since 1.1
     */
    public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler,
                                                   AnnotationResolver resolver) {
        super(handler, resolver);
    }

    /**
     * Ensures the <code>methodInvocation</code> is allowed to execute first before proceeding by calling the
     * {@link #assertAuthorized(org.apache.shiro.aop.MethodInvocation) assertAuthorized} method first.
     *
     * @param methodInvocation the method invocation to check for authorization prior to allowing it to proceed/execute.
     * @return the return value from the method invocation (the value of {@link org.apache.shiro.aop.MethodInvocation#proceed() MethodInvocation.proceed()}).
     * @throws org.apache.shiro.authz.AuthorizationException if the <code>MethodInvocation</code> is not allowed to proceed.
     * @throws Throwable if any other error occurs.
     */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        assertAuthorized(methodInvocation);
        return methodInvocation.proceed();
    }

    /**
     * Ensures the calling Subject is authorized to execute the specified <code>MethodInvocation</code>.
     * <p/>
     * As this is an AnnotationMethodInterceptor, this implementation merely delegates to the internal
     * {@link AuthorizingAnnotationHandler AuthorizingAnnotationHandler} by first acquiring the annotation by
     * calling {@link #getAnnotation(MethodInvocation) getAnnotation(methodInvocation)} and then calls
     * {@link AuthorizingAnnotationHandler#assertAuthorized(java.lang.annotation.Annotation) handler.assertAuthorized(annotation)}.
     *
     * @param mi the <code>MethodInvocation</code> to check to see if it is allowed to proceed/execute.
     * @throws AuthorizationException if the method invocation is not allowed to continue/execute.
     */
    public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        try {
            ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
        }
        catch(AuthorizationException ae) {
            // Annotation handler doesn't know why it was called, so add the information here if possible. 
            // Don't wrap the exception here since we don't want to mask the specific exception, such as 
            // UnauthenticatedException etc. 
            if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod()));
            throw ae;
        }         
    }
}

You can see that (2) above is called when AOP is executed   assertAuthorized   Call to   org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor#assertAuthorized. This calls the assertauthorized method of the specific Handler.

org.apache.shiro.authz.aop.RoleAnnotationHandler:

public class RoleAnnotationHandler extends AuthorizingAnnotationHandler {

    /**
     * Default no-argument constructor that ensures this handler looks for
     * {@link org.apache.shiro.authz.annotation.RequiresRoles RequiresRoles} annotations.
     */
    public RoleAnnotationHandler() {
        super(RequiresRoles.class);
    }

    /**
     * Ensures that the calling <code>Subject</code> has the Annotation's specified roles, and if not, throws an
     * <code>AuthorizingException</code> indicating that access is denied.
     *
     * @param a the RequiresRoles annotation to use to check for one or more roles
     * @throws org.apache.shiro.authz.AuthorizationException
     *          if the calling <code>Subject</code> does not have the role(s) necessary to
     *          proceed.
     */
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (!(a instanceof RequiresRoles)) return;

        RequiresRoles rrAnnotation = (RequiresRoles) a;
        String[] roles = rrAnnotation.value();

        if (roles.length == 1) {
            getSubject().checkRole(roles[0]);
            return;
        }
        if (Logical.AND.equals(rrAnnotation.logical())) {
            getSubject().checkRoles(Arrays.asList(roles));
            return;
        }
        if (Logical.OR.equals(rrAnnotation.logical())) {
            // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
            boolean hasAtLeastOneRole = false;
            for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
            // Cause the exception if none of the role match, note that the exception message will be a bit misleading
            if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
        }
    }

}

You can see that the core logic is also to parse the value on the annotation. Then call   Org.apache.shiro.subject.support.delegatingsubject#checkRoles (Java. Util. Collection < Java. Lang.string >) check whether there are corresponding permissions. Finally, the verification roles and permissions are the same as those above. For example, checkRoles is finally called to: org. Apache. Shiro. Realm. Authorizing realm #checkrole (Java. Lang. string, org. Apache. Shiro. Authz. Authorizationinfo)

    protected void checkRole(String role, AuthorizationInfo info) {
        if (!hasRole(role, info)) {
            String msg = "User does not have role [" + role + "]";
            throw new UnauthorizedException(msg);
        }
    }

 

Added by faizulbari on Fri, 29 Oct 2021 20:11:15 +0300