Golang的基于移动OTP的身份验证
#go #mongodb #twilio #gofiber

在这篇文章中,我们将在Golang开发基于移动OTP的身份验证API。

技术堆栈

golang

GO(或Golang)是由Google开发的开源编程语言。它强调简单,效率和并发支持。 GO广泛用于Web开发,系统编程和云本地应用程序。

https://go.dev/

gofiber

Gofiber是用GO(Golang)编写的轻巧且快速的网络框架。它建立在Fasthttp之上,使其成为构建高性能API和Web应用程序的有效选择。 Gofiber提供了中间件支持,路由和上下文处理之类的功能。

https://gofiber.io/

mongodb

mongoDB是NOSQL,面向文档的数据库,以其灵活性和可扩展性而闻名。它以JSON般的BSON格式存储数据,从而易于操纵和索引。 MongoDB广泛用于处理现代应用程序中的大规模,非结构化和实时数据。

https://www.mongodb.com/

twilio

Twilio是一个云通信平台,使开发人员能够使用API​​将消息,语音和视频功能集成到其应用程序中。它简化了构建沟通功能(例如SMS,MMS,电话等)的过程,允许企业以编程方式与客户互动。

https://www.twilio.com/

在项目内创建一个新的Directory auth和Init GO模块。

go mod init auth

安装所需软件包

go get github.com/gofiber/fiber/v2
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get github.com/twilio/twilio-go
go get go.mongodb.org/mongo-driver/mongo

为我们的项目创建文件夹结构

Project
├── README.md
├── config
│   └── config.go
├── database
│   ├── connection.go
│   └── database.go
├── go.mod
├── go.sum
├── handler
│   └── auth.go
├── main.go
├── middleware
│   └── auth.go
├── model
│   └── user.go
├── router
│   └── router.go
├── schema
│   ├── auth.go
│   └── response.go
└── util
    ├── twilio.go
    └── user.go
  • config文件夹包含配置,即env变量配置。

  • database文件夹包含数据库连接和与db相关的类型,即mongodb驱动程序设置。

  • handler文件夹包含路由处理程序,即注册处理程序,登录处理程序等...

  • middleware文件夹包含在任何处理程序执行之前或之后运行的路由中间件

  • model包含mongodb架构,即用户架构。

  • router包含路由初始化,并将API路由映射到特定的路由处理程序,即验证路由。

  • schema包含应用程序架构,即传入请求身体模式。

  • util包含可重复使用的数据库查询和应用程序助手功能,即通过电话找到用户,发送短信等...

  • .env包含环境变量,即twilio api-key,mongodb-uri等...

  • 复制所有.env.example并将其粘贴到.env中。

  • main.go是GO应用程序的入口点。

接下来,我将向您展示此项目所需的路线。

- /api/auth
    - /register
    - /login
    - /verify_otp
    - /resend_otp
    - /me

main.go

package main

import (
    "auth/database"
    "auth/router"
    "log"
)

func main() {
    app := router.New()
    err := database.Connect()
    if err != nil {
        panic(err)
    }
    log.Fatal(app.Listen(":3000"))
}

如果您熟悉Express,则会发现gofiber语法与Express(节点Web Framework)相似。

在这里我们做了3件事: -

  • 初始化了我们的路由器。

  • 与MongoDB数据库连接。

  • ,然后启动我们的服务器以在端口3000

  • 上收听

database/database.go

package database

import "go.mongodb.org/mongo-driver/mongo"

const Users = "users"

type MongoInstance struct {
    Client *mongo.Client
    Db     *mongo.Database
}

用户const商店收集名称,以便我们可以重复使用此变量,在需要查询用户数据的情况下收集用户。

我们还创建了一个结构来存储MongoDB客户端和数据库指针。

database/connection.go

package database

