Skip to content

Latest commit

 

History

History
996 lines (506 loc) · 43.1 KB

File metadata and controls

996 lines (506 loc) · 43.1 KB

八、保护应用

在最后一章中,我们将在应用中实现一些安全措施。事实上,在“安全”的幌子下,我们将执行许多不同的任务。首先,我们将为我们的域提供一个 SSL 证书,然后我们可以将用户和应用之间的敏感交互限制到 HTTPS。随后,我们可以为应用的用户实现安全登录,并将密码存储在数据库的加密字段中。

虽然这看起来有很多任务要完成,但还是有一些好消息。到目前为止,我们在安全方面一直很努力。让我们快速回顾一下我们已经采取的一些安全措施。

  • 访问凭据仅存储在 OpsWorks 中,而不存储在源代码中。
  • 我们的应用中的各种角色和用户只拥有使用他们需要的服务的权限,尽管您可以更进一步,将 IAM 权限限制在特定的 ARN id 上(这是前面提到的一个建议)。
  • 我们的数据库只接受来自 OpsWorks 实例和我们本地 IP 地址的连接。

我们已经在最佳实践方面非常努力了,这真的没有那么难。现在,我们只需确保用户可以安全地连接到我们的应用,并确保他们的凭证得到适当的加密。

使用 HTTPS

你可能已经注意到了浏览器角落地址栏旁边的小挂锁图标(见图 8-1 ),并想知道这是什么意思。这个挂锁已经成为一个通用的标志,表明您通过 HTTPS 而不是未加密的 HTTP 连接到主机。

A978-1-4842-0653-9_8_Fig1_HTML.jpg

图 8-1。

The padlock in the address bar indicates an HTTPS connection

您甚至可以单击挂锁来查看用于验证主机连接的 SSL 证书的更多详细信息。我们将在我们的应用中添加这一层安全性,允许用户通过 HTTPS 进行连接,并使用可信证书对连接进行签名。

当我们使用 HTTPS 协议与我们的应用进行通信时,我们使用的是一种加密传输方法,这种方法由被称为证书颁发机构的可信第三方进行验证。加密与 HTTPS 的通信本身就是一门科学。

我们将使用 HTTPS 来加密用户(客户端)和 CloudFront 之间以及 CloudFront 和负载均衡器之间的流量。这意味着数据在客户端和负载平衡器之间被安全地加密。应用将开始使用会话 cookies,通过负载平衡器将用户的会话连接到特定的实例。我们还将限制对实例的 HTTP 请求,以便只接受来自 ELB 的请求。因为用户只能通过负载均衡器连接来创建会话,所以用户会话被劫持的可能性最多只是理论上的。

SECURITY IS A SHARED RESPONSIBILITY

有时,会在 SSL 协议中发现漏洞,AWS 会快速部署所需的补丁并通过电子邮件通知其客户。因此,您可以放心,当发现漏洞时,AWS 将立即采取行动消除威胁。

作为 AWS 客户,您的责任主要在于遵守其推荐的最佳安全实践。到目前为止,我们已经做到了这一点,将 IAM 角色和用户限制在他们需要的权限内,并将访问密钥和凭证暴露的风险降至最低。

换句话说,AWS 负责云的安全,而您负责云中的安全。当然,SSL 是 AWS 代表您维护的许多活动部件之一。如需了解更多信息,请查看位于 http://aws.amazon.com/compliance/shared-responsibility-model/ 的 AWS 共享安全模型。

值得注意的是,EC2 实例和负载平衡器之间的通信没有获得额外的加密层。增加这一额外的加密层被称为后端认证,这超出了初学者书籍的范围。本书中的课程将为您提供一个安全的应用,但是如果您有特定的安全遵从标准要遵守,您应该彻底检查这些协议。

从我们在 AWS 控制台花费的时间来看,应该很清楚 HTTPS 是各种 AWS 服务支持的传输方法。然而,我们已经把这个特性放到了一边,直到最后,所以我们可以全面地实现这个协议。因为我们从自己的域提供内容,所以我们必须将该域的有效 SSL 证书上传到 AWS。请记住,对我们应用的请求首先通过 CloudFront 和我们的 ELB,然后被这些服务修改。支持 SSL 将要求我们重新配置这两种服务。

当然,存储和验证 SSL 证书属于身份和访问管理的管辖范围。不过,不要在 IAM 中寻找 SSL certificate 选项卡,因为没有这个选项卡!一旦我们有了有效的证书,我们就必须使用 AWS 命令行界面(CLI)将它上传到 IAM。

SSL 证书生成

首先,我们必须为我们的域生成并签署一个 SSL 证书。这是一个多步骤的过程,在其中的一些时间里,你只能靠自己。我尽了最大努力将课程保存在 AWS 控制台中,在那里很容易看到正在做什么,并避免依赖第三方工具。不幸的是,要完成这一部分,我们必须使用许多命令行工具。现在让我们开始安装软件的旅程。

