在一个用户登录后,即身份认证通过,只能证明该登录身份是合法的,至于具体能访问系统中的什么资源,需要通过授权来控制。一般系统中都是通过用户关联角色、角色再关联权限来实现判断一个用户是否有某资源的使用权限,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中的PermissionsAuthorizationFilterRolesAuthorizationFilter就是用于权限控制的Filter,名称分别为permsrolesPermissionsAuthorizationFilter用于判断用户访问某URL时是否有相应权限,RolesAuthorizationFilter用于判断用户访问某URL时是否有相应角色。

由于Shiro中Filter继承体系比较复杂,要想理解Shiro权限是如何控制的就必须先理解Filter的继承体系,以及理解继承体系中父类Filter的特点及作用。由于Filter继承体系庞大,下面只列出PermissionsAuthorizationFilterRolesAuthorizationFilter的继承关系。

下面对继承关系中一些重要的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;}public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {Subject subject = getSubject(request, response);// 访问时需要的权限String[] perms = (String[]) mappedValue;// 调用Subject对象的isPermitted或isPermittedAll来判断是否有权限boolean isPermitted = true;if (perms != null && perms.length > 0) {    if (perms.length == 1) {        if (!subject.isPermitted(perms[0])) {            isPermitted = false;        }    } else {        if (!subject.isPermittedAll(perms)) {            isPermitted = false;        }    }}return isPermitted;}

    isPermittedisPermittedAll最终都委托给了ModularRealmAuthorizer.isPermittedModularRealmAuthorizer.isPermittedAll方法,至于为什么为会委托给ModularRealmAuthorizer请参看:Shiro源码分析----登录流程。

    下面以ModularRealmAuthorizer.isPermitted为例,分析一下是如何进行权限判断的:

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

    在使用Shiro时,Realm对象包含了认证与授权信息,在实际应用时,一般都是存储在数据库中的,且Realm一般都会自定义实现。实现自定义的Realm时,一般继承自org.apache.shiro.realm.AuthorizingRealm类,下面是一个例子:

    public class UserRealm extends AuthorizingRealm {   private UserService userService;   public void setUserService(UserService userService) {       this.userService = userService;   }// 从数据库中获取权限信息   @Override   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {       String username = (String)principals.getPrimaryPrincipal();       SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();    // 从数据库中查询当前用户所拥有的角色       authorizationInfo.setRoles(userService.findRoles(username));    // 从数据库中查询当前用户所拥有的权限       authorizationInfo.setStringPermissions(userService.findPermissions(username));       return authorizationInfo;   }// 从数据库中获取认证信息   @Override   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {       String username = (String)token.getPrincipal();       User user = userService.findByUsername(username);       if(user == null) {           throw new UnknownAccountException();//没找到帐号       }       if(Boolean.TRUE.equals(user.getLocked())) {           throw new LockedAccountException(); //帐号锁定       }       //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现       SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(               user.getUsername(), //用户名               user.getPassword(), //密码               ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt               getName()  //realm name       );       return authenticationInfo;   }}

    AuthorizingRealm.isPermetted方法中就是根据用户所拥有的权限与访问时需要的权限进行匹配,如果有权限则继续执行Filter链,反之则重定向至配置的未授权页面。
    理解了PermissionsAuthorizationFilter的判断逻辑,那么RolesAuthorizationFilter的判断逻辑就很容易理解了,因为其流程是一样的,只是RolesAuthorizationFilter是基于用户角色进行判断的。

  8. RolesAuthorizationFilter:实现了isAccessAllowed方法:

    @SuppressWarnings({"unchecked"})public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {Subject subject = getSubject(request, response);// 获取访问时需要的角色String[] rolesArray = (String[]) mappedValue;if (rolesArray == null || rolesArray.length == 0) {    //no roles specified, so nothing to check - allow access.    return true;}Set<String> roles = CollectionUtils.asSet(rolesArray);// 委托给Subject.hasAllRoles方法return subject.hasAllRoles(roles);}

    同理,hasAllRoles方法,最终都委托给了ModularRealmAuthorizer.hasAllRoles方法

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

    AuthorizingRealm.hasAllRoles方法中就是根据用户所拥有的角色与访问时需要的角色进行匹配,如果有角色则继续执行Filter链,反之则重定向至配置的未授权页面。

至此,Shiro授权流程分析完毕,如有错误之处,敬请指正。
-------------------------------- END -------------------------------

及时获取更多精彩文章,请关注公众号《Java精讲》。

©著作权归作者所有:来自51CTO博客作者Java精讲的原创作品,如需转载,请注明出处,否则将追究法律责任

好知识,才能预见未来

赞赏

0人进行了赞赏支持

更多相关文章

  1. 快速提高技术的方法 & 长期欢迎投稿
  2. C/C++ 性能优化背后的方法论:TMAM
  3. 深入理解Java Stream流水线
  4. 配置 eslint 去掉 no-unused-vars 报错 方法汇总
  5. 运维助力敏捷交付-我们的运维看板
  6. 后台六 功能权限(阅读)
  7. 重要|Spark driver端得到executor返回值的方法
  8. MongoDB用户权限管理
  9. 一对多、多对多查询,最简单的方法请拿好

随机推荐

  1. C#实现操作字符串的方法总结
  2. ASP.NET Core应用程序运行Vue并且部署在I
  3. C#中关于foreach实现的原理详解
  4. C#中pdf生成图片文字水印类的实现实例
  5. IIS如何实现部署asp.net mvc网站的方法
  6. C#编写Windows服务程序的图文详解
  7. C#中值类型与引用类型的详细介绍
  8. ASP.NET Core类库项目中如何实现读取配置
  9. C#实现杨辉三角的示例
  10. C#使用Free Spire.Presentation实现对PPT