Golang中如何用Gin实现Bearer Token认证

Golang中如何用Gin实现Bearer Token认证 目前,我正在学习如何使用承载令牌(Bearer Token)实现HTTP身份验证,但不知道从哪里开始。我查阅了网上的指南,但总感觉遗漏了什么。我目前进行身份验证的方式是使用Gin进行路由,并使用bcrypt将哈希密码以十六进制形式存储在PostgreSQL中,然后根据用户提供的密码验证哈希值。以下是网上示例指南的内容。我是否应该直接为Gin的基本身份验证写出用户的用户名和密码?另外,这对于已经列出的新创建用户应该如何工作?我对此不太清楚。

网上实现

package main

import (
    "crypto/rand"
    "encoding/hex"
    "github.com/gin-gonic/gin"
    "net/http"
    "strings"
)

var tokens []string

func main() {
    r := gin.Default()
    r.POST("/login", gin.BasicAuth(gin.Accounts{
        "admin": "secret",
    }), func(c *gin.Context) {
        token, _ := randomHex(20)
        tokens = append(tokens, token)

        c.JSON(http.StatusOK, gin.H{
            "token": token,
        })
    })
    r.GET("/resource", func(c *gin.Context) {
        bearerToken := c.Request.Header.Get("Authorization")
        reqToken := strings.Split(bearerToken, " ")[1]
        for _, token := range tokens {
            if token == reqToken {
                c.JSON(http.StatusOK, gin.H{
                    "data": "resource data",
                })
                return
            }
        }
        c.JSON(http.StatusUnauthorized, gin.H{
            "message": "unauthorized",
        })
    })
    r.Run() // Listen and serve on 0.0.0.0:8080 (for Windows "localhost:8080")
}

