В этом руководстве я собираюсь поделиться образцом проекта, который содержит функции регистрации, входа в систему, авторизации и проверки электронной почты. Эти функции можно использовать для запуска нового бессерверного проекта. Я поделюсь кодом на GitHub в конце статьи, чтобы люди могли начать с существующего кода. Стек технологий ниже:
- Голанг
- Горм
- AWS Lambda
- AWS API Gateway
- AWS RDS Postgresql
- AWS SES
- AWS SQS
Вы можете разработать этот проект с помощью уровня бесплатного пользования AWS. Вам не нужно ничего платить AWS.
Позже в этом уроке я дам несколько советов и приемов по каждому из этих модулей.
Начнем с создания проекта с AWS SAM.
sam init --runtime go1.x --name serverlessExample
Я предполагал, что вы знакомы с AWS SAM. Если вам нужно поискать некоторую информацию об этом, вы можете найти мою предыдущую статью ниже.
Сэм создает для нас исходную функцию и template.yaml.
Начнем с функции регистрации
ФУНКЦИЯ ПОДПИСКИ
Соберу все лямбда-функции в папку functions. Итак, создайте структуру папок ниже.
Функция регистрации требует подключения к базе данных для создания пользователя. Я использовал Amazon RDS Postgresql.
- Создайте экземпляр базы данных, а затем сделайте его общедоступным для подключения извне AWS.
Код коннектора базы данных:
package database import ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" "os" ) type PostgresConnector struct { } func (p *PostgresConnector) GetConnection() (db *gorm.DB, err error) { username := os.Getenv("db_user") password := os.Getenv("db_pass") dbName := os.Getenv("db_name") dbHost := os.Getenv("db_host") dbURI := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password) fmt.Println(dbURI) return gorm.Open("postgres", dbURI) }
Коннектор считывает информацию из переменных среды, а затем открывает соединение.
Код объекта пользователя:
package entity import "github.com/jinzhu/gorm" type User struct { gorm.Model Email string Password string `json:"-"` EmailVerified bool LoginTry int }
Для простоты моя модель очень сырая. Но вы легко можете расширить эту модель.
`json:"-"` means do not show at json
Наша модель gorm и коннектор БД готовы, поэтому мы можем легко приступить к написанию кода.
Я создал модель для анализа тела запроса.
package main type SignupRequest struct { Email string `validate:"required,email"` Password string `validate:"required"` }
Код функции регистрации
package main import ( ... ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { .... } .... func main() { lambda.Start(handler) }
Исходный код очень длинный, я хочу описать некоторые части этих блоков.
Приведенная ниже часть используется для демаршалинга тела запроса на мой запрос на регистрацию.
var signupRequest SignupRequest jsonErr := json.Unmarshal([]byte(request.Body), &signupRequest) if jsonErr != nil { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.JsonParseError) return events.APIGatewayProxyResponse{ StatusCode: http.StatusBadRequest, Body: body.ConvertToJson(), }, nil }
Я создал файл сообщений об ошибках, чтобы хранить все сообщения об ошибках в определенном файле.
package errormessage const ( Ok = 99 DatabaseError = 100 JsonParseError = 101 UserAlreadyExist = 102 UserNameOrPasswordWrong = 103 CaptchaNeeded = 104 TokenIsNotValid = 105 ) var statusText = map[int]string{ DatabaseError: "DATABASE_ERROR", JsonParseError: "JSON_PARSE_ERROR", UserAlreadyExist: "USER_ALREADY_EXIST", UserNameOrPasswordWrong: "USERNAME_OR_PASSWORD_WRONG", Ok: "OK", CaptchaNeeded: "CAPTCHA_NEEDED", TokenIsNotValid: "TOKEN_IS_NOT_VALID", } func StatusText(code int) string { return statusText[code] }
После синтаксического анализа тела запроса на мою модель мне нужно проверить эти обязательные поля. Я использовал валидатор с открытым исходным кодом для Golang. В следующем блоке кода показано, как проверить ввод.
v := validator.New() validateErr := v.Struct(signupRequest) if validateErr != nil { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.JsonParseError) return events.APIGatewayProxyResponse{ StatusCode: http.StatusBadRequest, Body: body.ConvertToJson(), }, nil }
Внутри моего запроса на регистрацию есть заявление
`validate:"required,email"`
где указано, что это поле является обязательным и его тип - электронная почта.
После проверки ввода нам нужно проверить, существует ли пользователь, если не создать нового пользователя.
postgresConnector := database.PostgresConnector{} dbConn, dbErr := postgresConnector.GetConnection() defer dbConn.Close() if dbErr != nil { fmt.Print(dbErr) return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "", }, nil } dbConn.AutoMigrate(&entity.User{}) var users []entity.User filter := &entity.User{} filter.Email = signupRequest.Email dbConn.Where(filter).Find(&users) if users != nil && len(users) > 0 { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.UserAlreadyExist) return events.APIGatewayProxyResponse{ StatusCode: http.StatusBadRequest, Body: body.ConvertToJson(), }, nil } newUser := &entity.User{} newUser.Email = signupRequest.Email newUser.Password = hashAndSalt(signupRequest.Password) dbConn.Create(&newUser)
По соображениям безопасности вы должны хранить хешированный пароль. Моя функция хеширования:
func hashAndSalt(pwd string) (hashed string) { hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) if err != nil { log.Println(err) } return string(hash) }
Кодовая часть регистрации готова. Перейдем к make-файлу и template.yaml
Makefile
.PHONY: clean build clean: rm -rf ./bin/signup/signupl build: GOOS=linux GOARCH=amd64 go build -o bin/signup/signup ./functions/signup
Template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > serverlessExample Sample SAM Template for serverlessExample # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 5 Parameters: dbname: Type: String Default: example username: Type: String Default: postgres password: Type: String Default: password host: Type: String Default: localhost Resources: SignupFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: bin/signup Handler: signup Runtime: go1.x Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html Events: Signup: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /signup Method: POST Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: db_user: !Ref username db_pass: !Ref password db_name: !Ref dbname db_host: !Ref host Outputs: ApiURL: Description: "API URL" Value: !Sub 'https://${ExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
Вышеупомянутый шаблон автоматически создает лямбда-функцию и ее роль с помощью следующих команд.
$make clean build $sam package --template-file template.yaml --s3-bucket YOURS3BUCKETNAME --output-template-file packaged.yaml $aws cloudformation deploy --template-file PATH/packaged.yaml --stack-name serverlessexample --capabilities CAPABILITY_IAM --parameter-overrides dbname=AA username=BB password=CC host=DD
ПОЛЕЗНЫЕ СОВЕТЫ
Произошла ошибка во время развертывания, подробности можно увидеть в событиях CloudFormation.
Вы можете протестировать в Консоли AWS или локально.
ФУНКЦИЯ ВХОДА
Функция входа в систему будет более сложной, чем функция регистрации. Из-за требований авторизации, Также после некоторого неправильного входа наша программа требует captcha. Для простоты на данный момент капча связана только с идентификатором пользователя. Но вы можете улучшить эту логику.
package main type LoginRequest struct { Email string `validate:"required,email"` Password string `validate:"required"` CaptchaId string CaptchaResponse string } type LoginResponse struct { AccessToken string RefreshToken string }
Функция входа также начинается с синтаксического анализа и проверки, как и функция регистрации. Поэтому я пропускаю эти части. Затем он проверяет имя пользователя и пароль.
if user.LoginTry >= 5 && !validateCaptcha(loginRequest) { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.CaptchaNeeded) response := events.APIGatewayProxyResponse{ StatusCode: http.StatusUnauthorized, Body: body.ConvertToJson(), } return createApiLoginFailResponse(response, user, dbConn) } passwordErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginRequest.Password)) if passwordErr != nil { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.UserNameOrPasswordWrong) response := events.APIGatewayProxyResponse{ StatusCode: http.StatusUnauthorized, Body: body.ConvertToJson(), } return createApiLoginFailResponse(response, user, dbConn) }
Если количество неудачных попыток входа в систему больше пяти, возникает ошибка, связанная с необходимостью ввода кода. Я опишу создание и проверку капчи ниже.
func createApiLoginFailResponse(response events.APIGatewayProxyResponse, user entity.User, dbConn *gorm.DB) (events.APIGatewayProxyResponse, error) { if user.ID > 0 { user.LoginTry = user.LoginTry + 1 dbConn.Save(user) if user.LoginTry >= 5 { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.CaptchaNeeded) return events.APIGatewayProxyResponse{ StatusCode: http.StatusUnauthorized, Body: body.ConvertToJson(), }, nil } else { return response, nil } } else { return response, nil } }
Если имя пользователя и пароль верны, будет сгенерирован токен доступа и обновления. Github.com/dgrijalva/jwt-go используется для операций jwt.
func CreateTokens(user entity.User) (model.TokenSet, error) { accessTokenExpireAt := time.Now().Add(1 * time.Hour) tokenStr, signErr := CreateToken(user, "Access", accessTokenExpireAt) if signErr != nil { return model.TokenSet{}, signErr } refreshTokenExpireAt := time.Now().Add(24 * time.Hour) refreshTokenStr, signErr := CreateToken(user, "Refresh", refreshTokenExpireAt) if signErr != nil { return model.TokenSet{}, signErr } return model.TokenSet{AccessToken: tokenStr, ExpireAt: accessTokenExpireAt, RefreshToken: refreshTokenStr, RefreshExpireAt: refreshTokenExpireAt}, nil } func ValidateToken(token string) (*jwt.Token, error) { return jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("jwt_key")), nil }) } func CreateToken(user entity.User, tokenType string, expireTime time.Time) (string, error) { var claim model.CustomClaims claim.Id = string(user.ID) claim.Type = tokenType expiresAt := expireTime claim.ExpiresAt = expiresAt.Unix() token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) jwtKey := os.Getenv("jwt_key") tokenStr, signErr := token.SignedString([]byte(jwtKey)) return tokenStr, signErr }
Токен доступа действителен 1 час. Токен обновления действителен в течение 24 часов.
При подписании токена jwt эта библиотека дает вам стандартное требование. Но мне нужен тип поля, чтобы понять тип токена. Итак, я написал собственный токен ниже.
package model import "github.com/dgrijalva/jwt-go" type CustomClaims struct { jwt.StandardClaims Type string }
После успешного входа в систему API возвращает пользователю токены доступа и обновления.
Создание и проверка Captcha
Github.com/dchest/captcha используется для операций с капчей.
Но наша бессерверная функция будет убита после выполнения. Поэтому вам нужно хранить ваши капчи в кеше. Эта библиотека поддерживает настраиваемые хранилища, но вам нужно потратить некоторое время на разработку.
Пользовательский код магазина:
package common import ( "fmt" "github.com/go-redis/redis" "log" "os" "time" ) type CustomizeRdsStore struct { RedisClient *redis.Client ExpireAt time.Duration } func GetStore() *CustomizeRdsStore { return NewStore(time.Duration(1 * time.Hour)) } func NewStore(expireAt time.Duration) *CustomizeRdsStore { client := redis.NewClient(&redis.Options{ Addr: os.Getenv("redis_url"), Password: "", // no password set DB: 0, // use default DB }) c := new(CustomizeRdsStore) c.RedisClient = client c.ExpireAt = expireAt return c } func (s CustomizeRdsStore) SetWithOverrideExpire(id string, value string, expireAt time.Duration) { err := s.RedisClient.Set(id, value, expireAt).Err() if err != nil { log.Println(err) } } func (s CustomizeRdsStore) SetWithoutExpire(id string, value string) { err := s.RedisClient.Set(id, value, 0).Err() if err != nil { log.Println(err) } } // customizeRdsStore implementing Set method of Store interface func (s CustomizeRdsStore) Set(id string, value []byte) { err := s.RedisClient.Set(id, string(value), s.ExpireAt).Err() if err != nil { log.Println(err) } } // customizeRdsStore implementing Get method of Store interface func (s CustomizeRdsStore) Get(id string, clear bool) (value []byte) { val, err := s.RedisClient.Get(id).Result() if err != nil { log.Println(err) return []byte{} } if clear { err := s.RedisClient.Del(id).Err() if err != nil { log.Println(err) return []byte{} } } return []byte(val) }
func (s CustomizeRdsStore) Установить (строка идентификатора, значение [] байт) и
func (s CustomizeRdsStore) Get (строка идентификатора, очистить bool)
реализован для интерфейса Магазина. Остальные функции - это мои вспомогательные функции.
Создать код Captcha:
package main import ( "bytes" "encoding/base64" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/dchest/captcha" "github.com/yunuskilicdev/serverlessNear/common" "github.com/yunuskilicdev/serverlessNear/common/errormessage" "github.com/yunuskilicdev/serverlessNear/common/model" "net/http" "time" ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { store := common.NewStore(time.Duration(5 * time.Minute)) captcha.SetCustomStore(store) captchaResponse := model.CaptchaResponse{} captchaId := captcha.New() var ImageBuffer bytes.Buffer captcha.WriteImage(&ImageBuffer, captchaId, 300, 90) captchaResponse.Id = captchaId captchaResponse.Image = base64.StdEncoding.EncodeToString(ImageBuffer.Bytes()) body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.Ok) body.ResponseObject = captchaResponse return events.APIGatewayProxyResponse{ Body: body.ConvertToJson(), StatusCode: http.StatusOK, }, nil } func main() { lambda.Start(handler) }
Проверка Captcha будет выполняться внутри функции входа в систему.
func validateCaptcha(request LoginRequest) bool { if request.CaptchaId == "" || request.CaptchaResponse == "" { return false } store := common.GetStore() captcha.SetCustomStore(store) return captcha.VerifyString(request.CaptchaId, request.CaptchaResponse) }
Добавьте функцию входа в makefile и template.yaml так же, как и в подписку.
Наш API возвращает токен доступа, поэтому мы можем добавить авторизацию в наш API.
РАЗРЕШЕНИЕ
В Aws Lambda Go есть пример настраиваемой функции аутентификации. Я отредактировал этот код, чтобы проверить токен JWT.
package main import ( "context" "errors" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/yunuskilicdev/serverlessExample/common" ) // Help function to generate an IAM policy func generatePolicy(principalId, effect, resource string) events.APIGatewayCustomAuthorizerResponse { authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalId} if effect != "" && resource != "" { authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{ Version: "2012-10-17", Statement: []events.IAMPolicyStatement{ { Action: []string{"execute-api:Invoke"}, Effect: effect, Resource: []string{resource}, }, }, } } // Optional output with custom properties of the String, Number or Boolean type. authResponse.Context = map[string]interface{}{ "stringKey": "stringval", "numberKey": 123, "booleanKey": true, } return authResponse } func handleRequest(ctx context.Context, event events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) { token := event.AuthorizationToken parse, e := common.ValidateToken(token) if e != nil || !parse.Valid { return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized") } return generatePolicy("user", "Allow", event.MethodArn), nil } func main() { lambda.Start(handleRequest) }
Нам нужно добавить API и функцию внутри template.yaml
ExampleApi: Type: AWS::Serverless::Api Properties: StageName: Prod Auth: DefaultAuthorizer: MyLambdaTokenAuthorizer Authorizers: MyLambdaTokenAuthorizer: FunctionArn: !GetAtt CustomAuthorizerFunction.Arn CustomAuthorizerFunction: Type: AWS::Serverless::Function Properties: CodeUri: bin/authorizer Handler: authorizer Runtime: go1.x Environment: Variables: db_user: !Ref username db_pass: !Ref password db_name: !Ref dbname db_host: !Ref host jwt_key: !Ref jwt redis_url: !Ref redisurl
Функция регистрации не требует токена доступа. Поэтому вам нужно указать, что функция регистрации не имеет авторизации, как показано ниже.
SignupFunction: Type: AWS::Serverless::Function Properties: CodeUri: bin/signup Handler: signup Runtime: go1.x Tracing: Active Events: Signup: Type: Api Properties: RestApiId: !Ref ExampleApi Auth: Authorizer: 'NONE' Path: /signup Method: POST Environment: Variables: db_user: !Ref username db_pass: !Ref password db_name: !Ref dbname db_host: !Ref host jwt_key: !Ref jwt
ФУНКЦИЯ USERINFO
Для доступа к информации о пользователе требуется токен доступа. Поэтому я создаю функцию userinfo, чтобы продемонстрировать работу функции аутентификации.
ПОЛЕЗНЫЕ СОВЕТЫ
Auth не работает, когда вы запускаете свою функцию локально.
Функция Userinfo
package main import ( "encoding/binary" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/yunuskilicdev/serverlessExample/common" "github.com/yunuskilicdev/serverlessExample/common/errormessage" "github.com/yunuskilicdev/serverlessExample/common/model" "github.com/yunuskilicdev/serverlessExample/database" "github.com/yunuskilicdev/serverlessExample/database/entity" "net/http" ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { token := request.Headers["Authorization"] userId := common.GetStore().Get(token, false) postgresConnector := database.PostgresConnector{} dbConn, dbErr := postgresConnector.GetConnection() defer dbConn.Close() if dbErr != nil { body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.DatabaseError) response := events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: body.ConvertToJson(), } return response, nil } var userFilter entity.User u, _ := binary.Uvarint(userId) userFilter.ID = uint(u) var user entity.User dbConn.Where(userFilter).Find(&user) body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.Ok) body.ResponseObject = user return events.APIGatewayProxyResponse{ Body: body.ConvertToJson(), StatusCode: http.StatusOK, }, nil } func main() { lambda.Start(handler) }
ОТПРАВКА ЭЛЕКТРОННОЙ ПОЧТЫ ДЛЯ ПРОВЕРКИ
После регистрации я хочу отправить электронное письмо пользователям, чтобы проверить их электронную почту. Отправка писем может выполняться асинхронно. Поэтому я буду использовать AWS Simple Queue Service для написания запросов на отправку почты. Затем другая лямбда-функция читает из очереди и завершает отправку электронной почты.
ПОЛЕЗНЫЕ СОВЕТЫ
Создание роли будет инициировано при первом развертывании функций в AWS. Чтобы писать SQS, вам нужно разрешение для этой роли.
- ReceiveMessage
- Удалить сообщение
- GetQueueAttributes
- Отправить сообщение
Будет добавлена функция внутренней регистрации под кодом
store := common.GetStore() expireAt := time.Now().Add(1 * time.Hour) token, jsonErr := common.CreateToken(*newUser, "Mail", expireAt) store.SetWithOverrideExpire(token, string(newUser.ID), expireAt.Sub(time.Now())) var mailRequest model.SendVerificationMailRequest mailRequest.UserId = newUser.ID mailRequest.Token = token mailRequest.Email = newUser.Email emailJsonData, _ := json.Marshal(mailRequest) s := string(emailJsonData) u := string(os.Getenv("email_queue_url")) sess, err := session.NewSession(&aws.Config{ Region: aws.String("eu-west-1")}, ) if err != nil { fmt.Println(err) } sqsClient := sqs.New(sess) sqsClient.ServiceName = os.Getenv("email_queue") input := sqs.SendMessageInput{ MessageBody: &s, QueueUrl: &u, } _, jsonErr = sqsClient.SendMessage(&input) if jsonErr != nil { fmt.Println(jsonErr) }
Для отправки сообщения будут использоваться параметры email_queue и email_queue_url.
Поэтому вам нужно добавить их в template.yaml
Функция отправителя электронной почты
package main import ( "context" "encoding/json" "fmt" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/yunuskilicdev/serverlessExample/common" "github.com/yunuskilicdev/serverlessExample/common/model" ) const ( // Replace [email protected] with your "From" address. // This address must be verified with Amazon SES. Sender = "[email protected]" // Replace [email protected] with a "To" address. If your account // is still in the sandbox, this address must be verified. Recipient = "[email protected]" // Specify a configuration set. To use a configuration // set, comment the next line and line 92. //ConfigurationSet = "ConfigSet" // The subject line for the email. Subject = "Amazon SES Test (AWS SDK for Go)" // The HTML body for the email. HtmlBody = "<h1>Amazon SES Test Email (AWS SDK for Go)</h1><p>This email was sent with " + "<a href='https://aws.amazon.com/ses/'>Amazon SES</a> using the " + "<a href='https://aws.amazon.com/sdk-for-go/'>AWS SDK for Go</a>.</p>" //The email body for recipients with non-HTML email clients. TextBody = "This email was sent with Amazon SES using the AWS SDK for Go." // The character encoding for the email. CharSet = "UTF-8" ) func handler(ctx context.Context, sqsEvent events.SQSEvent) error { for _, message := range sqsEvent.Records { fmt.Printf("The message %s for event source %s = %s \n", message.MessageId, message.EventSource, message.Body) var request model.SendVerificationMailRequest json.Unmarshal([]byte(message.Body), &request) common.SendMail(request.Token) } return nil } func main() { lambda.Start(handler) }
Эта функция отличается от других. Потому что эта функция будет запускаться SQS. Таким образом, его конфигурация шаблона yaml также отличается.
SendMailFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: bin/sendemail Handler: sendemail Runtime: go1.x Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html Events: UserInfo: Type: SQS # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Queue: !Ref emailQueue # NOTE: FIFO SQS Queues are not yet supported BatchSize: 10 Enabled: false Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: db_user: !Ref username db_pass: !Ref password db_name: !Ref dbname db_host: !Ref host jwt_key: !Ref jwt redis_url: !Ref redisurl email_queue: !Ref emailQueue email_queue_url: !Ref emailQueueUrl prod_link: !Sub 'https://${ExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
Общая функция отправки почты
package common import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ses" "os" ) const ( // Replace [email protected] with your "From" address. // This address must be verified with Amazon SES. Sender = "[email protected]" // Replace [email protected] with a "To" address. If your account // is still in the sandbox, this address must be verified. Recipient = "[email protected]" // Specify a configuration set. To use a configuration // set, comment the next line and line 92. //ConfigurationSet = "ConfigSet" // The subject line for the email. Subject = "Please verify your mail" // The HTML body for the email. HtmlBody = "<h1>Email verification mail</h1><p>" + "<a href='%s'>Amazon SES</a>" //The email body for recipients with non-HTML email clients. TextBody = "This email was sent to verify your mail" // The character encoding for the email. CharSet = "UTF-8" ) func SendMail(token string) { sess, err := session.NewSession(&aws.Config{ Region: aws.String("eu-west-1")}, ) if err != nil { fmt.Println(err) } // Create an SES session. svc := ses.New(sess) verifyLink := os.Getenv("prod_link") + "verifyemail?token=" + token // Assemble the email. input := &ses.SendEmailInput{ Destination: &ses.Destination{ CcAddresses: []*string{}, ToAddresses: []*string{ aws.String(Recipient), }, }, Message: &ses.Message{ Body: &ses.Body{ Html: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(fmt.Sprintf(HtmlBody, verifyLink)), }, Text: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(TextBody), }, }, Subject: &ses.Content{ Charset: aws.String(CharSet), Data: aws.String(Subject), }, }, Source: aws.String(Sender), // Uncomment to use a configuration set //ConfigurationSetName: aws.String(ConfigurationSet), } // Attempt to send the email. result, err := svc.SendEmail(input) // Display error messages if they occur. if err != nil { if aerr, ok := err.(awserr.Error); ok { switch aerr.Code() { case ses.ErrCodeMessageRejected: fmt.Println(ses.ErrCodeMessageRejected, aerr.Error()) case ses.ErrCodeMailFromDomainNotVerifiedException: fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) case ses.ErrCodeConfigurationSetDoesNotExistException: fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) default: fmt.Println(aerr.Error()) } } else { // Print the error, cast err to awserr.Error to get the Code and // Message from an error. fmt.Println(err.Error()) } return } fmt.Println("Email Sent to address: " + Recipient) fmt.Println(result) }
ПОЛЕЗНЫЕ СОВЕТЫ
Вам необходимо добавить политику AmazonSESFullAccess к роли функций sendemail.
ПОЛЕЗНЫЕ СОВЕТЫ
Вам необходимо подтвердить почтовый адрес отправителя в Консоли AWS.
ФУНКЦИЯ ЭЛЕКТРОННОЙ ПОЧТЫ ДЛЯ ПРОВЕРКИ
package main import ( "encoding/binary" "fmt" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/yunuskilicdev/serverlessExample/common" "github.com/yunuskilicdev/serverlessExample/common/errormessage" "github.com/yunuskilicdev/serverlessExample/common/model" "github.com/yunuskilicdev/serverlessExample/database" "github.com/yunuskilicdev/serverlessExample/database/entity" "net/http" ) func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { token := request.QueryStringParameters["token"] validateToken, err := common.ValidateToken(token) if err != nil { fmt.Println(err) } claims := validateToken.Claims.(*model.CustomClaims) if validateToken.Valid && claims.Type == "Mail" { store := common.GetStore() value := store.Get(token, true) var userFilter entity.User u, _ := binary.Uvarint(value) userFilter.ID = uint(u) postgresConnector := database.PostgresConnector{} dbConn, dbErr := postgresConnector.GetConnection() defer dbConn.Close() if dbErr != nil { fmt.Print(dbErr) return events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "", }, nil } var user entity.User dbConn.Where(userFilter).Find(&user) user.EmailVerified = true dbConn.Save(&user) body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.Ok) body.ResponseObject = user return events.APIGatewayProxyResponse{ Body: body.ConvertToJson(), StatusCode: http.StatusOK, }, nil } body := model.ResponseBody{} body.Message = errormessage.StatusText(errormessage.TokenIsNotValid) body.ResponseObject = nil return events.APIGatewayProxyResponse{ Body: body.ConvertToJson(), StatusCode: http.StatusBadRequest, }, nil } func main() { lambda.Start(handler) }
Итак, все функции готовы :)
ПОЛЕЗНЫЕ СОВЕТЫ
Когда я использую Elasticache для кеширования, подключение к elasticache было очень долгим. Поэтому я предпочитаю использовать Redis, установленный на экземпляре EC2. Если ваши функции и EC2 находятся в одной группе безопасности VPC, ваша функция может получить доступ к экземплярам EC2.
Github