认证方式
常见的认证方式可分为4种:
- BASIC 认证(基本认证)
- DIGEST 认证(摘要认证)
- SSL 客户端认证
- FormBase 认证(基于表单认证)
BASIC 认证
BASIC 认证(基本认证)是从 HTTP/1.0 就定义的认证方式。
BASIC 认证虽然采用 Base64 编码方式,但这不是加密处理,因此安全性不高,而且没有注销操作的设计,因此并不常用。
步骤
- 客户端发送请求(未带认证信息);
- 服务器返回 401,告知需要验证。响应头部举例:
WWW-Authenticate: Basic realm="Input Your ID and Password."
- 客户端发送的字符串内容是由用户 ID 和密码构成,两者中间以冒号(:)连接后,再经过 Base64 编码处理。 请求头部举例:
Authorization: Basic Z3Vlc3Q6Z3Vlc3Q=
当用户代理为浏览器时,用户仅需输入用户 ID 和密码即可,浏览器会自动完成到 Base64 编码的转换工作。 - 服务器读取 Authorization 字段,验证通过返回 200,否则重复②。
示意图如下:(来自MDN)
实现
// 基于 express 实现 BASIC 认证
// 学习 express :https://github.com/expressjs/express/
// 将代码复制进一个 express 项目的路由部分,跑起来,用浏览器访问即可体验。
// 用户名:Yee,密码:123456
router.get('/more/login', function (req, res) {
const auth = req.headers.authorization
if (auth) {
const [type, credentials] = auth.split(' ')
const [username, password] = atob(credentials).split(':')
if (type === 'Basic' && username === 'Yee' && password === '123456') {
res.end('Authorization Succeeded')
return
}
}
res.setHeader('WWW-Authenticate', 'Basic realm="Input Your ID and Password."')
res.status(401)
res.end('Authorization Required')
})
DIGEST 认证
为弥补 BASIC 认证存在的弱点,从 HTTP/1.1 起就有了 DIGEST 认证。
DIGEST 认证同样使用质询 / 响应的方式(challenge/response),但不会像 BASIC 认证那样直接发送明文密码。 过程类似于 BASIC 认证,示意图参考上面的。
虽然相比 BASIC 认证安全性有所提高,但还是没有注销操作的设计,也无法验证防止用户伪装,因此使用有限。
步骤
- 客户端发送请求(未带认证信息);
- 服务器返回 401,告知需要验证,响应头部举例:
WWW-Authenticate: Digest realm="DIGEST", nonce="MOSQZ0itBAA=44abb6784cc9cbfc605a5b0893d36f23de 95fcff", algorithm=MD5, qop="auth"
首部字段 WWW-Authenticate 内必须包含 realm 和 nonce 这两个字段的信息,其他均为可选参数。客户端就是依靠向服务器回送这两个 值进行认证的。 - 客户端回传②中的所有信息,并加上 username、url、response 字段,response 就是经过用户名密码等信息加密而已的字符串;请求头部举例:
Authorization: Digest username="guest", realm="DIGEST", nonce="MOSQZ0itBAA=44abb6784cc9cbfc605a5b0893d36f23de95f cff", uri="/digest/", algorithm=MD5, response="df56389ba3f7c52e9d7551115d67472f", qop=auth, nc=00000001, cnonce="082c875dcb2ca740"
- 服务器使用相同的加密方法得到一个字符串,将它与 response 比较,相同就通过返回 200,否则重复②。
SSL 客户端认证
借由 HTTPS 的客户端证书完成认证的方式。凭借客户端证书认证,服务器可确认访问是否来自已登录的客户端。具体过程会在 HTTPS 篇中讲到。
在多数情况下,SSL 客户端认证不会仅依靠证书完成认证,一般会和基于表单认证组合形成一种双因素认证(Two-factor authentication)来使用。 第一个认证因素的 SSL 客户端证书用来认证客户端计算机,另一个认证因素的密码则用来确定这是用户本人的行为。。
基于表单的认证
由于使用上的便利性及安全性问题,HTTP 协议标准提供的 BASIC 认证和 DIGEST 认证几乎不怎么使用。因此实际 Web 开发中,还是由开发者自行实现一套认证机制,安全性高低自然由开发者自己控制。
虽然基于表单的认证方式并不是在 HTTP 协议中定义,是由 Web 应用程序各自实现,但业内还是产生了一些通用解决方案,比如 session-cookie 机制(这部分将在“会话跟踪技术”中详讲)。
会话跟踪技术
cookie
cookie 实际上是服务器保存在客户端上的一小段的文本信息。以键值对的形式保存,并由客户端维护其有效期。服务器通过响应报文头 set-cookie 进行设置(种)。当客户端再次请求该源时,会在请求报文头里将有效的 cookie 提交给服务器。
cookie 遵循同源策略。cookie 这种保存并自动回传一定数据的特性,使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
关于什么是同源策略,可以参考本人另一篇文章:
传送门
响应头 set-cookie
形如:
set-cookie: <key>=<value>; Expires=<date>; Secure; HttpOnly
key 为属性名,value 为值,一个 set-cookie 设置一个 key,如需设置多个 key,只需要同时返回多个 set-cookie,例子中 Expires、Secure、HttpOnly 为可选值。所有可选属性如下:
属性名 | 说明文字 |
---|---|
Expires | 超时时间点,默认是 Session,即关闭浏览器时失效。(注1) |
Max-Age | 失效前的秒数。优先级高于 Expires。 |
Domain | 可以使用这个 cookie 的域,二级域名可指定为一级,一级只能指定为一级。 |
Path | 可以使用这个 cookie 的路径,默认为文档所在的文件目录,父级路径可用于所有子级(即设置为根路径“/”就可用于该域名的所有路径)。 |
Secure | 限制该 cookie 只通过 HTTPS 传递。 |
HttpOnly | 限制该 cookie 只由服务器读写,不能被 js 获取到。 |
SameSite | Chrome51 开始支持,可对跨域的 cookie 进行限制。优先级高于 CORS 的设置,不论是 src、form 发起的请求,还是 ajax 请求。(注2) |
注1:现代浏览器多开选项卡和窗口都不会影响 Session 周期,即超时时间设置为 Session 的 cookie 会一直有效,除非彻底退出浏览器应用。 (即使关掉所有窗口,但应用还在后台也不影响)
注2:SameSite可以指定为Strict、Lax、None三个值,默认为Lax:
- Strict 为最严格模式,将会完全禁止第三方 cookie,即不可能在跨域的情况下携带 cookie,不论 Access-Control-Allow-Credentials 是否设置;
- Lax 只允许链接、预加载、GET 表单三种情况发送 cookie;
- None 为无限制,但必须启用 Secure 属性,即只在 HTTPS 的环境下才发送 cookie。
注3:哪些情况下cookie会被认为是第三方的?
源(发起请求页面的域名) | cookie 域 | 是否被认为第三方 |
---|---|---|
publisher.com | publisher.com | 否 |
publisher.com | demo.com | 是 |
demo.com(通过publisher.com页面的iframe加载) | demo.com | 是 |
请求头 cookie
形如:
cookie: <key>=<value>; <key>=<value>...
key 为属性名,value 为值,会一次返回该源下的所有有效 key,以分号为分割。这个结果与在浏览器执行 document.cookie 获取到的值相同(无HttpOnly的时)。
session
session 是服务器记录客户状态的机制。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。客户端再次访问时只需要从该 session 中查找该用户的状态。
session-cookie 机制
一般 session 与 cookie 配合使用,构成会话跟踪技术,即 session-cookie 机制。
服务器生成 session 的 id(图上叫 sessionid)后,就将它通过 set-cookie 传递到客户端,客户端保存这个 sessionid,下次请求通过 cookie 回传到服务器,服务器即可通过 sessionid 查询到用户的 session,进而获得用户状态。
示意图:
如何理解“登录状态过期”?
我们日常使用中经常会遇到登录状态过期的情况,这一般就是 session-cookie 机制引起的。它可分为三种情况的原因:
- 保存在客户端的 cookie 过期或被移除了;
- 另一种是保存在服务器的 session 过期或被移除;
- 两边的登录信息不一致。
连接管理模型
在 HTTP/1.x 里有多种模型:短连接,长连接,和 HTTP 管线化(pipelining)。
HTTP 管线化实用性不高,并没有浏览器支持,已经被 HTTP/2.0 的多路复用特性所取代,这里就不详细讨论了。
连接的长与短只是相对的概念,并没有严格意义上规定多久才算长。讨论 HTTP 的长连接与短连接,本质是讨论 TCP 连接的复用情况。
连接模型由请求报文头和响应报文头里的 Connection 字段决定,值为 close 则为短连接,值为 Keep-Alive 则为长连接(这里与 TCP 里的 keepalive 是不同的概念)。HTTP/1.0 默认是短连接,HTTP/1.1 默认是长连接。只有服务器与客户端协商一直才会进行长连接,并且双方在任意时刻都可以关闭,彼此都不受影响。
扩展阅读:
TCP 的 KeepAlive 机制意图在于保活、心跳,检测连接错误。当一个 TCP 连接两端长时间没有数据传输时(通常默认配置是2小时),发送 keepalive 探针,探测链接是否存活。
短连接(Multiple Connection)
也叫多重连接,客户端和服务器之间进行 HTTP 操作,每次建立一次 TCP 连接,结束就断开连接,下个请求要重新建立 TCP 连接。
基础篇(传送门)中讲到HTTP协议在 1.0 版本之前的一个特点是“无连接”,这个也就是短连接,只是在后续版本种变成了可选项。
示意图:
长连接(Persistent Connection)
也叫持续连接,客户端和服务器之间进行 HTTP 操作,只建立一次 TCP 连接,多次资源请求都复用该 TCP 连接,完成后再关闭。
当请求报文头有 Connection: Keep-Alive 就会告知服务器客户端支持长连接,服务器如果也支持长连接,则会返回带有 Keep-Alive 字段的响应报文头。
下面例子中,服务器告知客户端,连接超时时间为 10 秒,也就是 10 秒内必须有下一个请求,否则 TCP 就会关闭;然后最多维持 500 秒,也就是说 500 秒后会强制关闭 TCP。
Connection: Keep-Alive
=> 服务器也支持长连接 =>
Keep-Alive: timeout=10, max=500
Connection: Keep-Alive
示意图:
缺陷及变通手段
长连接虽然增加了 TCP 连接的复用率,但实质上还是基于请求/响应模式的,并不能实现及时更新、或是服务器主动向客户端发送信息。于是就有了“ajax 轮询”、“ajax 长轮询(Long Poll)”这两种变通手段。
- ajax轮询:原理很简单,就是每隔一段时间就发起一次请求,已到达及时更新的目的。
- ajax长轮询:服务器接收到请求后如果没有更新内容就不响应,客户端也不做超时处理一直处于等待状态,一直等到有更新内容,服务器才响应,客户端接收到响应后又再发起请求,如此往复。
这两种变通手段都是以消耗大量服务器资源为代价,因此为了根本解决这个问题,后来发展出了 WebSocket 协议,这就是后话了。
与 HTTP 协作的 Web 服务
Web 代理(Web Proxy)
代理扮演的是“中间人”角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送 HTTP 报文。代理可能不止一层。
HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive);转发时,需要附加 Via 首部字段以标记出经过的主机信息;同时向服务器发送请求,并将收到的响应转发给客户端。
示意图:
作用
利用缓存技术减少网络带宽的流量,组织内部针对特定网站的访问控制,以获取访问日志为主要目的,等等。
缓存代理
最广泛的应用就是 CDN 网络。代理转发响应时,会预先将资源的副本(缓存)保存在代理服务器上。当代理再次接收到对相同资源的请求时,就可以不从源服务器那里获取资源,而是将之前缓存的资源作为响应返回。反之, 则叫“非缓存代理”。
透明代理
转发请求或响应时,不对报文做任何加工的代理类型被称为透明代理。反之,对报文内容进行加工的代理被称为“非透明代理”。
网关(Gateway)
网关的工作机制和代理十分相似。 他们的区别:代理是转发相同协议的数据,网关是转换不同协议的数据。
其实大部分的 API 服务器都可以视为网关,因为它需要连接 redis,mysql 等其他服务器以满足业务需求。
示意图:
隧道(Tunnel)
隧道的目的是确保客户端能与服务器进行安全的通信,但它对于双方都是透明的。隧道本身不会去解析 HTTP 请求。也就是说,请求保持原样(可能中间有转码和解码的过程)中转给之后的服务器。隧道会在通信双方断开连接时结束。
HTTPS 算是最常见的应用,我们为了访问外网使用的“那种工具”,部分也属于这类。
示意图:
内容协商机制
指客户端和服务器就响应的资源内容进行交涉,然后提供给客户端最为合适的资源。内容协商会以响应资源的语言,字符集,编码方式等作为判断的基准。HTTP 中主要以“服务器驱动”的协商方式进行。
协商方式及优缺点
客户端驱动
客户端发起请求,服务器发送可选项列表,客户端作出选择后再发送第二次请求。
- 优点:容易实现,给用户选择权。
- 缺点:增加访问次数和延迟。
服务器驱动
服务器检查客户端的请求头部集并决定提供哪个版本的页面。(现在最普遍)
- 优点:比较快,没有额外开销,支持优先级匹配。
- 缺点:头部集都不匹配的时候,服务器只能猜测。
透明协商
某个中间设备(通常是缓存代理)代表客户端进行协商。
- 优点:比较快。
- 缺点:需要中间设备,非 HTTP 标准。
服务器驱动内容协商
客户端通过请求报文头传递告诉服务器支持情况,并且可以带有近似匹配的优先级,服务器回应的响应报文头里进行确认,就完成了内容协商。
相关请求报文头
字段名 | 说明文字 |
---|---|
Accept | 告诉服务器自己能接受的媒体类型 |
Accept-Language | 能接受的语言 |
Accept-Charset | 能接受的字符集(如 unicode) |
Accept-Encoding | 能接受的编码方式(如 utf-8) |
相关响应报文头
字段名 | 说明文字 |
---|---|
Content-Type | 对应 Accept |
Content-Language | 对应 Accept-Language |
Content-Type | 对应 Accept-Charset |
Content-Encoding | 对应 Accept-Encoding |
近似匹配(优先级)
比如:
Accept-Encoding:en;q=0.5,fr;q=0.0,nl;q=1.0,tr;q=0.0
这里nl的优先级是1.0,最高优先级,所以会优先返回。
断点续传和多线程
通过在报文头里两个参数实现的,客户端发请求时对应的是 Range,服务器响应时对应的是 Content-Range 。
请求报文头
Range用于请求头中,指定第一个字节的位置和最后一个字节的位置。
格式为:
Range:(unit=first byte pos) - [last byte pos]
响应头
HTTP/1.1 200 Ok(不使用断点续传方式)
HTTP/1.1 206 Partial Content(使用断点续传方式)
Content-Range用于响应头中,在发出带Range的请求后,服务器会在Content-Range头部返回当前接受的范围和文件总大小。
格式为:
Content-Range:bytes(unit first byte pos) - [last byte pos]/[entity length](文件总大小)
例子
- 客户端下载一个 1024K 的文件 已经下载了其中 512K;
- 网络中断,客户端请求续传 因此需要在 HTTP 头申明本次需要续传的片段“Range:bytes=512000- ”这个头通知服务端从文件的 512K 位置开始传输文件;
- 服务端收到断点续传请求,从文件的 512K 位置开始传输,并且在 HTTP 头中增加“Content-Range:bytes 512000- /1024000”并且此时服务返回的 HTTP 状态码应该 206,而不是 200。
关于多线程
断点续传是被动的增量下载,多线程是主动的分片下载,但使用都是 Range 模式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。