云原生安全与DevOps保障
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 用户身份验证的方法

对Web应用的安全运行来说,验证用户身份是最困难的任务之一。设计糟糕的身份验证机制可能会给一个组织带来严重的后果,而且这种事情发生的频率比你想象的还要高。你应该尽一切可能避免将任何密码存储在应用中,这是必须遵守的一条规则。将这些事情交给其他人来完成,并依靠身份提供商来帮你对用户进行身份验证。

本节我们会讨论身份提供商,但是并非所有应用都得依靠它们,所以我们将从最简单的身份验证方法——HTTP基本身份验证开始。

3.3.1 HTTP基本身份验证

HTTP基本身份验证,顾名思义,就是支持在浏览器和Web应用之间进行身份验证的最简单方式。要验证特定用户的身份,浏览器将创建一个包含用户名、一个冒号及用户密码的字符串,然后使用Base64将字符串编码,并放到身份验证HTTP标头里发送给应用。

img

在接收方这一侧,应用执行逆向操作,从解码后的Base64身份验证标头中提取出用户名和密码。

除实现简单之外,当Web应用向浏览器发送一个带有WWW-Authenticate标头的401 HTTP状态码时,浏览器会自动地提示用户输入用户名和密码。

我们来为发票应用实现基本身份验证。首先,你需要一个用户和一个密码,比如samantha和1ns3cur3。将它们作为常量定义在源代码中。这显然很不安全,但我们用这种方法来展示一下HTTP基本身份验证的行为。稍后,你会使用其他安全得多的方法代替这种身份验证方法。

img

接下来,你要在处理发票应用的主页提供请求之前加上身份验证的步骤。在发票应用的getIndex()函数中添加一些代码,这些代码会解析和请求一起发过来的Authorization标头,并将提交过来的用户名和密码与源代码中定义的用户名和密码进行对比。

img

这段代码需要用户名和密码来保护发票应用的主页。你还要让浏览器将Authorization标头发给发票应用,可以使用代码清单3.21中的requestBasicAuth()函数完成。

img

这个函数用HTTP状态码401回应浏览器发送来的、未经身份验证的请求,这会弹出对话框,要求用户输入凭证,如图3.12所示。

img

图3.12 当提示需要HTTP基本身份验证的时候,Firefox向用户显示一个登录对话框,要求用户提供用户名和密码,它们将放在Authorization标头里发送。

HTTP基本身份验证很简单,因此也很流行,但它本身并不安全,原因如下:

•密码在互联网上以明文传输。现在,这已经在TLS中得到了解决。

•Web应用必须在数据库中维护全部用户密码,以检查身份验证请求。

第4章我们将讨论如何为发票应用的基础设施加上TLS,并对浏览器和应用之间的通信进行加密,从而防止网络中的监听器窃取凭证。保存和管理所有用户的密码依然是一个棘手的问题,我们马上展开讨论。

3.3.2 密码管理

无论基础设施的安全措施如何固若金汤,你的数据库终会有被泄露给大众的那一天。现在,这几乎就是一条自然法则,以至于我们在风险评估中总是会问这样一个问题:“如果在Twitter上看到你的数据库泄露了,你该怎么办?”

数据库泄露带来的第一个影响就是用户密码被公开。用户往往会在不同的账户中使用同样的密码,而当攻击者取得了在线照片存储账户的访问权之后,就可以轻易地获得同一用户的银行账户访问权。因此,我们要用不可逆的方式存储密码,这样数据库泄露也不会暴露用户的原始密码。

存储不可逆密码的算法有好几种:bcrypt(链接3.10)、scrypt(链接3.11)、argon2(链接3.12)及PBKDF2(链接3.13)。它们大同小异。存储步骤大致如下:

1.把明文的用户密码作为输入。

2.读取数个随机字节,称为salt值。

3.计算用户名密码加salt值的哈希H1:H1=hash(password+salt)。

4.将哈希H1和salt值保存到数据库中。

这类算法不会在数据库中保存明文的用户密码,保存的只是密码的哈希和数个随机字节的salt值。验证通过比较用户提交的值和数据库中的哈希来完成,步骤如下:

1.获取明文的用户密码。

2.从数据库中读取哈希H1和salt值。

3.计算用户名密码加salt值的哈希H2:H2=hash(password+salt)。

4.如果H2和H1相等,用户提交的密码就和数据库中保存的值是匹配的。