在此过程中,您必须从证书颁发机构获得一个签名证书。您必须完成证书颁发机构要求的步骤,我可以提供您必须采取的步骤的一般描述。我们假设您有能力验证您的应用所使用的域,因为当您获得证书签名时,这在某种程度上是必需的。在接下来的几个步骤中,您必须同时使用 web 浏览器和命令行界面。打开终端或其等效物开始。

安装 OpenSSL

第一步是在您的机器上安装 OpenSSL。OpenSSL 是一个开源加密工具,我们将使用它来生成私钥和证书签名请求(CSR)。您的机器可能已经安装了它。通过在命令行中键入openssl来找出答案。如果这会打开一个 OpenSSL 提示符,而不是抛出一个错误,那么您就可以开始了。

如果没有安装 OpenSSL,可以在这里下载 Mac/Linux 的: www.openssl.org/source 。在 Windows 上,你可以在这里下载一个二进制: www.openssl.org/related/binaries.html

Note

如果你在安装 OpenSSL 时遇到问题,试试 wiki: http://wiki.openssl.org/index.php/Compilation_and_Installation

创建密钥和 CSR

完成安装后,您必须生成一个私钥和相应的证书签名请求,即 CSR。在命令行界面中,输入以下命令:

openssl req -new -newkey rsa:2048 -nodes -keyout photoalbums.key -out photoalbums.csr

代替photoalbums.keyphotoalbums.csr,您可以使用与我们正在获取证书的域相关的文件名。在 CLI 中,系统会提示您填写一些问题。您将填写每一项并按下 Return 键继续。

Country Name (2 letter code) [AU]:

State or Province Name (full name) [Some-State]:

Locality Name (e.g., city) []:

Organization Name (e.g., company) [Internet Widgits Pty Ltd]:

Organizational Unit Name (e.g., section) []:

Common Name (e.g., server FQDN or YOUR name) []:

Email Address []:

大多数这些字段都是不言自明的。继续输入您的国家、州/省、地区和组织信息。在“常用名”字段中,输入您的域名,不要输入 www。一定要用你的邮箱地址。

完成这些后,系统会提示您输入可选字段。您应该按下 Return 键,不输入任何信息。

Please enter the following 'extra' attributes to be sent with your certificate request

A challenge password []:

An optional company name []:

现在在您工作的目录中应该有一个.key.csr。接下来,我们必须向证书颁发机构请求一个证书。

申请证书

我们必须向证书颁发机构提交我们的密钥和证书签名请求。您使用哪个提供商完全由您决定。亚马逊不做任何推荐,而是将用户导向这里的部分列表: www.dmoz.org/Computers/Security/Public_Key_Infrastructure/PKIX/Tools_and_Services/Third_Party_Certificate_Authorities/

使用出售由证书颁发机构签名的证书的供应商通常更容易。根据您用来注册域名的公司,它也可能提供证书服务。或者,NameCheap 是一个信誉良好的厂商,你可以通过他们获得一个由 Comodo 签名的证书,大约 9 美元/年。使用 NameCheap 大约需要十分钟。如果只是想要一个有效的证书进行开发,这可能是阻力最小的路径。

在 SSL 证书供应商处,您将首先选择一个证书包/价格。一旦你支付了费用,你就可以开始申请证书和验证域名了。您将被要求提供 CSR。为此,您必须在纯文本编辑器中打开.csr。内容应该如下所示:

-----BEGIN CERTIFICATE REQUEST-----

FYvKlArPvGZYWNmCMeNDjwa3pxtHWVu6CeXsXUsU4Axwaqtc60VMofEoQCqfwCi+

CDLLoSnwMQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAAzFDJs+FNdUgvNmdsBO

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

FzAVBgNVBAMTDmNsb3VkeWV5ZXMubmV0MRwwGgYJKoZIhvcNAQkBFg1hZGFtQGNy

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

nNuScyAUWgX3yXjeGhCszUIfNMbGEHL3oOKsWvpYP/Kj+ESr5DDrNujHol9n3CQz

CDLLoSnwMQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAAzFDJs+FNdUgvNmdsBO

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

5qeETlUtIHyw9rDLSwF/wvMWS/86uSyuq3wR7GiDPIYSjs5dIWqmAleyroKRaMZd

PyrafU/eidGboCv83NYMSUUyJ0xDVCbIe4EoJUnpOmzu7e07vZDbB5cZDCaJWpuo

l5tf9361gLcJrKxwiHuPffipf9vv4q0M1jwdNgKtUNGSq11FdiYqlfXR87iSTMEI

SevmFhb6EkqLe1sEeDODqKj/FcDZYYjISNEe6ftwPGdBEivRXJpHIH/11wQRQuSw

7ws=

-----END CERTIFICATE REQUEST-----

选择文件的全部内容,并复制粘贴到出现提示的字段中。还可能会要求您选择 web 服务器类型。如果可以的话,选择 Apache+OpenSSL。如果证书有效,域名将自动从您生成 CSR 时输入的通用名称中提取。然后,证书颁发机构将尝试验证该域。为此,它可以向注册为域管理员的电子邮件地址发送一封带有验证码的电子邮件。作为参考,您可以通过在命令行中键入whois yourdomain.com并在响应中找到管理员电子邮件来找到它。您也可以在申请证书的域中选择一个电子邮件地址。在任何情况下,期待一个验证过程的发生,很可能是通过电子邮件。

