_本文假设您已经安装了 Golang,并且您知道如何设置一个简单的 golang 应用程序。如果没有,请不要担心这篇文章会教你如何做到这一点。

本文还假设你有一个可用于本教程的工作 mongodb 数据库如果不参考这篇文章的第一部分获取一个_

假设您想构建一个仅对您的用户群或注册用户可用的安全 Restful API,您将如何实现? 🤔 本文将教你如何在 Golang 中做到这一点。这个想法在其他语言中基本相同,所以一旦你掌握了这个概念,在任何地方实现它都不应该太难。

一般概念

  1. 首先,用户从客户端设备登录,该设备使用用户的详细信息向服务器进行 API 调用

  2. 服务器从用户那里验证这些详细信息,以确保用户存在于系统中

  3. 验证完成后,服务器向客户端设备发送一个安全令牌。

  4. 客户端现在可以使用接收到的令牌来授予用户对 api 资源的访问权限。

[](https://res.cloudinary.com/practicaldev/image/fetch/s--e17_PePa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/95i70w5nmik4oieorris.jpg)

发展概况

  1. 设置。
  • 应用程序设置

  • 数据库设置

  1. 创建用户模块。
  • 用户模型

3.安装并实现bcrypt包。

  • 密码哈希

  • 登录时验证密码

  • 注册服务

  • 登录服务

4.安装并实现JWT-GO包。

  • 构建令牌生成器

  • 构建令牌验证器

  • 登录时更新令牌

  1. 实现 JWT 中间件进行验证。

6.设置路线。

  1. 编写 main.go 文件。

开始吧!

这是您将使用的文件结构

[](https://res.cloudinary.com/practicaldev/image/fetch/s--XNEiEFaK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/yxoor7c5hll7hdqcqaxm.png)

设置应用程序

$ go build
$ ./new

进入全屏模式 退出全屏模式

go build./new
go mod init
PORT=8000
MONGODB_URL={{insert the db link here}}

进入全屏模式 退出全屏模式

  • 创建一个名为“database”的文件夹(包)。此文件夹将包含一个文件,该文件管理您的数据库连接并打开您的收藏。创建文件并命名为“databaseConnection.go”并用以下代码填充它。
package database

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

//DBinstance func
func DBinstance() *mongo.Client {
    err := godotenv.Load(".env")

    if err != nil {
        log.Fatal("Error loading .env file")
    }

    MongoDb := os.Getenv("MONGODB_URL")

    client, err := mongo.NewClient(options.Client().ApplyURI(MongoDb))
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

    defer cancel()
    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to MongoDB!")

    return client
}

//Client Database instance
var Client *mongo.Client = DBinstance()

//OpenCollection is a  function makes a connection with a collection in the database
func OpenCollection(client *mongo.Client, collectionName string) *mongo.Collection {

    var collection *mongo.Collection = client.Database("cluster0").Collection(collectionName)

    return collection
}

进入全屏模式 退出全屏模式

  • 在您的根文件夹中创建一个 Main.go 文件并暂时将其留空

您将从创建用户模型开始。用户模型基本上是系统中每个用户的用户详细信息结构。每个用户必须至少有一个

  • 用户 ID(标识用户的唯一字符串)

  • 名字

  • 姓氏

  • 电子邮件

  • 密码

  • 令牌(带有用户详细信息的签名 jwt 令牌)

  • 刷新令牌(用于简单刷新页面的空令牌)

models/userModel.go
package models

import (
    "time"

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

//User is the model that governs all notes objects retrived or inserted into the DB
type User struct {
    ID            primitive.ObjectID `bson:"_id"`
    First_name    *string            `json:"first_name" validate:"required,min=2,max=100"`
    Last_name     *string            `json:"last_name" validate:"required,min=2,max=100"`
    Password      *string            `json:"Password" validate:"required,min=6""`
    Email         *string            `json:"email" validate:"email,required"`
    Phone         *string            `json:"phone" validate:"required"`
    Token         *string            `json:"token"`
    Refresh_token *string            `json:"refresh_token"`
    Created_at    time.Time          `json:"created_at"`
    Updated_at    time.Time          `json:"updated_at"`
    User_id       string             `json:"user_id"`
}

进入全屏模式 退出全屏模式

Bcrypt和密码管理

现在让我们继续获取bcrypt包、验证器包和gin包,它们将分别用于密码管理、结构验证和路由管理。

controllers/userController.go
package controllers

import (
    "context"
    "fmt"
    "log"

    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"

    "user-athentication-golang/database"

    helper "user-athentication-golang/helpers"
    "user-athentication-golang/models"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "golang.org/x/crypto/bcrypt"
)

var userCollection *mongo.Collection = database.OpenCollection(database.Client, "user")
var validate = validator.New()

进入全屏模式 退出全屏模式

controllers/userController.go
//HashPassword is used to encrypt the password before it is stored in the DB
func HashPassword(password string) string {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Panic(err)
    }

    return string(bytes)
}

//VerifyPassword checks the input password while verifying it with the passward in the DB.
func VerifyPassword(userPassword string, providedPassword string) (bool, string) {
    err := bcrypt.CompareHashAndPassword([]byte(providedPassword), []byte(userPassword))
    check := true
    msg := ""

    if err != nil {
        msg = fmt.Sprintf("login or passowrd is incorrect")
        check = false
    }

    return check, msg
}

进入全屏模式 退出全屏模式

controllers/userController.gohelper.GenerateAllTokenshelper.UpdateAllTokens
//CreateUser is the api used to tget a single user
func SignUp() gin.HandlerFunc {
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User

        if err := c.BindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        validationErr := validate.Struct(user)
        if validationErr != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Error()})
            return
        }

        count, err := userCollection.CountDocuments(ctx, bson.M{"email": user.Email})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while checking for the email"})
            return
        }

        password := HashPassword(*user.Password)
        user.Password = &password

        count, err = userCollection.CountDocuments(ctx, bson.M{"phone": user.Phone})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while checking for the phone number"})
            return
        }

        if count > 0 {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "this email or phone number already exists"})
            return
        }

        user.Created_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.Updated_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.ID = primitive.NewObjectID()
        user.User_id = user.ID.Hex()
        token, refreshToken, _ := helper.GenerateAllTokens(*user.Email, *user.First_name, *user.Last_name, user.User_id)
        user.Token = &token
        user.Refresh_token = &refreshToken

        resultInsertionNumber, insertErr := userCollection.InsertOne(ctx, user)
        if insertErr != nil {
            msg := fmt.Sprintf("User item was not created")
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }
        defer cancel()

        c.JSON(http.StatusOK, resultInsertionNumber)

    }
}

