在这篇文章中,我们将在Golang开发基于移动OTP的身份验证API。
技术堆栈
golang
GO(或Golang)是由Google开发的开源编程语言。它强调简单,效率和并发支持。 GO广泛用于Web开发,系统编程和云本地应用程序。
gofiber
Gofiber是用GO(Golang)编写的轻巧且快速的网络框架。它建立在Fasthttp之上,使其成为构建高性能API和Web应用程序的有效选择。 Gofiber提供了中间件支持,路由和上下文处理之类的功能。
mongodb
mongoDB是NOSQL,面向文档的数据库,以其灵活性和可扩展性而闻名。它以JSON般的BSON格式存储数据,从而易于操纵和索引。 MongoDB广泛用于处理现代应用程序中的大规模,非结构化和实时数据。
twilio
Twilio是一个云通信平台,使开发人员能够使用API将消息,语音和视频功能集成到其应用程序中。它简化了构建沟通功能(例如SMS,MMS,电话等)的过程,允许企业以编程方式与客户互动。
在项目内创建一个新的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_NAME
和DATABASE_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
}
我们已经为api
和auth
添加了初始化的光纤,因此我们确实需要一次又一次地键入每个路径。它还提供了一种更好的方法,将中间件应用于一组路线,而不是将相同的中间件应用于每个路线。
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()将执行下一个处理程序功能。
默认情况下,ContextKey
是user
,但我们已将其更改为payload
。
您可以在github中探索完整的代码库
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)