diff --git a/core/src/main/java/com/usthe/sureness/matcher/util/TirePathTree.java b/core/src/main/java/com/usthe/sureness/matcher/util/TirePathTree.java index 12b5c55ce9089c3035a8d95b025c2a69a5c5adc9..0112d653e665766e6367fe74c1a4a8cecb803167 100644 --- a/core/src/main/java/com/usthe/sureness/matcher/util/TirePathTree.java +++ b/core/src/main/java/com/usthe/sureness/matcher/util/TirePathTree.java @@ -4,19 +4,16 @@ package com.usthe.sureness.matcher.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Queue; -import java.util.Set; +import java.util.*; import java.util.regex.Pattern; /** * Improved dictionary matching tree - * support regular * ** + * support regular: str*str, *, ** + * the * in str*str can match zero or more character eg: *.html -> content.html * the * can match zero or one directory * the ** can match zero or more directories - * Match priority: Raw string > * > ** + * Match priority: Raw string > str*str > * > ** * @author tomsun28 * @date 19:25 2019-01-18 */ @@ -34,6 +31,10 @@ public class TirePathTree { private static final int PATH_NODE_NUM_3 = 3; private static final int PATH_NODE_NUM_2 = 2; private static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/+"); + /** + * str - Pattern.compile(str) + */ + private static final Map PATTERN_MAP = new HashMap<>(8); /** * root node @@ -121,12 +122,15 @@ public class TirePathTree { * @return java.lang.String roles eg: [role1,role2] */ public String searchPathFilterRoles(String path) { - if (path == null || "".equals(path) || !path.startsWith(URL_PATH_SPLIT)) { + if (path == null || "".equals(path)) { return null; } if (logger.isTraceEnabled()) { logger.trace("sureness - searchPathFilterRoles, path is {}", path); } + if (!path.startsWith(URL_PATH_SPLIT)) { + path = URL_PATH_SPLIT.concat(path); + } path = PATH_SPLIT_PATTERN.matcher(path).replaceAll("/"); path = path.substring(1).toLowerCase(); String[] tmp = path.split("==="); @@ -156,8 +160,9 @@ public class TirePathTree { return null; } + String currentNodeData = current.getData(); // fast fail - if (isNoMatchString(current.getData(), urlPac[currentFlow])) { + if (isNoMatchString(currentNodeData, urlPac[currentFlow])) { return null; } if (currentFlow == urlPac.length - 1 && (NODE_TYPE_MAY_PATH_END.equals(current.getNodeType()))) { @@ -181,13 +186,13 @@ public class TirePathTree { } String matchRole = null; - if (current.getData().equals(urlPac[currentFlow])) { + if (currentNodeData.equals(urlPac[currentFlow]) || isPatternStr(currentNodeData)) { matchRole = searchPathRoleInChildren(current, urlPac, currentFlow, method); if (matchRole != null) { return matchRole; } } - if (current.getData().equals(MATCH_ONE)) { + if (currentNodeData.equals(MATCH_ONE)) { matchRole = searchPathRoleInChildren(current, urlPac, currentFlow - 1, method); if (matchRole != null) { return matchRole; @@ -197,7 +202,7 @@ public class TirePathTree { return matchRole; } } - if (current.getData().equals(MATCH_ALL)) { + if (currentNodeData.equals(MATCH_ALL)) { matchRole = searchPathRoleInChildren(current, urlPac, currentFlow - 1, method); if (matchRole != null) { return matchRole; @@ -234,6 +239,15 @@ public class TirePathTree { return matchRole; } } + if (current.getPatternChildren() != null) { + for (String patternChild : current.getPatternChildren()) { + Node matchPatternNode = current.getChildren().get(patternChild); + matchRole = searchPathRole(matchPatternNode, urlPac, currentFlow + 1, method); + if (matchRole != null) { + return matchRole; + } + } + } if (current.getChildren().containsKey(MATCH_ONE)) { Node matchOneNode = current.getChildren().get(MATCH_ONE); matchRole = searchPathRole(matchOneNode, urlPac, currentFlow + 1, method); @@ -250,9 +264,9 @@ public class TirePathTree { /** * Determine whether the pattern does not match pathNode - * @param pattern pattern eg: * ** + * @param pattern pattern eg: raw str, *html, *, ** * @param pathNode pathNode - * @return match return true, else false + * @return not match return true, else false */ private boolean isNoMatchString(String pattern, String pathNode) { if (pattern == null && pathNode == null) { @@ -261,8 +275,47 @@ public class TirePathTree { if (pattern == null || pathNode == null) { return true; } - return !(pattern.equals(pathNode) || MATCH_ONE.equals(pattern) - || MATCH_ALL.equals(pattern)); + if (pattern.equals(pathNode) || MATCH_ONE.equals(pattern) + || MATCH_ALL.equals(pattern)) { + return false; + } + if (!isPatternStr(pattern)) { + return true; + } + Pattern matcher = PATTERN_MAP.get(pattern); + if (matcher == null) { + String patternStr = makeJavaPatternStr(pattern); + matcher = Pattern.compile(patternStr); + PATTERN_MAP.put(pattern, matcher); + } + return !matcher.matcher(pathNode).matches(); + } + + /** + * if is a pattern string eg: *html , *.js + * @param str string + * @return yes return true else false + */ + private boolean isPatternStr(String str) { + return str != null + && str.contains(MATCH_ONE) + && !MATCH_ONE.equals(str) + && !MATCH_ALL.equals(str); + } + + /** + * make the java pattern str for sureness rule + * @param pattern pattern + * @return java pattern + */ + private String makeJavaPatternStr(String pattern) { + return pattern + .replace("*", "\\S*") + .replace(".", "\\.") + .replace("?","\\?") + .replace("+", "\\+") + .replace("$", "\\$") + .replace("^","\\^"); } /** @@ -270,12 +323,15 @@ public class TirePathTree { * @param path path = /api/v1/host/detail===GET===[role2,role3,role4] */ private void insertNode(String path, Node rootNode) { - if (path == null || "".equals(path) || !path.startsWith(URL_PATH_SPLIT)) { + if (path == null || "".equals(path)) { return; } if (logger.isTraceEnabled()) { logger.trace("sureness - begin insertNode, path is {}", path); } + if (!path.startsWith(URL_PATH_SPLIT)) { + path = URL_PATH_SPLIT.concat(path); + } path = PATH_SPLIT_PATTERN.matcher(path).replaceAll("/"); // remove the first / path = path.substring(1).toLowerCase(); @@ -292,6 +348,16 @@ public class TirePathTree { for (String urlData : urlPac) { if (!current.getChildren().containsKey(urlData)) { current.insertChild(urlData); + // if urlData is match *xxx, insert patternChildren eg: *html + if (isPatternStr(urlData)) { + Pattern pattern = PATTERN_MAP.get(urlData); + if (pattern == null) { + String patternStr = makeJavaPatternStr(urlData); + pattern = Pattern.compile(patternStr); + PATTERN_MAP.put(urlData, pattern); + } + current.insertPatternChild(urlData); + } } pre = current; current = current.getChildren().get(urlData); @@ -330,6 +396,9 @@ public class TirePathTree { /** children nodes **/ private Map children; + /** pattern children list **/ + private List patternChildren; + private Node(String data, String nodeType) { this.data = data; this.nodeType = nodeType; @@ -345,10 +414,17 @@ public class TirePathTree { this.children.put(data, new Node(data)); } - private void insertChild(String data,String nodeType) { + private void insertChild(String data, String nodeType) { this.children.put(data, new Node(data, nodeType)); } + private void insertPatternChild(String data) { + if (patternChildren == null) { + patternChildren = new LinkedList<>(); + } + this.patternChildren.add(data); + } + private String getNodeType() { return nodeType; } @@ -373,5 +449,8 @@ public class TirePathTree { this.children = children; } + public List getPatternChildren() { + return patternChildren; + } } } diff --git a/core/src/main/resources/sureness-sample.yml b/core/src/main/resources/sureness-sample.yml index a713356bdc4f20ae29208fd24c0aad99090789d0..b8ba82489dcc630986f10ac34391cabf9e195ee8 100644 --- a/core/src/main/resources/sureness-sample.yml +++ b/core/src/main/resources/sureness-sample.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom diff --git a/core/src/test/java/com/usthe/sureness/matcher/util/TirePathTreeTest.java b/core/src/test/java/com/usthe/sureness/matcher/util/TirePathTreeTest.java index 6c37e59c27b4b7fa9207d613f292cd2b2672a013..2dd94ec17122d889445ab1d0df682f6c446570a4 100644 --- a/core/src/test/java/com/usthe/sureness/matcher/util/TirePathTreeTest.java +++ b/core/src/test/java/com/usthe/sureness/matcher/util/TirePathTreeTest.java @@ -44,25 +44,37 @@ public class TirePathTreeTest { paths.add("/api/v3/host===put===[role2,role3,role4]"); paths.add("/api/v2/detail===put===[role2,role3,role4]"); paths.add("/api/v2/mom===put===[role2,role3,role4]"); + // match *xxx eg: *.html + paths.add("/api/v6/host/*.html===get===[role1]"); + // priority: equals normal path > match *xxx + paths.add("/api/v6/book/content.js===get===[role3,role4]"); + paths.add("/api/v6/book/*.js===get===[role2]"); // match * paths.add("/api/*/ha/*===put===[role2,role4]"); - // priority: equals normal path > match * + // priority: equals normal path > match *xxx > match * paths.add("/api/v4/mom/ha===put===[role3,role4]"); + paths.add("/api/*html/mom/ha===put===[role4]"); paths.add("/api/*/mom/ha===put===[role2,role4]"); //match ** paths.add("/api/mi/**===put===[role5]"); paths.add("/api/mo/**/day===get===[role6]"); paths.add("/api/day/**/day/mo===put===[role7]"); - // priority: equals normal path > match * > match ** + // priority: equals normal path > match *xxx > match * > match ** paths.add("/api/v5/day/book===put===[role5]"); paths.add("/api/v5/**===put===[role6]"); + paths.add("/api/demo/book/*app/egg===get===[role3]"); paths.add("/api/demo/book/*/egg===get===[role1]"); paths.add("/api/demo/book/**/egg===get===[role2]"); paths.add("/**===get===[role9]"); paths.add("/er/**/swagger===get===[role10]"); paths.add("/swagger/**===get===[role11]"); + // ignore resource + paths.add("/**/*.html===post===[role8]"); + paths.add("/**/*.css===post===[role8]"); + paths.add("/**/*.js===post===[role8]"); + root.buildTree(paths); - Assert.assertEquals(22, root.getResourceNum()); + Assert.assertEquals(30, root.getResourceNum()); } @Test @@ -78,17 +90,24 @@ public class TirePathTreeTest { Assert.assertEquals("[role3,role4]", root.searchPathFilterRoles("/api/v2/host===put")); Assert.assertEquals("[role2,role3,role4]", root.searchPathFilterRoles("/api/v1/host===put")); Assert.assertEquals("[role2,role3,role4]", root.searchPathFilterRoles("/api/v3/host===put")); + // match *xxx eg: *.html + Assert.assertEquals("[role1]", root.searchPathFilterRoles("/api/v6/host/content.html===get")); + // priority: equals normal path > match *xxx + Assert.assertEquals("[role3,role4]", root.searchPathFilterRoles("/api/v6/book/content.js===get")); + Assert.assertEquals("[role2]", root.searchPathFilterRoles("/api/v6/book/other.js===get")); // match * Assert.assertEquals("[role2,role4]", root.searchPathFilterRoles("/api/v2/ha/host===put")); - // priority: equals normal path > match * + // priority: equals normal path > match *xxx > match * Assert.assertEquals("[role3,role4]", root.searchPathFilterRoles("/api/v4/mom/ha===put")); + Assert.assertEquals("[role4]", root.searchPathFilterRoles("/api/v4html/mom/ha===put")); Assert.assertEquals("[role2,role4]", root.searchPathFilterRoles("/api/v6/mom/ha===put")); // match ** Assert.assertEquals("[role5]", root.searchPathFilterRoles("/api/mi/tom/hello===put")); Assert.assertEquals("[role6]", root.searchPathFilterRoles("/api/mo/tom/hello/day/day===get")); Assert.assertEquals("[role7]", root.searchPathFilterRoles("/api/day/day/day/day/book/day/mo===put")); - // priority: equals normal path > match * > match ** + // priority: equals normal path > match *xxx > match * > match ** Assert.assertEquals("[role5]", root.searchPathFilterRoles("/api/v5/day/book===put")); + Assert.assertEquals("[role3]", root.searchPathFilterRoles("/api/demo/book/tom-app/egg===get")); Assert.assertEquals("[role1]", root.searchPathFilterRoles("/api/demo/book/tom/egg===get")); Assert.assertEquals("[role2]", root.searchPathFilterRoles("/api/demo/book/tom/good/egg===get")); Assert.assertEquals("[role6]", root.searchPathFilterRoles("/api/v5/mom/ha===put")); @@ -97,6 +116,10 @@ public class TirePathTreeTest { Assert.assertEquals("[role10]", root.searchPathFilterRoles("/er/swagger===get")); Assert.assertNull(root.searchPathFilterRoles("/api/v6/book/ha/good===put")); Assert.assertEquals("[role11]", root.searchPathFilterRoles("/swagger===get")); + // ignore resource + Assert.assertEquals("[role8]", root.searchPathFilterRoles("/content-ui.html===post")); + Assert.assertEquals("[role8]", root.searchPathFilterRoles("/api/user/ui.js===post")); + Assert.assertEquals("[role8]", root.searchPathFilterRoles("/node/v2/demo.css===post")); } } \ No newline at end of file diff --git a/docs/cn/path-match.md b/docs/cn/path-match.md index 7c4331ab30fbc08b9dec35f6ef8a2c498c7fe5c8..61c4cbd32a102d113c37cc9e389e1157b7c4cc97 100644 --- a/docs/cn/path-match.md +++ b/docs/cn/path-match.md @@ -2,20 +2,22 @@ 我们配置的资源格式为:`requestUri===httpMethod`, 即请求的路径加上其请求方式(`post,get,put,delete...`)作为一个整体被视作一个资源 `eg: /api/v2/book===get` `get`方式请求`/api/v2/book`接口数据 -这里的`requestUri`支持url路径匹配符匹配: `*`, `**` +这里的`requestUri`支持url路径匹配符匹配: `str*str`, `*`, `**` 通配符 | 描述 - --- | --- + --- | --- +`str*str` | 字符串中的*匹配0个或者多个任意字符 `*` | 匹配0个或1个目录 `**` | 匹配0个或多个目录 样例 | 说明 --- | --- + `*.html` | 可以匹配 `content.html`, `user-ui.html` 等 `/api/*/book` | 可以匹配 `/api/user/book` 或 `/api/book` 等 `/**` | 可以匹配任何路径 `/**/foo` | 可以匹配 `/api/user/book/foo` 等 -匹配优先级: 原始字符串 > `*` > `**` +匹配优先级: 原始字符串 > `str*str` > `*` > `**` 最长路径匹配原则: eg: `requestUri` 为`/app/book/foo`,若存在两个路径匹配模式`/app/**`和`/app/book/*`,则会匹配到`/app/book/*` \ No newline at end of file diff --git a/docs/path-match.md b/docs/path-match.md index 63cfcafde726256b5138d15566da07a560382558..6fbde4273725e410b33ac6f5840de4cdcf85701b 100644 --- a/docs/path-match.md +++ b/docs/path-match.md @@ -2,21 +2,23 @@ We treat restful requests as a resource, resource format like `requestUri===httpMethod`. That is the request uri + request method(`post,get,put,delete...`) is considered as a resource as a whole. `eg: /api/v2/book===get` -The `requestUri` here support url path match: `*`, `**` +The `requestUri` here support url path match: `str*str`, `*`, `**` Wildcard | Describe --- | --- + `str*str` | the * in string match 0 or more characters `*` | Match 0 or 1 directories `**` | Match 0 or more directories Sample | Note --- | --- + `*.html` | can match `content.html`, `user-ui.html` etc `/api/*/book` | can match `/api/user/book` or `/api/book` etc `/**` | can match any path `/**/foo` | can match `/api/user/book/foo` etc -Match priority: Raw string > `*` > `**` +Match priority: Raw string > `*.html` > `*` > `**` Longest path matching principle: eg: when `requestUri` is `/app/book/foo`,If there are two matching patterns - `/app/**` and `/app/book/*`,will match`/app/book/*` \ No newline at end of file diff --git a/sample-bootstrap/src/main/resources/sureness.yml b/sample-bootstrap/src/main/resources/sureness.yml index a713356bdc4f20ae29208fd24c0aad99090789d0..b8ba82489dcc630986f10ac34391cabf9e195ee8 100644 --- a/sample-bootstrap/src/main/resources/sureness.yml +++ b/sample-bootstrap/src/main/resources/sureness.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom diff --git a/sample-tom/src/main/resources/db/data.sql b/sample-tom/src/main/resources/db/data.sql index cd793d0eafa5f1960a2513342773b70f6bf78da8..34eea68430dc44592bc0c4ccaaa55a50cba7a2a0 100644 --- a/sample-tom/src/main/resources/db/data.sql +++ b/sample-tom/src/main/resources/db/data.sql @@ -13,6 +13,9 @@ insert into auth_resource (id, name, code, uri, type, method, status, descriptio insert into auth_resource (id, name, code, uri, type, method, status, description) values (109, 'Delete role', 'DELETE_ROLE', '/role/*', 'role', 'DELETE', 1, null); insert into auth_resource (id, name, code, uri, type, method, status, description) values (110, 'Get role', 'GET_ROLES', '/role/*/*', 'role', 'GET', 1, null); insert into auth_resource (id, name, code, uri, type, method, status, description) values (111, 'User get custom token', 'ACCOUNT_CUSTOM_TOKEN', '/auth/custom/token', 'account', 'POST', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (112, 'Static Resource', 'Static Resource', '/**/*.html', 'static', 'GET', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (113, 'Static Resource', 'Static Resource', '/**/*.js', 'static', 'GET', 9, null); +insert into auth_resource (id, name, code, uri, type, method, status, description) values (114, 'Static Resource', 'Static Resource', '/**/*.css', 'static', 'GET', 9, null); -- ---------------------------- -- Records of auth_role diff --git a/samples/javalin-sureness/src/main/resources/sureness.yml b/samples/javalin-sureness/src/main/resources/sureness.yml index a713356bdc4f20ae29208fd24c0aad99090789d0..b8ba82489dcc630986f10ac34391cabf9e195ee8 100644 --- a/samples/javalin-sureness/src/main/resources/sureness.yml +++ b/samples/javalin-sureness/src/main/resources/sureness.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom diff --git a/samples/ktor-sureness/resources/sureness.yml b/samples/ktor-sureness/resources/sureness.yml index a713356bdc4f20ae29208fd24c0aad99090789d0..b8ba82489dcc630986f10ac34391cabf9e195ee8 100644 --- a/samples/ktor-sureness/resources/sureness.yml +++ b/samples/ktor-sureness/resources/sureness.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom diff --git a/samples/quarkus-sureness/src/main/resources/sureness.yml b/samples/quarkus-sureness/src/main/resources/sureness.yml index a713356bdc4f20ae29208fd24c0aad99090789d0..b8ba82489dcc630986f10ac34391cabf9e195ee8 100644 --- a/samples/quarkus-sureness/src/main/resources/sureness.yml +++ b/samples/quarkus-sureness/src/main/resources/sureness.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom diff --git a/samples/spring-webflux-sureness/src/main/resources/sureness.yml b/samples/spring-webflux-sureness/src/main/resources/sureness.yml index 31374a3c0623e6d60275a9b0532f8349d00c25b4..df10f80166606f3a0ab543e76f7594b6bad31103 100644 --- a/samples/spring-webflux-sureness/src/main/resources/sureness.yml +++ b/samples/spring-webflux-sureness/src/main/resources/sureness.yml @@ -24,6 +24,9 @@ excludedResource: - /api/v3/host===get - /api/v3/book===get - /api/v1/account/auth===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get # account info # there are three account: admin, root, tom