这种方法的安全性来自哈希算法的抵抗力:攻击者几乎完全不可能从哈希中恢复密码。开发人员不应该自己编写哈希算法,而应该使用经过专业的密码专家评审过的算法。几乎所有语言都提供了安全的哈希算法实现。以下这个例子使用了Go语言提供的PBKDF2。

img

以上代码输出的是要保存到数据库中的十六进制哈希和salt值。

img

密码哈希技术看起来简单,但数以百计的在线服务都没有正确地实现它。密码学是一个复杂的领域,不知道在什么时候就会犯错,比如,不同用户共用一个salt值,或者某个哈希参数的值设置得过低。

如果你必须在应用中实现密码哈希,请确保使用的是安全的算法,并请专业人员对你的代码进行审计。我们接下来讨论的方法要更安全一些,就是让外部服务来处理用户身份验证,而应用自己不再保存任何密码。

3.3.3 身份提供商

管理用户和密码是一项劳神费力的工作。不仅因为密码数据有泄露的可能性,而且用户也容易弄丢密码或是重复使用相同的密码。对应用来说,用户密码的管理需要许多定制的功能(重置密码邮件、多因子认证,等等),这些定制功能不会给应用自身带来任何价值,但实现起来却费时费力。

通常更好的方式是让其他人来承担这些成本。大多数现代应用都支持通过第三方登录,这些第三方被称为身份提供商(IdP,Identity Provider)。Google、Microsoft、Facebook、GitHub,还有很多其他服务都可以作为IdP,用户可以使用他们拥有的任何一个身份提供商的账户来登录应用,而不用每个网站都创建一个新账户。

有一些协议实现了通常被称为单点登录(SSO,Single Sign-On)的技术,这种技术让用户登录一次就可以在多个服务中传播身份。大型企业中流行的是安全断言标记语言(SAML,Security Assertion Markup Language),但是它需要对XML文档进行签名和验证,因此实现起来可能会很难。最近几年,OAuth2和OpenID Connect越来越流行,因为它们定义了一个比SAML更容易在应用中实现的协议。

新一代联合身份:OpenID Conne ct

OpenID Connect是在OAuth2的基础上专为网站用户验证身份而构建的协议。OAuth2是一个强大且复杂的管理认证和授权的框架,而OpenID Connect则是实现起来更为简单的OAuth2子集。如果你想更深入地了解OAuth2和OpenID Connect,你应该阅读Antonio Sanso和Justin Richer合著的OAuth2 in Action(Manning出版社于2017年出版)一书。

图3.13展示了用户通过IdP登录应用的一系列步骤:

1.首先,用户访问应用,应用提示用户登录。

2.登录按钮将用户重定向到IdP,使用的地址在其查询字符串中包含了自定义参数。

3.IdP提示用户登录(或者重用已存在的会话),并通过第二次重定向让用户回到应用。

4.第二次重定向包含了一个授权码,应用将它提取出来并用来交换令牌。

5.应用使用API令牌从IdP获取用户信息。

6.这时候,用户才算登录了应用,并且可以继续使用该应用。

这一套连接流程显然比HTTP基本身份验证要复杂得多,但额外的复杂度带来了显著的安全优势:应用再也不用管理用户密码,甚至都不用访问用户密码。尽管请求流程看起来很复杂,但应用和身份提供商的集成却很简单。作为演示,我们将使用Google作为IdP,向发票应用添加OpenID Connect的支持。

img

图3.13 OpenID Connect/OAuth2舞蹈让用户可以通过第三方登录应用。

首先你要从Google拿到一个客户端ID和一个密钥。要拿到这些信息,你可以在链接3.14的Credentials控制台里创建一个OAuth 客户端ID,如图3.14所示。除应用名称和类型之外,页面上还需要两条信息:

•授权的JavaScript源(Authorized JavaScript Origin),这是托管应用的域的列表,只有来自这些域的JavaScript才能向Google IdP发起查询。

•授权的重定向URL(Authorized Redirect URL),这是用户登录后可以被重定向的地址列表。在这里列出所有可以接受的URL,稍后在每次OAuth舞蹈时选择将用户重定向到其中一个地址。

img

图3.14 ***.google.com的Web控制台会生成在应用中使用的客户端ID和密钥。

完成创建步骤后,控制台上将会显示一个客户端ID和一个密钥。接下来,你可以在发票应用的代码中创建一个配置,并在Google的IdP中使用这些凭证信息。Go有一个OAuth2包:golang.org/x/oauth2,你可以在实现的时候使用它。代码清单3.24展示了OAuth2配置,它用到了与Google交互的凭证信息及URL。

