作为一名web应用开发人员,我们几乎天天都在和URL链接打交道,本文来盘点一下那些和URL相关的知识点
1.一个完整URL会包含哪些内容
虽然URL明面上看就是一串简单的字符串,但是它的结构可不简单,举个例子:
http://localhost:8080/path1/path2/123.html?a=1&b=2#title这个链接可以拆分成以下结构:
下面我们我们使用 JavaScript new 一个URL对象验证一下:
属性剖析:
-
-
协议(protocol):
最常见的协议就是
http协议了,此外还有https,值得注意的是,该属性包含了后面的冒号
-
-
-
主机名(host):
主机名一般是一个ip地址或者一个域名,协议和主机名之间通过
//来连接起来,如http://www.baidu.com
-
-
-
端口号(port):
端口号是一个0到65535之间的整数,是由web服务器来指定的,http协议的默认端口号是80,https协议的默认端口号是443,还有一些常用的端口号如8080、9080、3000等,主机名和端口号之间通过
:来连接起来,如http://www.baidu.com:80,由于默认端口的存在,http://www.baidu.com等同于http://www.baidu.com:80,https://www.baidu.com等同于https://www.baidu.com:443
-
-
-
资源路径(pathname):
资源路径是web服务器指定的一个页面访问路径
-
-
-
查询参数(search):
查询参数是一段?开始的字符串,问号后面是一连串键值对,键值对的属性和属性值之间用等号连接,多个键值对之间使用&符号连接,即以下形式:
?查询属性1=属性值1&查询属性2=属性值2,在一个URL中,查询参数是可选的
-
-
-
锚点(hash):
锚点是一段以#开始的字符串,从#开始到整个URL字符串结束都属于锚点的值,因此查询参数必须放在锚点前面
注:锚点是前端特有的属性,后端程序一般不会处理锚点值
-
-
-
源(origin):
源包括协议、主机名、端口号以及它们之间的连接符,浏览器的同源策略中的同源指的就是两个URL的源是一样的。
-
URL中的特殊字符和编码
URL中有部分字符有特殊含义,如果有需要将这些字符加入URL中时需要将其进行编码,前端中URL编码相关的方法有
encodeURI和encodeURIComponent,它们对应的解码方法分别是decodeURI和decodeURComponent,encodeURI一般用于编码完整的 URI;encodeURIComponent操作的是组成 URI 的某些片段,如查询参数、锚点、或它们之中的一小段。
特别地,我们在URL编解码时需要注意以下字符
-
-
+,编码值为 %2B+号用于查询参数中时,后端会将+解析为空格,而前端不会受影响,所以当前端需要向后端传递+号时,需要将+号替换为%2B
-
-
-
空格,编码值是 %20
URL中的空格可以用
+号表示
-
-
-
%,编码值是 %25URL解码函数对单独出现的%进行解析时会导致程序出现异常,因此无论是前端还是后端,在使用相关函数时注意捕获异常,避免程序发生无法预知的错误。
-
-
-
中文
如图:实例化后URL链接中的中文全部被编码了
如果想要拿到原始的中文字符,只能先进行解码
-
HTML页面中的URL
从一篇文章讲起
还是举个例子,有一篇博客文章内容的访问网址为:http://localhost:8080/blog/article.html?articleId=1000&title=URL链接那些事
这个页面一开始啥也没有,只有一个架子,代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文章详情</title>
</head>
<body>
<!-- 文章标题 -->
<h1 id="title"></h1>
<!-- 文章内容 -->
<div id="main"></div>
</body>
</html>
进入页面后我们需要通过查询参数中的文章ID和标题渲染页面内容,我们可以通过location这个全局对象拿到当前网页中URL相关参数
我们先在body标签结束之前使用script标签引入一段js,然后在js文件中编写后续的功能
<script src="./article.js"></script>
script的src属性表示的是这个js文件的URL路径,它以./开头的,表示相对路径,浏览器在请求这个js文件时会将它解析为http://localhost:8080/blog/article.js,类似windows文件系统中的相对路径,此外我们也可以使用../回溯到上一层路径(如果有的话)。
查询参数
先通过以下代码拿到查询参数:
function getUrlParams() {
let search = location.search.substring(1)
let paramArr = search.split("&")
let paramObj = {}
paramArr.forEach(str => {
let arr = str.split("=")
paramObj[arr[0]] = decodeURIComponent(arr[1])
})
return paramObj
}
let urlParams = getUrlParams()
console.log("urlParams", urlParams)
拿到查询参数后我们就可以请求文章内容和渲染文章标题了
document.getElementById("title").innerHTML = urlParams.title
fetch("/api/getArticleById?id=" + urlParams.articleId).then(async res => {
let data = await res.json()
document.getElementById("main").innerHTML = data.content
})
注意看我们在请求接口时写的是
/api/getArticleById?id=1000,它以/开头,表示的是绝对路径,但是浏览器会自动帮我们拼上http://localhost:8080这段基础路径,这部分就是文章开头提到的 源(origin) ,这个基础路径可以类比成windows系统中的盘符,但是在HTML中它是可以被改变的,我们可以使用base标签来更改基础路径,但是加上这个标签后就没法使用相对路径了,因此这个标签的使用频率非常低,这里就不展开描述了。
XSS漏洞
上面的4行代码中存在2个XSS安全漏洞 第一处:反射型XSS
document.getElementById("title").innerHTML = urlParams.title
攻击者可以构造http://localhost:8080/blog/article.html?articleId=1000&title=%3Cscript%3Ealert()%3C/script%3E这样的路径,再配合短链,钓鱼等方式引诱受害者点击链接即可达到攻击目的。但是现代浏览器也做了此类漏洞的防护功能,需要复现以下效果还需修改一下浏览器的安全策略或者使用旧版的浏览器。
从代码层面,这个漏洞修复方法就是把
innerHTML换成innerText,这样攻击者注入的恶意代码只会当成文本显示而不会被执行
第二处:存储型XSS
document.getElementById("main").innerHTML = data.content
问题还是出现在innerHTML,博客网站的文章内容大都是作者通过富文本编辑器写作后将渲染结果中的html标签存储到数据库中,而这些标签在二次渲染过程中由于无法区分哪些是富文本自动生成的,哪些是用户输入的,只能全都一股脑全部解析执行,这就给了攻击者可乘之机,攻击者很容易就可以在正常的文章中夹带一些恶意代码,而用户在浏览这些文章时也是完全无法察觉的。这个漏洞目前浏览器也还没有成熟的防护机制。
从代码层面,innerHTML是没法替换掉的,因为替换掉的话文章就没法正常显示了。目前前端防止该类攻击可以使用黑名单过滤的方式,通过字符串匹配过滤掉一些可能会执行js代码的字符,也可以使用白名单的方式,通过代码解析,移除掉白名单之外的标签或标签属性。
再说锚点
前面说过,锚点是前端特有的属性,在早期,锚点主要起到页面滚动定位的作用,当锚点值初始化或改变时,浏览器会扫描html文档中有没有哪些标签节点的id或name属性与锚点值一致,如果存在,浏览器会将该节点滚动到可视区域,在页脚高度允许的情况下,匹配节点会滚动到可视区域最上面。 例如上面我们渲染完文章内容后想定位到第2段,我们观察到第二段的p标签的id属性为section2,我们就可以这样写代码实现定位功能。
fetch("/api/getArticleById?id=" + urlParams.articleId).then(async res => {
let data = await res.json()
document.getElementById("main").innerHTML = data.content
+ setTimeout(() => {
+ location.href = "#section2"
+ })
})
整个URL中,除了锚点变化不会导致页面重新载入,其他任意部分改变都会导致页面重载,这个特性后面诞生了单页面应用(SPA),因为页面视图切换体验比传统的多页面应用(MPA)要好得多,也是得到了广泛的应用。
end^_^