應用程式的身份驗證在系統中非常重要,但也非常敏感,需要考慮各種實作、安全性、驗證。
我決定發表一篇文章來演示 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,以存取面板並透過 cognito 進行搜索,在建立池後,選擇選項將使用者目錄新增至您的應用程式。
對於提供者類型,選擇Cognito 使用者池選項,您可以選擇允許使用電子郵件、使用者名稱和電話登錄,您只能選擇電子郵件,選擇您想要的更喜歡,選擇assim 到第一階段:
我還需要配置一些東西,我們開始吧!
下一步:
這個階段也發生過:
接下來,選擇使用 Cognito 發送電子郵件選項,因此我們不需要配置任何內容來觸發電子郵件。
下一步,在用戶池名稱中輸入您想要的名稱,在應用程式用戶端名稱中也輸入您想要的名稱並繼續。
在最後階段,我們不需要更改任何內容,只需完成並建立使用者池即可。
擁有一切,訪問或認知>;用戶池,選擇您剛剛創建的池,這部分將列出您應用程式的所有用戶,並且可以撤銷用戶的令牌、停用、驗證等功能。
我們將指定池的ID,以便能夠使用AWS的Go sdk,訪問創建的池應用程式整合 > > 應用程式客戶端列表並查看我們的客戶端ID:
讓我們將此 ID 保存在 .env 檔案中:
COGNITO_CLIENT_ID=client_id
請記住,您仍然需要擁有 AWS 憑證,通常位於 /Users/your-user/.aws 目錄中,如果您尚未配置它,請參閱此處如何進行設定。
讓我們將 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 中儲存的使用者欄位。
然後我們建立一個名為 CognitoInterface 的接口,我們將擁有我們將使用的方法,首先我們只有 SignUp 它將接收指向 User 結構的指標。
然後我們將有另一個名為 cognitoClient 的結構,它將包含 NewCognitoClient 的實例,它將成為我們的建構子。
如前所述,NewCognitoClient 就像我們的建構函數,我們將在其中建立與 AWS 的會話並傳回此連接。此連接可以是全域變量,在我們的例子中,我們不會這樣做,由您來檢查哪種方法最適合您的用例。
現在讓我們實現註冊:
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 的AttributeType 來組裝我們將發送到AWS SDK 的SignUp 的參數,請注意,custom_id 是我們的自訂字段,需要先自訂,否則將不會被接受,我們剛剛使用Google 包創建了一個uuid,該字段只是為了展示如何使用自訂屬性。
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 }
非常簡單,我們將使用cognito套件中的ConfirmSignUpInput來組合參數,記住使用者名稱是使用者的電子郵件。最後,我們將呼叫ConfirmSignUp並傳遞confirmationInput。
記住我們只回傳錯誤,您可以改進並檢查錯誤訊息的類型。
這應該是最常用的功能,讓我們建立一個名為 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) }
我們的登入將會收到使用者登入。
讓我們實現:
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 }
我們將使用 aws cognito 套件中的 InitiateAuth 函數,我們需要傳遞使用者名稱(使用者的電子郵件)、密碼和 AuthFlow,該欄位指的是我們允許的存取類型,在我們的範例中為 USER_PASSWORD_AUTH。
如果您收到以下錯誤:
您信任所有代理,這並不安全。我們建議您設定一個值
需要啟用 ALLOW_USER_PASSWORD_AUTH 流程,將其配置為在 aws 面板上存取 cognito,請前往:
用戶池 > Selecione seu 池 > > 應用程式整合 > 應用程式用戶端清單 > 客戶選擇端,將開啟此畫面:
Click on edit and in Authentication flows select the option ALLOW_USER_PASSWORD_AUTH then save, with this you can now log in with the user's password and email.
Para mostrar como utilizar o token jwt fornecido pelo cognito vamos criar um endpoint que mostra os dados do usuário salvos no cognito apenas com o token.
Let's create another function called GetUserByToken that will receive a token and return a struct of type GetUserOutput that we will get from the cognito package.
type CognitoInterface interface { SignUp(user *User) error ConfirmAccount(user *UserConfirmation) error SignIn(user *UserLogin) (string, error) GetUserByToken(token string) (*cognito.GetUserOutput, error) }
If you click on GetUserOutput you will see what is inside this struct
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"` }
inside the _ struct{} there are custom attributes that we created for our user, in our case the custom_id.
Let's implement:
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 }
We use GetUser from the cognito package, it only needs an AccessToken which is the token provided by cognito itself.
Finally, we will update the user's password. To do this, we will need the email address and the new password. We already have the UserLogin struct with the fields we need. We will reuse it. If you wish, create a new one just for this function. Let's create the UpdatePassword function:
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 }
Let's implement:
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 }
We will use the AdminSetUserPassword function from the cognito package, we need to pass the user's email and the new password, in addition we have to pass the UserPoolId, we will put the COGNITO_USER_POOL_ID in the .env file, to search in aws just access your pool and copy the User pool ID
We will also pass Permanent, informing that it is a permanent password, you could pass false, so Cognito would create a temporary password for the user, this will depend on the strategy you will use in your application.
Let's create our main.go, this will be the file where we will start cognito and create our routes.
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) } }
First we will load our envs with the godotenv package, then we start our cognito client, passing the COGNITO_CLIENT_ID, which we got earlier, then we start gin and create a server, that's enough.
Let's create a function inside the main.go file itself, let's call it 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 }
Something simple, we just convert what we receive in the body to our struct using gin's ShouldBindJSON, then we call the SignUp that we created in cognito.go.
Now let's create the endpoint inside the main.go function:
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"}) })
We call the function we just created CreateUser, if there is an error we throw a StatusBadRequest, if it is successful a StatusCreated, let's test.
Let's do a go mod tidy downloading all the packages, then we'll run the application with go run main.go
Now we can create a call in the request.http file and execute:
POST http://localhost:8080/user HTTP/1.1 content-type: application/json { "Name": "John Doe", "email": "wivobi1159@bitofee.com", "password": "Pass@1234" }
If everything is correct we will receive the message:
{ "message": "user created" }
Now entering the Cognito panel on AWS, and accessing the pool then the users, we will have our user there:
Note that the user we created above is not confirmed, let's confirm it!
Create a function called ConfirmAccount in the main.go file:
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 }
Same concept we used before, let's convert the body to the UserConfirmation struct and pass it to ConfirmAccount in cognito.go.
Let's create the endpoint:
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"}) })
It's also simple, we just handle the error and return a message, let's create our call and test it:
POST http://localhost:8080/user/confirmation HTTP/1.1 content-type: application/json { "email": "wivobi1159@bitofee.com", "code": "363284" }
We will receive the message:
{ "message": "user confirmed" }
Now accessing Cognito again on the AWS panel, notice that the user is confirmed, remembering that you need to enter a valid email, you can use a temporary email to play around, but it needs to be valid, as Cognito will send the confirmation code and it needs to be a valid code to confirm successfully.
Now let's create our token, to do this in the main.go file create a function called SignIn, this function will return an error and a token.
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 }
Same pattern as the other functions, we convert the body to the UserLogin struct and pass it to SignIn of cognito.go.
Let's create the endpoint:
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}) })
Now we return a token to the user, let's create the call and test:
POST http://localhost:8080/user/login HTTP/1.1 content-type: application/json { "email": "wivobi1159@bitofee.com", "password": "Pass@1234" }
When making the call we will receive our jwt token:
{ "token": "token_here" }
If we get the jwt token we can see what's inside, using the website jwt.io.
Now we will list the user data saved in cognito using only the token, to do this create a function called GetUserByToken in main.go and we will need a struct to represent the response that we will return to the user, we will create it in main as well:
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() {}
Now the function:
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 }
This will be the biggest function, we need to map what we receive from Cognito to our UserResponse struct, we do this with a for and a switch, of course we could improve it, but for the sake of example we will keep it like this. Also to map custom attributes we need to put custom before, like custom:custom_id.
We also check if the user passed the token in the header, if not we return an error.
Let's create the endpoint:
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}) })
We perform the same validation as the other endpoints, but now we check the error type and if it is of the token not found type we return a StatusUnauthorized.
Let's test:
GET http://localhost:8080/user HTTP/1.1 content-type: application/json Authorization: Bearer token_jwt
Let's receive the user:
{ "user": { "id": "50601dc9-7234-419a-8427-2a4bda92d33f", "name": "John Doe", "email": "wivobi1159@bitofee.com", "custom_id": "cb748d09-40de-457a-af23-ed9483d69f8d", "email_verified": true } }
Finally, let's create the UpdatePassword function that will update the user's password:
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 }
We also make it mandatory to inform the token in the header, the rest of the function is what we have already done previously.
Let's create the last endpoint:
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"}) })
Let's make the call:
PATCH http://localhost:8080/user/password HTTP/1.1 content-type: application/json Authorization: Bearer token_jwt { "email": "wivobi1159@bitofee.com", "password": "NovaSenha2@2222" }
Now when you update your password and try to log in you will receive an error, and if you use the new password, everything will work.
In this post we talk a little about Cognito, one of the many AWS services that many people don't know about but that helps a lot in the evolution of your system.
Cognito's practicality goes beyond what I've discussed. Making a basic login is simple, but Cognito stands out for already providing you with an account verification system "ready", a login option with social networks (which can be quite annoying to implement without Coginito), two-factor authentication, among others, and it also has AWS security to protect user data.
Cognito has more functionality, it's worth seeing all of them in the documentation.
Project repository
See the post on my blog here
Subscribe and receive notification of new posts, participate
以上是使用 Golang 和 AWS Cognito 進行身份驗證的詳細內容。更多資訊請關注PHP中文網其他相關文章!