img

配置就绪后,接下来就要实现OAuth流程的第一步,包括将用户转给Google并要求他们进行身份验证。在发票应用中,给主页加上验证用户的链接。

img

这个链接只会将用户转到一个位于/authenticate的新终端节点。这个终端节点使用正确的参数来创建一个重定向URL,并将用户重定向到Google。身份验证终端节点的代码如代码清单3.26所示。

img

应用和IdP之间来回重定向也需要使用CSRF令牌来防范CSRF攻击。OAuth2流程用到的令牌类型和之前保护表单提交用到的令牌类型是一样的。

发票应用返回的重定向URL将之前定义的配置参数传给IdP。URL见链接3.15,并且设置了下面这些查询字符串参数:

img

对于IdP,用户会看到登录提示,要求他们同意登录操作。如果他们同意了,Google会将他们重定向到发票应用,并且将oauth授权码放在重定向URL的查询字符串中。

我们回到发票应用,你要增加一个终端节点/oauth2callback来处理IdP返回的重定向请求。在处理这个请求的时候,首先要验证CSRF令牌,然后从URL的参数中提取oauth授权码。

这个授权码被用于交换API令牌,而令牌将被用来直接从Google获取关于用户的信息(使用代码清单3.24中配置的TokenURL地址)。现在,应用可以确保用户能够正确地通过身份认证。Google提供的信息可以被应用用来识别用户,并可能据此给用户授予各种权限。代码清单3.27实现了使用API令牌获取用户信息的那部分工作流。

img

我们对OpenID Connect/OAuth2的实现过程只是简要地介绍了一些细节,但你应该明白了其思路:通过多次HTTP重定向,应用可以依靠第三方对用户进行身份验证。使用IdP是保护现代Web应用最有效的手段之一,因为凭证的处理和保护完全交给了IdP。应用不再需要防止暴力攻击,也不需要实现密码强度检查,更不需要支持多因子身份验证,这些都交给IdP去处理了。你应该始终在应用中尝试使用IdP,而不是自己实现密码管理。

OpenID Connect保护了身份认证阶段的安全,但应用还要负责创建和管理会话。我们马上来讨论这个话题。

3.3.4 会话和Cookie的安全性

在使用HTTP基本身份认证的时候,浏览器的每次请求都会带上Authorization标头一起发送。应用在每次收到用户的请求时,都可以对用户名和密码进行校验。你不需要会话,因为身份验证一直在持续地进行。

如果应用依赖IdP,每一次用户请求都需要一次如此复杂的ouath舞蹈,那么应用是吃不消的。应用必须在用户通过身份验证后马上创建一个会话,并在收到新的请求时检查会话的合法性。

会话可以是有状态的,也可以是无状态的:

•有状态的会话需要在数据库中保存一个会话ID,并且检查用户是不是在每次请求中都发送了会话ID。在处理请求之前,应用会对数据库中保存的会话状态进行验证。

•无状态的会话不会在服务端保存任何数据,而只是简单地检查用户是否拥有可信的和最新的会话cookie。对于高性能应用来说,无状态的会话带来的好处是不需要每次请求都往返于数据库。

无状态的会话带来了性能优势,但缺少了在服务端销毁会话的能力,因为服务器不知道哪些会话是活跃的,而哪些不是。对于有状态的会话,销毁一个会话就和删除数据库记录一样简单,一旦会话被销毁,就要强制用户重新进行身份验证。

当恶意用户滥用你的应用时,销毁会话通常十分关键,并且还能阻止心怀不满的雇员在解约后继续频繁地访问应用。根据应用仔细斟酌你需要哪种会话类型,并尽量使用有状态的会话。

3.3.5 测试身份验证

对于外部测试来说,身份验证可能是少数几个较为复杂的领域之一。像ZAP这样的漏洞扫描器,可以通过扫描网站来发现缺少必要身份验证的页面和资源,但这种扫描效果很有限,而且无法确定OpenID Connect这样的流程的正确性。

除了依靠外部扫描器,开发人员还应该编写单元测试来对应用的身份验证层进行验证。QA团队应该将身份验证流程作为应用验证的一部分来执行。靠人力测试的身份验证流程不如自动化测试高效,但在OpenID Connect、SAML及其他身份验证层得到扫描器支持之前,这是最好的方法了。

现在,发票应用已经相当安全了,但它的安全性严重依赖外部库,而这些库未来有可能被破坏。在结束WebAppSec这一章之前,我们来讨论一下让依赖保持与时俱进的技术。