一个web项目中大都会有权限管理这个功能。这个功能从细粒度上讲,会最少牵涉到五张表:用户表、用户-角色表、角色表、角色-权限表、权限表。权限表中会管理项目中所有的权限。
今天(2018-05-29)发现项目中有些用户应该有某些权限但是没有,于是就在页面上操作一下、记录请求路径、添加路径到权限表中、添加数据到角色-权限表中。
忽然想到,应该写一个程序来检查一下程序中的哪些请求路径不在权限表中,于是就有了这篇博文。
第一步 获取项目中所有路径
SpringMVC中提供了注解(@ReqeustMapping)方式来提供Http请求,要实现这个功能,可以使用至少如下两种技术:
- 遍历项目*Controller文件,使用Java反射解析并读取出请求路径
- 使用Spring提供的功能
前者实现起来比较麻烦(但思路很清晰),后者就比较方便了。
可能有人会注意到SpringMVC项目在启动时会打印很多项目中的路径,附上一图
我们要用的就是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就是配置了很多匿名请求路径。这样的话,还是写个程序处理比较方便。配置文件如下:
我们的目的是把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
类型,其实这不是恶心,是严谨。