Http权威指南笔记(十)——认证

现在大多数网站都会在cookie等客户端识别机制的基础上建立自己的认证机制。但是HTTP规范中提供的原生认证机制还是有必要了解下,了解这些后才能更好理解那些自己建立的认证机制。

HTTP原生认证功能一般分为基本认证摘要认证。基本认证相对简单,但是安全性相对较弱,摘要认证要复杂一些,当然安全性也会高一些。在介绍这两种认证方式之前,我们先看下HTTP中涉及到认证的一些通用概念。

1 HTTP认证机制

所谓认证就是出具一定的东西证明你的身份。HTTP中提供了一个**质询 / 响应(challenge/response)**框架来提供认证功能。

1.1 认证过程

当Web服务器收到一条HTTP请求时,如果该请求的资源是需要认证的,服务器就会返回一个”认证质询“的响应,客户端收到该响应后,会要求用户提供响应的认证资料(如用户名/密码等),随后客户端会携带上认证证书重新发起请求,服务器收到后会对该请求的证书进行验证,如果验证通过就返回正常所需响应,如果验证失败,可以返回一条错误信息或者再次返回一条”认证质询“的响应。整个过程如下图所示:
HTTP认证过程

1.2 认证有关的协议和首部

HTTP 通过一组可定制的控制首部,为不同的认证协议提供了一个可扩展框架。总体来说根据上述的认证步骤可以将首部概括为下表的内容:

步骤 首 部 描 述 方法/状态
请求 第一条请求没有认证信息 GET
质询 WWW-Authenticate 服务器用 401 状态拒绝了请求,说明需要用户提供用户名和密码。服务器上可能会分为不同的区域,每个区域都有自己的密码,所以服务器会在 WWW-Authenticate 首部对保护区域进行描述。同样,认证算法也是在 WWW-Authenticate 首部中指定的 401 Unauthorized
授权 Authorization 客户端重新发出请求,但这一次会附加一个 Authorization 首部,用来说明认证算法、用户名和密码 GET
成功 Authentication-Info 如果授权证书是正确的,服务器就会将文档返回。有些授权算法会在可选的 Authentication-Info 首部返回一些与授权会话相关的附加信息 200 OK

将上述的步骤和首部结合起来,描述认证过程如下图所示:
HTTP认证步骤
当然,上面的步骤只是一个概括性描述,实际使用过程中,根据使用的认证协议不同,中间可能会有一些不同。后面会具体介绍HTTP官方提供的两种认证协议(基本认证和摘要认证)。

1.3 安全域

所谓的安全域就是,将一组资料统组织管理起来,形成一个访问权限控制。这个访问权限控制点所有资料组合起来就是一个安全域。有些时候我们需要将不同的资源组织成不同的安全域进行不同的访问权限控制,比如对于一个公司普通职员来说,财务数据属于一个公司保密资料,只有特定认证后的员工才能访问,所以需要建立一个安全域。对于员工的的个人家庭文档,属于个人隐私,这个时候又会建立一个安全域。
在HTTP中,在WWW-Authenticate中提供了一个realm指令,用于指定安全域。在上面1.2小节的示例中,我们的WWW-Authenticate就包含一个”Family“的安全域。起始本质就是提供一个字符串作为一个安全域的标识。

介绍完HTTP中的一些认证基本知识,下面我们分别对基本认证和摘要认证进行一些简单的阐述介绍。

2 基本认证

2.1 基本认证过程

在基本认证中,Web服务器如果需要提出质询,可以通过返回一个401状态码,并用 WWW-Authenticate 响应首部指定要访问的安全域,客户端在收到该响应后,就会要求用户提供用户名/密码,然后将这些信息进行一些处理后,用Authorization发送给服务器。具体认证过程参考前面的1.2小节即可,那一小节的举例就是基本认证的步骤。
基本认证过程中的首部信息如下表所示:

