美文网首页java高级开发
基于shiro改造支持restful请求

基于shiro改造支持restful请求

作者: 老鼠AI大米_Java全栈 | 来源:发表于2019-01-21 22:45 被阅读96次

    shiro是一个不错的java安全框架,但对restful接口支持不是很好,可以通过简单的改选,让它支持restful接口。

    首先说明设计的这个安全体系是是RBAC(基于角色的权限访问控制)授权模型,即用户--角色--资源,用户不直接和权限打交道,角色拥有资源,用户拥有这个角色就有权使用角色对应用户的资源。所有这里没有权限一说,签发jwt里面也就只有用户所拥有的角色而没有权限。

    为啥说是真正的restful风格集成,虽说shiro对rest不友好但他本身是有支持rest集成的filter–HttpMethodPermissionFilter,这个shiro rest的 风格拦截器,会自动根据请求方法构建权限字符串( GET=read,POST=create,PUT=update,DELETE=delete)构建权限字符串;eg: /users=rest[user] , 会 自动拼接出user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)。

    但是这样感觉不利于基于jwt的角色的权限控制,在细粒度上验权url(即支持get,post,delete鉴别)就更没法了(个人见解)。打个比方:我们对一个用户签发的jwt写入角色列(role_admin,role_customer)。对不同request请求:url="api/resource/",httpMethod="GET"url="api/resource",httpMethod="POST",在基于角色-资源的授权模型中,这两个url相同的请求对HttpMethodPermissionFilter是一种请求,用户对应的角色拥有的资源url=”api/resource”,只要请求的url是”api/resource”,不论它的请求方式是什么,都会判定通过这个请求,这在restful风格的api中肯定是不可取的,对同一资源有些角色可能只要查询的权限而没有修改增加的权限。

    可能会说在jwt中再增加权限列就好了嘛,但是在基于用户-资源的授权模型中,虽然能判别是不同的请求,但是太麻烦了,对每个资源我们都要设计对应的权限列然后再塞入到jwt中,对每个用户都要单独授权资源这也是不可取的。

    对shiro的改造这里自定义了一些规则:
    shiro过滤器链的url=url+"=="+httpMethod
    eg:对于url="api/resource/",httpMethod="GET"的资源,其拼接出来的过滤器链匹配url=api/resource==GET
    这样对相同的url而不同的访问方式,会判定为不同的资源,即资源不再简单是url,而是url和httpMethod的组合。基于角色的授权模型中,角色所拥有的资源形式为url+"=="+httpMethod
    这里改变了过滤器的过滤匹配url规则,重写PathMatchingFilterChainResolver的getChain方法,增加对上述规则的url的支持。

    分析

    首先先回顾下 Shiro 的过滤器链,一般我们都有如下配置:

    /login.html = anon
    /login = anon
    /users = perms[user:list]
    /** = authc
    

    其中 /users 请求对应到 perms 过滤器,对应的类: org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter,其中的 onAccessDenied 方法是在没有权限时被调用的, 源码如下:

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
    
        Subject subject = getSubject(request, response);
        // 如果未登录, 则重定向到配置的 loginUrl
        if (subject.getPrincipal() == null) {
            saveRequestAndRedirectToLogin(request, response);
        } else {
            // 如果当前用户没有权限, 则跳转到 UnauthorizedUrl
            // 如果没有配置 UnauthorizedUrl, 则返回 401 状态码.
            String unauthorizedUrl = getUnauthorizedUrl();
            if (StringUtils.hasText(unauthorizedUrl)) {
                WebUtils.issueRedirect(request, response, unauthorizedUrl);
            } else {
                WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }
        return false;
    }
    

    我们可以在这里可以判断当前请求是否时 AJAX 请求,如果是,则不跳转到 logoUrl 或 UnauthorizedUrl 页面,而是返回 JSON 数据。

    还有一个方法是 pathsMatch,是将当前请求的 url 与所有配置的 perms 过滤器链进行匹配,是则进行权限检查,不是则接着与下一个过滤器链进行匹配,源码如下:

    protected boolean pathsMatch(String path, ServletRequest request) {
        String requestURI = getPathWithinApplication(request);
        log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, requestURI);
        return pathsMatch(path, requestURI);
    }
    

    方法

    了解完这两个方法,我来说说如何利用这两个方法来实现功能。
    我们可以从配置的过滤器链来入手,原先的配置如:

    /users = perms[user:list]
    

    我们可以改为 /user==GET,/user==POST 方式。== 用来分隔, 后面的部分指 HTTP Method。
    使用这种方式还要注意一个方法,即:org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 中的 getChain 方法,用来获取当前请求的 URL 应该使用的过滤器,源码如下:

    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        // 1. 判断有没有配置过滤器链, 没有一个过滤器都没有则直接返回 null
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }
    
        // 2. 获取当前请求的 URL
        String requestURI = getPathWithinApplication(request);
    
        // 3. 遍历所有的过滤器链
        for (String pathPattern : filterChainManager.getChainNames()) {
    
            // 4. 判断当前请求的 URL 与过滤器链中的 URL 是否匹配.
            if (pathMatches(pathPattern, requestURI)) {
                if (log.isTraceEnabled()) {
                    log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +
                            "Utilizing corresponding filter chain...");
                }
                // 5. 如果路径匹配, 则获取其实现类.(如 perms[user:list] 或 perms[user:delete] 都返回 perms)
                // 具体对  perms[user:list] 或 perms[user:delete] 的判断是在上面讲到的 PermissionsAuthorizationFilter 的 pathsMatch 方法中.
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }
    
        return null;
    }
    

    这里大家需要注意,第四步的判断,我们已经将过滤器链,也就是这里的 pathPattern 改为了 /xxx==GET 这种方式,而请求的 URL 却仅包含 /xxx,那么这里的 pathMatches 方法是肯定无法匹配成功,所以我们需要在第四步判断的时候,只判断前面的 URL 部分。
    整个过程如下:

    在过滤器链上对 restful 请求配置需要的 HTTP Method,如:/user==DELETE
    修改 PathMatchingFilterChainResolver 的 getChain 方法,当前请求的 URL 与过滤器链匹配时,过滤器只取 URL 部分进行判断。
    修改过滤器的 pathsMatch 方法,判断当前请求的 URL 与请求方式是否与过滤器链中配置的一致。
    修改过滤器的 onAccessDenied 方法,当访问被拒绝时,根据普通请求和 AJAX 请求分别返回 HTML 和 JSON 数据。

    实现

    过滤器链添加 http method

    在我的项目中是从数据库获取的过滤器链,所以有如下代码:

    public Map<String, String> getUrlPermsMap() {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/lib/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
    
        List<Menu> menus = selectAll();
        for (Menu menu : menus) {
            String url = menu.getUrl();
            if (!"".equals(menu.getMethod())) {
                url += ("==" + menu.getMethod());
            }
            String perms = "perms[" + menu.getPerms() + "]";
            filterChainDefinitionMap.put(url, perms);
        }
        filterChainDefinitionMap.put("/**", "authc");
        return filterChainDefinitionMap;
    }
    

    如: /xxx==GET = perms[user:list]这里的 getUrl,getMethod 和 getPerms 分别对应 /xxx,GET 和 user:list。
    不过需要注意的是,如果在 XML 里配置,会被 Shiro 解析成 /xxx 和 =GET = perms[user:list],解决办法是使用其他符号代替 ==。

    修改 PathMatchingFilterChainResolver 的 getChain 方法

    由于 Shiro 没有提供相应的接口,且我们不能直接修改源码,所以我们需要新建一个类继承 PathMatchingFilterChainResolver 并重写 getChain 方法,然后替换掉 PathMatchingFilterChainResolver 即可。

    首先继承并重写方法:

    import org.apache.shiro.web.filter.mgt.FilterChainManager;
    import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    
    public class RestPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver {
    
        private static final Logger log = LoggerFactory.getLogger(RestPathMatchingFilterChainResolver.class);
    
        @Override
        public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
            FilterChainManager filterChainManager = getFilterChainManager();
            if (!filterChainManager.hasChains()) {
                return null;
            }
    
            String requestURI = getPathWithinApplication(request);
    
            //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()) {
    
                String[] pathPatternArray = pathPattern.split("==");
    
                // 只用过滤器链的 URL 部分与请求的 URL 进行匹配
                if (pathMatches(pathPatternArray[0], requestURI)) {
                    if (log.isTraceEnabled()) {
                        log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +
                                "Utilizing corresponding filter chain...");
                    }
                    return filterChainManager.proxy(originalChain, pathPattern);
                }
            }
    
            return null;
        }
    }
    

    然后替换掉 PathMatchingFilterChainResolver,它是在 ShiroFilterFactoryBean 的 createInstance 方法里初始化的。


    image.png

    所以同样的套路,继承 ShiroFilterFactoryBean 并重写 createInstance 方法,将 new PathMatchingFilterChainResolver(); 改为 new RestPathMatchingFilterChainResolver(); 即可。

    代码如下:

    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.filter.mgt.FilterChainManager;
    import org.apache.shiro.web.filter.mgt.FilterChainResolver;
    import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
    import org.apache.shiro.web.mgt.WebSecurityManager;
    import org.apache.shiro.web.servlet.AbstractShiroFilter;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.BeanInitializationException;
    
    public class RestShiroFilterFactoryBean extends ShiroFilterFactoryBean {
    
        private static final Logger log = LoggerFactory.getLogger(RestShiroFilterFactoryBean.class);
    
        @Override
        protected AbstractShiroFilter createInstance() {
    
            log.debug("Creating Shiro Filter instance.");
    
            SecurityManager securityManager = getSecurityManager();
            if (securityManager == null) {
                String msg = "SecurityManager property must be set.";
                throw new BeanInitializationException(msg);
            }
    
            if (!(securityManager instanceof WebSecurityManager)) {
                String msg = "The security manager does not implement the WebSecurityManager interface.";
                throw new BeanInitializationException(msg);
            }
    
            FilterChainManager manager = createFilterChainManager();
    
            //Expose the constructed FilterChainManager by first wrapping it in a
            // FilterChainResolver implementation. The AbstractShiroFilter implementations
            // do not know about FilterChainManagers - only resolvers:
            PathMatchingFilterChainResolver chainResolver = new RestPathMatchingFilterChainResolver();
            chainResolver.setFilterChainManager(manager);
    
            //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
            //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
            //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
            //injection of the SecurityManager and FilterChainResolver:
            return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
        }
    
        private static final class SpringShiroFilter extends AbstractShiroFilter {
            protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
                super();
                if (webSecurityManager == null) {
                    throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
                }
                setSecurityManager(webSecurityManager);
                if (resolver != null) {
                    setFilterChainResolver(resolver);
                }
            }
        }
    }
    

    最后记得将 ShiroFilterFactoryBean 改为 RestShiroFilterFactoryBean

    XML 方式:

    <bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean">
        <!-- 参数配置略 -->
    </bean>
    

    Bean 方式:

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean();
        // 参数配置略
        return shiroFilterFactoryBean;
    }
    

    修改过滤器的 pathsMatch 方法

    同样新建一个类继承原有的 PermissionsAuthorizationFilter 并重写 pathsMatch 方法:

    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.util.StringUtils;
    import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
    import org.apache.shiro.web.util.WebUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 修改后的 perms 过滤器, 添加对 AJAX 请求的支持.
     */
    public class RestAuthorizationFilter extends PermissionsAuthorizationFilter {
    
        private static final Logger log = LoggerFactory
                .getLogger(RestAuthorizationFilter.class);
    
        @Override
        protected boolean pathsMatch(String path, ServletRequest request) {
            String requestURI = this.getPathWithinApplication(request);
    
            String[] strings = path.split("==");
    
            if (strings.length <= 1) {
                // 普通的 URL, 正常处理
                return this.pathsMatch(strings[0], requestURI);
            } else {
                // 获取当前请求的 http method.
                String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase();
    
                // 匹配当前请求的 http method 与 过滤器链中的的是否一致
                return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI);
            }
        }
    }
    

    修改过滤器的 onAccessDenied 方法

    同样是上一步的类,重写 onAccessDenied 方法即可:

    /**
     * 当没有权限被拦截时:
     *          如果是 AJAX 请求, 则返回 JSON 数据.
     *          如果是普通请求, 则跳转到配置 UnauthorizedUrl 页面.
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        Subject subject = getSubject(request, response);
        // 如果未登录
        if (subject.getPrincipal() == null) {
            // AJAX 请求返回 JSON
            if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) {
                if (log.isDebugEnabled()) {
                    log.debug("用户: [{}] 请求 restful url : {}, 未登录被拦截.", subject.getPrincipal(), this.getPathWithinApplication(request));                }
                Map<String, Object> map = new HashMap<>();
                map.put("code", -1);
                im.zhaojun.util.WebUtils.writeJson(map, response);
            } else {
                // 其他请求跳转到登陆页面
                saveRequestAndRedirectToLogin(request, response);
            }
        } else {
            // 如果已登陆, 但没有权限
            // 对于 AJAX 请求返回 JSON
            if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) {
                if (log.isDebugEnabled()) {
                    log.debug("用户: [{}] 请求 restful url : {}, 无权限被拦截.", subject.getPrincipal(), this.getPathWithinApplication(request));
                }
    
                Map<String, Object> map = new HashMap<>();
                map.put("code", -2);
                map.put("msg", "没有权限啊!");
                im.zhaojun.util.WebUtils.writeJson(map, response);
            } else {
                // 对于普通请求, 跳转到配置的 UnauthorizedUrl 页面.
                // 如果未设置 UnauthorizedUrl, 则返回 401 状态码
                String unauthorizedUrl = getUnauthorizedUrl();
                if (StringUtils.hasText(unauthorizedUrl)) {
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);
                } else {
                    WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
                }
            }
    
        }
        return false;
    }
    

    重写完 pathsMatch 和 onAccessDenied 方法后,将这个类替换原有的 perms 过滤器的类:

    XML 方式:

    <bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean">
        <!-- 参数配置略 -->
        <property name="filters">
            <map>
                <entry key="perms" value-ref="restAuthorizationFilter"/>
            </map>
        </property>
    </bean>
    
    <bean id="restAuthorizationFilter" class="im.zhaojun.shiro.filter.RestAuthorizationFilter"/>
    

    Bean 方式:

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean();
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("perms", new RestAuthorizationFilter());
    
        // 其他配置略
        return shiroFilterFactoryBean;
    }
    

    这里只改了 perms 过滤器,对于其他过滤器也是同样的道理,重写过滤器的 pathsMatch 和 onAccessDenied 方法,并覆盖原有过滤器即可。

    相关文章

      网友评论

        本文标题:基于shiro改造支持restful请求

        本文链接:https://www.haomeiwen.com/subject/ixtkjqtx.html