//Login is the api used to tget a single user
func Login() gin.HandlerFunc {
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User
        var foundUser models.User

        if err := c.BindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        err := userCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&foundUser)
        defer cancel()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "login or passowrd is incorrect"})
            return
        }

        passwordIsValid, msg := VerifyPassword(*user.Password, *foundUser.Password)
        defer cancel()
        if passwordIsValid != true {
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }

        token, refreshToken, _ := helper.GenerateAllTokens(*foundUser.Email, *foundUser.First_name, *foundUser.Last_name, foundUser.User_id)

        helper.UpdateAllTokens(token, refreshToken, foundUser.User_id)

        c.JSON(http.StatusOK, foundUser)

    }
}

进入全屏模式 退出全屏模式

代币管理

helpers/tokenHelper.go
package helper

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "user-athentication-golang/database"

    jwt "github.com/dgrijalva/jwt-go"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

进入全屏模式 退出全屏模式

helpers.tokenHelper.go
// SignedDetails
type SignedDetails struct {
    Email      string
    First_name string
    Last_name  string
    Uid        string
    jwt.StandardClaims
}

进入全屏模式 退出全屏模式

helpers.tokenHelper.go
var userCollection *mongo.Collection = database.OpenCollection(database.Client, "user")

var SECRET_KEY string = os.Getenv("SECRET_KEY")

// GenerateAllTokens generates both teh detailed token and refresh token
func GenerateAllTokens(email string, firstName string, lastName string, uid string) (signedToken string, signedRefreshToken string, err error) {
    claims := &SignedDetails{
        Email:      email,
        First_name: firstName,
        Last_name:  lastName,
        Uid:        uid,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24)).Unix(),
        },
    }

    refreshClaims := &SignedDetails{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(168)).Unix(),
        },
    }

    token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY))
    refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(SECRET_KEY))

    if err != nil {
        log.Panic(err)
        return
    }

    return token, refreshToken, err
}

进入全屏模式 退出全屏模式

helpers.tokenHelper.go
//ValidateToken validates the jwt token
func ValidateToken(signedToken string) (claims *SignedDetails, msg string) {
    token, err := jwt.ParseWithClaims(
        signedToken,
        &SignedDetails{},
        func(token *jwt.Token) (interface{}, error) {
            return []byte(SECRET_KEY), nil
        },
    )

    if err != nil {
        msg = err.Error()
        return
    }

    claims, ok := token.Claims.(*SignedDetails)
    if !ok {
        msg = fmt.Sprintf("the token is invalid")
        msg = err.Error()
        return
    }

    if claims.ExpiresAt < time.Now().Local().Unix() {
        msg = fmt.Sprintf("token is expired")
        msg = err.Error()
        return
    }

    return claims, msg
}

