最近在工作里看了蛮多流量,有一天突然注意到http的请求头内容里的Authorization头,看到时突然有种既熟悉又好像有哪里不对的感觉,仔细看了下单词拼写才发现和之前经常见到的身份认证头Authentication不同,先前比较熟悉的还是身份认证(Authentication)中的Basic、JWT等,并且最近挖SRC的过程中看到很多认证方式,遇到了也只能挑熟悉的下手。于是起了兴趣研究一下HTTP中的认证方式,本文主要从理论内容上浅析一下HTTP API中的授权认证(纸上谈兵版)。

引言

众所周知,HTTP协议是无状态的,也就是其对于事物处理没有记忆能力,服务器不知道客户端是什么状态,当客户端給服务器发送http请求后,服务器根据请求将响应数据发送回来后就不会记录其他信息,即使是使用Keep-Alive也没办法改变此类结果。因此,在我们需要获得用户是否登录的状态时,就需要·检查用户的登录状态。通常用户成功登录后,服务器会为用户分配一个登录凭证(Token),这个登录凭证一般又会有两种存放形式,一是以Cookie的方式存储在客户端,另外一种则是以Session的方式存储在服务器端,在客户端存放SessionID。

不过,Token的存放和管理知识解决了“我是谁”这个问题,即Authentication(身份认证),其主要是验证了请求的来源是否可信,但这还不够。试想如果一个用户登录了系统,如果不对其权限加以限制,那么这个用户是不是能查看甚至修改其他用户的敏感数据?

这就引出了 Authorization(授权)的概念,与身份认证不同,授权的核心主要在于回答服务器“你能做什么”这个问题,它的任务就是在验证了你的身份后,决定你是否有权限访问特定资源或执行特定操作。

为什么Authentication和Authorization在HTTP的世界里被分开使用,还得从HTTP的无状态特性说起。

HTTP的无状态特性

HTTP协议的设计初衷时简单、高效,无状态的意思是,每次客户端发送请求时,服务器都不会记住之前的任何信息。例如:

  1. 第一次请求获取一个页面,服务器会处理并返回内容;

  2. 第二次请求提交表单,服务器处理完提交的数据后,同样也不会记得上一次请求发生了什么。

这种无状态特性虽然在资源开销和实现上有一定优势,但是在需要记住用户状态的场景中却显得比较鸡肋。比如,一个用户登录后,服务器如何知道用户的状态》因此就需要借助一些额外的机制来实现:

  1. Cookie:用户登录成功后,服务器向客户端发送一个带有Token的Cookie,客户端每次发送请求时,都会自动将这个Cookie附带到请求头中,以便服务器识别用户身份。
  2. Session:用户登录成功后,服务器生成一个Session并将其ID,也就是SessionID返回给客户端,客户端存储这个ID并在请求中带上,服务器通过ID查找对应用户会话。

这两种方法都有各自的优缺点,但它们共同的目的都是解决HTTP协议自身的无状态带来的问题,为客户端和服务器之间的联系建立一个“上下文”。

HTTP API认证的意义和威胁

在互联网的世界里,API是各种服务之间的交流途径,从前端应用获取用户数据,到后端服务之间同步信息,API的每一次调用都可能设计敏感资源或关键操作。没有认证的API就像一个没门锁的房子,任何人都能随意进出,可能能够肆意删除、修改、查看你的资源。

认证的目的

因此,认证的核心目的就是确认请求的身份来源是否可信,确保请求者的身份真实可靠。例如,一个登录操作需要确认用户的用户名和密码是否匹配,一个支付操作需要验证请求的来源是否是经过授权的客户端。这些验证确保了API的安全运行,并防止未经授权的访问。

简单来说,认证回答了你是谁这个问题,为后续的授权和操作奠定了基础。

常见的危险

