使用 Golang 和 AWS Cognito 进行身份验证
什么是Cognito?
应用程序的身份验证在系统中非常重要,但也非常敏感,需要考虑各种实现方式、安全性和验证方法。
我决定写一篇博文,介绍一下Cognito,这是 AWS 提供的一款非常棒的工具,可以帮助您对 Web 和移动应用程序进行用户身份验证和验证,但很多人并不了解它。
Cognito 是一个 AWS 平台,负责创建和验证用户访问数据,以及注册用户和存储其信息,此外还能生成 OAuth 令牌,并且 Cognito 还可以提供所有用户验证。
我们可以创建一些用户数据,例如:电子邮件、姓名、电话号码、出生日期、昵称、性别、网站等等,我们还可以添加自定义字段。
Cognito 仍然允许我们与“联合提供商”(也称为社交登录)一起使用,例如 Google、Facebook 和 GitHub,我们不会在本文中讨论这一点,但使用 Cognito 是可以做到这一点的。
我们该怎么办?
我们将创建一些端点来展示 Cognito 的工作原理,我们将创建用户、确认电子邮件、登录、使用 Cognito 提供的令牌搜索用户、更新信息。
项目设置
我们将做一些非常简单的事情,我们不会去考虑项目负责人,我们只想关注知识的运用。
为了创建端点,我们将使用gin。
让我们创建以下文件:
-
我们应用程序的入口点
main.go位于项目根目录。 -
.env保存认知凭证 -
一个名为cognitoClient 的粘贴文件,位于一个名为
cognito.go -
有一个名为 的文件
request.http,用于完成您的请求。
结构如下:
在 AWS 上设置 Cognito
在开始编写代码之前,我们将在 AWS 中配置 Cognito,以便访问面板并通过 Cognito 进行搜索。创建池之后,选择“将用户目录添加到您的应用程序”选项。
对于提供商类型,选择Cognito 用户池选项,您可以选择允许使用电子邮件、用户名和电话号码登录,也可以仅选择电子邮件登录,选择您偏好的方式,然后选择 assim 进入第一阶段:
我还需要配置一些东西,走吧!
- 密码策略模式允许您选择特定的策略,让我们取消Cognito 的默认设置。
- 多因素身份验证允许我们的登录进行双因素身份验证,我们先不使用,但如果需要,您可以实施它,您可以选择不使用 MFA。
- 最后,或者说用户帐户恢复,您可以选择恢复帐户的方式,您可以选择电子邮件。
下一步:
- 自助注册,我们将允许任何人进行注册,请留下您的选择。
- Cognito 辅助验证和确认,允许 Cognito 负责确认用户的身份,进行检查,并选择“发送电子邮件消息,验证电子邮件地址”选项。
- 验证属性更改,选中此选项后,更新用户电子邮件时需要再次进行验证。
- 必填属性,选择创建新用户时要设为必填的字段,您将选择选项,电子邮件(和姓名),并且您的父亲的姓名也是必填项。
- 自定义属性是可选的,但您可以添加自定义字段,例如,您可以创建一个名为“
custom_id任意”的字段uuid。
这一阶段也发生了:
接下来,选择“使用 Cognito 发送电子邮件”选项,这样我们就无需配置任何内容来触发电子邮件。
下一步,在“用户池名称”中输入您想要的名称,在“应用程序客户端名称”中也输入您想要的名称,然后继续。
最后阶段我们不需要做任何更改,只需完成并创建用户池即可。
在“访问或认知 > 用户池”中,选择您刚刚创建的池,此部分将列出您的应用程序的所有用户,并且可以撤销用户的令牌、停用、验证等功能。
我们将指定池的 ID,以便能够使用Go SDK for AWS 来访问已创建的池。在“应用程序集成” > “应用程序客户端列表”中,您可以看到我们的客户端 ID:
让我们把这个ID保存到.env文件中:
COGNITO_CLIENT_ID=client_id
请记住,您仍然需要 AWS 凭证,通常位于/Users/your-user/.aws目录中。如果您尚未配置,请参阅此处了解如何配置。
实施 Cognito
让我们把 Cognito 部分单独放到一个文件中。
用户注册
在文件中cognito.go,我们将初始化 Cognito 并创建界面:
package congnitoClient
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
cognito "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
"github.com/google/uuid"
)
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
}
type cognitoClient struct {
cognitoClient *cognito.CognitoIdentityProvider
appClientID string
}
func NewCognitoClient(appClientId string) CognitoInterface {
config := &aws.Config{Region: aws.String("us-east-1")}
sess, err := session.NewSession(config)
if err != nil {
panic(err)
}
client := cognito.New(sess)
return &cognitoClient{
cognitoClient: client,
appClientID: appClientId,
}
}
func (c *cognitoClient) SignUp(user *User) error {
return nil
}
首先,我们创建一个名为 的结构体User,该结构体将包含我们需要保存到 Cognito 中的用户字段。
然后我们创建一个interface名为 `<T>` 的类CognitoInterface,其中将包含我们将要使用的方法,首先我们只有一个 `<T>`,SignUp它将接收一个User指向结构体的指针。
然后我们将创建另一个结构体,cognitoClient其中包含我们的实例,NewCognitoClient该实例将是我们的构造函数。
如前所述,NewCognitoClient它将类似于我们的构造函数,我们将在其中创建与 AWS 的会话并返回此连接。此连接可以是全局变量,但在我们的示例中我们不会这样做,您需要根据自己的用例来判断哪种方法最佳。
现在让我们来实现SignUp:
func (c *cognitoClient) SignUp(user *User) error {
userCognito := &cognito.SignUpInput{
ClientId: aws.String(c.appClientID),
Username: aws.String(user.Email),
Password: aws.String(user.Password),
UserAttributes: []*cognito.AttributeType{
{
Name: aws.String("name"),
Value: aws.String(user.Name),
},
{
Name: aws.String("email"),
Value: aws.String(user.Email),
},
{
Name: aws.String("custom:custom_id"),
Value: aws.String(uuid.NewString()),
},
},
}
_, err := c.cognitoClient.SignUp(userCognito)
if err != nil {
return err
}
return nil
}
我们将使用Cognito 来组装要发送到AWS SDK 的AttributeType参数,请注意,我们的自定义字段需要放在前面,否则将不会被接受,我们刚刚使用 Google 包创建了一个 uuid,此字段只是为了演示如何使用自定义属性。SignUpcustom_idcustom
该ClientId字段指的是COGNITO_CLIENT_ID我们的环境,我们将在启动时传递它main.go。
这就是我们拯救用户所需要的,很简单,对吧?
别忘了以以下内容开始项目:
go mod init <your project name>
并安装必要的软件包:
go mod tidy
确认账户
我们来创建另一个函数,用于通过电子邮件验证用户帐户。要验证帐户,用户需要输入通过电子邮件发送的验证码。让我们创建一个新的结构体,并将新ConfirmAccount方法添加到接口中:
type UserConfirmation struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
}
现在让我们来实现:
func (c *cognitoClient) ConfirmAccount(user *UserConfirmation) error {
confirmationInput := &cognito.ConfirmSignUpInput{
Username: aws.String(user.Email),
ConfirmationCode: aws.String(user.Code),
ClientId: aws.String(c.appClientID),
}
_, err := c.cognitoClient.ConfirmSignUp(confirmationInput)
if err != nil {
return err
}
return nil
}
很简单,我们将使用ConfirmSignUpInputcognito 包中的方法来组装参数,记住,其中Username是用户的电子邮件地址。最后,我们将调用ConfirmSignUp该方法并传递参数confirmationInput。
请记住,我们只返回了错误信息,您可以改进并检查错误消息的类型。
登录
这应该是使用频率最高的功能,我们来创建一个名为 `method` 的方法SignIn和一个结构体:
type UserLogin struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
SignIn(user *UserLogin) (string, error)
}
我们SignIn将收到一个UserLogin。
让我们来实现:
func (c *cognitoClient) SignIn(user *UserLogin) (string, error) {
authInput := &cognito.InitiateAuthInput{
AuthFlow: aws.String("USER_PASSWORD_AUTH"),
AuthParameters: aws.StringMap(map[string]string{
"USERNAME": user.Email,
"PASSWORD": user.Password,
}),
ClientId: aws.String(c.appClientID),
}
result, err := c.cognitoClient.InitiateAuth(authInput)
if err != nil {
return "", err
}
return *result.AuthenticationResult.AccessToken, nil
}
我们将使用InitiateAuthaws cognito 包中的函数,我们需要传递username(用户的电子邮件),password以及AuthFlow,此字段指的是我们将允许的访问类型,在我们的例子中是USER_PASSWORD_AUTH。
如果您收到类似这样的错误信息:
You trusted all proxies, this is NOT safe. We recommend you to set a value
需要启用该ALLOW_USER_PASSWORD_AUTH流程,要进行配置,请访问 AWS 控制面板上的 Cognito,然后转到:
用户池>选择您的池>应用集成>应用客户端列表>选择客户端,将打开此屏幕:
点击编辑,在身份验证流程中选择ALLOW_USER_PASSWORD_AUTH选项,然后保存,这样您就可以使用用户的密码和电子邮件登录了。
列出用户
Paramostrar como utilizar o token jwt fornecido pelo cognito vamos criar um end quemostra os bados do usuário salvos no cognito apenas com o token.
让我们创建另一个函数,GetUserByToken该函数将接收一个令牌并返回一个结构体,GetUserOutput该结构体的类型将从 cognito 包中获取。
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
SignIn(user *UserLogin) (string, error)
GetUserByToken(token string) (*cognito.GetUserOutput, error)
}
点击后GetUserOutput即可查看该结构体内部的内容。
type GetUserOutput struct {
_ struct{} `type:"structure"`
MFAOptions []*MFAOptionType `type:"list"`
PreferredMfaSetting *string `type:"string"`
UserAttributes []*AttributeType `type:"list" required:"true"`
UserMFASettingList []*string `type:"list"`
Username *string `min:"1" type:"string" required:"true" sensitive:"true"`
}
其中_ struct{}包含我们为用户创建的自定义属性,在本例中为custom_id。
让我们来实现:
func (c *cognitoClient) GetUserByToken(token string) (*cognito.GetUserOutput, error) {
input := &cognito.GetUserInput{
AccessToken: aws.String(token),
}
result, err := c.cognitoClient.GetUser(input)
if err != nil {
return nil, err
}
return result, nil
}
我们使用GetUsercognito 包,它只需要一个AccessTokencognito 本身提供的标记。
更新密码
最后,我们将更新用户的密码。为此,我们需要用户的电子邮件地址和新密码。我们已经有了UserLogin包含所需字段的结构体,我们将重复使用它。如果您愿意,也可以专门为此函数创建一个新的结构体。让我们创建该UpdatePassword函数:
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
SignIn(user *UserLogin) (string, error)
GetUserByToken(token string) (*cognito.GetUserOutput, error)
UpdatePassword(user *UserLogin) error
}
让我们来实现:
func (c *cognitoClient) UpdatePassword(user *UserLogin) error {
input := &cognito.AdminSetUserPasswordInput{
UserPoolId: aws.String(os.Getenv("COGNITO_USER_POOL_ID")),
Username: aws.String(user.Email),
Password: aws.String(user.Password),
Permanent: aws.Bool(true),
}
_, err := c.cognitoClient.AdminSetUserPassword(input)
if err != nil {
return err
}
return nil
}
我们将使用AdminSetUserPasswordcognito 包中的函数,需要传入用户的电子邮件地址和新密码,此外还需要传入一个文件UserPoolId,该文件将放在COGNITO_USER_POOL_ID一个文件中。.env要在 AWS 中查找该文件,只需访问您的存储池并复制该文件即可。User pool ID
我们还会传递一个信息Permanent,告知用户这是一个永久密码;您也可以传递一个信息false,这样 Cognito 就会为用户创建一个临时密码,这取决于您在应用程序中使用的策略。
创建主界面
让我们创建我们的 .cognito.xmlmain.go文件,我们将在这个文件中启动 Cognito 并创建我们的路由。
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
cognitoClient := congnitoClient.NewCognitoClient(os.Getenv("COGNITO_CLIENT_ID"))
r := gin.Default()
fmt.Println("Server is running on port 8080")
err = r.Run(":8080")
if err != nil {
panic(err)
}
}
首先,我们将使用godotenv包加载我们的环境,然后启动 cognito 客户端,传递COGNITO_CLIENT_ID我们之前获取的参数,然后启动 gin 并创建一个服务器,这就足够了。
创建端点
创建用户
让我们在main.go文件内部创建一个函数,我们把它命名为CreateUser:
func CreateUser(c *gin.Context, cognito congnitoClient.CognitoInterface) error {
var user congnitoClient.User
if err := c.ShouldBindJSON(&user); err != nil {
return errors.New("invalid json")
}
err := cognito.SignUp(&user)
if err != nil {
return errors.New("could not create use")
}
return nil
}
很简单,我们只需使用 gin 的函数将接收到的内容转换为我们的结构体ShouldBindJSON,然后调用SignUp我们在 中创建的cognito.go。
现在让我们在函数内部创建端点main.go:
r.POST("user", func(context *gin.Context) {
err := CreateUser(context, cognitoClient)
if err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusCreated, gin.H{"message": "user created"})
})
我们调用刚刚创建的函数CreateUser,如果出现错误,则抛出异常StatusBadRequest,如果成功,则返回一个值StatusCreated,让我们进行测试。
让我们先go mod tidy下载所有软件包,然后运行应用程序。go run main.go
现在我们可以在request.http文件中创建一个调用并执行:
POST http://localhost:8080/user HTTP/1.1
content-type: application/json
{
"Name": "John Doe",
"email": "wivobi1159@bitofee.com",
"password": "Pass@1234"
}
如果一切正常,我们将收到以下信息:
{
"message": "user created"
}
现在进入 AWS 上的 Cognito 控制面板,访问池,然后访问用户,我们就能找到我们的用户了:
确认用户
请注意,我们上面创建的用户尚未经过验证,让我们来验证一下!
ConfirmAccount在文件中创建一个名为以下函数的函数main.go:
func ConfirmAccount(c *gin.Context, cognito congnitoClient.CognitoInterface) error {
var user congnitoClient.UserConfirmation
if err := c.ShouldBindJSON(&user); err != nil {
return errors.New("invalid json")
}
err := cognito.ConfirmAccount(&user)
if err != nil {
return errors.New("could not confirm user")
}
return nil
}
和之前一样,让我们把主体转换成UserConfirmation结构体,然后传递给ConfirmAccountin cognito.go。
让我们创建端点:
r.POST("user/confirmation", func(context *gin.Context) {
err := ConfirmAccount(context, cognitoClient)
if err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusCreated, gin.H{"message": "user confirmed"})
})
也很简单,我们只需处理错误并返回一条消息,让我们创建调用并进行测试:
POST http://localhost:8080/user/confirmation HTTP/1.1
content-type: application/json
{
"email": "wivobi1159@bitofee.com",
"code": "363284"
}
我们将收到以下信息:
{
"message": "user confirmed"
}
现在再次通过 AWS 控制面板访问 Cognito,请注意用户已确认。请记住,您需要输入有效的电子邮件地址。您可以使用临时电子邮件地址进行测试,但该电子邮件地址必须有效,因为 Cognito 会发送确认码,并且该确认码必须有效才能成功确认。
登录
现在让我们创建令牌,为此,请在main.go文件中创建一个名为 `token` 的函数SignIn,该函数将返回一个错误和一个令牌。
func SignIn(c *gin.Context, cognito congnitoClient.CognitoInterface) (string, error) {
var user congnitoClient.UserLogin
if err := c.ShouldBindJSON(&user); err != nil {
return "", errors.New("invalid json")
}
token, err := cognito.SignIn(&user)
if err != nil {
return "", errors.New("could not sign in")
}
return token, nil
}
与其他函数一样,我们将主体转换为UserLogin结构体并将其传递给SignInof cognito.go。
让我们创建端点:
r.POST("user/login", func(context *gin.Context) {
token, err := SignIn(context, cognitoClient)
if err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusCreated, gin.H{"token": token})
})
现在我们token向用户返回一个结果,让我们创建调用并进行测试:
POST http://localhost:8080/user/login HTTP/1.1
content-type: application/json
{
"email": "wivobi1159@bitofee.com",
"password": "Pass@1234"
}
发起调用时,我们将收到我们的 JWT 令牌:
{
"token": "token_here"
}
如果我们获得了 jwt 令牌,我们可以使用网站jwt.io查看其中的内容。
列出用户
现在我们将仅使用令牌列出 Cognito 中保存的用户数据,为此,我们将创建一个名为GetUserByToken`in` 的函数main.go,并且我们需要一个结构体来表示我们将返回给用户的响应,我们main也将在 `in` 中创建它:
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CustomID string `json:"custom_id"`
EmailVerified bool `json:"email_verified"`
}
func main() {}
现在是函数:
func GetUserByToken(c *gin.Context, cognito congnitoClient.CognitoInterface) (*UserResponse, error) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if token == "" {
return nil, errors.New("token not found")
}
cognitoUser, err := cognito.GetUserByToken(token)
if err != nil {
return nil, errors.New("could not get user")
}
user := &UserResponse{}
for _, attribute := range cognitoUser.UserAttributes {
switch *attribute.Name {
case "sub":
user.ID = *attribute.Value
case "name":
user.Name = *attribute.Value
case "email":
user.Email = *attribute.Value
case "custom:custom_id":
user.CustomID = *attribute.Value
case "email_verified":
emailVerified, err := strconv.ParseBool(*attribute.Value)
if err == nil {
user.EmailVerified = emailVerified
}
}
}
return user, nil
}
这将是最大的功能,我们需要将从 Cognito 接收到的数据映射到我们的UserResponse结构体中,我们使用 `a`for和 `a`来实现这一点switch,当然我们可以改进它,但为了示例起见,我们先保持这样。此外,要映射自定义属性,我们需要custom在前面加上 `a`,例如 ` custom:custom_ida`。
我们还会检查用户是否在请求头中传递了令牌,如果没有,则返回错误。
让我们创建端点:
r.GET("user", func(context *gin.Context) {
user, err := GetUserByToken(context, cognitoClient)
if err != nil {
if err.Error() == "token not found" {
context.JSON(http.StatusUnauthorized, gin.H{"error": "token not found"})
return
}
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{"user": user})
})
我们执行与其他端点相同的验证,但现在我们检查错误类型,如果是该token not found类型,则返回一个StatusUnauthorized。
我们来测试一下:
GET http://localhost:8080/user HTTP/1.1
content-type: application/json
Authorization: Bearer token_jwt
让我们接收用户:
{
"user": {
"id": "50601dc9-7234-419a-8427-2a4bda92d33f",
"name": "John Doe",
"email": "wivobi1159@bitofee.com",
"custom_id": "cb748d09-40de-457a-af23-ed9483d69f8d",
"email_verified": true
}
}
更新密码
最后,我们来创建UpdatePassword更新用户密码的函数:
func UpdatePassword(c *gin.Context, cognito congnitoClient.CognitoInterface) error {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if token == "" {
return errors.New("token not found")
}
var user congnitoClient.UserLogin
if err := c.ShouldBindJSON(&user); err != nil {
return errors.New("invalid json")
}
err := cognito.UpdatePassword(&user)
if err != nil {
return errors.New("could not update password")
}
return nil
}
我们还强制要求在请求头中提供令牌,其余功能与我们之前已经完成的功能相同。
让我们创建最后一个端点:
r.PATCH("user/password", func(context *gin.Context) {
err := UpdatePassword(context, cognitoClient)
if err != nil {
if err.Error() == "token not found" {
context.JSON(http.StatusUnauthorized, gin.H{"error": "token not found"})
return
}
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{"message": "password updated"})
})
我们来打电话吧:
PATCH http://localhost:8080/user/password HTTP/1.1
content-type: application/json
Authorization: Bearer token_jwt
{
"email": "wivobi1159@bitofee.com",
"password": "NovaSenha2@2222"
}
现在,当您更新密码并尝试登录时,您会收到错误提示;如果您使用新密码,一切都会正常运行。
最后考虑因素
在这篇文章中,我们将简单介绍一下 Cognito,它是 AWS 众多服务之一,很多人可能并不了解它,但它对系统的演进却大有裨益。
Cognito 的实用性远不止我刚才提到的这些。虽然设置基本登录很简单,但 Cognito 的突出之处在于它已经提供了现成的账户验证系统、社交网络登录选项(如果没有 Cognito,实现起来会非常麻烦)、双因素身份验证等等,而且它还利用 AWS 安全来保护用户数据。
Cognito 的功能更多,建议查阅文档了解所有功能。
存储库链接
项目仓库
请点击此处查看我博客上的文章。
订阅并接收新帖子的通知,参与互动
文章来源:https://dev.to/wiliamvj/authentication-with-golang-and-aws-cognito-577e












