发布的尼尔·布鲁克斯2018年12月18日
使用AWS Cognito管理Symfony项目中的身份验证
我们的一个前端工程师Sebastian最近一直在做一些兼职项目,其中一个项目包括在AWS Cognito处理他的用户管理。当他带我参观他一直在做的事情时,这让我思考。”把Symfony的认证也交给Cognito有多容易?”。
为什么Cognito ?
如果您不熟悉Cognito,可以将其总结如下:它提供了一种中心机制,用于登录和管理用户,并在web应用程序中对用户进行身份验证。
为了简化这一点,可以将Cognito看作是应用程序当前的简化版本用户
表,但保存在数据库外部。
美妙之处在于你可以访问它用户
' table '直接从PHP后端,Python Lambda函数,或React前端。您甚至不需要构建自己的自定义身份验证API端点。
Cognito还内置支持多因素认证,密码重置,电子邮件和短信确认,社交登录(Facebook, Twitter等),以及更多。这意味着您可以为自己的用户提供更多的选择来注册您的服务。
为什么我要把用户转移到Cognito?
没有明确的理由让你需要将用户移出应用程序数据库。事实上,在很多情况下,我会建议你这么做让他们待在原地.然而,将用户移出数据库可以带来很多好处:
- 您的数据库/ PHP模型结构可能会得到改进,一旦您不再考虑
用户
S和更多与你的商业模式相关的东西;作者
年代,客户端
年代,等等 - 您可以将身份验证从应用程序的数据关注点中移开
- 让Amazon去操心数据安全、密码哈希等问题,而不是自己去做
此外,如果您通过在线搜索找到此页面PHP / Symfony AWS Cognito
在美国,你可能已经很清楚Cognito是什么,以及你为什么想要使用它。
轻微的偏差
许多Cognito使用示例表明,它的主要目标是允许用户直接从客户端应用程序登录。提供了服务器端(如PHP、NodeJS等)身份验证,但没有很好地记录。
就我个人而言,我经常发现AWS的文档很难浏览,如果没有看到其他社区的例子用其他语言实现了同样的事情,我想我会努力使它成功。谢谢开发社区,你们太棒了。
开始
我们要做的第一件事是创建一个新的Symfony 4项目,并让它在本地运行:
作曲家创建-项目symfony/网站骨架cognito-logincdcognito-login
接下来,让我们通过调用new来创建一个用户身份验证系统制造商
包。我们会告诉你的制造商
我们不想要存储我们的安全用户
对象,我们的应用程序是不将检查密码本身(记住,我们将把所有这些头疼的事情都交给AWS):
php bin/console make:user安全用户类名(如用户)[用户):>用户是否要存储用户数据在数据库(通过学说)?(是的/不)[是的]:>no输入唯一的属性名“显示”的名字为用户(例如,email,用户名,uuid)[电子邮件):>这个应用程序需要哈希/检查用户密码?选择不如果不需要密码,或者其他系统会检查/散列密码(例如,单点登录服务器).这个应用程序需要吗哈希/检查用户密码?(是的/不)[是的]:>src/Security/User.php created: src/Security/UserProvider.php updated: config/packages/ Security .phpyaml成功!
现在,让我们使用制造商
再次生成登录表单:
使用哪种类型的认证做你想要什么?[空的身份:[0]空的身份[1]登录表单验证器>要创建的验证方的类名(例如AppCustomAuthenticator):>CognitoAuthenticator选择名称为控制器类(例如SecurityController)[SecurityController]:>SecurityController输入需要认证的User类(例如应用程序\ Entity\ U爵士)[应用程序\ S安全\ User):>应用程序\ S安全\ Usrc/Security/CognitoAuthenticator.php更新:config/packages/ Security .php创建:src/Controller/SecurityController.php创建:templates/security/login.html嫩枝成功!
如果你打开配置/包/ security.yaml
你会注意到制造商
Bundle已经配置了用户提供程序和防火墙来使用我们正在创建的类:
安全:供应商:app_user_provider:id:App \安全\ UserProvider防火墙:#……主要:匿名:真正的警卫:身份验证器:-App \安全\ CognitoAuthenticator
既然我们在这里,让我们加上注销
防火墙配置security.yaml
:
安全:#……防火墙:#……主要:#……注销:路径:app_logout
并添加/注销
路径配置/ routes.yaml
:
app_logout:路径:/注销
接下来我们需要将AWS PHP SDK安装到我们的项目中:
作曲家需要aws / aws-sdk-php
现在唯一要做的就是启动本地web服务器,这样我们就可以边运行边测试了:
作曲家需要symfony / web-server-bundle——开发php bin /控制台服务器:运行
一旦web服务器开始运行,你就可以访问Symfony开发环境了。注意调试栏中用户图标旁边的' n/a ',这表示我们目前没有经过身份验证。
创建用户池
我们想在Cognito中创建一个“用户池”,它将保存我们所有的用户及其密码。你可以通过AWS CLI完成所有这些操作,但当我第一次做一些事情时,我更喜欢能够点击并可视化我正在做的事情,所以我将在我的web浏览器中使用AWS控制台。 当你第一次访问Cognito页面时,你会被问到是想管理你的“用户池”还是“身份池”。身份与那些被允许直接使用您的AWS服务的人有关,这不是我们想要的。我们的目标是创建一个允许使用我们的应用程序的用户池。所以我们需要进入“用户池”管理。 现在创建一个新的用户池。给它起个名字,然后选择“review defaults”选项。 配置文件实体,我可以存储在我的应用程序数据库,并添加 ADMIN_NO_SRP_AUTH”,取消勾选“生成客户机的秘密”。在撰写本文时(2018年12月),PHP SDK与Cognito中的客户端机密不兼容。 管理(保持其他设置不变)。我们可以稍后在防火墙设置中利用这一点。 把它们连接在一起
创建了用户池并准备好PHP站点开始登录用户后,如何让Symfony向Cognito提交身份验证? 首先,我们将创建一个AWS Cognito适配器类,我们可以使用它来保存AWS配置设置,并将应用程序的请求代理到SDK。 创建 如果您以前使用过AWS PHP SDK,那么您将知道配置它的最简单方法是使用环境变量,它将自动检测环境变量。添加你的账户的 现在我们应该在中配置服务 如果我们去 让我们打开 首先,我们需要注入刚刚创建的桥接服务: 然后我们想使用客户端在我们的用户池中查找用户,通过电子邮件地址进行搜索: 现在我们应该实现 现在,如果我们重新提交表单,就会得到异常 再加上 当我们这次重新提交表单时,我们会得到异常 为了演示的目的,我将重定向到登录表单: 最后要解决的是 这一次,当我刷新表单时,我被重定向到相同的登录表单,但是现在调试栏显示我正在使用我的电子邮件地址登录。我可以从AWS Cognito管理面板禁用用户(尝试一下,他们将无法登录!),而我自己的应用程序代码/实体完全不了解身份验证机制。事实上,在这个示例应用中,我们没有配置一个数据库连接或Doctrine实体!用户名
作为一个属性(看看我们是如何解耦的用户
从配置文件
对象?)。src /桥/ AwsCognitoClient.php
内容如下:<?php名称空间App \桥;使用Aws \ CognitoIdentityProvider \ CognitoIdentityProviderClient;类AwsCognitoClient{私人美元的客户;私人poolId美元;私人clientId美元;公共函数__construct(字符串poolId美元,字符串clientId美元,字符串美元的地区=“eu-west-2”,字符串美元的版本=“最新”){这个美元->客户端=CognitoIdentityProviderClient::工厂([“地区”= >美元的地区,“版本”= >美元的版本]);这个美元->poolId=poolId美元;这个美元->clientId=clientId美元;}}
AWS_ACCESS_KEY_ID
而且AWS_SECRET_ACCESS_KEY
到你的.env
文件。说到这里,让我们再加上COGNITO_POOL_ID
而且COGNITO_CLIENT_ID
我们在AWS web界面中记录的值:#……aws / aws-sdk-php # # # # # # >AWS_ACCESS_KEY_ID=AKIAJSKM……AWS_SECRET_ACCESS_KEY=VRJ106jEn7D2Q7……COGNITO_POOL_ID=eu-west-2_……COGNITO_CLIENT_ID=7警察甲……# # # < aws / aws-sdk-php # # #
配置/ services.yaml
:服务:#……应用\ \ AwsCognitoClient桥梁:参数:poolId美元:'% env (COGNITO_POOL_ID) % 'clientId美元:'% env (COGNITO_CLIENT_ID) % '
/登录
试着用我们刚创建的一个用户登录,首先发生的是我们得到一个异常TODO:填写loadUserByUsername()
.src /安全/ UserProvider.php
和修复。类UserProvider实现了UserProviderInterface{/** * @var AWSCognitoClient */私人cognitoClient美元;公共函数__construct(AWSCognitoClientcognitoClient美元){这个美元->cognitoClient=cognitoClient美元;}/ /……
/ /……公共函数loadUserByUsername(美元的用户名){美元的结果=这个美元->cognitoClient->findByUsername(美元的用户名);如果(数(美元的结果[“用户”])= = =0){扔新UsernameNotFoundException();}$ user=新用户();$ user->setEmail(美元的用户名);返回$ user;}
findByUsername ()
方法src /桥/ AwsCognitoClient.php
:/ /……使用Aws \结果;类AwsCognitoClient{/ /……公共函数findByUsername(字符串美元的用户名):结果{返回这个美元->客户端->listUsers([“UserPoolId”= >这个美元->poolId,“过滤”= >“电子邮件=\”".美元的用户名."\”"]);}
检查…/Security/CognitoAuthenticator.php中的凭据
.让我们更新src /安全/ CognitoAuthenticator.php
要使用我们的适配器检查密码:使用应用\ \ AwsCognitoClient桥梁;使用Aws \ CognitoIdentityProvider \ \ CognitoIdentityProviderException异常;/ /……私人cognitoClient美元;公共函数__construct(RouterInterface美元的路由器,CsrfTokenManagerInterfacecsrfTokenManager美元,AwsCognitoClientcognitoClient美元){这个美元->路由器=美元的路由器;这个美元->csrfTokenManager=csrfTokenManager美元;这个美元->cognitoClient=cognitoClient美元;}/ /……公共函数checkCredentials(美元的凭证,用户界面$ user){试一试{这个美元->cognitoClient->checkCredentials(美元的凭证[“电子邮件”],美元的凭证[“密码”]);}抓(CognitoIdentityProviderException美元的例外){返回假;}返回真正的;}
checkCredentials ()
方法到适配器类(注意,前端SDK示例使用initiateAuth
进行凭据检查,但是当使用带有预先配置了访问令牌和秘密的SDK的服务器端实现时,您应该使用adminInitiateAuth
相反):/ /……公共函数checkCredentials(美元的用户名,美元的密码):结果{返回这个美元->客户端->adminInitiateAuth([“UserPoolId”= >这个美元->poolId,“ClientId”= >这个美元->clientId,“AuthFlow”= >“ADMIN_NO_SRP_AUTH”,//匹配前面的'基于服务器的登录'复选框设置“AuthParameters”= >[“用户名”= >美元的用户名,“密码”= >美元的密码]]);}
TODO:在Security/CognitoAuthenticator.php中提供一个有效的重定向
/ /……公共函数onAuthenticationSuccess(请求美元的请求,TokenInterface美元的令牌,providerKey美元){如果(定位路径美元=这个美元->getTargetPath(美元的请求->getSession(),providerKey美元)){返回新RedirectResponse(定位路径美元);}返回新RedirectResponse(这个美元->路由器->生成(“app_login”));}
TODO:在Security/UserProvider.php中填写refreshUser()
.我们已经在同一个类中有了通过用户名获取用户的方法,所以让我们重用它:/ /……公共函数refreshUser(用户界面$ user){如果(!$ user运算符用户){扔新UnsupportedUserException(sprintf(“无效的用户类“%s”。”,get_class($ user)));}返回这个美元->loadUserByUsername($ user->getEmail());}