当您的证书请求被批准时,您将从证书颁发机构收到几个扩展名为.crt的文件。它们应该有逻辑地命名,否则会给你贴上标签。其中一个文件是根 CA 证书;另一个是你的 SSL 证书,应该命名为yourdomain _com.crt之类的东西。除此之外,您还将拥有一个或多个中级 CA 证书。

在我们将这些用于 AWS 服务之前,我们必须解决一个兼容性问题。AWS 只接受 X.509 PEM 格式的证书。我们可以使用 OpenSSL 将我们的证书转换成 PEM。在 CLI 中执行以下命令,然后按回车键:

openssl rsa -in photoalbums.key -text > aws-private.pem

这是在您的机器上生成的私钥,它将被上传到 AWS。接下来,让我们转换公钥,它是由证书颁发机构提供的。输入以下命令,用证书的实际文件名替换yourdomain_com:

openssl x509 -inform PEM -in yourdomain_com.crt > aws-public.pem

现在您有了自己的公钥和私钥。我们需要的最后一个文件是证书链。不需要太多的细节(我不是这方面的权威),有两种类型的认证机构:root 和 intermediate。每个证书由可信机构颁发给一个机构,形成了从我们的域通过中间 ca 到根证书机构的信任链。证书链是通过将来自中间证书颁发机构的证书按顺序拼接在一起而生成的,反映了这种信任链。我们必须创建 X.509 PEM 格式的证书链,就像公钥和私钥一样。

当您收到证书时,它们应该附有一个有序列表,如下所示,但命名约定取决于您使用的证书颁发机构:

  • 根 CA 证书— AddTrustExternalCARoot.crt
  • 中级 CA 证书— CANameAddTrustCA.crt
  • 中级 CA 证书— CANameDomainValidationSecureServerCA.crt
  • 您的 PositiveSSL 证书— yourdomain_com.crt

当我们创建证书链时,我们将记下中间 CA 证书的顺序并颠倒它们。在命令行上,输入以下内容:

(openssl x509 -inform PEM -in CANameDomainValidationSecureServerCA.crt; openssl x509 -inform PEM -in CANameAddTrustCA.crt) >> aws-certchain.pem

明确一下,第二个中间 CA 证书是第一个,第一个是第二个。它们被组合成一个名为aws-certchain.pem的单个.pem文件。如果您打开该文件,它看起来会像下面这样(只是长得多):

-----BEGIN CERTIFICATE-----

MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB

hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G

A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV

bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB

0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap

lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf

+AZxAeKCINT+b72x

-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----

MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv

MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk

ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF

VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq

hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw

B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx

PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR

pu/xO28QOG8=

-----END CERTIFICATE-----

现在我们准备好使用证书了!

AWS 命令行界面(CLI)

如前所述,IAM 仪表板中没有 SSL 视图。相反,我们必须使用 AWS 命令行界面将我们的证书上传到 AWS。为此,我们必须在您的计算机上安装和配置 AWS CLI 工具。在您的操作系统上安装 AWS CLI 的说明可在此处获得: http://docs.aws.amazon.com/cli/latest/userguide/installing.html

按照亚马逊提供的步骤操作,应该没有问题。现在,您可以通过命令行界面执行我们在控制台中执行的任何命令。在我们开始使用它之前,我们必须确保正确配置了权限。

配置权限

我们可以使用我们的 photoadmin IAM 用户,并生成一个访问密钥,用于命令行界面。在 AWS 控制台中导航到 IAM,并从左侧导航栏中选择用户。

第 1 章中,我们让 photoadmin 用户成为 PhotoAdmins 组的成员。用户没有任何独特的策略,而是从 IAM 组继承权限。我们希望使用这个用户将 SSL 证书上传到 IAM,但是给整个组完全的 IAM 访问权限似乎有点极端。我们可以在用户级别临时授予该用户额外的权限。

从用户列表中选择 photoadmin。在用户详细信息视图中,单击附加策略。您将再次选择一个托管策略。向下滚动(或通过在策略类型过滤器字段中键入 iam 来过滤列表)直到找到 IAMFullAccess 并单击复选框(参见图 8-2 )。

A978-1-4842-0653-9_8_Fig2_HTML.jpg

图 8-2。

IAM Full Access policy

单击附加策略以附加它并返回到用户详细信息视图。当您返回到 user detail 视图时,您将看到我们的用户现在在用户和组级别上都有策略,如图 8-3 所示。虽然您不能在这里编辑组策略,但是您可以轻松地编辑或删除用户策略。

A978-1-4842-0653-9_8_Fig3_HTML.jpg

图 8-3。

IAM user and group policies

如果您有兴趣,可以单击“显示策略”来查看策略声明。它应该类似于下面的代码。

