今年是入行的第 5 个年头,首次在掘金发文。
前言
这是一篇适用于 URL 黑白名单场景校验的解决方案分享,该方案适用场景如:(1)App 端 Webview 加载 URL 时的白名单 (2)小程序 SDK 中用于检验小程序请求的目标地址是否满足配置项 (3)其他用于校验 URL 是否满足黑白名单配置范围的场景。本文由前言、思路和代码、测试与用例三部分构成。
笔者提示:本文中的处理代码使用 Kotlin 编写,默认宽处理;且方案和代码都是笔者突发奇想随兴所思所写,仅本地验证未经大规模用例测试,慎原样拷贝食用。
思路和代码
在 URL 黑白名单校验的场景中,数据角色分为:(1)规则串(2)待校验URL串。规则串通常关注 URI 中的协议(Scheme)、域名(Host)、端口(Port)、路径(Path)四部分构成,各部分需支持*通配符。而此处默认待校验URL串已经过标准化处理,即包含Scheme部分是一个合法的 URI。
一个比较直观的思路是将规则串解析为正则表达式处理,但这样解析而来的正则可能过于复杂。若退一步:分别构建各部分的正则串,最后合并校验结果,在逻辑上也会相对更新清晰易理解些。比如:
- Scheme:这部分指 URI 中
://前的串,如http://www.juejin.cn中的http部串 - Host:这部分指 URI 中的域名部分,如
www.juejin.cn这部分的通配支持通常要求有直接单级通配和多级通配,如*.cn可匹配juejin.cn,而**.cn可匹配www.juejin.cn。 - Port:端口部分通常指
:80串,其值通常是0~65535 - Path:这部分指 URI 中的路径部分,如
/root/path/file,该部分的通配通常也有单级和多级的区别,如/*可匹配/root,而/root/path则需规则/**才能满足匹配
Talk is cheap, let me show the code
import java.net.URI
object UriMatcher {
private const val DEBUG = true
private const val EMPTY = ""
private val ANY = ".*".toRegex()
private val STAR = "(?<![.])[*]".toRegex()
/**
* 校验url是否满足rule指定的规则
*
* @param rule 规则串,最终会解析成uri的scheme/host/port/path四个子规则,若留空则默认该匹配任意值
* 规则串中可使用*表示通配,如 http* 可匹配 http 和 https
* 单个*在不能匹配分隔符.|/,成对**则可通配分隔符,
* 如域名部分 *.com 不能匹配 www.baidu.com,但 **.com 可以
* 同理路径部分,如 /r* 不能匹配 /root/path 但 /r** 可以
* @param url 需要是规范的uri格式,形如 scheme://host[:port][/path]
*/
fun match(rule: String, url: String): Boolean {
// 解析URL
val uri = try {
URI.create(url)
} catch (e: RuntimeException) {
null
} ?: return false
if (DEBUG) println("uri: ${uri.scheme} , ${uri.host} , ${uri.port} , ${uri.path}")
// 解析Schema规则
val rules = build(rule) ?: return false
// 校验Scheme
var str = rules[1].replace("://", "").replace("*", ".*")
if (isInvalid(if (str.isBlank()) ANY else str.toRegex(), uri.scheme ?: EMPTY)) {
return false
}
// 校验Host
str = rules[2].replace(".", "[.]").replace("**", ".*")
.replace(STAR, "[^.]*")
if (isInvalid(if (str.isBlank()) ANY else str.toRegex(), uri.host ?: EMPTY)) {
return false
}
// 校验Port
str = rules[3].replace(":", "").replace("*", "\\d*")
if (isInvalid(
if (str.isBlank()) ANY else str.toRegex(), if (uri.port == -1) {
EMPTY
} else {
uri.port.toString()
}
)
) return false
// 校验Path
str = rules[4].replace("**", ".*").replace(STAR, "[^/]*")
if (isInvalid(
if (str.isBlank()) ANY else str.toRegex(), if (uri.path.isNullOrBlank()) {
"/"
} else {
uri.path
}
)
) return false
return true
}
private fun build(rule: String): Array<String>? {
val result =
"([a-z*]+://)?([^/?:]+)?(:[\\d*]{1,5})?((?:/?|/[^/]+)*)?"
.toRegex().matchEntire(rule.ifBlank { "*" })
?: return null
return result.groupValues.toTypedArray()
}
private fun isInvalid(regex: Regex, input: String): Boolean {
val matched = regex.matches(input)
if (DEBUG) println("test($matched): $regex <- $input")
return !matched
}
}
测试与用例
以下为笔者测试的部分用例,如有发现问题欢迎评论区补充。
public class JMain {
public static void main(String[] args) {
UriMatcher matcher = UriMatcher.INSTANCE;
// false
System.out.println(matcher.match("http://juejin.cn/", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://juejin.cn/", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://*.cn/", "https://juejin.cn"));
// false
System.out.println(matcher.match("http*://*.juejin.cn/", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://*juejin.cn/", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://**juejin.cn/", "https://www.juejin.cn"));
// true
System.out.println(matcher.match("http*://juejin.*/", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://juejin.*:*", "https://juejin.cn:8080"));
// true
System.out.println(matcher.match("http*://juejin.*:*", "https://juejin.cn"));
// true
System.out.println(matcher.match("http*://juejin.*/post/*", "https://juejin.cn/post/123"));
// true
System.out.println(matcher.match("http*://juejin.*/**", "https://juejin.cn/post/123"));
}
}