import (
    "auth/config"
    "context"
    "log"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var Mg MongoInstance

func Connect() error {
    dbName := config.Config("DATABASE_NAME")
    uri := config.Config("DATABASE_URI") + dbName
    client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
    if err != nil {
        return err
    }
    log.Printf("Connected with databse %s", dbName)
    db := client.Database(dbName)
    Mg = MongoInstance{
        Client: client,
        Db:     db,
    }

    return nil
}


我们在此处定义了软件包级别变量MG和存储数据库信息,以便我们可以使用此变量查询我们的数据库。

DATABASE_NAMEDATABASE_URI是一个环境变量,其值是从.env文件加载的。

上下文软件包通常用于传递链中或跨goroutines中不同函数调用之间的上下文信息。该软件包提供了通过呼叫堆栈传播取消信号和截止日期的能力。

GO的上下文软件包中的context.TODO()函数用于创建一个新的空上下文,当您没有更具体的上下文或不确定要使用哪个上下文时。

config/config.go

package config

import (
    "fmt"
    "os"

    "github.com/joho/godotenv"
)

// Config func to get env value
func Config(key string) string {
    // load .env file
    err := godotenv.Load(".env")
    if err != nil {
        fmt.Print("Error loading .env file")
    }
    return os.Getenv(key)
}

在这里,我们使用github.com/joho/godotenv软件包来加载来自.env file的ENV变量。

router/router.go

package router

import (
    "auth/handler"
    "auth/middleware"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/compress"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/monitor"
)

func New() *fiber.App {
    app := fiber.New()
    api := app.Group("/api")
    auth := api.Group("/auth")

    auth.Post("/register", handler.Register)
    auth.Post("/login", handler.Login)
    auth.Post("/verify_otp", handler.VerifyOTP)
    auth.Post("/resend_otp", handler.ResendOTP)
    auth.Get("/me", middleware.Protected(), handler.GetCurrentUser)

    return app
}

我们已经为apiauth添加了初始化的光纤,因此我们确实需要一次又一次地键入每个路径。它还提供了一种更好的方法,将中间件应用于一组路线,而不是将相同的中间件应用于每个路线。

model/user.go


package model

import "go.mongodb.org/mongo-driver/bson/primitive"

type User struct {
    ID    primitive.ObjectID `json:"id" bson:"_id"`
    Name  string             `json:"name"`
    Phone string             `json:"phone"`
    Otp   string             `json:"otp,omitempty"`
}

我们创建了一个用户模型,以将用户数据始终存储在MongoDB数据库中。

schema/response.go

package schema

type ResponseHTTP struct {
    Success bool   `json:"success"`
    Data    any    `json:"data"`
    Message string `json:"message"`
}

我们将使用此模式以一致的方式发送HTTP响应。

schema/auth.go

package schema

type RegisterBody struct {
    Name  string `json:"name"`
    Phone string `json:"phone"`
}

type LoginSchema struct {
    Phone string `json:"phone"`
}

type VerifyOTPSchema struct {
    Phone string `json:"phone"`
    Otp   string `json:"otp"`
}

我们将使用此模式来解析传入的请求身体数据。

handler/auth.go

package handler

import (
    "auth/model"
    "auth/schema"
    "auth/util"

    "github.com/gofiber/fiber/v2"
)

我们以后添加了所需的软件包导入,我们将在util中添加许多实用程序功能。

// ...
// ...
func Register(c *fiber.Ctx) error {
    // request body data
    body := new(schema.RegisterBody)
    if err := c.BodyParser(body); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    // validate duplicate mobile number

    user, err := util.FindUserByPhone(body.Phone)

    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    if user != nil {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: "Phone number already in use",
        })
    }

    // create new user

    id, err := util.InsertUser(body)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
        Success: true,
        Data: fiber.Map{
            "id": id,
        },
        Message: "Account registered successfully",
    })
}

在这里,我们添加了一个登录处理程序,如果已经注册了特定用户,它将将OTP发送到手机号码。

  • 首先,它将解析请求身体数据并将其存储在体内。

  • 接下来,它将使用手机号码从数据库中验证用户。

  • 生成OTP并在数据库中更新用户收集的OTP字段。

  • 并将生成的OTP发送到用户的手机号码。

func VerifyOTP(c *fiber.Ctx) error {
    // request body data
    body := new(schema.VerifyOTPSchema)
    if err := c.BodyParser(body); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    // find phone in database
    user, err := util.FindUserByPhone(body.Phone)

    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    if user == nil {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: "Phone number not exists",
        })
    }

    if user.Otp != body.Otp {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: "Incorrect Otp",
        })
    }

    // generate jwt token
    token, err := util.GenerateJWT(user.ID.Hex())
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    // remove old otp from db
    util.UpdateUser(user.ID, map[string]any{
        "otp": "",
    })

    return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
        Success: true,
        Data: fiber.Map{
            "token": "Bearer " + token,
        },
        Message: "Account login successfully",
    })
}