质询/响应 首部语法及描述
质询(服务器发往客户端) 网站的不同部分可能有不同的密码。域就是一个引用字符串,用来命名所请求的文档集,这样用户就知道该使用哪个密码了:
WWW-Authenticate: Basic realm=quoted-realm
响应(客户端发往服务器) 用冒号(:)将用户名和密码连接起来,然后转换成 Base-64 编码,这样在用户名和密码中包含国际字符会稍微容易一些,也能尽量避免通过观察网络流量并只进行一些粗略的检查就可以获取用户名和密码情况的发生:
Authorization: Basic base64-username-and-password

可以看到,基本认证过程中是没有使用到Authorization-Info首部的。

2.2 基本认证的加密算法

HTTP 基本认证将(由冒号分隔的)用户名和密码打包在一起,并用 Base-64 编码方式对其进行编码。Base-64编码简单说就是将8个字节序列划分为6个字节的块,每个块会在一个特殊的由64个字符组成的字母表中选择一个字符。详细的信息感兴趣的朋友可以自己搜索下。

2.3 代理认证

代理作为服务器角色的时候,也是可以要求对客户端进行认证的,比如一些公司或者组织会统一使用一个认证代理,集中管理员工对公司资源的访问策略。代理认证和服务器认证的过程基本一致,只是首部稍微有些差别,如下表所示:

Web服务器 代理服务器
Unauthorized status code: 401 Unauthorized status code: 407
WWW-Authenticate Proxy-Authenticate
Authorization Proxy-Authorization
Authentication-Info Proxy-Authentication-Info

2.4 基本认证的问题

基本认证虽然实现简单,但是其在安全性上非常薄弱,存在以下几个安全问题:

  1. 基本认证会直接发送用户名和密码,虽然用户名和密码是经过Base-64编码的,但是这种编码是可逆的,而且非常容易反向编码获取真正的用户名和密码。相当于是一种“明文”传输了。
  2. 针对第一个问题,如果是专用的客户端和服务器,协商好之后,可以在编码之前就对用户名和密码进行加密处理,即便是这样处理后,也可以拦截到加密后的用户名和密码,进行重放攻击。
  3. 基本认证中客户端对服务器没有进行验证,恶意第三方很容易冒充服务器接收客户端发送的用户名和密码。

所以我们如果只是单纯是用基本认证,没有配合其他安全措施(如SSL)使用的话,安全性是非常弱的。所以这种一般适用于一些对安全性要求不太高的情形,如:公司对内部员工的管理上。

3 摘要认证

上面介绍了基本认证,可以看到基本认证的安全性很低,所以HTTP提供了另外一种安全性更高,当然实现也相对复杂一些的摘要认证方式。
相对于基本认证,摘要认证的安全性提升主要有一下几个方面:

  • 不会发送明文用户名和密码
  • 防止恶意的重放认证攻击
  • 可以有选择的防止对报文内容的修改

3.1 摘要认证改进的地方

3.1.1 对于密码的保护

上面提到,摘要认证不会明文发送密码。因为摘要认证是通过对数据(这里简单看成密码)进行不可逆的加密方式(常见的是MD5)后进行传输的。即使拦截到传输的内容,也没法逆转获得密码。服务收到认证请求后,也只能是通过对密码进行同样方式加密和对比加密后的摘要,而不是直接对比密码,看是否一致。

3.1.2 防止重放攻击

如果只是对认证信息进行不可逆的加密处理,攻击者在拦截到摘要信息后,同样可以发动重放攻击,所以我们还需要一个手段来防止这样的事情。在摘要认证中,服务器可以发送一个随机数给客户端,客户端在计算摘要的时候,需要加上该随机数。由于该随机数会经常变化,所以加密后的摘要也会经常变化。这样就可以攻击者拦截到摘要发起重复攻击了。

3.1.3 摘要认证的握手机制

摘要认证和基本认证的流程大体上一致。但是摘要认证还添加了一些新的选项,具体握手机制如下图所示:
摘要认证握手机制
从上面可以看到,这里客户端也可以选择对服务器质询,所以相对于基本认证,不容易受到一些冒充服务器骗取信息。
这里我们将基本认证和摘要认证的过程进行对比如下:
基本认证和摘要认证对比

