Shiro源码分析 - 授权流程

本文深入剖析了Shiro权限控制的实现原理,包括权限Filter的工作流程、权限验证的具体步骤及自定义Realm的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注意,该授权流程指的是,在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链,反之则重定向至配置的未授权页面。

 

转载于:https://my.oschina.net/thinwonton/blog/1504078

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值