在 Golang 中使用 JWT 构建安全的 API

在本文中,我们将创建一个简单的 RESTful API 来使用 JWT 注册、登录和授权用户 ,首先让我们看看我们在这里尝试解决的问题。

问题和解决方案

问题

这里的问题很简单:您有一个由客户端(Web、移动应用程序、CLI 等)使用的 API,您想保护它并且只授权注册用户访问它。

解决方案

解决方案是使用 JSON Web 令牌(简称 JWT)来登录和授权用户,如下面的简单图片所示

显示客户端和 API 服务器之间如何进行授权的图表

  • 客户端将通过将凭据发送到 API 服务器来登录用户。
  • API 服务器将验证用户凭据,签署 JWT,并在 HTTP 响应中返回它。
  • 客户端将使用接收到的 JWT 访问 API 资源。
  • API 服务器将验证 JWT 并授权用户访问资源。

什么是 JWT?

JWT 或 JSON 网络令牌是一个数字签名的字符串,用于在各方之间安全地传输信息。 这是一个 RFC7519 标准。

JWT 由三部分组成:

header.payload.signature

下面是一个示例 JWT。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SfJ6SfyfQ.SfJsflwcsfjfqsfyfqmdiyfqsfj5kwiwibm

标题

标头是一个 Base64 编码的字符串,它包含令牌类型( JWT 在这种情况下)和签名算法( HMAC SHA256 在这种情况下,或 HS256 简称)。

{
 "alg": "HS256",
 "typ": "JWT"
}

有效载荷

负载是包含声明的 Base64 编码字符串。 声明是与用户和令牌本身相关的数据集合。 示例声明有:( exp 到期时间)、 iat (发布时间)、 name (用户名)和 sub (主题)。

{
 "sub": "1234567890",
 "name": "John Doe",
 "iat": 1516239022
}

签名

签名是一个有符号的字符串。 对于 HMAC 签名算法,我们使用 Base64 编码的标头、Base64 编码的有效负载和签名密钥来创建它。

HMACSHA256(
 base64UrlEncode(header) + "." +
 base64UrlEncode(payload),
 secret)

Golang 中的 JWT

现在我们对 JWT 有了更好的了解,让我们在 Golang 中创建我们的小型身份验证 API。

启动 Go 模块

在您的 中创建一个新目录 GOPATH ,调用它 authapp ,然后启动一个 Go 模块。

go mod init

数据库

为了便于理解,我将使用 SQLite 数据库。
为了在 Go 中处理 SQL 数据库,我强烈建议使用像 GORM 这样 的 ORM ,所以让我们将它与 SQLite 驱动程序一起安装。

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

现在创建一个名为 database 的新文件夹,并使用此代码启动一个全局数据库对象。

// database/database.go

package database

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// GlobalDB a global db object will be used across different packages
var GlobalDB *gorm.DB

// InitDatabase creates a sqlite db
func InitDatabase() (err error) {
    GlobalDB, err = gorm.Open(sqlite.Open("auth.db"), &gorm.Config{})
    if err != nil {
        return
    }

    return
}

现在让我们测试一下。

// database/database_test.go
package database

import (
    "testing"
)

func TestInitDB(t *testing.T) {
    err := InitDatabase()
    assert.NoError(t, err)
}

如果测试通过会输出以下信息:
正常输出
我们的数据库就准备好了。让我们继续讨论用户模型。

用户模型

我们案例中的用户模型很简单:它有姓名、电子邮件和密码。
用户密码应该在数据库中散列,为了实现这一点,我们使用了很棒的 bcrypt 库。

go get golang.org/x/crypto/bcrypt

这是完整的 User 模型,其中包含在数据库中创建记录、散列和检查密码的函数。

// models/models.go

package models

import (
    "authapp/database"

    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

// User defines the user in db
type User struct {
    gorm.Model
    Name     string `json:"name"`
    Email    string `json:"email" gorm:"unique"`
    Password string `json:"password"`
}

// CreateUserRecord creates a user record in the database
func (user *User) CreateUserRecord() error {
    result := database.GlobalDB.Create(&user)
    if result.Error != nil {
        return result.Error
    }

    return nil
}

// HashPassword encrypts user password
func (user *User) HashPassword(password string) error {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        return err
    }

    user.Password = string(bytes)

    return nil
}

// CheckPassword checks user password
func (user *User) CheckPassword(providedPassword string) error {
    err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword))
    if err != nil {
        return err
    }

    return nil
}

现在我们测试上面的所有代码。

// models/models_test.go

package models