尽管认证机制可以为API增加一层安全防护,但是在实际使用中,我们也经常能发现一些风险和危害:

  1. 数据窃取:如果仅是通过HTTP协议明文传输认证信息(如用户名、密码、token等),攻击者可能可以通过网络嗅探(例如在公共WiFi中)获取到这些敏感信息。
  2. 重放攻击:攻击者在截取了用户的有效请求后,即使不解密内容,也可能通过重放用户的请求,反复尝试访问受保护的资源。例如,拦截用户的支付请求后,攻击者可以多次重发相同的支付请求。
  3. 伪造身份:攻击者通过伪造认证信息(如Token、SessionID),冒充合法用户发送请求,绕过系统的身份验证。例如,生成一个假的JWT Token以试图欺骗服务器,访问其他用户的数据,这通常会导致一些水平或垂直越权漏洞。
  4. 中间人攻击:攻击者可能会在用户和服务器之间,拦截并篡改两者的通信内容,此时即使认证信息被加密,攻击者仍可以通过恶意代理服务窃取和利用敏感信息。

HTTP 中的 Authorization 和 Authentication 头

在使用HTTP协议的过程中,AuthorizationAuthentication 头是最常见的认证和授权方式。

Authorization(认证)

Authorization头的主要任务是验证客户端的身份,确保发出请求的用户是合法、可信的,通过身份认证,服务器才能知道请求的来源是否可靠,为后续的授权和资源访问奠定基础。

常见的认证机制

Basic Authorization

在最基础的认证机制中,客户端会将用户名和密码组合成username:password的格式,然后使用Base64编码后,通过Authentication头发送给服务器,例如:

1
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

当浏览器或客户端首次请求和访问服务器上的受保护资源时,服务器会返回一个401状态码以及一个WWW-Authenticate头部,指示客户端需要进行认证。服务器收到这个请求后,会解码Base64字符串,提取用户名和密码进行验证。

但是因为Base64仅仅是编码而非加密,这种机制通常需要结合HTTPS协议进行使用,否则可能会在数据传输过程中产生安全问题。

Digest Authentication

Digest认证基于质询-响应的认证机制,通过数字摘要来验证用户的身份,相较于Basic认证方法使用用户名密码的方式,提供了更高的安全性。在Digest认证中,不会直接发送密码,而是发送摘要信息,这样即使在非安全的通道上也不会因被截获数据而泄露密码。

当用户尝试访问受保护资源时,服务器会向客户端发送一个挑战(challenge),要求客户端提供有效的身份验证信息。客户端收到后,使用用户的凭证和约定的摘要算法生成摘要信息,并将摘要信息随请求内容发送给服务器,服务器使用相同的密码对响应进行验证。

DIgest认证流程模拟:

  1. 客户端请求:客户端发起请求到服务器端

  2. 服务器返回一个认证挑战,返回401 Unauthorized状态吗以及WWW- Authenticate头,其中包含认证类型(Digest)、随机生成的nonce值以及其它必要的参数,如realm等。WWW-Authenticate的值示例如下:

    1
    Digest realm="myrealm", qop="auth", nonce="unique-nonce", opaque="0000000000000000"

    realm:表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码

    nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击 = 官方建议每次请求都不同

  3. 客户端响应:客户端收到对应的信息后,使用用户名、密码、nonce、请求方法、请求的URL和其它参数一起计算一个摘要,并将其与认证请求一起发送给服务器。此时Header中的Authorization示例如下:

    1
    Digest username="user1", realm="myrealm", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/protected", qop=auth, nc=00000001, cnonce="0a4f113b", response="dd51a70556e6a3342945ef0feac79afb", opaque=""
  4. 服务器验证:服务器使用存储的密码和客户端发送的参数计算摘要,如果与客户端发送的摘要匹配,则认证成功。

可以看出,Digest认证方法的安全性相比Basic更高,主要是因为其不会以明文或可解码的形式传输密码,并且可以通过改变nonce的值防止重放攻击。

JWT Authentication

