如何在 AWS 上托管的 Angular JS 单页应用程序中使用 SAML





5.00/5 (7投票s)
本文介绍了在 Amazon Web Services 上托管的 Node.js 网站设置单点登录 (SSO) 并为 HTTPS 配置 Elastic Beanstalk 的过程。
引言
本文介绍了如何在 Amazon Web Services (AWS) 上托管的、带有自定义域名的单页应用程序 (SPA) 中使用单点登录 (SSO)。
要讲解的内容很多,让我们一步步来。今天我们将学习如何
- 安装 Node.js 以在本地运行 Express 单页应用程序
- 注册 Auth0 账户
- 在 Auth0 中设置客户端并获取元数据
- 在 Express.js 中为单页应用程序设置单点登录 (SSO) 代码
- 在本地测试解决方案
- 通过 .ebextensions 配置 Elastic Beanstalk 以支持 HTTPS
- 准备 zip 文件并将其部署到 Elastic Beanstalk
- 设置 Amazon Web Services Elastic Beanstalk 以托管 Node.js 网站
- 设置 Amazon Web Services Route 53 以托管自定义域名
背景
SAML
SAML 代表安全断言标记语言。我们不会深入研究 SAML,但简而言之
SAML 是一种开放标准,用于在各方之间交换身份验证和授权数据,特别是身份提供者和服务提供者之间。顾名思义,SAML 是一种基于 XML 的标记语言,用于安全断言(服务提供者用于做出访问控制决策的声明)。维基百科
我们在此处使用的标准版本是 2.0,这是撰写本文时(2018 年 6 月)的最新版本。
SAML 通信的主要参与者,除了最终用户之外,还有服务提供者 (SP) 和身份提供者 (IdP)。在本例中,SP 是用户想要使用的网站。IdP 是一个服务,它持有授权访问该网站的人员数据库。在此演示中,SP 将是我们正在构建的网站,而 IdP 将是 Auth0。
SSO
SSO 代表单点登录。当有许多人需要访问不同地方,而您不希望他们单独管理所有不同系统的密码时,您可以实现 SSO,让 IdP 为他们处理。
AWS
Amazon Web Services (AWS) 是许多不同服务的集合。有关 AWS 的更深入信息,请参阅aws.amazon.com。您可以免费创建 AWS 账户,并且许多服务也是免费的。如果您决定跟随操作,我们将使用的服务是 Route 53 和 Elastic Beanstalk。有关这些的更多信息,您可以参考以下链接。
SAML 和 SSO
想象一下,您的企业多年来开发了许多不同的系统,每个系统都在自己的孤岛中运行。跨部门工作的人员必须记住每个系统的密码。他们可能还需要定期更新密码,更糟糕的是,密码的过期时间可能不同步。然后,通过公司合并,您会将企业规模翻倍,并获得更多系统。如果您的人员可以使用一个站点进行身份验证,并获得对所有服务的访问权限,而无需每天多次进行登录过程,那不是很棒吗?隆重推出 SAML 和 SSO。
正如我之前提到的,维基百科将 SAML 定义为“交换身份验证和授权数据的开放标准”。那么什么是交换身份验证和授权数据的开放标准呢?SAML 是一个开放标准,因为它不是专有的。它定义了与用户访问特定服务的身份验证相关的数据结构。“SA”在 SAML 中代表安全断言,“ML”代表标记语言。SAML 定义包含用户访问信息的 XML 文档。请记住,这里正在进行的交互是在服务提供者“SP”和身份提供者“IdP”之间进行的。服务提供者将请求身份验证,而 IdP 断言用户已通过身份验证。在我们的例子中,SP 是一个网站,但这不一定如此,任何可以通过 HTTP 进行通信的应用程序都可以参与协商。(当然,您可能希望为此使用 HTTPS)
此过程可以通过不同的方式实现。您的企业可以创建一个仪表板,允许用户登录,然后从仪表板上显示的选项中选择以访问各种系统。这被称为*IdP 发起的 SSO*。另一种实现方式是通过参与的各种系统。当用户尝试登录任何系统时,该系统会与 IdP 进行检查。这称为*SP 发起的 SSO*。如果用户已在 IdP 登录,则响应会立即返回。如果用户未通过身份验证,则 IdP 将执行该步骤,然后发送响应。如果用户无法通过身份验证或未获得服务访问权限,则响应将不会返回到 SP。
够了,让我们开始吧 - 示例应用程序
让我们先看看骨架应用程序。解压示例文件,然后查看根目录下的文件夹和文件。
在这篇文章中,我们不会深入探讨 Express 应用程序的构建方式或此处所有文件的用途。如果您想了解更多关于 Express 应用程序的信息,可以访问Express Web 框架和Express 教程。对于本文,我们将重点关注如何在本地运行该网站进行开发和测试,如何实现单点登录,以及如何将网站部署到 AWS。
这是一个单页应用程序 (SPA)。一旦用户获得对该站点的访问权限,所有流量都将转到单个页面“index2.html”。它命名为 index2 是为了防止 Web 服务器在用户仅输入域名时提供“默认”页面。该应用程序在 Node.js 环境中本地运行。这允许您的本地计算机在没有 Web 服务器的情况下运行应用程序。在此处Nodejs.org 查找有关 Node.js 的更多信息。如果您还没有安装 Node.js,也可以从那里下载并安装。
安装 Node.js 时,请务必勾选将 nodejs 添加到系统路径环境变量的选项。Node.js 从命令行运行,因此请打开命令窗口,导航到文件所在的位置(在文件夹窗口中按住 Shift 键并右键单击,以便在上下文菜单中包含命令窗口选项)。我们需要做的第一件事是安装站点所需的包。如果您打开示例中的 node_modules 文件夹,您会看到它是空的。Node.js 示例通常是这种情况。依赖项按需下载。为此,我们将使用Node Package Manager,它作为 Node.js 的一部分自动安装。
让我们先看看 NPM 将安装什么。导航回根文件夹并打开 package.json 文件。
{
"name": "AngularJS-AWS-SSO",
"version": "1.0.0",
"description": "Sample site to show SSO on AWS",
"scripts": {
"start": "node express.js",
"start:debug": "npm run build && cross-env NODE_ENV=production node --inspect --trace-warnings express.js",
"start:build": "npm run build && cross-env NODE_ENV=production node express.js",
"build": "webpack --config ./webpack/config.prod.js --progress --colors --optimize-minimize"
},
"dependencies": {
"angular": "^1.6.2",
"angular-route": "^1.6.2",
"body-parser": "^1.17.1",
"cookie-encryption": "^1.6.0",
"cookie-parser": "^1.4.3",
"cors": "^2.7.1",
"express": "^4.15.0",
"morgan": "^1.8.1",
"passport": "^0.2.2",
"samlify": "^2.3.6"
},
"devDependencies": {
"babel-core": "^6.23.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.22.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": ".....
},
"license": "ISC",
"author": "Mike Baker"
}
package.json 文件包含两个“dependencies”部分。一个用于运行时,一个用于开发。这两个部分都将由 npm 包安装程序使用。我们可以通过提供包名来使用 npm 安装单个包,但现在我们需要所有包。
在命令行窗口中键入 npm install
并按 Enter 键。
我第一次这样做时,控制台中显示了几个指向 Python 的错误消息。如果发生这种情况,您可能需要从Python 下载下载并安装 Python。请务必获取 2.7.xx 版本而不是最新版本。一些依赖文件需要 2.7 版本。
如果一切顺利,您应该会看到大量软件包正在下载。如果您再次打开 node_modules 文件夹,您应该会看到大约 750 个文件夹,尽管 package.json 文件中的文件夹数量远少于此。每个软件包都有自己的 package.json。节点包管理器会处理每个软件包自己的 package.json,并安装每个软件包的所有依赖项。
npm 包管理器除了安装包之外,还可以用于运行应用程序。让我们再次查看 package.json 文件顶部。
{
"name": "AngularJS-AWS-SSO",
"version": "1.0.0",
"description": "Sample site to show SSO on AWS",
"scripts": {
"start": "node express.js",
"start:debug": "npm run build && cross-env NODE_ENV=production node --inspect --trace-warnings express.js",
"start:build": "npm run build && cross-env NODE_ENV=production node express.js",
"build": "webpack --config ./webpack/config.prod.js --progress --colors --optimize-minimize"
},
“scripts”部分包含几个可以通过 npm 运行的脚本。例如,如果我们输入 npm run start
,npm 将运行名为“start”的脚本,该脚本将运行 node express.js
。我们也可以直接在命令行窗口中键入 node express.js
来达到相同的结果。
我们现在还无法运行该站点。请记住,该页面将从 IdP(身份提供者)请求 SSO 信息。我们还没有设置 IdP,所以让我们开始设置。
更多 SAML
身份提供者 (IdP)
有许多 IdP 服务可用。我选择用于进行内部测试的服务是 Auth0。注册是免费的,到目前为止我还没有被收费。您可以通过访问Auth0.com 来注册 Auth0。输入要用作用户 ID 的电子邮件并创建密码。
首次登录后,系统会要求您创建域。
设置账户类型。在域名之后,您需要填写有关公司类型的一些信息。
下一步是设置一个测试用户账户。设置账户后,您应该会看到“仪表板”。在屏幕左侧,单击“Users”,然后单击“Create User”按钮。
在弹出的窗口中填写您想要用于测试用户的值,然后单击“SAVE”。
完成公司和测试用户的设置后,我们就可以创建客户端了。Auth0 中的客户端一词指的是将使用 SSO 的应用程序。换句话说,就是我们的“服务提供者”。
创建客户端后,我们将需要保存 Client ID 以备后用,然后更改一些设置。单击“Settings”链接,将“Client ID”复制到剪贴板并保存在某处。(我们将保存一些内容以备后用,所以您可能需要创建一个文件夹来存放一些文本文件。)
我们可以使用许多这些设置的默认值,因此我们将只关注我们需要在此处更改的设置。滚动到“Allowed Callback URLs”部分。
“Allowed Callback URLs”条目告诉 Auth0 用户登录后可以将其结果发送到哪里。这称为“断言使用者服务”。条目需要域名和 Express 路由 '/sso/acs'。我们将使用 Node.js 在本地运行站点进行测试,因此我们需要允许回调到 localhost。更新设置以匹配图片。请务必使用您网站将使用的域名(该步骤稍后会进行)。图片看起来像两个单独的行,带有回车符,但实际上不是。只有一行,三个项目,以逗号和空格作为分隔符。我们也需要为 Allowed Logout URLs 做同样的事情。在这些条目中,区别在于路由,即 '/sso/logout'。
在此屏幕上还需要进行最后一步。滚动到屏幕底部,然后单击“Show Advanced Settings”。
在“Advanced Settings”部分,单击“Endpoints”,然后单击 SAML Metadata URL 旁边的“Copy”图标。
SAML 元数据
我之前提到过,SAML 使用 XML 来定义 SP(服务提供者)和 IdP(身份提供者)之间的交互和信息中继。为了做到这一点,必须提前在 SP 和 IdP 之间建立协议。这是使用 SAML 元数据 XML 文件完成的。我们为 SP 和 IdP 生成元数据,并将它们合并到 SP(我们的 Web 应用程序)中。这些元数据文件中的值由 samlify 库用来构建在用户尝试使用应用程序时发送到 IdP 的信息。
在上一步中,我们将一个“SAML Metadata URL”复制到了剪贴板。将该 URL 粘贴到一个新的浏览器窗口地址栏中。根据您的浏览器设置,您可能会在浏览器中看到一些 XML,或者您可能会被提示下载文件。如果您在窗口中看到 XML,请切换到源代码,以便将其复制到剪贴板并将其保存在名为 COMPANY_IDP_metadata_localhost.xml 的文件中。这是 IdP 元数据。
接下来我们需要 SP 元数据。Auth0 没有用于生成 SP 元数据的工具,但有一个在samltool.com。导航到该网站,单击菜单,然后选择“ONLINE TOOLS”。
SSO 使用涉及私钥和证书的加密来(可选地)加密 SAML 消息。对于 localhost 测试,我们可以使用自签名证书。单击菜单左侧的“X.509 CERTS”,然后单击“Obtain Self-Signed Certs”。填写框中的信息……
向下滚动并单击“GENERATE SELF-SIGNED CERTS”按钮。您将获得一个私钥和一个 X.509 证书。复制这两个框中的内容并将它们保存在单独的文件中。
我们终于准备好构建 SP 元数据文件了。单击菜单左侧的“BUILD METADATA”,然后单击“SP”。
您可能还记得我们在早期步骤中将 Client ID 保存在了一个文本文件中,以及 X.509 证书。我们现在需要它们来填写这些框。找到这些项目并将表单填写如下
滚动到所有可选项目下方,然后单击“BUILD SP METADATA”按钮,一个新的框将出现,其中包含我们需要的 XML 格式元数据。复制该窗口中的内容并将其保存到名为“COMPANY_SP_metadata_localhost.xml”的文件中。
SAML 元数据 xml
让我们打开 IdP 元数据,看看一些关键值。如果您按照说明操作,文件应为 COMPANY_IDP_metadata_localhost.xml。
<EntityDescriptor entityID="urn:test-dev.auth0.com" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIC/zCCAeegAwIB...VBLvQ/NpACq/</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test-dev.auth0.com/samlp/G87R....Mr6l/logout"/>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test-dev.auth0.com/samlp/G87RO....8Mr6l/logout"/>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test-dev.auth0.com/samlp/G87R....Mr6l"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test-dev.auth0.com/samlp/G87R....Mr6l"/>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="E-Mail Address" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Given Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Name" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Surname" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="Name ID" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>
</IDPSSODescriptor>
</EntityDescriptor>
在上面的 XML 示例中,我*高亮显示*了一些项目。entityID 是 IdP 的 ID。这包括您在 Auth0 中设置账户时使用的域。下一个项目表示这是一个 IdP 项目。KeyDescriptor 提供了有关正在使用的加密选项的信息。在这种情况下,它告诉我们其中包含的 X509 证书数据用于签名数据。SingleLogoutService 告诉我们这个 IdP 支持单点注销以及其 URL。单点注销意味着当用户注销其中一个应用程序时,它应该从所有应用程序中注销。SingleSignOnService 意味着同样的事情,但用于登录而不是注销。我高亮显示的最后一段信息是“identity/claims”部分。此部分提供了 IdP 在有人登录时(稍后我们将看到)返回给我们的所有数据项列表。IdP 将验证用户并向我们返回信息,在某种意义上声明“我声称拥有此电子邮件地址、此名字、此姓氏”等等。
接下来,我们将查看 SP 元数据。还记得文件名吗?COMPANY_SP_metadata_localhost.xml
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"validUntil="2018-06-24T15:04:39Z" cacheDuration="PT604800S" entityID="https://:8080/sso/G87RO....Mr6l">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIICjDCCAfWgA...OFN1X+k9gnBbdvVV1lik4wY=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIICjDCCAfWgA...OFN1X+k9gnBbdvVV1lik4wY=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<mdSingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://:8080/sso/logout"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://:8080/sso/acs" index="1"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>
我高亮显示的第一项是“validUntil”值。生成此值的工具为您提供了几天的时间,直到过期。您可能需要给自己更多时间。之后,您会注意到 entityID,它是从您在表单中填写的数据中复制的。紧接着下一行是 SPSSODescriptor,因此我们知道我们在这里谈论的是 SP。请注意,这里有两个 KeyDescriptor 部分。我们可以看到它一次用于签名,一次用于加密。我高亮显示的最后两项是 SingleLogoutService 和 AssertionConsumerService。由于我们谈论的是 SP,SingleLogoutService 指定了将处理来自 IdP 的注销消息的路由。AssertionConsumerService 指定了在有人成功登录后 IdP 发送 POST 消息的 URL。当一切就绪并可以测试运行应用程序时,我们将逐步完成此过程。我们快到了。
回到示例应用程序
让我们回到示例应用程序,这次打开“resources”文件夹。
将 COMPANY_SP_metadata_localhost.xml 和 COMPANY_IDP_metadata_localhost.xml 文件替换为您之前保存的文件。然后打开 dummy private-key_localhost.pem 文件(它只是一个文本文件,您可以使用记事本打开它),并将其内容替换为您在 SAML 工具中生成并保存的私钥。
接下来,退回 resources 文件夹,然后打开 'build.bat' 文件。我们不会对其进行任何更改,只是看看它的作用。这是文件中的摘录。
if %~1==production GOTO :production
if %~1==staging GOTO :staging
if %~1==localhost GOTO :localhost
GOTO :syntax
:production
copy resources\https-instance_production.config .ebextensions\https-instance.config
copy resources\private-key_production.pem server\config\private-key.pem
copy resources\COMPANY_IDP_metadata_production.xml server\config\COMPANY_IDP_metadata.xml
copy resources\COMPANY_SP_metadata_production.xml server\config\COMPANY_SP_metadata.xml
GOTO :buildit
:staging....
我通常的工作流程包括在 localhost(或 VM 服务器,但这是另一回事)上进行初步开发和测试,然后有一个用于真实环境但不对公众开放的暂存服务器,然后是生产构建。Express 应用程序使用“server/config/”文件夹中的私钥和元数据文件,但每个构建都需要不同的文件。此批处理文件使用 resources 文件夹,并根据您在运行批处理文件时指定的项,将正确的私钥、SP 和 IdP 元数据文件放在适当的位置。对于暂存和生产环境,它还会打包 Amazon Web Services 将需要部署到 Elastic Beanstalk 的 zip 文件。是的,有很多方法可以在没有批处理文件和 goto 的情况下做到这一点。也许下次我会尝试 Python。现在,它使用安装在默认位置的 WinZip 命令行工具。关闭批处理文件,并使用命令行窗口(Shift+右键单击)运行 build localhost
。
您应该看到三次“1 file(s) copied”,现在正确的元数据和私钥文件已到位并准备就绪。让我们运行应用程序看看会发生什么。在命令行窗口中输入 npm run start:build.
。您应该看到“Express server running at localhost:8080”。现在您可以打开浏览器窗口,并在地址栏中输入“https://:8080”。Auth0 登录框应该会显示。
它是如何工作的呢?让我们看看 express.js 中的代码。打开该文件,我将描述每个部分。
顶部我们包含了 cookie-parser 和 cookie-encryption。当我们从 IdP 收到响应时,我们会存储一个加密的 cookie,表明用户已登录。我们还包含了 'samlify' 库,它将处理通信和解析 SP 和 IdP 之间流动的数据。顶部还设置了将使用的 localhost 端口。这可以通过启动脚本设置,但我们只是让它默认为 8080(当然,如果您不想使用 8080,可以将其更改为任何数字)。
const cookieParser = require('cookie-parser');
const cookiee = require('cookie-encryption');
...
const saml = require('samlify');
const port = process.env.PORT || 8080;
接下来是创建 ServiceProvider 变量 'sp' 和 IdentityProvider 变量 'idp'。您可以看到这里的代码使用“./server/config/”文件夹中的私钥和元数据文件。这些文件是由批处理文件放置在那里,并且由批处理文件重命名,以便相同的代码可以处理任何构建目标,无论是 localhost、暂存还是生产。`privateKeyPass` 值是一个空字符串。当您创建自签名证书时,说明中没有包含设置密码,尽管那里有一个用于此的框。如果您设置了密码,那么您将在此处输入。
在 IdP 设置中,请注意 isAssertionEncrypted 设置为 false。如果您将其设置为 true,则 samlify 会将其包含在 SAML 数据中,让 IdP 加密响应。当响应进来时,samlify 库将使用元数据中的 X.509 证书在解析之前对其进行解密。
// sso
var ServiceProvider = saml.ServiceProvider;
var IdentityProvider = saml.IdentityProvider;
var sp = ServiceProvider({
privateKey: fs.readFileSync('./server/config/private-key.pem'),
privateKeyPass: '',
requestSignatureAlgorithm: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
metadata: fs.readFileSync('./server/config/COMPANY_SP_metadata.xml')
});
var idp = IdentityProvider({
isAssertionEncrypted: false,
metadata: fs.readFileSync('./server/config/COMPANY_IDP_metadata.xml')
});
下一部分设置了我们将用于在有人登录后存储 cookie 的 cookieVault。COOKIE_CODE 只是我随意选择的一组随机字符。
// get encrypted cookie
const COOKIE_CODE = 'w450n84bn09ba0w3ba300730a93nv3070ba5qqvbv07';
const cookieHoursMaxAge = 12;
var cookieVault = cookiee(COOKIE_CODE, {
cipher: 'aes-256-cbc',
encoding: 'base64',
cookie: 'ssohist',
maxAge: cookieHoursMaxAge * 60 * 60 * 1000,
httpOnly: true
});
接下来,我们将跳到 express.js 文件的底部附近,查看 app.get(*, .... 这一行会捕获所有未在任何先前项目(Express 应用程序从上到下工作)中匹配的流量。
第二行读取 cookie 以查看它是否等于 COOKIE_CODE。请记住,这是我们用于指示登录是否完成的标志。第一次访问此处时,它不会等于,因此它会转到 else。有一个测试可以排除 'favicon' 调用,如果不是 favicon,则会重定向到自身的 /sso/login 路由。所以我们来看看。
// Check for login cookie and go to index2 if found
// otherwise send to /sso/login
// Send all requests to index.html so browserHistory works
app.get('*', function (req, res) {
if (cookieVault.read(req) === COOKIE_CODE) {
res.sendFile(path.join(__dirname, publicPath, 'index2.html'))
} else {
if (!req.url.includes('favicon'))
res.redirect('/sso/login');
}
})
在这里,我们调用上面创建的 sp (ServiceProvider) 变量来创建一个请求,让 IdP 登录用户。然后,应用程序重定向到 IdP 系统上的 URL。
// call the IdP for login.
app.get('/sso/login', function (req, res) {
url = sp.createLoginRequest(idp, 'redirect');
res.redirect(url.context);
});
还记得 IdP 元数据中的 SingleSignOn 条目吗?
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test-dev.auth0.com/samlp/G87....Mr6l"/>
Samlify 使用 idp 变量和“redirect”选项来确定重定向。最终结果是 Auth0 显示的登录对话框。
当用户完成登录步骤后,IdP 会发送一个包含 SAML 响应的 POST 消息。此 POST 请求将发送到 SP_metadata 文件中 AssertionConsumerService 的 URL。该条目中的 /sso/acs 与 Express 路由 app.post('/sso/acs/' ...) 匹配。我们处理该请求的代码是这个
// if parsing the response results in a nameid being present then they logged in
// store the cookie and proceed, otherwise redirect back to login
app.post('/sso/acs', function (req, res) {
sp.parseLoginResponse(idp, 'post', req)
.then(parseResult => {
if (parseResult.extract.nameid) {
cookieVault.write(req, COOKIE_CODE);
res.redirect("/");
} else {
res.redirect('/sso/login');
}
})
.catch(console.error);
});
正如注释所示,此函数确保响应中包含 nameid。在函数开头,我们调用 sp.parseLoginResponse。这会返回一个 Promise,我们使用 .then(parseResult ... 来检查响应中是否有 nameid。如果找到 nameid,它将写入 cookie 并重定向到“/”。如果未找到 nameid,我们将再次重定向到 /sso/login,这会返回到 IdP 以显示登录对话框。当我们重定向到站点根目录“/”时,cookie 已写入,这次测试通过,响应为 index2.html。
您可能已经注意到我们实际上并没有在这里使用 nameid 值。它正在检查它是否存在,然后将 COOKIE_CODE 写入 cookie。根据您构建的应用程序类型,您可能希望在此处放置更强大的内容。例如,您可以使用 nameid 从数据库中查找用户,并根据谁在使用您的应用程序来允许特定的权限。此外,您可以看到 .catch 函数只是将错误回显到控制台。如果您想在无法解析响应时显示错误消息,可以在此处执行此操作(或者您可以调用一个通用错误报告模块)。
SAML 流量
SSO 事务可以由 SP 或 IdP 启动。这里提供的示例代码使用 SP 发起的登录。当用户尝试使用网站时,app.get(*, ...) 函数会拦截所有流量并检查 cookie 以查看该会话是否已登录。如果 cookie 存在,则流量将被发送到 index2.html 文件。如果 cookie 不存在,则流量将被重定向到 /sso/login 处理程序。/sso/login 处理程序准备登录请求并重定向到该 URL,因此是 SP 发起的登录。那么幕后发生了什么?我们可以看到这种交换吗?实际上,我们可以。有几个扩展程序可以监视 SAML 流量在 SP 和 IdP 之间传输的过程。您可以在Chrome Web Store中找到它们。
我选择了第一个。安装完成后,您可以按 F12 打开开发者工具,然后在选项卡中找到 SAML 来查找 SAML 扩展程序(它可能隐藏在此图片中)。
打开 SAML 选项卡后,我们可以导航到 localhost:8080 站点,查看我们的应用程序和 IdP 之间的流量交换。在图片中,您可以看到应用程序向 IdP 发送了两个 GET 请求,以及 IdP 向我们的 /sso/acs 发送的一个 POST 请求。在底部的选项卡中,您可以探索 SAML 格式的数据、请求和响应。
好的,localhost 测试已全部完成。是时候准备将应用程序部署到 Amazon Web Services 了。首先,请记住:
SP 发起的登录 == SP 调用 IdP 请求安全断言。
我们通过 const saml = require('samlify'); 将 samlify 库包含到 express 应用程序中。
NodeJS 允许我们在 localhost 上运行内容以进行开发和测试。
SP 具有 SP 元数据,IdP 具有 IdP 元数据。A) 真但未完成,SP 同时具有 SP 和 IdP 元数据。
准备内容
SAML 元数据
还记得我们使用了 samltool 网站并创建了自己的私钥、自签名 X.509 证书以及 SP 的元数据吗?这对于 localhost 测试来说还可以,但您可能无法将其用于暂存和/或生产环境。这取决于 IdP 和 SP 的设置方式。一些 IdP 系统要求您上传 SP 的 SAML 元数据。它们将其合并到 SP 的设置中,并使用该信息来确保流量来自其批准的来源之一。在其他情况下,IdP 可能要求证书颁发机构 (CA) 发布自己的证书,以建立所谓的“信任锚”(又名“锚定信任”)。在这种情况下,IdP 只会确保流量是使用该 CA 颁发的证书进行签名/加密的。
在我们的 Auth0 测试案例中,我们将 SP 元数据和 IdP 元数据合并到应用程序中,以便 SP 可以验证发布到 /sso/acs 的断言是否来自 IdP。对于暂存和生产目标,这仍然适用,但它将使用您特定 IdP 在配置相应系统时提供的元数据。
注意:从安全角度来看,将 SAML 使用的私钥和证书与下一节中使用的 SSL 证书相同,很可能不被视为最佳实践。
HTTPS 证书
获取 SSL 证书超出了本文的范围(它已经够长了)。但是,一旦您拥有了它们,我们就需要将它们集成到我们的设置中,现在就开始吧。
为了使用 HTTPS,需要在服务器上安装 SSL 证书和关联的私钥。当我们设置 AWS 上的应用程序(我们即将这样做)时,服务器实例是按需创建的,因此您不会在那里进行安装和设置。AWS 通过从名为 EB Extension 的特殊文件中获取证书来处理这种情况。AWS 将在部署包(zip 文件)的根目录中查找名为 .ebextensions 的文件夹中的扩展。我们在项目中创建了此文件夹。该文件夹中有两个文件:https-instance-single.config 和 http-instance.config。http-instance-single.config 永远不变,对于所有支持 SSL 的站点都相同。
Resources:
sslSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]}
IpProtocol: tcp
ToPort: 443
FromPort: 443 CidrIp: 0.0.0.0/0
我们需要自定义的文件是 https-instance.config。再次,我们需要为暂存和生产目标使用不同的文件。`build` 批处理文件将在此处发挥作用。build.bat 文件使用“resources”文件夹中的文件,因此请打开该文件夹并找到 https-instance_staging.config 文件并打开它
files:
/etc/nginx/conf.d/https.conf:
mode: "000644"
owner: root
group: root
content: |
# HTTPS server
server {
listen 443;
server_name localhost;
ssl on;
ssl_certificate /etc/pki/tls/certs/server.crt;
ssl_certificate_key /etc/pki/tls/certs/server.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://nodejs;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
/etc/pki/tls/certs/server.crt:
mode: "000400"
owner: root
group: root
content: |
-----BEGIN CERTIFICATE-----
MIIFUTC...
SSL Security Cert in PEM format goes here
make sure each line is indented with 8 spaces
...BWj4iNx5R18HtP
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFD
If this cert came with intermediate certs. Those are
also included.
Lcw=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIE/zCC...
Include all the intermediate certificates in order 1, 2, n but not the root
...OzeXEodm2h+1dK7E3IDhA=
-----END CERTIFICATE-----
/etc/pki/tls/certs/server.key:
mode: "000400"
owner: root
group: root
content: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKC...
The private key goes here,
indented 8 spaces just like the certs
...Nn23WsSqctqP2Ce9g==
-----END RSA PRIVATE KEY-----
您看到的第一项是“files:”。后面跟着 /etc/nginix/conf.d/http.conf。如果您在 Linux 系统上工作并设置了 Nginx,您会识别出该文件名是 http 的配置文件。仔细检查后,您会看到另外两个文件:/etc/pki/tls/certs/server.crt 和 /etc/pki/tls/certs/server.key。这些文件都有“mode”、“owner”、“group”和“content”属性。因此,我们可以得出结论,这个文件所做的就是获取此文件的内容,并在实例启动之前创建其他文件,从而使我们想要的配置在启动时就已到位。
我们需要以文本格式插入证书。这称为 PEM 格式。当您为 SAML 元数据生成私钥和 X.509 证书时,它们就是该格式。纯文本带有 -----BEGIN... 和 -----END... 标签。当您从证书颁发机构 (CA) 获取证书时,它可能以 PEM 格式提供。如果不是,您可以使用此处的工具进行转换。要查看您的证书是否以 PEM 格式提供,只需用记事本打开它们,看看是否是带有 BEGIN 和 END 标签的文本。如果不是带有这些标签的文本,请与证书颁发机构核实,以确保您知道您拥有的格式。此外,可能还有中间证书。您需要确保每个证书都采用 PEM 格式。
一旦您获得了所有证书和私钥的 PEM 格式,请返回到此文件,并在 /etc/pki/tls/certs/server.crt 文件中找到需要插入的位置。如注释中所述,您需要确保证书缩进 8 个空格,并且包含开始和结束标签。插入主证书和任何中间证书,但不要插入根证书。如果您有两个中间证书,那么您将从域证书开始,然后是中间证书 1,然后是中间证书 2。
您将对私钥执行相同的操作。这必须是您在请求证书时提交给 CA 的私钥。如果私钥和证书不匹配,服务器将无法启动,并且日志中会记录不匹配的情况。
当暂存站点的证书和私钥内容到位后,为生产站点重复证书和私钥的过程。如果您购买了所谓的通配符 SSL 证书或多域名证书,那么您可以将相同的私钥和证书用于两者。否则,您需要使用特定于目标站点的文件。
从安全角度来看,将 SAML 使用的私钥和证书与 SSL 使用的证书相同,很可能不被视为最佳实践。
打包应用程序
好的,我们已经完成了 SSO 代码的设置,完成了应用程序的测试,获取了证书并将其放置在项目的配置文件中,现在准备将其部署到 AWS。在此过程中,我们将需要上传包含我们站点代码和资源的 zip 文件。批处理文件现在很有用。使用命令行窗口运行批处理文件,命令为 build staging
。现在,/server/config/ 文件夹将包含正确的 SAML 元数据、私钥和 https-instance.config 文件。批处理文件还创建了一个名为 COMPANY-YYYYMMDDHHMMSS.zip 的 zip 文件(COMPANY 加上时间戳)。这是我们的部署包。
AWS 设置
我们已经走了很长一段路,新上线网站的准备工作几乎完成了。要执行接下来的步骤,您需要为新域名准备 SSL 证书。如果您实际上没有要部署的实时站点,则可以按照屏幕截图进行操作。
首先,您需要一个 AWS 账户。请访问aws.amazon.com 注册一个。
填写下面的屏幕上的框
当您到达主屏幕时,您会看到 AWS 提供的各种服务。我们首先要寻找 Elastic Beanstalk。
单击“Create new Application”,然后输入弹出对话框中的值。
当您在 AWS 中创建应用程序空间时,它需要一个运行环境。这是下一步。单击“Actions”,然后单击“Create environment”。
我们构建的是一个将在 Web 服务器上运行的 Web 应用程序(但我们不想费心维护 Web 服务器,所以我们使用 AWS)。您可能会注意到它没有提及 HTTPS,不用担心 - HTTPS 配置在我们之前设置的 ebextensions 中。
AWS 读取应用程序名称并预填充环境名称。您需要提供子域名,并使用“Check availabiltiy”按钮进行测试,直到找到可用的域名。找到可用的域名后,向下滚动。
我们想要一个使用 Node.js 的预配置平台,我们已经创建了一个包(由批处理文件组装的 zip 文件),因此请选择“Upload your code”单选按钮,然后单击“Upload”按钮。
单击“Choose File”并导航到项目文件夹中的 zip 文件。选择文件后,单击“Upload”,上传完成后单击“Create Environment”。
当您单击“Create Environment”时,将启动一个过程,执行所有步骤来“启动”您的新服务器实例。如果一切顺利,您将看到启动成功的消息,并且页面将重新加载,聚焦于您全新的机器。
哎呀!出了问题。页面重新加载时,我们看到大红色的感叹号,健康状况为“Degraded”。通过访问左侧的“Logs”来查看此问题。
单击“Request Logs”,然后单击“Full Logs”,然后单击“Download”。您将获得一个包含“Logs”文件夹的 zip 文件,其中包含许多不同的日志。我使用“Windows Grep”搜索日志文件夹中任何“error”的实例。
WindowsGrep 显示了一系列错误,涉及我之前提到的主密钥和证书不匹配。
因此,我们修复了密钥和证书不匹配的问题,然后返回仪表板。单击“Upload and Deploy”按钮,用更正后的 zip 文件替换现有的错误 zip 文件。该过程会替换 zip 文件,并使用新内容重新启动实例初始化。
成功!应用程序显示为绿色!我们现在可以尝试使用了吗?您可能会认为我们还不能使用,因为我们还没有在 AWS 中设置域名,也没有向域名注册商提供任何信息,所以 DNS 将无法解析到正确的位置。您说的没错,但有一种方法可以进行测试。在屏幕顶部,您会看到您在早期步骤中设置的域名。在我的示例中是“samlapp.us-east-1.elasticbeanstalk.com”。您可以单击该链接,网站将在浏览器中打开(可能在新选项卡或新窗口中)。
如果您在浏览器中运行该网站,您应该会看到 IdP 的登录屏幕。此时,登录将无法工作,因为正如我们刚才提到的,该域名将无法解析到正确的位置。IdP 的批准回调 URL 列表中没有此 URL。我们希望通过公司域名来运行此应用程序。为此,我们需要引入 Amazon 的 Route 53 服务。在我们离开 Elastic Beanstalk 之前,让我们快速查看一下您刚刚构建的应用程序的一些设置。单击菜单中的“Configuration”项。
“Software”框告诉您在此环境中选择的平台,Node.js 6.13.1。“Instances”框告诉我们目前有一个实例,正在使用哪种类型。“Capacity”告诉我们这是一个单实例,“Load Balancer”告诉我们没有使用负载均衡器。容量和负载均衡器的配置当然是协同工作的。单击“Capacity”内部,您可以看到唯一的其他选项是“Load Balanced”。此示例站点使用单个实例,因为在这种特定情况下,用户数量预计不高。如果您期望您的网站流量很大,那么您应该配置多个实例和负载均衡器。
使用 Amazon Web Services 中的 Elastic Beanstalk 创建新的应用程序环境。
.ebextensions 用于自定义 Elastic Beanstalk 的部署。
要上传到 Elastic Beanstalk 环境的部署包是一个 zip 文件,其中包含应用程序但**不包含** node_modules 文件夹。
Route 53
我们有一个自定义域名,并且想要使用 AWS Elastic Beanstalk 应用程序。这意味着需要 Route 53。在 AWS 屏幕的最顶部,点击“Services”,向下滚动一点,找到“Route 53”(或在搜索框中输入 Route 53)。
在接下来的屏幕中,我使用的是生产域名,所以您不会在名称中看到“staging”。
当您第一次打开 Route 53 时,您会看到一些快速入门选项。我们将选择“DNS Management”。
然后选择“Create Hosted Zone”。这将带您进入列出托管区域的屏幕,您需要再次点击“Create Hosted Zone”。
将您的域名输入顶部框,添加注释,并将选择设置为“Public”。单击“Create”。
您会看到两个已创建的记录集。一个集合是 NS 记录集(用于名称服务器)。这些是您提供给域名注册商的值。域名传播到 DNS 系统并开始工作可能需要长达两天的时间。为了让我们为该域名使用 Elastic Beanstalk 服务器,我们需要创建一个“Alias Record Set”。
单击“Create Record Set”。在左侧面板中,我们将输入四次值。两个用于 ipv4,一个带 www,一个不带 www,然后两个用于 ipv6,一个带 www,一个不带 www。所以对于第一个,在“Name”框中输入 www,在“Type”框中选择 A - IPv4 address,单击“Alias”旁边的“Yes”单选按钮,然后点击“Alias Target”框内的区域。您应该会看到一个像图中所示的下拉框。找到您之前创建的 Elastic Beanstalk 环境。选择它并将其放入框中后,将其突出显示并复制到剪贴板。
花点时间看看“Alias Target”下方的选项。您可以将其设置为 CloudFront 分发名称,这适用于您正在全球分发的静态网站;如果您决定创建带有负载均衡器的多个实例,则可以选择负载均衡器。
再次为另一个“A - IPv4 address”类型重复该过程,但这次在“Name”框中输入 www。这样就完成了 mydomain 和 www.mydomain 的 IPv4 配置。重复该过程,但对于接下来的两个,在“Type”框中选择 AAAA - IPv6 address。这次您可能看不到 Elastic Beanstalk 实例,这就是为什么您将其复制到剪贴板。只需将其粘贴进去。完成后,仪表板应如下所示
现在我们已经学到了
Route 53 用于将域名流量定向到 AWS 资源,例如 Elastic Beanstalk 实例或负载均衡器。
我们必须创建四个记录集来处理 IPv4 和 IPv6 的流量。
就是这样。一个单页应用程序,使用 SAML2 进行单点登录,部署到 Amazon Web Services Elastic Beanstalk,并通过 Route 53 和自定义域名提供服务。
关注点
参见
- Samlify SAML js 库。
- Auth0 单页应用程序文档。
- OneLogin SAML 在线工具包。
- Chrome SAML 工具。
- Chrome Node Inspector 工具。
- AWS Elastic Beanstalk 文档。
- 使用配置文件 (.ebextensions) 高级环境定制
- 为 Elastic Beanstalk 环境配置 HTTPS。
- 在运行 Node.js 的 EC2 实例上终止 HTTPS。
- AWS Route 53 文档。
看看你还记得什么
问:SSO 是什么意思?
- 单点登录
- 独立登录
- 微妙的登录
A) 单点登录
SAML 代表
- 安全辅助标记语言
- 安全断言标记语言
- 安全访问管理列表
A) 安全断言标记语言
真还是假……IdP 用于控制对单个站点或服务的访问。
A) 错误,IdP 可用于允许许多不同的人访问许多不同的服务。
历史
2018 年 6 月 - 首次发布