购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 认证

对几乎所有的API来说,其关键内容之一就是区分授权和未授权访问的能力。能够正确地记录用户是很关键的,从安全的角度看这也是一个令人头痛的问题。

要做到安全并不容易,所以最好依照标准做法来实施以简化操作。

正如我们之前所说,这些只是一般性的提示,绝不是一套全面的安全实践,本书内容的重点也不在于安全。请保持对安全问题和解决方案的关注,因为这是个一直在不断发展的领域。

说到认证,其最重要的安全问题就是应当 在生产环境中始终使用HTTPS端点 。这样可以保护信息传输通道不被窃听,并且实现通信过程的保密。请注意,一个HTTP网站,仅意味着通信过程是私密的,而事实上你却有可能是在与“魔鬼”进行对话。但是,当API的用户向你发送密码和其他敏感信息时,为了避免外部用户会收到这些信息,采用HTTPS是最基本的要求。

通常情况下,大多数架构都会使用HTTPS,直到请求的数据到达数据中心或安全的网络以后,才在其内部使用HTTP。这样就可以对内部流动的数据进行检查,同时也保护了在互联网上流动的数据的安全性。虽然当前在内部安全网络中免加密不再那么重要了,但这么做依然能提高效率,因为用HTTPS进行编码的请求需要消耗更多的计算资源。

HTTPS端点对所有的访问都是有效的,但取决于采用的是HTML接口还是RESTful接口,其他具体实现的细节也各不相同。

2.3.1 HTML接口认证

通常情况下,HTML网页中的认证流程是这样的:

1.向用户展示一个登录界面。

2.用户输入他们的登录用户名和密码,并将其发送到服务器。

3.服务器验证该密码。如果正确,则返回一个带有会话ID的cookie。

4.浏览器收到响应数据并存储该cookie。

5.所有新的请求都将包含此cookie。服务器会验证该cookie并实现对用户的有效识别。

6.用户可以注销,删除cookie。用户进行此操作时,会向服务器发送一个请求,以删除会话ID。通常情况下,会话ID都会有一个到期时间,用于清理到期会话。这个到期时间可在每次访问时自动更新,或者在到期时强制用户重新登录。

设置cookie的Secure、HttpOnly和SameSite属性很重要。Secure属性确保cookie只会发送到HTTPS端点,而不是HTTP端点。HttpOnly属性能让cookie无法被JavaScript访问,这使得通过恶意代码获取cookie更加困难。该cookie将被自动发送到设置它的主机上。SameSite属性确保只有当源头是来自同一主机的Web页面时,才会发送cookie。SameSite属性可设置为Strict(严格)、Lax(宽松)和None(无)。Lax允许从不同的网站导航到该页面并发送cookie,而Strict则不允许这么做。

可以在Mozilla SameSite Cookie网站获取更多cookie相关的文档:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite。

针对cookie可能存在的不良用途是进行XSS(Cross-Site Scripting,跨站脚本)攻击。一个恶意构造的脚本会读取cookie,然后伪装成合法用户的身份向服务端发起恶意请求。

另一种突出的安全问题是 CSRF (Cross-Site Request Forgery,跨站请求伪造)攻击。这种攻击方法的原理是,基于用户在其他服务上成功登录的状况,利用其构造一个将在不同的、被攻击的网站上自动执行的URL。

例如,在访问某个论坛时,一个目的地是银行的URL会被调用,并以图像的形式呈现。如果用户登录到这家银行的网站,该URL构造的操作就会被执行。

SameSite属性大大降低了CSRF攻击的风险,但如果旧的浏览器不能识别该属性,那么此时银行网站返回的响应数据中,应当向用户提供一个随机的令牌,进而让用户在发送认证请求的同时带上cookie和一个有效的令牌。其他网站的页面并不会掌握有效的随机令牌,因而使得CSRF攻击难以实现。

cookie所包含的会话ID可以存储在数据库中,作为一个随机的唯一标识符或富数据令牌(rich token)。

随机标识符是一个随机的数字,用于在数据库中存储相关的信息,其内容主要是谁在访问系统以及会话何时到期。用户每次访问时,服务器都会对此会话ID进行查询,并检索相关信息。进行大规模系统部署时,访问量非常高,这时可能会出问题,因为这种模式的可扩展性比较差。所有Web Worker都要访问存储会话ID的数据库,所以可能会导致性能瓶颈。

可选的解决方案之一是创建一个富数据令牌。其原理是将所有需要的信息直接添加到cookie中,例如,在cookie中直接存储用户ID、到期时间等。这样就避免了对数据库的访问,但同时也使得cookie有可能被伪造,因为所有信息都是公开的。要解决这个问题,可采取cookie数字签名的机制。

cookie数字签名能证明数据是由一个受信任的登录服务器发出的,并且可以被任何其他服务器独立验证。这种方式更具可扩展性,从而避免了性能瓶颈。还可选择对传输的内容进行加密,以免被非法读取。

这种机制的另一个优点是,令牌的生成可以独立于常规业务系统。如果令牌能独立验证,那么就没必要把登录服务与普通的业务放在同一台服务器上。

还可以更进一步,单个令牌签署者可以为多个服务签发令牌。这也是实现 SSO (Single Sign-On,单点登录)的基础:登录到某个认证服务提供者,然后在几个相关的服务中使用同一个账户。这种机制在Google、Facebook或GitHub等服务中很常见,以避免为了某些网站专门去开发特定的登录流程。