Json Web Token是一种子包含的认证方式,也就是Token包含了用户的身份信息、签名,用于验证身份和权限。

JWT通常由头部(Header)、负载(Payload)、签名(Signature)三个部分组成,并且这三个部分都是被base64编码的。

  • 头部:包含了JWT的元数据,如类型(通常是JWT)和所使用的签名算法。
  • 负载:包含了实际需要传递的数据,通常会是用户的身份信息(如用户ID)以及一些元数据(如令牌的有效期)。
  • 签名:对前两部分进行签名以确保数据的完整性和真实性。并且,服务器端生成签名的时候会使用一个密钥(或私钥),客户端使用这个密钥(或公钥)来验证签名的有效性。

例如,当我们希望声明类型为jwt并声明签名算法为SHA256,此时我们Header头中的内容应该是这样的:

1
2
3
4
{
'typ':'jwt',
'alg':'SHA256'
}

base64编码后:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

第二部分的Payload中,假设想要存放的内容为id和exp:

1
2
3
4
{
'id':'10',
'exp':'2301597433'
}

base64编码后:

1
ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0=

第三部分进行签名,将header和payload分别base64编码后组合到一起(通过.连接),添加一个只有服务器知道的签名字符串(密钥或私钥),再使用header中约定的签名算法计算出一个签名信息:

1
signature = SHA256(base64encode(header) + '.' + base64encode(payload), 'SEVER_SECRET_KEY')

最终得到签名信息为:

1
05dd35b4d20c95430cd1b63406f861de7e4c1476f9dbffa25f30fe08baf8f530

只要有了签名信息,即使有人想要伪造身份,修改了第一、第二部分里的内容,但是由于他们不知道服务器端的密钥的内容,因此无法伪造正常数据。

将以上三个部分组合在一起就构成了完整的JWT头:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0=.05dd35b4d20c95430cd1b63406f861de7e4c1476f9dbffa25f30fe08baf8f530

可以看出,JWT为了做到安全性,导致其本身较长,可能会增加http请求开销,但适合分布式系统,在分布式部署的情况下,客户端的一个令牌就可以访问所有的服务器的同时,也避免了sessionid的多机数据共享。

HMAC Authentication

HMAC认证可用于验证消息的完整性和真实性,其结合了哈希函数和加密密钥,相较于Digest认证方法,不是单纯的使用哈希,能够确保传输的数据未被篡改,并验证消息发送者的身份。

HMAC认证使用一个密钥和一个哈希函数,把密钥和消息结合,生成一个唯一的签名,当接收方收到消息的时候,使用相同的密钥和哈希函数计算新的签名并和接收到的签名进行对比,以验证消息的完整性和来源。

由于较为复杂,深入的内容可以了解一下HMAC算法

Token-Based Authentication

(就是前面说的session 、cookie啥的,偷懒)

用户在登录成功后,服务器端就会随机生成并给用户分配一个token,本身不包含任何信息,不可能被伪造。

总结如下:

认证方式 优点 缺点 适用场景
Basic 简单、广泛支持 安全性低 内部系统,快速原型开发
Digest 更安全的质询-响应机制 配置复杂,现代中不常用 内部系统,对安全有一定需求
Token 无状态、扩展性强 Token 泄露风险 微服务架构,现代 Web 应用
JWT 自包含、跨服务支持 Token 体积较大 分布式系统,用户认证
HMAC 数据完整性验证强 密钥管理复杂 服务间通信,高安全性需求场景

Authorization(授权)

authorization头用于控制客户端对资源的访问权限,即使身份认证通过了,也需要检查权限,确保请求者只能访问被授权的资源,示例:

1
Authorization: Bearer <access-token>

Authentication和Authorization的关系与区别

  1. 任务不同

Authentication:确认身份,确保请求来源可信。