此处理程序将验证用户的OTP,并返回JWT携带者令牌,以响应OTP是否正确。

  • 首先,它将解析请求身体数据并将其存储在体内。

  • 验证提供的电话号码。

  • 验证提供的OTP。

  • 用用户ID作为有效载荷创建JWT令牌。

  • 从用户集合中删除旧的OTP。

func ResendOTP(c *fiber.Ctx) error {
    // request body data
    body := new(schema.VerifyOTPSchema)
    if err := c.Status(fiber.StatusBadRequest).BodyParser(body); err != nil {
        return c.JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    // find phone in database
    user, err := util.FindUserByPhone(body.Phone)

    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    if user == nil {
        return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: "Phone number not exists",
        })
    }

    otp := util.GenerateRandomNumber()

    // save otp in database
    util.UpdateUser(user.ID, map[string]any{
        "otp": otp,
    })
    // send otp to user phone

    err = util.SendOTP(user.Phone, otp)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
            Success: false,
            Data:    nil,
            Message: err.Error(),
        })
    }

    return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
        Success: true,
        Data:    nil,
        Message: "Sent otp to registered mobile number",
    })
}

此处理程序将处理OTP的重新安置到特定的手机号码。


func GetCurrentUser(c *fiber.Ctx) error {
    user := c.Locals("user").(*model.User)
    user.Otp = ""
    return c.Status(fiber.StatusOK).JSON(schema.ResponseHTTP{
        Success: true,
        Data:    user,
        Message: "Get current user",
    })
}

此处理程序将返回当前登录的用户。我们已经在Auth Middleware中添加了当地人的用户价值。我们删除了OTP值,以使用户无法在响应中获得OTP。

util/user.go

package util

import (
    "auth/config"
    "auth/database"
    "auth/model"
    "auth/schema"
    "context"
    "math/rand"
    "strconv"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)

func FindUserByPhone(phone string) (*model.User, error) {
    // Create a context and a collection instance
    ctx := context.TODO()
    collection := database.Mg.Db.Collection(database.Users)

    // Create a filter to find the user by phone number
    filter := bson.M{"phone": phone}

    // Create a variable to store the result
    var result model.User

    // Find the user with the given phone number
    err := collection.FindOne(ctx, filter).Decode(&result)

    if err != nil {
        if err == mongo.ErrNoDocuments {
            // If the error is ErrNoDocuments, it means no user was found
            return nil, nil
        }
        // Handle other potential errors
        return nil, err
    }

    return &result, nil
}

func InsertUser(user *schema.RegisterBody) (any, error) {
    // Create a context and a collection instance
    ctx := context.TODO()
    collection := database.Mg.Db.Collection(database.Users)

    // Insert the user into the collection
    result, err := collection.InsertOne(ctx, user)
    return result.InsertedID, err
}

func UpdateUser(userID primitive.ObjectID, updatedFields map[string]any) error {
    // Create a context and a collection instance
    ctx := context.TODO()
    collection := database.Mg.Db.Collection(database.Users)

    // Create a filter to find the user by ID
    filter := bson.M{"_id": userID}

    // Create an update with the provided fields
    update := bson.M{"$set": updatedFields}

    // Update the user document in the collection
    _, err := collection.UpdateOne(ctx, filter, update)
    return err

}

func FindUserById(userId string) (*model.User, error) {
    // Create a context and a collection instance
    id, err := primitive.ObjectIDFromHex(userId)
    if err != nil {
        return nil, err
    }
    ctx := context.TODO()
    collection := database.Mg.Db.Collection(database.Users)

    // Create a filter to find the user by phone number
    filter := bson.M{"_id": id}

    // Create a variable to store the result
    var result model.User

    // Find the user with the given phone number
    err = collection.FindOne(ctx, filter).Decode(&result)

    if err != nil {
        if err == mongo.ErrNoDocuments {
            // If the error is ErrNoDocuments, it means no user was found
            return nil, nil
        }
        // Handle other potential errors
        return nil, err
    }

    return &result, nil
}

func GenerateRandomNumber() string {
    // Generate a random number between 1000 and 9999 (inclusive)
    num := rand.Intn(9000) + 1000
    return strconv.Itoa(num)
}