进入全屏模式 退出全屏模式

helpers.tokenHelper.go
//UpdateAllTokens renews the user tokens when they login
func UpdateAllTokens(signedToken string, signedRefreshToken string, userId string) {
    var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)

    var updateObj primitive.D

    updateObj = append(updateObj, bson.E{"token", signedToken})
    updateObj = append(updateObj, bson.E{"refresh_token", signedRefreshToken})

    Updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
    updateObj = append(updateObj, bson.E{"updated_at", Updated_at})

    upsert := true
    filter := bson.M{"user_id": userId}
    opt := options.UpdateOptions{
        Upsert: &upsert,
    }

    _, err := userCollection.UpdateOne(
        ctx,
        filter,
        bson.D{
            {"$set", updateObj},
        },
        &opt,
    )
    defer cancel()

    if err != nil {
        log.Panic(err)
        return
    }

    return
}

进入全屏模式 退出全屏模式

中间件控制

middleware/authMiddleware.go
package middleware

import (
    "fmt"
    "net/http"

    helper "user-athentication-golang/helpers"

    "github.com/gin-gonic/gin"
)

// Authz validates token and authorizes users
func Authentication() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientToken := c.Request.Header.Get("token")
        if clientToken == "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No Authorization header provided")})
            c.Abort()
            return
        }

        claims, err := helper.ValidateToken(clientToken)
        if err != "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err})
            c.Abort()
            return
        }

        c.Set("email", claims.Email)
        c.Set("first_name", claims.First_name)
        c.Set("last_name", claims.Last_name)
        c.Set("uid", claims.Uid)

        c.Next()

    }
}

进入全屏模式 退出全屏模式

设置路由

routes/userRoutes.gomain.go
package routes

import (
    controller "user-athentication-golang/controllers"

    "github.com/gin-gonic/gin"
)

//UserRoutes function
func UserRoutes(incomingRoutes *gin.Engine) {
    incomingRoutes.POST("/users/signup", controller.SignUp())
    incomingRoutes.POST("/users/login", controller.Login())
}

进入全屏模式 退出全屏模式

Main.go 文件

现在,您可以通过插入以下代码使用您的 main.go 文件完成所有操作。以下代码包含 2 个虚拟 API,用于演示令牌的有效性。

中间件仅在用户路由之后使用,因为在登录和注册期间不需要令牌验证

package main

import (
    "os"

    middleware "user-athentication-golang/middleware"
    routes "user-athentication-golang/routes"

    "github.com/gin-gonic/gin"
    _ "github.com/heroku/x/hmetrics/onload"
)

func main() {
    port := os.Getenv("PORT")

    if port == "" {
        port = "8000"
    }

    router := gin.New()
    router.Use(gin.Logger())
    routes.UserRoutes(router)

    router.Use(middleware.Authentication())

    // API-2
    router.GET("/api-1", func(c *gin.Context) {

        c.JSON(200, gin.H{"success": "Access granted for api-1"})

    })

    // API-1
    router.GET("/api-2", func(c *gin.Context) {
        c.JSON(200, gin.H{"success": "Access granted for api-2"})
    })

    router.Run(":" + port)
}

进入全屏模式 退出全屏模式

运行测试

go build -o new -vgo run main.gohttp://localhost:8000

[](https://res.cloudinary.com/practicaldev/image/fetch/s--TwUsDqwM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/hmqxtrl368xspe05yhjp.png)

邮递员

http://localhost:8000

[](https://res.cloudinary.com/practicaldev/image/fetch/s--piLXUZ9k--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/spq3uzzszf6ifywfcbhp.png)[](https://res.cloudinary.com/practicaldev/image/fetch/s--AgWZbrOq--/ c_limit%2Cf_auto% 2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/3eq5pljokm985m4rgy2d.png)[](https://res.cloudinary .com/practicaldev/image /fetch/s--c3p90pEg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/qi9f2rpdt9l6bso2gjvu.png)[! 替代文本] (https://res.cloudinary.com/practicaldev/image/fetch/s--FOM4Q2uu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/i/ygsnf9pwb0d8t3r7734g.png)

结论

在本文中,您学习了如何使用工作中的 mongoDB 数据库设置简单的 golang 应用程序。您还创建了一个用户模块,同时使用注册和登录服务实现密码散列和验证等功能。

您继续使用JWT-GO包构建令牌生成器和验证器,用于在中间件中验证用户令牌。您可以通过添加更多 API 和探索各种身份验证方式来改进此应用程序。

查看本教程的存储库!