Authorization:分配权限,决定能否访问资源。

  1. 顺序依赖:先认证身份,再判断权限。没有通过认证的请求不会进入授权阶段。

  2. 重合场景:Bearer Token 等机制中,Authorization 头既可用于身份认证,也能传递权限信息。

安全思考和总结

认证和授权虽然是安全体系的基础,但是从很多的现实场景来看也并非绝对安全,之前遇到过很多未授权访问和越权漏洞都是由于认证和授权处没有做好校验。

防御的角度来看:

  1. 使用HTTPS。无论是Basic还是JWT,如果通过http传输,攻击者只需要一个简单的抓包工具,就能够嗅探到这些敏感信息,因此,强制使用HTTPS是任何认证机制的前提。
  2. Token和密钥管理。Token和密钥一旦泄露,攻击者就能随意伪造合法用户身份访问系统资源。避免将Token直接存储在用户的LocalStorage中,即使使用Cookie,也需设置HttpOnly和Secure标志,以防止XSS攻击获取。服务器端应该确保所有的密钥安全的存放在密钥管理系统中,防止泄露。
  3. 防止重放攻击给每个请求都添加时间戳,并设置一个合理的时间窗口,当服务器验证请求时,检查时间戳是否在有效范围内(挖洞的时候经常被加了时间戳的服务器恶心到)。使用一次性token,为每个请求都生成一个随机的nonce值,服务器验证后立马作废,确保token不会被重复使用。
  4. 多重认证机制组合。例如HTTPS+Token,HTTPS保护传输过程中的安全,Token用于认证请求来源。二次认证(MFA)等,二次认证就是对高敏感操作(如修改密码、转账等),要求用户验证身份(如短信验证码、硬件令牌等)。

攻击的角度来看的切入点:

  1. 掌握流量。使用一些流量嗅探工具(Wireshark、burp等)获取流量,尝试去利用认证凭证的缺陷,或通过抓取网络中未经加密的http流量,查看其中是否有泄露凭证信息。
  2. 窃取Token。利用XSS攻击可以帮助我们窃取存储在浏览器LocalStorage或Cookie中的token。通过日志、错误信息不安全的客户端存储等也可能得到token。
  3. 授权逻辑漏洞。即使身份认证通过,授权逻辑中也可能存在可利用的点。例如修改请求中的用户ID或用户名,在服务器端认证存在缺陷的情况下,可能造成水平越权漏洞。或是通过修改请求内容中一些参数,例如isadmin等,可能会帮助普通用户提高权限到管理员,造成垂直越权漏洞
  4. 利用JWT进行身份伪造。可以尝试将header中的签名算法篡改为none或弱加密算法,伪造合法的JWT欺骗服务器。获取并利用缺乏失效机制的JWT,可长期伪造身份。
  5. 私钥泄密。服务器的私钥泄密了的情况下,我们可以自行伪造身份,例如JWT、Session等。
  6. 多注意状态码。仔细观察系统返回的错误信息,寻找潜在的线索。比如返回是401 Unauthorized的情况下,可能是用户名或密码错误,可以通过检查状态码进行暴力破解。如果是403 Forbidden,提示了用户权限不足的情况下,可以给我们提供资源存在,并且有权限限制的信号,可能后续就是我们获取高权限后重点关注的资产。

最后,“世界上没有绝对安全的系统”,永远都要假设你的系统会被攻击,无论是防御还是攻击,思维的转变都是理解安全体系的关键点所在。

参考

https://zhuanlan.zhihu.com/p/677607499
https://zhuanlan.zhihu.com/p/677786212
https://zhuanlan.zhihu.com/p/677993394
https://zhuanlan.zhihu.com/p/678402752
https://zhuanlan.zhihu.com/p/679645365
https://juejin.cn/post/6969074624650805262
https://juejin.cn/post/7140887699325452319
https://blog.csdn.net/yuezhilangniao/article/details/121989974
https://isunman.com/2023/05/16/http-api-authentication-and-authorization/