import (
    "authapp/database"
    "os"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHashPassword(t *testing.T) {
    user := User{
        Password: "secret",
    }

    err := user.HashPassword(user.Password)
    assert.NoError(t, err)

    os.Setenv("passwordHash", user.Password)
}

func TestCreateUserRecord(t *testing.T) {
    var userResult User

    err := database.InitDatabase()
    if err != nil {
        t.Error(err)
    }

    err = database.GlobalDB.AutoMigrate(&User{})
    assert.NoError(t, err)

    user := User{
        Name:     "Test User",
        Email:    "test@email.com",
        Password: os.Getenv("passwordHash"),
    }

    err = user.CreateUserRecord()
    assert.NoError(t, err)

    database.GlobalDB.Where("email = ?", user.Email).Find(&userResult)

    database.GlobalDB.Unscoped().Delete(&user)

    assert.Equal(t, "Test User", userResult.Name)
    assert.Equal(t, "test@email.com", userResult.Email)

}

func TestCheckPassword(t *testing.T) {
    hash := os.Getenv("passwordHash")

    user := User{
        Password: hash,
    }

    err := user.CheckPassword("secret")
    assert.NoError(t, err)
}

输出成功2

现在让我们签署 JWT。

签署和验证 JWT

为了在 Golang 中签署和验证 JWT 令牌,我们将使用 jwt-go 包。

go get github.com/dgrijalva/jwt-go

现在让我们创建一个自定义包,我们可以在其中签名和验证令牌。

我简单地称它为 auth ,它具有以下结构和功能:

  • 一个 JwtWrapper 结构来包装签名密钥,发行人,以及到期时间
  • 一个 JwtClaim 结构来定制要求加入到令牌的参数
  • GenerateToken 使用 HS256 生成 24 小时后到期的令牌的函数
  • ValidateToken 验证令牌并返回声明的函数

下面是完整的代码。

// auth/auth.go

package auth

import (
    "errors"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
)

// JwtWrapper wraps the signing key and the issuer
type JwtWrapper struct {
    SecretKey       string
    Issuer          string
    ExpirationHours int64
}

// JwtClaim adds email as a claim to the token
type JwtClaim struct {
    Email string
    jwt.StandardClaims
}

// GenerateToken generates a jwt token
func (j *JwtWrapper) GenerateToken(email string) (signedToken string, err error) {
    claims := &JwtClaim{
        Email: email,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(j.ExpirationHours)).Unix(),
            Issuer:    j.Issuer,
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    signedToken, err = token.SignedString([]byte(j.SecretKey))
    if err != nil {
        return
    }

    return
}

//ValidateToken validates the jwt token
func (j *JwtWrapper) ValidateToken(signedToken string) (claims *JwtClaim, err error) {
    token, err := jwt.ParseWithClaims(
        signedToken,
        &JwtClaim{},
        func(token *jwt.Token) (interface{}, error) {
            return []byte(j.SecretKey), nil
        },
    )

    if err != nil {
        return
    }

    claims, ok := token.Claims.(*JwtClaim)
    if !ok {
        err = errors.New("Couldn't parse claims")
        return
    }

    if claims.ExpiresAt < time.Now().Local().Unix() {
        err = errors.New("JWT is expired")
        return
    }

    return

}

测试代码:

// auth/auth_test.go

package auth

import (
    "os"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestGenerateToken(t *testing.T) {
    jwtWrapper := JwtWrapper{
        SecretKey:       "verysecretkey",
        Issuer:          "AuthService",
        ExpirationHours: 24,
    }

    generatedToken, err := jwtWrapper.GenerateToken("jwt@email.com")
    assert.NoError(t, err)

    os.Setenv("testToken", generatedToken)
}

func TestValidateToken(t *testing.T) {
    encodedToken := os.Getenv("testToken")

    jwtWrapper := JwtWrapper{
        SecretKey: "verysecretkey",
        Issuer:    "AuthService",
    }

    claims, err := jwtWrapper.ValidateToken(encodedToken)
    assert.NoError(t, err)

    assert.Equal(t, "jwt@email.com", claims.Email)
    assert.Equal(t, "AuthService", claims.Issuer)
}

测试输出
到此为止,现在我们已经有了我们的用户模型和 JWT 签名/验证逻辑,是时候创建实际的 API 了。

API

在本节中,我们将创建三个 RESTful API 端点

  • [POST] /api/public/signup => 创建用户
  • [POST] /api/public/login => 用户登录并返回 JWT
  • [GET] /api/private/profile => 授权用户并返回请求的数据

但在开始之前,我们需要安装很棒的 gin-gonic Web 框架。

go get -u github.com/gin-gonic/gin

现在让我们设置我们的主文件来创建一个数据库和 gin 路由器并启动服务器。

// main.go

package main

import (
    "authapp/database"
    "log"

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

func setupRouter() *gin.Engine {
    r := gin.Default()
    
    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    
    return r
}

func main() {
    err := database.InitDatabase()
    if err != nil {
        log.Fatalln("could not create database", err)
    }
    
    database.GlobalDB.AutoMigrate(&models.User{})

    r := setupRouter()
    r.Run(":8080")
}

让我们来测试一下。

// main_test.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
    router := setupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/ping", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Equal(t, "pong", w.Body.String())
}

测试输出
我们的 HTTP 路由器已准备就绪。 现在让我们创建注册控制器。

注册

注册是一个公共 API; 它不应该需要身份验证。

要创建 gin 控制器,请创建一个名为的包 controllers 和一个名为 public.go .

请求是 POST。 我们应该获取有效负载并对其进行验证,将用户插入数据库,并返回插入的用户。

// controllers/public.go

package controllers

import (
    "authapp/models"
    "log"

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

// Signup creates a user in db
func Signup(c *gin.Context) {
    var user models.User

    err := c.ShouldBindJSON(&user)
    if err != nil {
        log.Println(err)

        c.JSON(400, gin.H{
            "msg": "invalid json",
        })
        c.Abort()

        return
    }

    err = user.HashPassword(user.Password)
    if err != nil {
        log.Println(err.Error())

        c.JSON(500, gin.H{
            "msg": "error hashing password",
        })
        c.Abort()

        return
    }

    err = user.CreateUserRecord()
    if err != nil {
        log.Println(err)

        c.JSON(500, gin.H{
            "msg": "error creating user",
        })
        c.Abort()

        return
    }

    c.JSON(200, user)
}

登录

这里登录很简单。 注册用户只需提供电子邮件和用户名,我们将为他们签署令牌。

// controllers/public.go

// LoginPayload login body
type LoginPayload struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

// LoginResponse token response
type LoginResponse struct {
    Token string `json:"token"`
}

// Login logs users in
func Login(c *gin.Context) {
    var payload LoginPayload
    var user models.User

    err := c.ShouldBindJSON(&payload)
    if err != nil {
        c.JSON(400, gin.H{
            "msg": "invalid json",
        })
        c.Abort()
        return
    }

    result := database.GlobalDB.Where("email = ?", payload.Email).First(&user)

    if result.Error == gorm.ErrRecordNotFound {
        c.JSON(401, gin.H{
            "msg": "invalid user credentials",
        })
        c.Abort()
        return
    }

    err = user.CheckPassword(payload.Password)
    if err != nil {
        log.Println(err)
        c.JSON(401, gin.H{
            "msg": "invalid user credentials",
        })
        c.Abort()
        return
    }

    jwtWrapper := auth.JwtWrapper{
        SecretKey:       "verysecretkey",
        Issuer:          "AuthService",
        ExpirationHours: 24,
    }

    signedToken, err := jwtWrapper.GenerateToken(user.Email)
    if err != nil {
        log.Println(err)
        c.JSON(500, gin.H{
            "msg": "error signing token",
        })
        c.Abort()
        return
    }

    tokenResponse := LoginResponse{
        Token: signedToken,
    }

    c.JSON(200, tokenResponse)

    return
}

让我们测试注册和登录。

// controllers/public_test.go

package controllers

import (
    "authapp/database"
    "authapp/models"
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestSignUp(t *testing.T) {
    var actualResult models.User

    user := models.User{
        Name:     "Test User",
        Email:    "jwt@email.com",
        Password: "secret",
    }

    payload, err := json.Marshal(&user)
    assert.NoError(t, err)

    request, err := http.NewRequest("POST", "/api/public/signup", bytes.NewBuffer(payload))
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    err = database.InitDatabase()
    assert.NoError(t, err)

    database.GlobalDB.AutoMigrate(&models.User{})

    Signup(c)

    assert.Equal(t, 200, w.Code)

    err = json.Unmarshal(w.Body.Bytes(), &actualResult)
    assert.NoError(t, err)

    assert.Equal(t, user.Name, actualResult.Name)
    assert.Equal(t, user.Email, actualResult.Email)
}

func TestSignUpInvalidJSON(t *testing.T) {
    user := "test"

    payload, err := json.Marshal(&user)
    assert.NoError(t, err)

    request, err := http.NewRequest("POST", "/api/public/signup", bytes.NewBuffer(payload))
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    Signup(c)

    assert.Equal(t, 400, w.Code)
}

func TestLogin(t *testing.T) {
    user := LoginPayload{
        Email:    "jwt@email.com",
        Password: "secret",
    }

    payload, err := json.Marshal(&user)
    assert.NoError(t, err)

    request, err := http.NewRequest("POST", "/api/public/login", bytes.NewBuffer(payload))
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    err = database.InitDatabase()
    assert.NoError(t, err)

    database.GlobalDB.AutoMigrate(&models.User{})

    Login(c)

    assert.Equal(t, 200, w.Code)

}

func TestLoginInvalidJSON(t *testing.T) {
    user := "test"

    payload, err := json.Marshal(&user)
    assert.NoError(t, err)

    request, err := http.NewRequest("POST", "/api/public/login", bytes.NewBuffer(payload))
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    Login(c)

    assert.Equal(t, 400, w.Code)
}

func TestLoginInvalidCredentials(t *testing.T) {
    user := LoginPayload{
        Email:    "jwt@email.com",
        Password: "invalid",
    }

    payload, err := json.Marshal(&user)
    assert.NoError(t, err)

    request, err := http.NewRequest("POST", "/api/public/login", bytes.NewBuffer(payload))
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    err = database.InitDatabase()
    assert.NoError(t, err)

    database.GlobalDB.AutoMigrate(&models.User{})

    Login(c)

    assert.Equal(t, 401, w.Code)

    database.GlobalDB.Unscoped().Where("email = ?", user.Email).Delete(&models.User{})
}

测试代码

我们一直在命令行进行测试,我们可以创建我们的路由并在 Postman 中测试。

将以下代码添加到 main.go 文件中。

// main.go

func setupRouter() *gin.Engine {
    r := gin.Default()

    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    
    //here
    api := r.Group("/api")
    {
        public := api.Group("/public")
        {
            public.POST("/login", controllers.Login)
            public.POST("/signup", controllers.Signup)
        }
    }

    return r
}

现在运行服务器。

go run main.go

让我们在 Postman 中测试一下。

postman测试1

postman测试2

可以在 JWT 中验证我们的令牌,查看输出信息是否正确。
jwt

现在,用户可以创建帐户、登录并接收令牌。

让我们创建一个受保护的路由,只有经过身份验证的用户才能访问它。

受保护的资源

此处的资源将是用户配置文件。 这很简单:它将用户数据返回给客户端。

// controllers/protected.go

package controllers

import (
    "authapp/database"
    "authapp/models"

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

// Profile returns user data
func Profile(c *gin.Context) {
    var user models.User

    email, _ := c.Get("email") // from the authorization middleware

    result := database.GlobalDB.Where("email = ?", email.(string)).First(&user)

    if result.Error == gorm.ErrRecordNotFound {
        c.JSON(404, gin.H{
            "msg": "user not found",
        })
        c.Abort()
        return
    }

    if result.Error != nil {
        c.JSON(500, gin.H{
            "msg": "could not get user profile",
        })
        c.Abort()
        return
    }

    user.Password = ""

    c.JSON(200, user)

    return
}

授权用户和验证令牌应该在中间件中进行。
现在,让我们测试一下配置文件控制器。

// controllers/protected_test.go

package controllers

import (
    "authapp/database"
    "authapp/models"
    "encoding/json"
    "log"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestProfile(t *testing.T) {
    var profile models.User

    err := database.InitDatabase()
    assert.NoError(t, err)

    database.GlobalDB.AutoMigrate(&models.User{})

    user := models.User{
        Email:    "jwt@email.com",
        Password: "secret",
        Name:     "Test User",
    }

    err = user.HashPassword(user.Password)
    assert.NoError(t, err)

    err = user.CreateUserRecord()
    assert.NoError(t, err)

    request, err := http.NewRequest("GET", "/api/protected/profile", nil)
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    c.Set("email", "jwt@email.com")

    Profile(c)

    err = json.Unmarshal(w.Body.Bytes(), &profile)
    assert.NoError(t, err)

    assert.Equal(t, 200, w.Code)

    log.Println(profile)

    assert.Equal(t, user.Email, profile.Email)
    assert.Equal(t, user.Name, profile.Name)
}

func TestProfileNotFound(t *testing.T) {
    var profile models.User

    err := database.InitDatabase()
    assert.NoError(t, err)

    database.GlobalDB.AutoMigrate(&models.User{})

    request, err := http.NewRequest("GET", "/api/protected/profile", nil)
    assert.NoError(t, err)

    w := httptest.NewRecorder()

    c, _ := gin.CreateTestContext(w)
    c.Request = request

    c.Set("email", "notfound@email.com")

    Profile(c)

    err = json.Unmarshal(w.Body.Bytes(), &profile)
    assert.NoError(t, err)

    assert.Equal(t, 404, w.Code)

    database.GlobalDB.Unscoped().Where("email = ?", "jwt@email.com").Delete(&models.User{})
}

测试通过

授权中间件

中间件位于客户端和资源之间,因此在我们访问数据库之前,将调用中间件来验证令牌并授权用户。 授权逻辑非常简单:

  • 检查授权标头中是否存在 JWT。
  • 检查令牌格式。
  • 验证令牌。
  • 继续到控制器。

创建一个名为的包 middlewares 并添加以下代码。

// middlewares/authz.go

package middlewares

import (
    "authapp/auth"
    "strings"

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

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

        extractedToken := strings.Split(clientToken, "Bearer ")

        if len(extractedToken) == 2 {
            clientToken = strings.TrimSpace(extractedToken[1])
        } else {
            c.JSON(400, "Incorrect Format of Authorization Token")
            c.Abort()
            return
        }

        jwtWrapper := auth.JwtWrapper{
            SecretKey: "verysecretkey",
            Issuer:    "AuthService",
        }

        claims, err := jwtWrapper.ValidateToken(clientToken)
        if err != nil {
            c.JSON(401, err.Error())
            c.Abort()
            return
        }

        c.Set("email", claims.Email)

        c.Next()

    }
}

测试代码:

// middlewares/authz_test.go

package middlewares

import (
    "authapp/auth"
    "authapp/controllers"
    "authapp/database"
    "authapp/models"
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func TestAuthzNoHeader(t *testing.T) {
    router := gin.Default()
    router.Use(Authz())

    router.GET("/api/protected/profile", controllers.Profile)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/protected/profile", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 403, w.Code)
}

func TestAuthzInvalidTokenFormat(t *testing.T) {
    router := gin.Default()
    router.Use(Authz())

    router.GET("/api/protected/profile", controllers.Profile)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/protected/profile", nil)
    req.Header.Add("Authorization", "test")

    router.ServeHTTP(w, req)

    assert.Equal(t, 400, w.Code)
}

func TestAuthzInvalidToken(t *testing.T) {
    invalidToken := "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    router := gin.Default()
    router.Use(Authz())

    router.GET("/api/protected/profile", controllers.Profile)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/protected/profile", nil)
    req.Header.Add("Authorization", invalidToken)

    router.ServeHTTP(w, req)

    assert.Equal(t, 401, w.Code)
}

func TestValidToken(t *testing.T) {
    var response models.User

    err := database.InitDatabase()
    assert.NoError(t, err)

    err = database.GlobalDB.AutoMigrate(&models.User{})
    assert.NoError(t, err)

    user := models.User{
        Email:    "test@email.com",
        Password: "secret",
        Name:     "Test User",
    }

    jwtWrapper := auth.JwtWrapper{
        SecretKey:       "verysecretkey",
        Issuer:          "AuthService",
        ExpirationHours: 24,
    }

    token, err := jwtWrapper.GenerateToken(user.Email)
    assert.NoError(t, err)

    err = user.HashPassword(user.Password)
    assert.NoError(t, err)

    result := database.GlobalDB.Create(&user)
    assert.NoError(t, result.Error)

    router := gin.Default()
    router.Use(Authz())

    router.GET("/api/protected/profile", controllers.Profile)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/protected/profile", nil)
    req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

    router.ServeHTTP(w, req)

    err = json.Unmarshal(w.Body.Bytes(), &response)
    assert.NoError(t, err)

    assert.Equal(t, 200, w.Code)
    assert.Equal(t, "test@email.com", response.Email)
    assert.Equal(t, "Test User", response.Name)

    database.GlobalDB.Unscoped().Where("email = ?", user.Email).Delete(&models.User{})
}

测试中间件
现在让我们在主路由器中添加一条正确的路由,并在 Postman 中自己查看结果。

// main.go

package main

import (
    "authapp/controllers"
    "authapp/database"
    "authapp/middlewares"
    "authapp/models"
    "log"

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

func setupRouter() *gin.Engine {
    r := gin.Default()

    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })

    api := r.Group("/api")
    {
        public := api.Group("/public")
        {
            public.POST("/login", controllers.Login)
            public.POST("/signup", controllers.Signup)
        }

        // here
        protected := api.Group("/protected").Use(middlewares.Authz())
        {
            protected.GET("/profile", controllers.Profile)
        }
    }

    return r
}

poatman测试api

文章到此结束。

参考文章:https://betterprogramming.pub/hands-on-with-jwt-in-golang-8c986d1bb4c0

打赏
评论区
头像
    头像
    pilgrimage
      

    观摩大佬

文章目录