3.2 摘要的计算

摘要认证的核心就是通过一些计算,得出摘要的过程。得出摘要的安全性越高,那么摘要认证的安全性也就越高。所以这里我们简单介绍下摘要认证的计算过程。

3.2.1 摘要计算的数据来源

上面我们基本认证加密的时候,数据就是用户名+冒号(:)+密码。摘要认证的数据来源就要相对复杂一些了。摘要认证中的数据总的分为两个部分:

  • A1——一个包含安全信息(用户名、密码、保护域和随机数等)的数据块
    这里的A1根据提供的算法不同,数据组成也不太相同,目前RFC2617定义了两种算法(MD5和MD5-sess),A1对应的数据分别如下:
算法 A1的值
MD5 A1 = <user>:<realm>:<password>
MD5-sess A1 = MD5(<user>:<realm>:<password>):<nonce>:<cnonce>

这里的nonce和cnonce分别代表服务器随机数和客户端随机数

  • A2——一个包含报文中非保密属性的数据块(比如 URL、请求方法和报文实体的主体部分等)
    这个数据一般只和报文自身信息相关,同样根据RFC2617定义的不同保护质量(qop),A2包含的数据也不相同,具体如下表所示:
qop A2
未定义 <request-method>:<uri-directive-value>
auth <request-method>:<uri-directive-value>
auth-int <request-method>:<uri-directive-value>:H(<request-entitybody>)

可以看到默认的是采用auth的方式,除非指定qop="auth-int"。这里的H(<request-entitybody>)代表一种算法,下面会详细介绍。

3.2.2 摘要计算函数

摘要计算过程中,涉及到两个函数,这里我们命名如下:
H(d)H(d)——这个函数就代表一个加密函数,一般指MD5,所以等价于MD5(d)MD5(d)。这里的d就代表待加密的数据。
KD(s,d)KD(s,d)——这个就是最终使用的摘要计算函数。一般也是使用MD5的方式,只是这里的输入数据s和d分别代表上面提到的保密数据和非保密数据连个部分,一般使用冒号(:)将两部分进行连接。所以最终这两个公司可以变形为如下:
H(d)=MD(d)H(d) = MD(d)
KD(s,d)=H(concatenate(&lt;secret&gt;:&lt;data&gt;))KD(s,d) = H(concatenate(&lt;secret&gt;:&lt;data&gt;))

3.2.3 摘要算法总结

根据上面两个小节的内容,根据RFC2617的定义,这里将算法汇总如下:

qop 摘要算法 备 注
未定义 KD(H(A1), <nonce>:H(A2)) 不推荐
auth 或 auth-int KD(H(A1), <nonce>:<nc>:<cnonce>:<qop>:H(A2)) 推荐

总结起来就是,在未定义qop的情况下,为了和RFC2069兼容,使用保密信息加上随机数的方式,如果定了qop为auth或者auth-int,除了保密信息和服务器随机数,还需要加上随机数计数(nc),客户端随机数(conce)和qop。这里如果我们将H和KD函数替换为我们常用的MD5加密方式,就可以得到如下算法:

qop 算  法 展开的算法
未定义 <undefined>
MD5
MD5-sess
MD5(MD5(A1):<nonce>:MD5(A2))
auth <undefined>
MD5
MD5-sess
MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))
auth-int <undefined>
MD5
MD5-sess
MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

从上面的表格可以看到,算法总结起来无外乎就是
MD5(MD5(A1):&lt;nonce&gt;:MD5(A2))MD5(MD5(A1):&lt;nonce&gt;:MD5(A2))

MD5(MD5(A1):&lt;nonce&gt;:&lt;nc&gt;:&lt;cnonce&gt;:&lt;qop&gt;:MD5(A2))MD5(MD5(A1):&lt;nonce&gt;:&lt;nc&gt;:&lt;cnonce&gt;:&lt;qop&gt;:MD5(A2))
只是根据算法和qop的不同,会影响到A1和A2两个数据块的组成而已。