{

"Version": "2012-10-17",

"Statement": [

{

"Effect": "Allow",

"Action": "iam:*",

"Resource": "*"

}

]

}

既然用户有了正确的策略,我们必须生成一个访问密钥。在安全凭据标题下,单击管理访问密钥。将出现一个带有“创建访问键”按钮的模式窗口。单击它,模式将会更新,提示您下载凭证的副本。这将是你这样做的唯一机会(见图 8-4 )。单击下载凭据以存储本地副本。

A978-1-4842-0653-9_8_Fig4_HTML.jpg

图 8-4。

Download IAM credentials

接下来,我们必须配置 CLI 以使用我们刚刚创建的凭据。在命令行上,键入aws configure。您将收到如下提示:

AWS Access Key ID [****************4ZAA]:

粘贴刚才保存的凭据中的访问密钥,然后按 Return 键。然后会提示您输入密码。

AWS Secret Access Key [****************FpdG]:

您将输入默认的 AWS 区域。输入us-east-1并按回车键。

Default region name [us-east-1]:

最后,系统会提示您设置默认输出格式。留空并按回车键。

Default output format [None]:

上传 SSL 证书

所有这些配置都导致了一个命令的执行。在一行中,我们将上传证书,并使它们在 CloudFront 中可访问。将证书路径设置为 CloudFront 很重要,因为在这个阶段,对我们的应用的所有请求都要通过 CloudFront。它是我们应用的看门人。确保您的.pem文件与您正在工作的文件在同一个目录中,并输入以下命令:

aws iam upload-server-certificate --certificate-body file://aws-public.pem --private-key file://aws-private.pem --certificate-chain file://aws-certchain.pem --server-certificate-name yourdomain _com --path /cloudfront/ www.yourdomain.com/

这里有一些细微的差别。重要的是,您的–path参数被设置为/cloudfront/ www. yourdomain .com/ ,使用您的域,并在末尾包括尾随斜杠。–server-certificate-name值(在命令中显示为yourdomain_com)应该只是您的域。file://路径是相对的,但是如果你必须使用绝对路径,使用file:///

当您按下 Return 键时,您将得到如下所示的错误或 JSON 响应:

{

"ServerCertificateMetadata": {

"ServerCertificateId": "ASCAJLNQBYPFYEYN5BQNU",

"ServerCertificateName": "yourdomain_com",

"Expiration": "2016-01-05T00:03:54Z",

"Path": "/cloudfront/www.yourdomain.com/T2】

"Arn": "arn:aws:iam::061246224738:server-certificate/cloudfront/``www.yourdomain.com/yourdomain_comT2】

"UploadDate": "2015-01-05T01:04:36.593Z"

}

}

启用 HTTPSin CloudFront

祝贺是理所当然的——这不是一项简单的任务。现在我们有了有效的 SSL 证书,我们必须检查我们的基础设施并启用它。我们必须做的第一件事是在 CloudFront。您已经完成了命令行。在 AWS 控制台中导航到 CloudFront 并选择您的发行版。在常规选项卡中,单击编辑。

我们必须在分发级别启用自定义 SSL 证书。在 SSL 证书头的旁边,您会看到默认的 CloudFront 证书被选中。切换到自定义 SSL 证书,并从下拉列表中选择您的证书(参见图 8-5 )。

A978-1-4842-0653-9_8_Fig5_HTML.jpg

图 8-5。

CloudFront custom SSL certificate

当您选择自定义 SSL 证书时,您必须确定是支持所有客户端还是仅支持那些支持服务器名称指示的客户端。为了支持所有客户端,您必须请求访问,并且您将支付比仅支持 SNI 的客户端高得多的费用。除非您的应用有支持所有客户端的特殊原因,否则您不太可能需要承担这笔额外的费用。这个主题超出了本书的范围,所以请选择后者,除非您有支持所有客户端的特殊原因。

点击右下方的是,编辑。正如我们所知,传播对 CloudFront 分发设置的更改需要几分钟时间。您可以关注您的发行版的 status 字段,等待它从 InProgress 变为 Deployed。完成后,在浏览器中访问你的 Hello World 页面,确保在 URL 前添加https://。你现在应该会看到地址旁边那个令人欣慰的小挂锁图标(参见图 8-1 )。

虽然现在可以通过 HTTPS 连接到我们的域,但这是可选的。你可以在浏览器的地址栏中输入你的 URL,只用http而不是https就可以看到这一点。挂锁会消失!目前,对于我们应用的某些部分来说,这可能是可以的。但是,如果用户要与我们的应用进行交互,我们需要为一些路径建立安全会话。如果你回想一下 CloudFront 上的课程,你会记得我们的发行版没有将 cookies 转发给负载均衡器。

在您的分发中,选择“行为”选项卡。您将看到,如图 8-6 所示,所有行为的查看器协议策略都设置为 HTTP 和 HTTPS。

A978-1-4842-0653-9_8_Fig6_HTML.jpg

图 8-6。

CloudFront behaviors—Viewer Protocol Policy