func randomHex(n int) (string, error) {
    bytes := make([]byte, n)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

参考:

gin package - github.com/gin-gonic/gin - Go Packages

thumbnail

Authentication for Go Applications: The Secure Way


更多关于Golang中如何用Gin实现Bearer Token认证的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

这很有帮助,谢谢

更多关于Golang中如何用Gin实现Bearer Token认证的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我想问如何创建令牌

这取决于你的设计,例如,你可以将令牌放入HTTP请求体中,然后由前端页面提取出来。

我是应该使用Postman来为我生成它,还是应该由数据库为我创建它?我提出这个问题是因为我是一个完全不懂如何实现令牌认证的初学者。

为了澄清,我想问的是承载令牌(Bearer Token)的实现如何处理新创建的用户,如果我应该在基础认证中间件中写入他们的登录信息。为了测试,我正在使用Postman,但我不知道如何将其与认证和承载令牌一起使用。

我不明白你的问题是什么。 如果你只是想知道如何返回令牌,你只需要在登录时响应令牌值(例如在响应体或响应头中)。 然后在接下来的正常请求中携带该令牌(例如在请求体、请求头或查询参数中)。

我理解用户会上传他们的密码,以便将其哈希值与数据库中存储的哈希值进行比较。我难以理解的是,一旦密码被验证为正确后,我该如何返回一个令牌值?PostgreSQL 无法存储访问令牌。

参考: PostgreSQL 认证方法

我目前正在一个小型CRUD API项目中尝试实现这个功能。 运行说明位于首页。

GitHub github.com

GitHub - ScriptMang/conch at tokenAuthorization

tokenAuthorization

一个使用Gin对模拟共享单车数据库执行CRUD操作的程序。此程序旨在本地运行。

这与Go语言或Gin框架无关。这是一个非常简单的网络工作逻辑(如果你不理解这个工作逻辑,不妨用最原始的HTTP交互来思考一下)。

首先是 /login,这是登录接口。用户上传用户名和密码,服务器验证用户名和密码。验证成功后,返回一个token值。一般来说,请求端不应直接接触原始密码,通常上传的是密码的哈希值。

接下来是 /resource,这是资源接口。用户需要提供登录时获得的token来证明访问的有效性。这个token验证过程通常被封装成中间件。token通常放在请求头中,一般的工具都有填充请求头的功能。

用户 → 登录 → token token → 资源1 token → 资源2 token → 资源3

你的问题很奇怪,好像你不知道令牌是什么。 令牌类似于Cookie,两者都用于记录状态。至于令牌携带什么信息,这由你决定。至少它应该能够映射到某些信息索引。 例如,如果是JWT格式,可能包含: sub(Subject):表示用户的唯一标识符。 iss(Issuer):表示令牌的签发者。 exp(Expiration):表示令牌的过期时间,采用UNIX时间戳格式。 iat(Issued At):表示令牌的签发时间。 aud(Audience):表示令牌的接收者。 hash(哈希):表示令牌的哈希校验值。 其他自定义字段(例如用户角色、权限等)。 这些内容由你设计,主要取决于你的业务需求,而不是去问别人如何生成令牌。

为了更清晰地说明我的问题,我的CRUD API是关于模拟一个自行车店的。 我通过传递一个账户对象到POST localhost:8080/users来创建一个账户,如果成功,信息将分别添加到usernamesusercontactspasswords表中。usernames表存储用户的用户名,usercontacts表存储他们的敏感数据(地址、名、姓),passwords表以加密哈希值存储他们的密码。当账户创建成功后,会返回响应 {user_id: #, msg: registered }

我当前的登录路由是POST localhost:8080/user/login。 它会对用户提供的密码进行哈希处理,并与数据库中当前存储的哈希密码进行比对。 然而,我希望在认证成功后返回一个令牌。 如果有什么遗漏,请告诉我。

JSON 对象

Account
{
  "username": string,
  "fname": string,
  "lname": string,
  "address": string,
  "password": string
}
usernames
{ 
  "id": int,
  "user_id": int,
  "username": string,
}
usercontacts
{ 
  "id": int,
  "user_id": int,
  "fname": string,
  "lname": string,
  "address": string
}
passwords
{ 
  "id": int,
  "user_id": int,
  "password": string,
}
Login-Creds
{
  "username": string,
  "pswd": []byte
}

路由

  • 创建账户 POST localhost:8080/users/ <account>

  • 登录 POST localhost:8080/user/login <login-creds>

我的实现

  • 路由 > main.go
  • 账户信息与认证 > accts.go
  • 字段错误处理 > fields.go

主文件

创建账户路由

func createAcct(r *gin.Engine) *gin.Engine {
	r.POST("/users/", func(c *gin.Context) {
		var acct accts.Account
		var acctErr fields.GrammarError
		var acctStatus *accts.Registered
		err := c.ShouldBind(&acct)
		if err != nil {
			acctErr.AddMsg(fields.BadRequest,
				"Binding Error: failed to bind fields to account object, mismatched data-types")
			c.JSON(fields.ErrorCode, acctErr)
			return
		}

		// validate account info
		acctStatus, acctErr = accts.AddAccount(&acct)

		// send response back
		errMsgSize := len(acctErr.ErrMsgs)
		switch {
		case errMsgSize > 0:
			c.JSON(fields.ErrorCode, acctErr)
		default:
			c.JSON(statusOK, *acctStatus)
		}

		//log.Println("Account: ", acct)
	})
	return r
}

登录路由

func logIn(r *gin.Engine) *gin.Engine {
	r.POST("/user/login", func(c *gin.Context) {
		var loginErr fields.GrammarError
		var rqstData respBodyData
		var userCred accts.LoginCred

		loginErr = rqstData.FieldErr
		err := c.ShouldBind(&userCred)
		if err != nil {
			loginErr.AddMsg(fields.BadRequest,
				"Binding Error: failed to bind fields to account object, mismatched data-types")
			c.JSON(fields.ErrorCode, loginErr)
			return
		}

		// validate account info
		authStatus, loginErr := accts.LogIntoAcct(userCred)
		if err != nil {
			loginErr.AddMsg(fields.BadRequest,
				"Binding Error: failed to bind fields to account object, mismatched data-types")
			c.JSON(fields.ErrorCode, loginErr)
			return
		}

		// send response back
		errMsgSize := len(loginErr.ErrMsgs)
		switch {
		case errMsgSize > 0:
			c.JSON(fields.ErrorCode, loginErr)
		default:
			c.JSON(statusOK, authStatus)
		}
	})
	return r
}

ACCTS 文件

添加账户

// adds the account info to the appropiate tables w/ the database
func AddAccount(acct *Account) (*Registered, fields.GrammarError) {
	acctErr := &fields.GrammarError{}
	validateAccount(acct, acctErr)

	if acctErr.ErrMsgs != nil {
		return nil, *acctErr
	}

	// if no errors add info to appropiate tables
	addUsername(acct, acctErr)
	if acctErr.ErrMsgs != nil {
		return nil, *acctErr
	}

	addUserContact(acct, acctErr)
	if acctErr.ErrMsgs != nil {
		return nil, *acctErr
	}

	// add passwords to table, don't if err existf
	addPassword(acct, acctErr)
	if acctErr.ErrMsgs != nil {
		// fmt.Printf("Errors in AddAccount Func, %v\n", acctErr.ErrMsgs)
		return nil, *acctErr
	}

	return &Registered{acct.ID, "registered"}, *acctErr
}

登录

// matches the client's username and pswd against the database
// if there's a match the user is logged in, otherwise
// there's an issue with username or password
func LogIntoAcct(userCred LoginCred) (*LoginStatus, fields.GrammarError) {
	// get the user struct check if it exists
	usrs, fieldErr := readUserByUsername(userCred.UserName)
	if fieldErr.ErrMsgs != nil {
		return nil, fieldErr
	}

	// get the stored pswd hash
	usr := usrs[0]
	pswds, fieldErr := ReadHashByID(usr.ID)
	if fieldErr.ErrMsgs != nil {
		return nil, fieldErr
	}

	// hash the given pswd and compare it to whats
	// stored in the databse
	hashedPswd := pswds[0].Password
	err := bcrypt.CompareHashAndPassword(hashedPswd, []byte(userCred.Password))

	if err != nil {
		// log.Printf("The Password Hash Comparison Failed: %v\n", err.Error())
		fieldErr.AddMsg(BadRequest, "Error: password is incorrect")
		return nil, fieldErr
	}

	return &LoginStatus{usr.ID, "LoggedIn"}, fieldErr
}

在Gin中实现Bearer Token认证需要结合中间件和数据库验证。以下是完整的实现方案:

package main

import (
    "crypto/rand"
    "encoding/hex"
    "net/http"
    "strings"
    "time"
    
    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID           uint      `gorm:"primaryKey"`
    Username     string    `gorm:"unique;not null"`
    PasswordHash string    `gorm:"not null"`
    CreatedAt    time.Time
}

type Token struct {
    ID        uint      `gorm:"primaryKey"`
    UserID    uint      `gorm:"not null"`
    Token     string    `gorm:"unique;not null"`
    ExpiresAt time.Time `gorm:"not null"`
    CreatedAt time.Time
}

var db *gorm.DB

func main() {
    // 初始化数据库连接
    dsn := "host=localhost user=postgres password=secret dbname=authdb port=5432 sslmode=disable"
    var err error
    db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    
    // 自动迁移表结构
    db.AutoMigrate(&User{}, &Token{})
    
    r := gin.Default()
    
    // 用户注册
    r.POST("/register", registerHandler)
    
    // 用户登录,返回Bearer Token
    r.POST("/login", loginHandler)
    
    // 需要认证的路由组
    authGroup := r.Group("/api")
    authGroup.Use(authMiddleware())
    {
        authGroup.GET("/resource", getResourceHandler)
        authGroup.POST("/logout", logoutHandler)
    }
    
    r.Run(":8080")
}

// 用户注册处理器
func registerHandler(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 检查用户是否已存在
    var existingUser User
    if result := db.Where("username = ?", req.Username).First(&existingUser); result.RowsAffected > 0 {
        c.JSON(http.StatusConflict, gin.H{"error": "username already exists"})
        return
    }
    
    // 使用bcrypt哈希密码
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
        return
    }
    
    // 创建用户
    user := User{
        Username:     req.Username,
        PasswordHash: string(hashedPassword),
        CreatedAt:    time.Now(),
    }
    
    if err := db.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{
        "message": "user created successfully",
        "user_id": user.ID,
    })
}