func GenerateJWT(id string) (string, error) {
    token := jwt.New(jwt.SigningMethodHS256)
    claims := token.Claims.(jwt.MapClaims)
    claims["userId"] = id
    claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
    return token.SignedString([]byte(config.Config("SECRET")))

}

在这里,我们添加了所有实用程序助手功能以查询我们的数据库和与Auth相关的助手功能。

util/twilio.go


package util

import (
    "auth/config"
    "fmt"
    "log"

    "github.com/twilio/twilio-go"

    openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

func SendOTP(to string, otp string) error {
    accountSid := config.Config("TWILIO_ACCOUNT_SID")
    authToken := config.Config("TWILIO_AUTH_TOKEN")

    client := twilio.NewRestClientWithParams(twilio.ClientParams{
        Username: accountSid,
        Password: authToken,
    })

    params := &openapi.CreateMessageParams{}

    params.SetTo(to)
    params.SetFrom(config.Config("TWILIO_PHONE_NUMBER"))

    msg := fmt.Sprintf("Your OTP is %s", otp)
    params.SetBody(msg)

    _, err := client.Api.CreateMessage(params)
    if err != nil {
        log.Println(err.Error())
        return err
    }
    log.Println("SMS sent successfully!")

    return nil
}

在这里,我们使用Twilio SMS服务将OTP发送到手机号码。您需要在Twilio中创建一个帐户才能使用他们的服务。

您可以从Twilio仪表板生成虚拟手机号码,还可以从仪表板复制帐户SID和Auth令牌。

middleware/auth.go

package middleware

import (
    "auth/config"
    "auth/schema"
    "auth/util"

    jwtware "github.com/gofiber/contrib/jwt"
    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v5"
)

// Protected protect routes
func Protected() fiber.Handler {
    return jwtware.New(jwtware.Config{
        SigningKey:     jwtware.SigningKey{Key: []byte(config.Config("SECRET"))},
        ErrorHandler:   jwtError,
        SuccessHandler: jwtSuccess,
        ContextKey:     "payload",
    })
}

func jwtSuccess(c *fiber.Ctx) error {
    payload := c.Locals("payload").(*jwt.Token)
    claims := payload.Claims.(jwt.MapClaims)
    userId := claims["userId"].(string)
    user, err := util.FindUserById(userId)
    if err != nil {
        return c.Status(fiber.StatusUnauthorized).JSON(schema.ResponseHTTP{
            Success: false,
            Message: "User not exists",
            Data:    nil,
        })
    }
    c.Locals("user", user)
    return c.Next()
}
func jwtError(c *fiber.Ctx, err error) error {
    if err.Error() == "Missing or malformed JWT" {
        return c.Status(fiber.StatusBadRequest).
            JSON(schema.ResponseHTTP{Success: false, Message: "Missing or malformed JWT", Data: nil})
    }
    return c.Status(fiber.StatusUnauthorized).
        JSON(schema.ResponseHTTP{Success: false, Message: "Invalid or expired JWT", Data: nil})
}

受保护的中间件将执行以下操作: -

  • 验证并解析JWT令牌并从中获得有效载荷。

  • 在错误或已过期的JWT令牌的情况下,使用JWTERROR处理程序功能处理错误。

  • 如果没有错误,则将执行jwtsuccess。

  • 我们通过用户找到用户。

  • 添加当地人中的用户信息以从任何柄访问。

  • c.next()将执行下一个处理程序功能。

默认情况下,ContextKeyuser,但我们已将其更改为payload

您可以在github中探索完整的代码库

GitHub logo harshmangalam / golang-mobile-otp-auth

使用Golang的基于移动OTP的身份验证

Golang Mobile OTP based Auth

Tech stack

  • golang
  • gofiber
  • mongodb
  • twilio sdk

Project structure

Project
├── README.md
├── config
│   └── config.go
├── database
│   ├── connection.go
│   └── database.go
├── go.mod
├── go.sum
├── handler
│   └── auth.go
├── main.go
├── middleware
│   └── auth.go
├── model
│   └── user.go
├── router
│   └── router.go
├── schema
│   ├── auth.go
│   └── response.go
└── util
    ├── twilio.go
    └── user.go

Routes

  • /api/auth
    • /register (create new account)
    • /login (sent otp to registered mobile number)
    • /verify_otp
    • /resend_otp
    • /me (get current loggedin user)