_本文假设您已经安装了 Golang,并且您知道如何设置一个简单的 golang 应用程序。如果没有,请不要担心这篇文章会教你如何做到这一点。
本文还假设你有一个可用于本教程的工作 mongodb 数据库如果不参考这篇文章的第一部分获取一个_
假设您想构建一个仅对您的用户群或注册用户可用的安全 Restful API,您将如何实现? 🤔 本文将教你如何在 Golang 中做到这一点。这个想法在其他语言中基本相同,所以一旦你掌握了这个概念,在任何地方实现它都不应该太难。
一般概念
-
首先,用户从客户端设备登录,该设备使用用户的详细信息向服务器进行 API 调用
-
服务器从用户那里验证这些详细信息,以确保用户存在于系统中
-
验证完成后,服务器向客户端设备发送一个安全令牌。
-
客户端现在可以使用接收到的令牌来授予用户对 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)
发展概况
- 设置。
-
应用程序设置
-
数据库设置
- 创建用户模块。
- 用户模型
3.安装并实现bcrypt包。
-
密码哈希
-
登录时验证密码
-
注册服务
-
登录服务
4.安装并实现JWT-GO包。
-
构建令牌生成器
-
构建令牌验证器
-
登录时更新令牌
- 实现 JWT 中间件进行验证。
6.设置路线。
- 编写 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 和探索各种身份验证方式来改进此应用程序。