В этом руководстве я собираюсь поделиться образцом проекта, который содержит функции регистрации, входа в систему, авторизации и проверки электронной почты. Эти функции можно использовать для запуска нового бессерверного проекта. Я поделюсь кодом на 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

Https://github.com/yunuskilicdev/serverlessExample