一个web项目中大都会有权限管理这个功能。这个功能从细粒度上讲,会最少牵涉到五张表:用户表、用户-角色表、角色表、角色-权限表、权限表。权限表中会管理项目中所有的权限。

今天(2018-05-29)发现项目中有些用户应该有某些权限但是没有,于是就在页面上操作一下、记录请求路径、添加路径到权限表中、添加数据到角色-权限表中。

忽然想到,应该写一个程序来检查一下程序中的哪些请求路径不在权限表中,于是就有了这篇博文。

第一步 获取项目中所有路径

SpringMVC中提供了注解(@ReqeustMapping)方式来提供Http请求,要实现这个功能,可以使用至少如下两种技术:

  • 遍历项目*Controller文件,使用Java反射解析并读取出请求路径
  • 使用Spring提供的功能

前者实现起来比较麻烦(但思路很清晰),后者就比较方便了。

可能有人会注意到SpringMVC项目在启动时会打印很多项目中的路径,附上一图 spring启动打印请求路径

我们要用的就是org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,使用JUnit的测试代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath*:applicationContext*.xml", "classpath*:springMVC-servlet.xml"})
public class SpringBaseTest {
    protected static final Logger LOG = Logger.getLogger(SpringBaseTest.class);

    @Autowired
    private RequestMappingHandlerMapping r;

    @Test
    public void testShowMapping() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = r.getHandlerMethods();
        Set<Map.Entry<RequestMappingInfo, HandlerMethod>> entrySet = handlerMethods.entrySet();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entrySet) {
            RequestMappingInfo key = entry.getKey();
             Set<String> patterns = key.getPatternsCondition().getPatterns();
            LOG.info(patterns);
        }
    }
}

上面程序就会打印出项目中所有的请求路径。

第二步 获取项目中的所有路径

这个就比较简单了,把系统中的权限表全部打印即可。

检查匹配路径,找出未添加在数据库中的路径

遍历项目中的所有请求路径,挨个和数据中的路径匹配,匹配不了则打印出该路径。思路并不复杂,代码如下:

 public void testMatchRequestMapping() throws Exception {
        // 存储项目中所有请求路径
        Set<String> allMappingsInProject = new HashSet<>(500);
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = r.getHandlerMethods();
        Set<Map.Entry<RequestMappingInfo, HandlerMethod>> entrySet = handlerMethods.entrySet();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entrySet) {
            RequestMappingInfo key = entry.getKey();
            HandlerMethod value = entry.getValue();
            Set<String> patterns = key.getPatternsCondition().getPatterns();
            allMappingsInProject.addAll(patterns);
        }
        // 获取数据库中的所有路径
        Set<String> funcpathList = getFuncpathList();

        // 循环匹配路径
        for (String allMappingInProject : allMappingsInProject) {
            boolean flag = false;
            // 使用startsWith的原因是因为项目中有些路径中包含变量,如/user/getByID/1,
            // 如果没有这种情况,就可以直接使用funcpathList.contains()来判断
            for (String funcpath : funcpathList) {
                if (allMappingInProject.startsWith(funcpath)) {
                    flag = true;
                    break;
                }
            }
            // 匹配不上就打印出来
            if (!flag) {
                System.out.println(allMappingInProject);
            }
        }
    }

至此,程序大致完成了。但是项目中一些通用路径如省市区接口是不需要打印出来的,而这里却打印出来了,可以在boolean flag = false;这一行前添加一个判断,如果该路径是被排除,就直接跳过,代码如下:

if (isExclude(allMappingInProject)) {
    continue;
}

扩展步骤 与集成Shiro的处理

而我们的项目中使用了Shiro及其对应的配置文件,其中的filterChainDefinitions就是配置了很多匿名请求路径。这样的话,还是写个程序处理比较方便。配置文件如下:

Shiro配置filterChainDefinitions

我们的目的是把filterChainDefinitions的配置读取出来,并把key-value对应的value值为anon的路径读出来添加到isExclude()方法里。简单的做法就是字符串拆分、再根据value值把key添加到一个Set集合里,如果请求路径符合该Set集合里的任何一个值,就跳过此请求。

而我却走了一条复杂的路才实现它。 首先我又去了解一下Shrio的非Web层用法。

(花了两个小时跟踪代码中……)

查看Shrio的配置类org.apache.shiro.spring.web.ShiroFilterFactoryBean会发现其存储filterChainDefinitions的对象是DefaultFilterChainManager,我的操作是使用Dom4j读取Shiro的filterChainDefinitions数据,再把它放到 DefaultFilterChainManager中(参考了ShiroFilterFactoryBean的源码),代码如下:

public static DefaultFilterChainManager getShiroChainDefinition() throws IOException, DocumentException {
    String filterChainDefinitions = null;
    ClassPathResource shiroResource = new ClassPathResource("/spring/applicationContext-shiro.xml");
    SAXReader reader = new SAXReader();
    Document doc = reader.read(shiroResource.getFile());
    Element rootElement = doc.getRootElement();
    List elements = rootElement.elements();
    for (Object elementObj : elements) {
        Element element = (Element) elementObj;
        String id = element.attribute("id").getValue();
        if ("shiroFilter".equals(id)) {
            List propertyList = element.elements("property");
            for (Object propertyObj : propertyList) {
                Element propertyElement = (Element) propertyObj;
                String propertyName = propertyElement.attribute("name").getValue();
                if ("filterChainDefinitions".equals(propertyName)) {
                    filterChainDefinitions = propertyElement.getStringValue();
                }
            }
        }
    }

    Ini ini = new Ini();
    ini.load(filterChainDefinitions);
    Ini.Section filterChainDefinitionSection = ini.getSections().iterator().next();

    DefaultFilterChainManager filterChainManager = new DefaultFilterChainManager();
    Set<Map.Entry<String, String>> entries = filterChainDefinitionSection.entrySet();
    for (Map.Entry<String, String> entry : entries) {
        // chainName即是访问路径
        filterChainManager.addToChain(entry.getKey(), entry.getValue());
    }

    return filterChainManager;
}

到此isExclude()方法也将完成。

/**
    * 排除掉shiro中的anon
    */
private boolean isExclude(String url) throws IOException, DocumentException {
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    DefaultFilterChainManager shiroChainDefinition = getShiroChainDefinition();
    Map<String, NamedFilterList> filterChains = shiroChainDefinition.getFilterChains();
    Set<Map.Entry<String, NamedFilterList>> entries = filterChains.entrySet();
    for (Map.Entry<String, NamedFilterList> entry : entries) {
        if (entry.getValue().get(0) instanceof AnonymousFilter) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                return true;
            }
        }
    }

    return false;
}

说明

  • 如上代码中使用Dom4j读取/spring/applicationContext-shiro.xml文件的操作甚是恶心。 我一直想使用xpath(如rootElement.selectNodes("/beans/bean[@id='shiroFilter']"))来获取shiroFilter节点,但一直没成,索性使用最简单的方法。
  • 恶心的还有org.apache.shiro.spring.web.ShiroFilterFactoryBean,该Bean在Spring中获取到的类型是org.apache.shiro.spring.web.ShiroFilterFactoryBean.SpringShiroFilter类型的,无法在ShiroFilterFactoryBean的类外使用Spring的ApplicationContext中获取到Shiro并把它强制转换成ShiroFilterFactoryBean.SpringShiroFilter类型,其实这不是恶心,是严谨。