// 用户登录处理器
func loginHandler(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 查找用户
    var user User
    if err := db.Where("username = ?", req.Username).First(&user).Error; err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
        return
    }
    
    // 验证密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
        return
    }
    
    // 生成Bearer Token
    tokenString, err := generateToken(32)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
        return
    }
    
    // 保存Token到数据库(设置24小时过期)
    token := Token{
        UserID:    user.ID,
        Token:     tokenString,
        ExpiresAt: time.Now().Add(24 * time.Hour),
        CreatedAt: time.Now(),
    }
    
    if err := db.Create(&token).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save token"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "token":      tokenString,
        "token_type": "Bearer",
        "expires_in": 86400, // 24小时秒数
        "user_id":    user.ID,
    })
}

// Bearer Token认证中间件
func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
            c.Abort()
            return
        }
        
        // 检查Bearer Token格式
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization format"})
            c.Abort()
            return
        }
        
        tokenString := parts[1]
        
        // 验证Token
        var token Token
        if err := db.Where("token = ? AND expires_at > ?", tokenString, time.Now()).First(&token).Error; err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
            c.Abort()
            return
        }
        
        // 将用户ID存储到上下文中供后续使用
        c.Set("user_id", token.UserID)
        c.Next()
    }
}

// 受保护资源处理器
func getResourceHandler(c *gin.Context) {
    userID, exists := c.Get("user_id")
    if !exists {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "user context not found"})
        return
    }
    
    // 这里可以根据userID获取用户信息
    var user User
    if err := db.First(&user, userID).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "user not found"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "data": "protected resource data",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
        },
    })
}

// 登出处理器
func logoutHandler(c *gin.Context) {
    authHeader := c.GetHeader("Authorization")
    parts := strings.Split(authHeader, " ")
    
    if len(parts) == 2 && parts[0] == "Bearer" {
        // 从数据库中删除或标记Token为过期
        db.Where("token = ?", parts[1]).Delete(&Token{})
    }
    
    c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
}

// 生成随机Token
func generateToken(length int) (string, error) {
    bytes := make([]byte, length)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

这个实现包含以下关键部分:

  1. 用户注册:使用bcrypt哈希密码并存储到PostgreSQL
  2. 用户登录:验证密码后生成Bearer Token并存储到数据库
  3. 认证中间件:验证Authorization头中的Bearer Token
  4. Token管理:Token有过期时间,支持登出功能
  5. 上下文传递:通过Gin的上下文传递用户信息

使用示例:

# 注册用户
curl -X POST http://localhost:8080/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"testpass"}'

# 登录获取Token
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"testpass"}'

# 访问受保护资源
curl -X GET http://localhost:8080/api/resource \
  -H "Authorization: Bearer <your_token_here>"

# 登出
curl -X POST http://localhost:8080/api/logout \
  -H "Authorization: Bearer <your_token_here>"

这个方案解决了内存存储Token的问题,使用数据库持久化存储,支持用户注册、登录、Token验证和登出完整流程。

回到顶部