对于需要 HTTPS 连接和会话 cookie 的请求,我们必须支持一些额外的行为。这些是我们需要限制的路线:

  • /users/login
  • /users/register
  • /users/logout
  • /albums/upload
  • /albums/delete
  • /photos/upload
  • /photos/delete

如果不用创造七种新行为就好了。看到什么模式了吗?我认为我们可以通过以下五条途径来实现我们的目标:

  • /users/login
  • /users/register
  • /users/logout
  • /*/upload
  • /*/delete

用相同的规则创建所有五种行为。原点应该是 ELB,查看器协议策略应该设置为仅 HTTPS。允许的 HTTP 方法应该是 GET、HEAD、OPTIONS、PUT、POST、PATCH、DELETE。转发头和转发 Cookies 应设置为 All(见图 8-7 )。

A978-1-4842-0653-9_8_Fig7_HTML.jpg

图 8-7。

Origin behavior settings

对所有路线重复此过程。完成后,请记住原点的顺序很重要。重新排列它们,使基于会话的行为位于顶部,如图 8-8 所示。

A978-1-4842-0653-9_8_Fig8_HTML.jpg

图 8-8。

Beahviors ordered

像往常一样,我们在 CloudFront 中所做的更改将需要几分钟来传播。同时,我们必须改变 CloudFront 中一个更加模糊的设置。当我们添加 ELB 作为这个 CloudFront 发行版的来源时,我们只允许 CloudFront 通过 HTTP 与 ELB 通信。这意味着 HTTPS 对 CloudFront 的请求将变成对 ELB 的 HTTP 请求。我们希望确保从我们在世界各地的 CloudFront edge 位置到美国东部数据中心的流量是加密的,因此该流量应该通过 HTTPS。

选择 Origins 选项卡,您将在 Origin Protocol Policy 列中看到该问题(参见图 8-9 )。

A978-1-4842-0653-9_8_Fig9_HTML.jpg

图 8-9。

CloudFront Origins

选择您的 ELB,然后单击编辑。在原始设置视图中,将原始协议策略从仅 HTTP 更改为匹配查看器,然后单击是,编辑。从现在开始,对 CloudFront 的 HTTP 请求将通过 HTTP 转发到 ELB,HTTPS 的请求也将通过 HTTPS 转发。传播这些更改也需要几分钟时间。

在 ELB 扶持 HTTPS

请记住,云锋边缘位置分散在全球各地,数据必须从云锋传输到不同地区的 ELB。为了适当地保护我们的流量,我们必须确保到达 CloudFront 的 HTTPS 请求可以被安全地转发到 ELB。这意味着我们的 SSL 证书也必须安装在 ELB 上,并且它需要配置为通过 HTTPS 接受请求。

正如您刚才看到的,HTTPS 请求在端口 443 上被侦听。CloudFront 现在在这个端口上将请求转发给 ELB。当 CloudFront 试图到达端口 443 上的源(ELB)时,它发现一个关闭的端口。CloudFront 认为它根本无法到达原点。

监听 HTTPS 连接

如果请求要通过,我们必须打开 ELB 上的端口。按照 AWS 的说法,我们的 ELB 必须在端口 443 上添加一个监听器。默认情况下,ELB 配置为仅侦听端口 80 上未加密的 HTTP 请求。

导航到 EC2,并从左侧导航栏中选择负载平衡器。选择 ELB 实例后,打开 Listeners 选项卡。您会看到只有端口 80 是打开的。单击“编辑”将 HTTPS 添加到您的 ELB 听众中。将出现一个模式窗口,您可以在其中配置侦听器。

使用负载平衡器端口 443 为 HTTPS(安全 HTTP)添加负载平衡器协议。实例协议应该是 HTTP,实例端口应该是 80。在 SSL 证书列中,单击更改。选择选择现有证书并选择您的证书。图 8-10 显示了完成的设置。

A978-1-4842-0653-9_8_Fig10_HTML.jpg

图 8-10。

Adding an HTTPS ELB listener

单击保存返回到上一视图。点击保存,会看到端口已经打开的确认,如图 8-11 所示。

A978-1-4842-0653-9_8_Fig11_HTML.jpg

图 8-11。

Confirmation that HTTPS listener was created

启用 Cookie 粘性

在 ELB 上还有一个设置我们必须改变。我们知道我们的应用将开始使用基于会话的 cookies。但是由于我们的应用运行在几个实例上,不能保证一旦用户登录,他或她会被两次定向到同一个实例。这将有效地阻止会话完全可用。幸运的是,有一个名为粘性的 ELB 特性,允许我们将用户的会话绑定到特定的实例,并转发相关的 cookie。这意味着,一旦用户在实例上启动了会话,该会话中的所有未来请求都将绑定到该实例。

返回负载平衡器的描述选项卡,找到端口配置设置(参见图 8-12 )。

A978-1-4842-0653-9_8_Fig12_HTML.jpg

图 8-12。

ELB Port Configuration

单击端口 443 配置旁边的编辑。在出现的模式视图中,选择“启用应用生成的 Cookie 粘性”。在 Cookie 名称字段中,输入 connect.sid(见图 8-13 )并点击保存。