3.2.4 随机数的选择

摘要认证的安全和随机数的选择有很大关系,随机数选择就显得比较重要。RFC2617中建议随机数采用如下方式生成:
BASE64(time-stamp H(time-stamp “:” Etag “:” private-key))
这里的time-stamp是服务器产生的时间戳。Etag是与请求资源相关的HTTP报文中Etag首部的值,private-key就是一个服务器自己知道的一个值。
通过这种方式计算出来的随机数,time-stamp可以控制我们随机数的有效期,加上Etag可以可以在资源更新后要求重新认证,防止重放攻击。
实际开发中,通常一般GET请求会是使用一个时间戳来控制有效期,而如POST或者PUT之类的请求,一般是使用一次性随机数,防止重复提交。

3.2.5 对称认证

所谓对称认证,就是客户端也可以对服务器进行质询认证。同样是通过客户端提供随机值来实现的。服务器可以根据这个随机值和共享的保密信息来生成摘要,然后放在响应的Authoriztion-Info首部中返回给客户端。
不过这个和请求认证摘要的计算方法稍微有点不同,因为响应中没有方法,所以相对于请求摘要A2数据区会缺少方法这一部分的数据,对比如下表所示:

qop A2数据
请求 响应
未定义 <request-method>:<uri-directive-value> :<uri-directive-value>
auth <request-method>:<uri-directive-value> :<uri-directive-value>
auth-int <request-method>:<uri-directive-value>:H(<request-entity-body>) :<uri-directive-value>:H(<response-entity-body>)

3.3 摘要认证会话和预授权

客户端响应对保护空间的 WWW-Authenticate 质询时,会启动一个此保护空间的认证会话(与受访问服务器的标准根结合在一起的域就定义了一个“保护空间”)。这个认证会话会一直持续到客户端收到了另一“保护空间”的质询,所以客户端在此期间应该记住一些与认证有关的值(如:用户名、密码、随机数、随机计数等),方便随时构建Authorization首部。
由于随机数是有时效性的,所以可能某个时候随机数就失效了,这个时候服务器收到随机数过时的认证请求后,应该返回一个携带新的随机数的状态码为401的响应,同时在响应中指定stale=true,用于告知客户端,随机数过期了,可以使用之前的用户名和密码等信息,加上新的随机数来重新认证即可。

如果我们对这种请求/质询的对话不进行优化,那么意味着每次我们发起请求都会走一遍请求/质询的流程。如果我们客户端除了第一次请求/质询后,后面每次发起请求的时候能够预先算出摘要,发起请求的时候直接将摘要放入Authorization首部,那么就不用每次都走一遍请求/质询的流程,这样会节约资源也会增加效率,这里所说的预先算出正确的摘要,就是预授权。下图展示了预授权处理后和普通请求/质询之间的流程差异:
预授权
通过前面的介绍,客户端计算摘要,如果我们第一次请求/质询后将用户名、密码等信息保存下来后,剩下的就是缺少服务器提供的随机值,如果我们能够预先知道随机值,那么就能正确的计算摘要了。一般有下面三种方式,可以让服务器预先将随机值给到客户端。

  1. 预先生成一个随机数
    可以在 Authentication-Info 成功首部中将下一个随机数预先提供给客户端。这个首部是与前一次成功认证的 200 OK 响应一同发送的。如下:
    Authentication-Info: nextnonce="<nonce-value>"
    虽然这样可以让客户端预先计算摘要的目的,但是这个方法也有个就是不支持管道的特性。这种处理,意味着必须在上一个请求发起后,并收到响应(整个事务流程完成)后才能发起下一个请求,就不能利用管道连续发出多个请求了。
  2. 受限随机值重用机制
    这种不是预先生成随机值并返回给客户端,而是将一个随机值设置一个可重用的次数或者时间。那么在这个有效期内,客户端就可以直接计算出摘要,同时这个方法还不破坏管道的特性。当这个随机值失效后,返回一个401错误并携带stale=true和新随机值的信息即可。但是也有个缺点,因为随机值是可以重用的,所以可能有受到重放攻击的风险。所以这个时候需要寻找到一个相对平衡的有效性限制。
  3. 同步生成随机数
    这种方式,就是服务器和客户端共享一些信息,然后用同一套机制生成随机数,那么客户端就可以随时自己生成随机数来进行摘要计算。但是这种方式对随机数的生成方式要做好保护措施,如果被第三方预测或者获取到,那就危险了。