这种通过令牌进行授权的操作模式,是OAuth授权框架的基础。

2.3.2 RESTful接口认证

OAuth已经成为API认证访问的通用标准,特别是针对RESTful API接口。

认证(authenticating)和授权(authorizing)是有区别的,从本质上讲,OAuth是一个授权系统。认证是确定用户是谁,而授权则是确定用户能够做什么。OAuth使用scope(作用域)的概念,并返回相关用户所具备的能力范围。

大多数OAuth的实现,如OpenID Connect,会在返回的令牌中同时包含用户信息,以验证用户并返回用户是谁。

OAuth基于这样的理念:通过一个授权者来检查用户的身份,并向他们提供一个令牌,令牌内带有允许用户登录的信息。服务提供者将收到这个令牌并登记该用户,如图2-3所示。

图2-3 认证流程

目前最常见的版本是OAuth 2.0,它具备灵活的登录和流程管理机制。请记住,OAuth并不完全是一个协议,而是提供了某些理念,这些理念可以根据具体的应用场景进行调整。

这意味着你可以用不同的方式来实现OAuth,关键是,不同的授权者会有不同的实现方式。在进行系统整合时,请仔细核实对应的文档。

一般来说,授权者使用OpenID Connect协议,该协议基于OAuth。

访问API的系统,可以直接是最终用户,也可以代表用户进行访问,两种方式有着显著区别。后者可能是某个访问Twitter等服务的智能手机App,或者是某个需要访问GitHub中用户所存数据的服务,如代码分析工具。应用程序本身并不执行API访问操作,而是传递用户的操作请求。

这个过程称为授权码授予(Authorization Code grant)。其主要特点是,授权提供者将向用户提供一个登录页面,并将其与认证令牌一起重定向至服务端。

例如,以下可作为授权码授予过程的调用流程:

如果访问API的系统直接来自终端用户,则可以使用客户端凭据授予模式(Client Credential grant type,简称客户端模式)的流程来代替。在这种情况下,第一次调用将发送client_id(用户ID)和client_secret(用户密码)来直接检索认证令牌。这个令牌将以头部字段的形式放到新的调用中,从而实现请求的认证。

注意,这里跳过了一个步骤,而且更容易实现自动化:

虽然OAuth允许使用第三方服务器来检索访问令牌,但这并非严格的限定,也可以使用与其他业务相同的服务器。这种特性对最后的认证流程是很有用的,因为在此阶段,Facebook或Google等第三方认证服务提供者所提供的登录功能并不是那么有用。在我们的示例系统中将使用客户端凭据授予模式。

自编码令牌

授权服务器返回的令牌内能够包含大量的信息,从而无须通过授权者进行外部检查。

正如我们所看到的,在令牌数据中包含用户信息对于确定用户是谁很重要。否则,流程虽然能以允许该请求的方式结束,但却没有区分具体用户的信息。

为了做到这一点,令牌通常被编码为 JWT (JSON Web Token,JSON Web令牌)。JWT是一种标准规范,它将JSON对象编码成一个URL安全的字符序列。

每个JWT令牌都包含以下元素:

❍一个头部。它包含了关于令牌如何编码的信息。

❍一个有效载荷。令牌的正文。这个对象中的某些字段称为claim(声明),是标准约定的,但它也可以分配自定义的claim。标准的claim并非必需字段,可以用来描述一些元素,如issuer(简称iss,即颁布者),或基于UNIX Epoch(纪元时间,亦称UNIX时间或POSIX时间,简称exp)格式的令牌到期时间。

❍一个数字签名。用于验证令牌是由适当来源产生的。签名数据基于头部的信息,使用了不同的算法。

一般来说,JWT令牌的数据是编码过的,但没有加密。标准的JWT库能解码其各部分的内容,并验证其数字签名是否正确。

可以在交互式工具中测试JWT令牌数据的不同字段和构成:https://jwt.io/。

例如,要用PyJWT(https://pypi.org/project/PyJWT/)来生成一个令牌,如果之前没有安装过,则需要先使用pip命令安装PyJWT:

然后,在打开Python解释器的同时,要创建一个带有用户ID的有效载荷的令牌,并以"secret"作为密钥、使用HS256算法对其进行签名,可以使用以下代码:

接下来就可以对JWT令牌进行解码并提取有效载荷。如果密钥不正确,就会产生一个错误:

所要使用的算法存储在头部信息中,但出于安全考虑,最好只使用预期的算法来验证令牌,而不要依赖头部信息。以前,有的JWT实现存在一些安全问题,以及令牌伪造问题,可以到这里了解相关内容:https://www.chosenplaintext.ca/2015/03/31/jwt-algorithm-confusion.html。

不过,最有趣的不是像HS256这样在编码和解码时采用相同数据的对称加密算法,而是类似RSA-256(RS256)这种非对称的公钥-私钥加密算法。这类算法可以用私钥对令牌进行编码,用公钥对其进行验证(即解码)。

这种应用模式很常见,因为公钥可以被公开广泛传播,但只有拥有私钥的专属授权者才能作为令牌的合法来源对其进行解密。

有效载荷内包含了用于用户识别的信息,同样也可以使用载荷内的信息对请求进行验证,在验证完成之后,则如前文所述继续后续流程。 eX2G5qznAcTHN2NRn6PYiYJPt08SnHzkVHxj7rAqk+wRzexWIlYKS+V3Go9lqtMZ

点击中间区域
呼出菜单
上一章
目录
下一章
×