A978-1-4842-0653-9_8_Fig13_HTML.jpg

图 8-13。

Enabling cookie stickiness

修改安全组

为了完成我们的配置,我们还需要采取一项额外的安全措施。我们知道 HTTPS 连接是在客户端和 CloudFront 之间以及 CloudFront 和负载均衡器之间协商的。仍然可以直接连接到实例,因为它们不在 VPC 中。虽然恶意用户不太可能发现我们的一个实例的公共 IP,但是我们仍然可以采取额外的措施,通过改变我们的安全组来防止他们直接连接到我们的实例。

在 EC2 中,选择左侧导航中的安全组链接。您将看到为 OpsWorks 自动生成的安全组列表。这些是应用于在各自的 OpsWorks 层中创建的实例的安全规则。要更改这些设置,您可以向堆栈分配一个自定义安全组,或者更改自动生成的安全组。尽管需要额外的努力,我们将创建我们自己的自定义安全组。

在安全组列表中,找到 AWS-OpsWorks-nodejsApp。打开上面的“动作”菜单,然后单击“复制到新文件”。将您的安全组命名为 Photoalbums-nodejs-App,并给它一个有用的描述,如 Photoalbums App 的安全组,限制 HTTP 和 HTTPS 连接。您不需要选择 VPC。

接下来,您必须更改入站 HTTP 和 HTTPS 连接的安全规则。找到 HTTP 行,并将源更改为自定义 IP。在 IP 字段中,输入 amazon-elb/amazon-elb-sg。找到 HTTPS,做同样的改变。完成后(参见图 8-14 ,点击创建。

A978-1-4842-0653-9_8_Fig14_HTML.jpg

图 8-14。

New security group

接下来,我们必须让我们的应用堆栈使用这个新的安全组。导航到 OpsWorks 并选择您的应用堆栈。我们不能让我们的应用层没有任何安全组,所以首先,我们必须添加自定义的安全组,然后删除默认的安全组。从导航菜单中选择层,然后单击应用层旁边的安全性。点击右上角的编辑按钮。从下拉列表中选择您的自定义安全组(参见图 8-15 )并点击保存。

A978-1-4842-0653-9_8_Fig15_HTML.jpg

图 8-15。

Custom security group

从导航菜单转到堆栈。转到堆栈设置,然后单击编辑。在底部,您将看到 OpsWorks 安全组的切换。如图 8-16 所示,当您将其设置为“否”并单击“保存”时,您的实例将不再使用自动生成的安全组。

A978-1-4842-0653-9_8_Fig16_HTML.jpg

图 8-16。

Use OpsWorks security groups

就这样!如果您尝试通过实例的公共 IP 地址连接到实例,请求将会失败。您可以直接连接到您的负载平衡器,但是单个实例与 web 流量隔离。从现在开始,我们只需要修改代码。

应用安全

是时候在我们的应用中构建身份验证了。在执行以下步骤时,请将这些安全技术视为建议。您可能已经有了自己的身份验证策略。无论如何,凭你的经验去做吧。虽然我们将实现许多加密模块中的一个,但您应该能够轻松地将这个模块换成另一个模块。同样,您可以根据自己的需要在应用中实现更严格或更宽松的安全性。这里的目标是演示一种技术,然后按照步骤使我们的应用符合我们在 AWS 中所做的更改。

我们需要在应用中实现两种安全模式。首先,我们必须开始存储密码,而且它们绝对必须在我们的数据库中加密。时不时你会听说一个数据库被泄露,里面有一堆以明文形式存储的密码,任何人都可以窃取。我们不想以那种方式制造新闻。

其次,我们不希望任何用户能够为其他人创建和删除相册或照片。现在,我们允许任何人代表任何其他用户创建和删除内容,只要他们将正确的用户 ID 作为参数传递。这显然是一个糟糕的想法。我们将开始使用安全会话,并将用户 ID 存储在会话 cookies 中。如果没有有效的用户 id 会话,删除内容的尝试将被忽略。

添加会话和加密模块

我们将不得不向我们的应用添加两个额外的模块:easycrypto 和 express-session。Easycrypto 是许多加密和解密密码字符串的库之一。如果您喜欢不同的库,请随意使用它。Express-session 是专门为 ExpressJS 应用构建的会话中间件——就像它听起来那样。

在您的package.json中,将以下内容添加到依赖对象:

"easycrypto": "0.1.1",

"express-session": "^1.7.6",

确保您没有任何尾随逗号,因为您的依赖项应该类似于以下内容:

{

...

"debug": "∼1.0.4",

"easycrypto": "0.1.1",

"express-session": "^1.7.6",

"jade": "∼1.5.0"

}

在命令行上,导航到您的工作目录并键入命令:npm install。您的依赖项应该会像以前一样自动安装。

添加密码加密

接下来,我们将添加密码加密/解密。该代码将完全在/lib/model/model-user.js内发生。打开该文件并在顶部添加一个名为encryptionKey的变量。将该值设置为您喜欢的任意随机字符串。

var encryptionKey   = '80smoviereferencegoeshere';

在文件的底部,我们将添加两个私有方法,用于生成散列密码和解密散列密码。添加清单 8-1 中的代码。

Listing 8-1. Encrypt/Decrypt Hash Password

function generatePasswordHash(password){

var easycrypto = require('easycrypto').getInstance();

var encrypted = easycrypto.encrypt(password, encryptionKey);

return encrypted;

}

function decryptPasswordHash(passwordHash) {

var easycrypto = require('easycrypto').getInstance();

var decryptedPass = easycrypto.decrypt(passwordHash, encryptionKey);

return decryptedPass;

}

当用户注册时,他/她会向我们的路由传递一个密码参数。我们想得到这个密码,散列它,并把它存储在数据库中。当用户稍后登录时,我们会将他/她提供的密码与存储在数据库中的密码进行比较。在我们现有的功能中,我们首先要改变的是保存到数据库中的密码。我们将首先在输入上调用generatePasswordHash(),而不是直接保存密码参数。用清单 8-2 替换createUser功能。

Listing 8-2. createUser

function createUser(params, callback){

var newUser = {

username: params.username,

password: generatePasswordHash(params.password),

email: params.email

}

var query = 'INSERT INTO users SET ? ';

connection.query(query, newUser, function(err, rows, fields) {

if (err) {

if(err.errno == 1062){

var error = new Error("This username has already been taken.");

callback(error);

} else {

callback(err);

}

} else {

callback(null, {message:'Registration successful!'});

}

});

}

现在,用清单 8-3 替换loginUser()。启用此功能后,使用纯文本密码的用户将无法再登录。

Listing 8-3. loginUser

function loginUser(params, callback){

connection.query('SELECT username, password, userID FROM users WHERE username=' + connection.escape(params.username), function(err, rows, fields) {

if(err){

callback(err);

} else if(rows.length > 0){

var decryptedPass = decryptPasswordHash(rows[0].password);

if(decryptedPass == params.password){

var response = {

username: rows[0].username,

userID: rows[0].userID

}

callback(null, response);

} else {

var error = new Error("Invalid login");

callback(error);

}

} else {

var error = new Error("Invalid login");

callback(error);

}

});

}

使用安全会话

这要稍微复杂一点,但它确实取决于您的应用的目标。如果你想将你的应用的某些部分限制为已验证的用户,那么实现起来会简单一些。在我们的例子中,所有的路由都混合了受限和非受限的 API 端点。因此,我们将在单个路由级别手动保护我们的应用。

我们将首先配置我们的应用来使用快速会话中间件。在server.js中,在包含express之后立即添加 express-session 中间件。

var express = require('express');

var expressSession = require('express-session');

var path = require('path');

然后,在其他app.use语句之前,添加以下内容:

app.use(expressSession({secret: 'ssshhhhh'}));

继续用你自己的秘密密钥替换秘密的值— ssshhhhh肯定不是最好的选择。对于快速会话,您可以修改更多的设置,但我们将保持不变。这是启用会话所需的全部内容;现在,我们必须在会话中获取和设置值。首先,当用户登录时,我们必须在他们的会话 cookies 中设置一个标识用户的值。为了简单起见,我们将使用用户 ID,尽管您可能会考虑诸如电子邮件或多个值之类的东西。打开/routes/users.js,定位/login路线。在model.loginUser()的回调中,设置会话的userID,如清单 8-4 所示。

Listing 8-4. /users/login

router.post('/login', function(req, res) {

if(req.param('username') && req.param('password') ){

var params = {

username: req.param('username').toLowerCase(),

password: req.param('password')

};

model.loginUser(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid login'});

} else {

req.session.userID = obj.userID;

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid login'});

}

});

同样,/logout路线也可以简化。它所要做的就是销毁当前会话(参见清单 8-5 )。

Listing 8-5. /users/logout

router.post('/logout', function(req, res) {

if(req.session){

req.session.destroy();

}

res.send({message: 'User logged out successfully'});

});

现在让我们让我们的关键路线需要会话 cookies,而不是POST参数。打开/routes/photos.js。定位/upload路线。我们将在对req.sessionreq.session.userID的条件检查中包含我们所有的功能。此外,我们将向模型发送req.session.userID,而不是req.param('userID')。用清单 8-6 中的代码替换您的代码。

Listing 8-6. /photos/upload

router.post('/upload', function(req, res) {

if(req.session && req.session.userID){

if(req.param('albumID') && req.files.photo){

var params = {

userID : req.session.userID,

albumID : req.param('albumID')

}

if(req.param('caption')){

params.caption = req.param('caption');

}

fs.exists(req.files.photo.path, function(exists) {

if(exists) {

params.filePath = req.files.photo.path;

var timestamp = Date.now();

params.newFilename = params.userID + '/' + params.filePath.replace('tmp/', timestamp);

uploadPhoto(params, function(err, fileObject){

if(err){

res.status(400).send({error: 'Invalid photo data'});

}

params.url = fileObject.url;

delete params.filePath;

delete params.newFilename;

model.createPhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

res.send(obj);

}

});

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

} else {

res.status(401).send({error: 'You must be logged in to upload photos'});

}

});

我们还需要在会话检查中包装/photos/delete。用清单 8-7 中的代码替换它。

Listing 8-7. /photos/delete

router.post('/delete', function(req, res) {

if(req.session && req.session.userID){

if(req.param('id')){

var params = {

photoID : req.param('id'),

userID : req.session.userID

}

model.deletePhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Photo not found'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid photo ID'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

我们也需要在/routes/albums.js中做这个改变。打开文件并用新函数覆盖/upload/delete(参见清单 8-8 )。

Listing 8-8. /albums routes

router.post('/upload', function(req, res) {

if(req.session && req.session.userID){

if(req.param('title')){

var params = {

userID : req.session.userID,

title : req.param('title')

}

model.createAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid album data'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album data'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

router.post('/delete', function(req, res) {

if(req.session && req.session.userID){

if(req.param('albumID')){

var params = {

albumID : req.param('albumID'),

userID : req.session.userID

}

model.deleteAlbum(params, function(err, obj){

if(err){

res.status(400).send({error: 'Album not found'});

} else {

res.send(obj);

}

});

} else {

res.status(400).send({error: 'Invalid album ID'});

}

} else {

res.status(401).send({error: 'Unauthorized to create album'});

}

});

我们还必须对我们的模型进行一些修改。以前,任何用户都可以删除任何人的相册或照片。混乱会接踵而至!我们必须确保用户只删除他们拥有的内容。

/lib/models/model-photos.js中,用清单 8-9 替换deletePhotobyID()方法。

Listing 8-9. Photos deletePhotoByID Method

function deletePhotoByID(params, callback){

var query = 'UPDATE photos SET published=0 WHERE photoID=' + connection.escape(params.photoID)  + ' AND userID=' + params.userID;

connection.query(query, function(err, rows, fields){

if(rows.length > 0){

callback(null, rows);

} else {

if(rows.changedRows > 0){

callback(null, {message: 'Photo deleted successfully'});

} else {

var deleteError = new Error('Unable to delete photo');

callback(deleteError);

}

}

});

}

您会注意到 SQL 查询发生了变化,限制用户更新他们自己的内容。此外,还有一个手动构造并返回给回调的新错误。如果一个UPDATE查询没有改变任何行,那么处理程序中的 rows 对象将拥有一个等于0的属性changedRows。虽然这本身不是一个错误,但是我们的应用应该将它视为一个错误。这意味着用户试图删除一张不存在或不属于他/她的照片。

我们需要将同样的逻辑应用到专辑中。打开/lib/models/model-albums.js并用清单 8-10 替换deleteAlbum()方法。

Listing 8-10. Albums deleteAlbum Method

function deleteAlbum(params, callback){

var query = 'UPDATE albums SET published=0 WHERE albumID=' + connection.escape(params.albumID) + ' AND userID=' + params.userID;

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.changedRows > 0){

callback(null, {message: 'Album deleted successfully'});

} else {

var deleteError = new Error('Unable to delete album');

callback(deleteError);

}

}

});

}

这就是我们要做的所有代码更改。将更改提交到您的代码库中。部署到您的 OpsWorks 实例,并等待该过程完成。完成后,您可以开始测试。您以前的用户现在将无效。应用将尝试解密它们,从而导致不匹配。最简单的测试方法是注册一个新用户,登录,然后创建一个相册。您应该能够通过向域或负载平衡器本身发出 HTTPS POST请求来成功实现这些操作。通过 HTTP 登录到域的尝试将被拒绝。

结论

最后一课到此结束!我们很快完成了各种任务。如果你回头看看我们在第 1 章中的位置,你可能会对自己印象深刻。

在创建这些经验教训的过程中,人们一直在争论该在哪里划线,尤其是在源代码方面。现在,我们有一个具有许多企业应用主要特征的应用。它安全、冗余、可扩展,并使用强大的缓存和通知。但是构建一个真正商业上可行的 web 应用并不是一件简单的任务,我们的仍然有很多缺点。只看用户,我们忽略了密码重置、用户搜索等等。目标与其说是教你如何编写应用,不如说是教你为 AWS 编写应用。希望从这里开始,你对自己在这个软件或你写的任何其他软件中推断这些想法的能力感到自信。

另一个主要挑战是与 AWS 保持同步(更不用说 Express 了!).亚马逊定期推出新功能。他们偶尔会收购一家初创公司,并在六个月后公布他们在 AWS 平台上开发的技术。掌握 AWS 中的新特性需要持续的警惕,但这也令人兴奋。事实上,在本书写作期间,一些特性发生了变化,需要一些修改。

您已经看到了我们所拥有的工具的威力,以及我们是如何快速地构建出十年前普通开发人员无法实现的东西。事实上,web 开发人员的角色在短时间内发生了巨大的变化,AWS 在这些变化中发挥了不小的作用。如果这本书给了你,读者,与这些变化一起成长的信心,那么你已经学到了书中最重要的一课。