3.4 增强保护质量

可以在三种摘要首部中提供 qop 字段:WWW-Authenticate、Authorization 和 Authentication-Info。该字段就是用于协商保护质量的。
服务器首先在 WWW-Authenticate 首部输出由逗号分隔的 qop 选项列表。然后客户端从中选择一个它支持且满足其需求的选项,并将其放在 Authorization 的 qop 字段中回送给服务器。
为了兼容RFC2029,所以qop被设定为一个可选字段。但是为了安全性着想,应该尽量满足提供qop支持并使用qop。
在RFC2617中提供了两种qop的可选值:也就是我们前进已经介绍了的auth和auth-int,两者的区别是auth-int带有报文完整性保护支持,因为使用auth-int的时候,使用H(d)算法的实时,会对报文实体而不是对报文信息继续散列计算。

3.5 摘要认证首部

其实我们的摘要认证首部在介绍前面内容的时候,都介绍的差不多了,这里我们结合前面的基本认证做一个汇总表格,如下:

阶段 基  本 摘  要
质询 WWW-Authenticate: Basic realm="<realm-value>" WWW-Authenticate: Digest
realm="<realm-value>“
nonce=”<nonce-value>"
[domain="<list-of-URIs>"]
[opaque="<opaque-token-value>"]
[stale=<true-or-false>]
[algorithm=<digest-algorithm>]
[qop="<list-of-qop-values>"]
[<extension-directive>]
响应 Authorization: Basic <base64(user:pass)> Authorization: Digest
username="<username>“
realm=”<realm-value>“
nonce=”<nonce-value>"
uri=<request-uri>
response="<32-hex-digit-digest>"
[algorithm=<digest-algorithm>]
[opaque="<opaque-token-value>"]
[cnonce="<nonce-value>"]
[qop=<qop-value>]
[nc=<8-hex-digit-nonce-count>]
[<extension-directive>]
Info n/a Authentication-Info:
nextnonce="<nonce-value>">
[qop="<list-of-qop-values>"]
[rspauth="<hex-digest>"]
[cnonce="<nonce-value>"]
[nc=<8-hex-digit-nonce-count>]

这里只是简单的介绍了首部,起始摘要相关的首部是非常复杂的,感兴趣的朋友可以自己去找找资料看看。

这里摘要认证和基本认证一样,我们不能简单实现了认证就完事,还需要考虑一些其他问题:

  • 多重质询:服务器可以提供一个可供客户端选择的质询列表,客户端选择自己支持一种方式
  • 差错处理:如果指令不对或者不当,服务器就应该返回400 Bad Request响应,同时记录下该次请求,如果发现有多次相同错误,有可能是攻击现象
  • 重写URI:代理可以重写URI,如对主机名标准化、转义一些字符等,这样URI被修改后,可能造成认证失败
  • Cache:同基本认证一样,对于有Authorization首部的响应,我们缓存的时候,应当谨慎处理。

到这里,我们认证的内容就介绍完了,认证是一个非常复杂的东西,本篇也仅仅就是简单介绍了一些基本的东西,真正感兴趣或者想要掌握的朋友,还是建议去看一些专门介绍的资料。特别是对于安全性这一块的内容,更加要注意,不管是基本认证和摘要认证,安全性和SSL比起来(后面会有文章来介绍HTTPS),都显得很薄弱,要特别注意处理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章