广告位联系
返回顶部
分享到

Golang在gin框架中使用JWT鉴权

Golang 来源:互联网 作者:佚名 发布时间:2024-07-03 22:20:09 人浏览
摘要

什么是JWT JWT,全称 JSON Web Token,是一种开放标准(RFC 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。JWT 的设计允许信息在各方之间安全地、 compactly(紧凑地)传输

什么是JWT

JWT,全称 JSON Web Token,是一种开放标准(RFC 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。JWT 的设计允许信息在各方之间安全地、 compactly(紧凑地)传输,因为其自身包含了所有需要的认证信息,从而减少了需要查询数据库或会话存储的需求。

JWT主要由三部分组成,通过.连接:

  • Header(头部):描述JWT的元数据,通常包括类型(通常是JWT)和使用的签名算法(如HS256、RS256等)。
  • Payload(载荷):包含声明(claims),即用户的相关信息。这些信息可以是公开的,也可以是私有的,但应避免放入敏感信息,因为该部分可以被解码查看。载荷中的声明可以验证,但不加密。
  • Signature(签名):用于验证JWT的完整性和来源。它是通过将Header和Payload分别进行Base64编码后,再与一个秘钥(secret)一起通过指定的算法(如HMAC SHA256)计算得出的。

JWT的工作流程大致如下:

  • 认证阶段:用户向服务器提供凭证(如用户名和密码)。服务器验证凭证无误后,生成一个JWT,其中包含用户标识符和其他声明,并使用秘钥对其进行签名。
  • 使用阶段:客户端收到JWT后,可以在后续的每个请求中将其放在HTTP请求头中发送给服务器,以此证明自己的身份。
  • 验证阶段:服务器收到JWT后,会使用相同的秘钥验证JWT的签名,确保其未被篡改,并检查过期时间等其他声明,从而决定是否允许执行请求。

JWT的优势在于它的无状态性,服务器不需要存储会话信息,这减轻了服务器的压力,同时也方便了跨域认证。但需要注意的是,JWT的安全性依赖于秘钥的安全保管以及对JWT过期时间等的合理设置。

API设计

这里设计两个公共接口和一个受保护的接口。

API 描述
/api/login 公开接口。用于用户登录
/api/register 公开接口。用于用户注册
/api/admin/user 保护接口,需要验证JWT

开发准备

初始化项目目录并切换进入

1

2

mkdir gin-jwt

cd gin-jwt

使用go mod初始化工程

1

go mod init gin-jwt

安装依赖

1

2

3

4

5

6

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

go get -u gorm.io/gorm

go get -u gorm.io/driver/postgres

go get -u github.com/golang-jwt/jwt/v5

go get -u github.com/joho/godotenv

go get -u golang.org/x/crypto

创建第一个API

一开始我们可以在项目的根目录中创建文件main.go

1

touch main.go

添加以下内容

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package main

import (

    "net/http"

    "github.com/gin-gonic/gin"

)

func main() {

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", func(c *gin.Context) {

            c.JSON(http.StatusOK, gin.H{

                "data": "test. register api",

            })

        })

    }

    r.Run("0.0.0.0:8000")

}

测试运行

1

go run main.go

客户端测试。正常的话会有以下输出

1

2

$ curl -X POST http://127.0.0.1:8000/api/register

{"data":"test. register api"}

完善register接口

现在register接口已经准备好了,但一般来说我们会把接口业务逻辑放在单独的文件中,而不是和接口定义写在一块。

创建一个控制器的包目录,并添加文件

1

2

mkdir controllers

touch controllers/auth.go

auth.go文件内容

1

2

3

4

5

6

7

8

9

10

package controllers

import (

    "net/http"

    "github.com/gin-gonic/gin"

)

func Register(c *gin.Context) {

    c.JSON(http.StatusOK, gin.H{

        "data": "hello, this is register endpoint",

    })

}

更新main.go文件

1

2

3

4

5

6

7

8

9

10

11

12

13

package main

import (

    "github.com/gin-gonic/gin"

    "gin-jwt/controllers"

)

func main() {

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", controllers.Register)

    }

    r.Run("0.0.0.0:8000")

}

重新运行测试

1

go run main.go

客户端测试

1

2

$ curl -X POST http://127.0.0.1:8000/api/register

{"data":"hello, this is register endpoint"}

解析register的客户端请求

客户端请求register api需要携带用户名和密码的参数,服务端对此做解析。编辑文件controllers/auth.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package controllers

import (

    "net/http"

    "github.com/gin-gonic/gin"

)

// /api/register的请求体

type ReqRegister struct {

    Username string `json:"username" binding:"required"`

    Password string `json:"password" binding:"required"`

}

func Register(c *gin.Context) {

    var req ReqRegister

    if err := c.ShouldBindBodyWithJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "data": err.Error(),

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "data": req,

    })

}

客户端请求测试

1

2

$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'

{"data":{"username":"zhangsan","password":"123456"}}

连接关系型数据库

一般会将数据保存到专门的数据库中,这里用PostgreSQL来存储数据。Postgres使用docker来安装。安装完postgres后,创建用户和数据库:

1

2

create user ginjwt encrypted password 'ginjwt';

create database ginjwt owner = ginjwt;

创建目录models,这个目录将包含连接数据库和数据模型的代码。

1

mkdir models

编辑文件models/setup.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

package models

import (

    "fmt"

    "log"

    "os"

    "github.com/joho/godotenv"

    "gorm.io/driver/postgres"

    "gorm.io/gorm"

)

var DB *gorm.DB

func ConnectDatabase() {

    err := godotenv.Load(".env")

    if err != nil {

        log.Fatalf("Error loading .env file. %v\n", err)

    }

    // DbDriver := os.Getenv("DB_DRIVER")

    DbHost := os.Getenv("DB_HOST")

    DbPort := os.Getenv("DB_PORT")

    DbUser := os.Getenv("DB_USER")

    DbPass := os.Getenv("DB_PASS")

    DbName := os.Getenv("DB_NAME")

    dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)

    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {

        log.Fatalf("Connect to database failed, %v\n", err)

    } else {

        log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)

    }

    // 迁移数据表

    DB.AutoMigrate(&User{})

}

新建并编辑环境配置文件.env

1

2

3

4

5

DB_HOST=127.0.0.1

DB_PORT=5432

DB_USER=ginjwt

DB_PASS=ginjwt

DB_NAME=ginjwt

创建用户模型,编辑代码文件models/user.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

package models

import (

    "html"

    "strings"

    "golang.org/x/crypto/bcrypt"

    "gorm.io/gorm"

)

type User struct {

    gorm.Model

    Username string `gorm:"size:255;not null;unique" json:"username"`

    Password string `gorm:"size:255;not null;" json:"password"`

}

func (u *User) SaveUser() (*User, error) {

    err := DB.Create(&u).Error

    if err != nil {

        return &User{}, err

    }

    return u, nil

}

// 使用gorm的hook在保存密码前对密码进行hash

func (u *User) BeforeSave(tx *gorm.DB) error {

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)

    if err != nil {

        return err

    }

    u.Password = string(hashedPassword)

    u.Username = html.EscapeString(strings.TrimSpace(u.Username))

    return nil

}

更新main.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package main

import (

    "github.com/gin-gonic/gin"

    "gin-jwt/controllers"

    "gin-jwt/models"

)

func init() {

    models.ConnectDatabase()

}

func main() {

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", controllers.Register)

    }

    r.Run("0.0.0.0:8000")

}

更新controllers/auth.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

package controllers

import (

    "net/http"

    "gin-jwt/models"

    "github.com/gin-gonic/gin"

)

// /api/register的请求体

type ReqRegister struct {

    Username string `json:"username" binding:"required"`

    Password string `json:"password" binding:"required"`

}

func Register(c *gin.Context) {

    var req ReqRegister

    if err := c.ShouldBindBodyWithJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "data": err.Error(),

        })

        return

    }

    u := models.User{

        Username: req.Username,

        Password: req.Password,

    }

    _, err := u.SaveUser()

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "data": err.Error(),

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "message": "register success",

        "data":    req,

    })

}

重新运行服务端后,客户端测试

1

2

$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'

{"data":{"username":"zhangsan","password":"123456"},"message":"register success"}

添加login接口

登录接口实现的也非常简单,只需要提供用户名和密码参数。服务端接收到客户端的请求后到数据库中去匹配,确认用户是否存在和密码是否正确。如果验证通过则返回一个token,否则返回异常响应。

首先在main.go中注册API

1

2

3

4

5

6

7

8

9

10

// xxx

func main() {

    // xxx

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", controllers.Register)

        public.POST("/login", controllers.Login)

    }

}

在auth.go中添加Login控制器函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

// api/login 的请求体

type ReqLogin struct {

    Username string `json:"username" binding:"required"`

    Password string `json:"password" binding:"required"`

}

func Login(c *gin.Context) {

    var req ReqLogin

    if err := c.ShouldBindBodyWithJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

        return

    }

    u := models.User{

        Username: req.Username,

        Password: req.Password,

    }

    // 调用 models.LoginCheck 对用户名和密码进行验证

    token, err := models.LoginCheck(u.Username, u.Password)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": "username or password is incorrect.",

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "token": token,

    })

}

LoginCheck方法在models/user.go文件中实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

package models

import (

    "gin-jwt/utils/token"

    "html"

    "strings"

    "golang.org/x/crypto/bcrypt"

    "gorm.io/gorm"

)

func VerifyPassword(password, hashedPassword string) error {

    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))

}

func LoginCheck(username, password string) (string, error) {

    var err error

    u := User{}

    err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error

    if err != nil {

        return "", err

    }

    err = VerifyPassword(password, u.Password)

    if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {

        return "", err

    }

    token, err := token.GenerateToken(u.ID)

    if err != nil {

        return "", err

    }

    return token, nil

}

这里将token相关的函数放到了单独的模块中,新增相关目录并编辑文件

1

2

mkdir -p utils/token

touch utils/token/token.go

以下代码为token.go的内容,包含的几个函数在后面会用到

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

package token

import (

    "fmt"

    "os"

    "strconv"

    "strings"

    "time"

    "github.com/gin-gonic/gin"

    "github.com/golang-jwt/jwt/v5"

)

func GenerateToken(user_id uint) (string, error) {

    token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))

    if err != nil {

        return "", err

    }

    claims := jwt.MapClaims{}

    claims["authorized"] = true

    claims["user_id"] = user_id

    claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()

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

    return token.SignedString([]byte(os.Getenv("API_SECRET")))

}

func TokenValid(c *gin.Context) error {

    tokenString := ExtractToken(c)

    fmt.Println(tokenString)

    _, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {

        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

        }

        return []byte(os.Getenv("API_SECRET")), nil

    })

    if err != nil {

        return err

    }

    return nil

}

// 从请求头中获取token

func ExtractToken(c *gin.Context) string {

    bearerToken := c.GetHeader("Authorization")

    if len(strings.Split(bearerToken, " ")) == 2 {

        return strings.Split(bearerToken, " ")[1]

    }

    return ""

}

// 从jwt中解析出user_id

func ExtractTokenID(c *gin.Context) (uint, error) {

    tokenString := ExtractToken(c)

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {

        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

        }

        return []byte(os.Getenv("API_SECRET")), nil

    })

    if err != nil {

        return 0, err

    }

    claims, ok := token.Claims.(jwt.MapClaims)

    // 如果jwt有效,将user_id转换为浮点数字符串,然后再转换为 uint32

    if ok && token.Valid {

        uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)

        if err != nil {

            return 0, err

        }

        return uint(uid), nil

    }

    return 0, nil

}

在.env文件中添加两个环境变量的配置。TOKEN_HOUR_LIFESPAN设置token的过期时长,API_SECRET是jwt的密钥。

1

2

TOKEN_HOUR_LIFESPAN=1

API_SECRET="wP3-sN6&gG4-lV8>gJ9)"

测试,这里改用python代码进行测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

import requests

import json

headers = {

    "Content-Type": "application/json",

}

resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)

def register(username: str, password: str):

    req_body = {

        "username": username,

        "password": password,

    }

    resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)

    print(resp.text)

def login(username: str, password: str):

    req_body = {

        "username": username,

        "password": password,

    }

    resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)

    print(resp.text)

    if resp.status_code == 200:

        return resp.json()["token"]

    else:

        return ""

if __name__ == "__main__":

    username = "lisi"

    password = "123456"

    register(username, password)

    token = login(username, password)

    print(token)

创建JWT认证中间件

创建中间件目录和代码文件

1

2

mkdir middlewares

touch middlewares/middlewares.go

内容如下

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package middlewares

import (

    "gin-jwt/utils/token"

    "net/http"

    "github.com/gin-gonic/gin"

)

func JwtAuthMiddleware() gin.HandlerFunc {

    return func(c *gin.Context) {

        err := token.TokenValid(c)

        if err != nil {

            c.String(http.StatusUnauthorized, err.Error())

            c.Abort()

            return

        }

        c.Next()

    }

}

在main.go文件中注册路由的时候使用中间件

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

func main() {

    models.ConnectDatabase()

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", controllers.Register)

        public.POST("/login", controllers.Login)

    }

    protected := r.Group("/api/admin")

    {

        protected.Use(middlewares.JwtAuthMiddleware())

        protected.GET("/user", func(c *gin.Context) {

            c.JSON(http.StatusOK, gin.H{

                "status":  "success",

                "message": "authorized",

            })

        })

    }

    r.Run("0.0.0.0:8000")

}

在controllers/auth.go文件中实现CurrentUser

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

func CurrentUser(c *gin.Context) {

    // 从token中解析出user_id

    user_id, err := token.ExtractTokenID(c)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": err.Error(),

        })

        return

    }

    // 根据user_id从数据库查询数据

    u, err := models.GetUserByID(user_id)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": err.Error(),

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "message": "success",

        "data": u,

    })

}

在models/user.go文件中实现GetUserByID

1

2

3

4

5

6

7

8

9

10

11

12

// 返回前将用户密码置空

func (u *User) PrepareGive() {

    u.Password = ""

}

func GetUserByID(uid uint) (User, error) {

    var u User

    if err := DB.First(&u, uid).Error; err != nil {

        return u, errors.New("user not found")

    }

    u.PrepareGive()

    return u, nil

}

至此,一个简单的gin-jwt应用就完成了。

客户端测试python脚本

服务端的三个接口这里用python脚本来测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

import requests

import json

headers = {

    # "Authorization": f"Bearer {token}",

    "Content-Type": "application/json",

}

resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)

def register(username: str, password: str):

    req_body = {

        "username": username,

        "password": password,

    }

    resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)

    print(resp.text)

def login(username: str, password: str):

    req_body = {

        "username": username,

        "password": password,

    }

    resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)

    print(resp.text)

    if resp.status_code == 200:

        return resp.json()["token"]

    else:

        return ""

def test_protect_api(token: str):

    global headers

    headers["Authorization"] = f"Bearer {token}"

    resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)

    print(resp.text)

if __name__ == "__main__":

    username = "lisi"

    password = "123456"

    register(username, password)

    token = login(username, password)

    test_protect_api(token)

运行脚本结果

{"message":"register success"}
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MTk5NDA0NjAsInVzZXJfaWQiOjZ9.qkzn0Ot9hAb54l3RFbGUohHJ9oezGia5x_oXppbD2jQ"}
{"data":{"ID":6,"CreatedAt":"2024-07-03T00:14:20.187725+08:00","UpdatedAt":"2024-07-03T00:14:20.187725+08:00","DeletedAt":null,"username":"wangwu","password":""},"message":"success"}

完整示例代码

目录结构

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

├── client.py  # 客户端测试脚本

├── controllers  # 控制器相关包

│   └── auth.go  # 控制器方法实现

├── gin-jwt.bin  # 编译的二进制文件

├── go.mod  # go 项目文件

├── go.sum  # go 项目文件

├── main.go  # 程序入口文件

├── middlewares  # 中间件相关包

│   └── middlewares.go  # 中间件代码文件

├── models  # 存储层相关包

│   ├── setup.go  # 配置数据库连接

│   └── user.go  # user模块相关数据交互的代码文件

├── README.md  # git repo的描述文件

└── utils  # 工具类包

    └── token  # token相关工具类包

        └── token.go  # token工具的代码文件

main.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

package main

import (

    "log"

    "github.com/gin-gonic/gin"

    "gin-jwt/controllers"

    "gin-jwt/middlewares"

    "gin-jwt/models"

    "github.com/joho/godotenv"

)

func init() {

    err := godotenv.Load(".env")

    if err != nil {

        log.Fatalf("Error loading .env file. %v\n", err)

    }

}

func main() {

    models.ConnectDatabase()

    r := gin.Default()

    public := r.Group("/api")

    {

        public.POST("/register", controllers.Register)

        public.POST("/login", controllers.Login)

    }

    protected := r.Group("/api/admin")

    {

        protected.Use(middlewares.JwtAuthMiddleware()) // 在路由组中使用中间件

        protected.GET("/user", controllers.CurrentUser)

    }

    r.Run("0.0.0.0:8000")

}

controllers

  • auth.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

package controllers

import (

    "net/http"

    "gin-jwt/models"

    "gin-jwt/utils/token"

    "github.com/gin-gonic/gin"

)

// /api/register的请求体

type ReqRegister struct {

    Username string `json:"username" binding:"required"`

    Password string `json:"password" binding:"required"`

}

// api/login 的请求体

type ReqLogin struct {

    Username string `json:"username" binding:"required"`

    Password string `json:"password" binding:"required"`

}

func Login(c *gin.Context) {

    var req ReqLogin

    if err := c.ShouldBindBodyWithJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

        return

    }

    u := models.User{

        Username: req.Username,

        Password: req.Password,

    }

    // 调用 models.LoginCheck 对用户名和密码进行验证

    token, err := models.LoginCheck(u.Username, u.Password)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": "username or password is incorrect.",

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "token": token,

    })

}

func Register(c *gin.Context) {

    var req ReqRegister

    if err := c.ShouldBindBodyWithJSON(&req); err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "data": err.Error(),

        })

        return

    }

    u := models.User{

        Username: req.Username,

        Password: req.Password,

    }

    _, err := u.SaveUser()

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "data": err.Error(),

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "message": "register success",

    })

}

func CurrentUser(c *gin.Context) {

    // 从token中解析出user_id

    user_id, err := token.ExtractTokenID(c)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": err.Error(),

        })

        return

    }

    // 根据user_id从数据库查询数据

    u, err := models.GetUserByID(user_id)

    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{

            "error": err.Error(),

        })

        return

    }

    c.JSON(http.StatusOK, gin.H{

        "message": "success",

        "data": u,

    })

}

models

  • setup.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

package models

import (

    "fmt"

    "log"

    "os"

    "gorm.io/driver/postgres"

    "gorm.io/gorm"

)

var DB *gorm.DB

func ConnectDatabase() {

    var err error

    DbHost := os.Getenv("DB_HOST")

    DbPort := os.Getenv("DB_PORT")

    DbUser := os.Getenv("DB_USER")

    DbPass := os.Getenv("DB_PASS")

    DbName := os.Getenv("DB_NAME")

    dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)

    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {

        log.Fatalf("Connect to database failed, %v\n", err)

    } else {

        log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)

    }

    // 迁移数据表

    DB.AutoMigrate(&User{})

}

  • user.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

package models

import (

    "errors"

    "gin-jwt/utils/token"

    "html"

    "strings"

    "golang.org/x/crypto/bcrypt"

    "gorm.io/gorm"

)

type User struct {

    gorm.Model

    Username string `gorm:"size:255;not null;unique" json:"username"`

    Password string `gorm:"size:255;not null;" json:"password"`

}

func (u *User) SaveUser() (*User, error) {

    err := DB.Create(&u).Error

    if err != nil {

        return &User{}, err

    }

    return u, nil

}

// 使用gorm的hook在保存密码前对密码进行hash

func (u *User) BeforeSave(tx *gorm.DB) error {

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)

    if err != nil {

        return err

    }

    u.Password = string(hashedPassword)

    u.Username = html.EscapeString(strings.TrimSpace(u.Username))

    return nil

}

// 返回前将用户密码置空

func (u *User) PrepareGive() {

    u.Password = ""

}

// 对哈希加密的密码进行比对校验

func VerifyPassword(password, hashedPassword string) error {

    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))

}

func LoginCheck(username, password string) (string, error) {

    var err error

    u := User{}

    err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error

    if err != nil {

        return "", err

    }

    err = VerifyPassword(password, u.Password)

    if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {

        return "", err

    }

    token, err := token.GenerateToken(u.ID)

    if err != nil {

        return "", err

    }

    return token, nil

}

func GetUserByID(uid uint) (User, error) {

    var u User

    if err := DB.First(&u, uid).Error; err != nil {

        return u, errors.New("user not found")

    }

    u.PrepareGive()

    return u, nil

}

utils

  • token/token.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

package token

import (

    "fmt"

    "os"

    "strconv"

    "strings"

    "time"

    "github.com/gin-gonic/gin"

    "github.com/golang-jwt/jwt/v5"

)

func GenerateToken(user_id uint) (string, error) {

    token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))

    if err != nil {

        return "", err

    }

    claims := jwt.MapClaims{}

    claims["authorized"] = true

    claims["user_id"] = user_id

    claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()

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

    return token.SignedString([]byte(os.Getenv("API_SECRET")))

}

func TokenValid(c *gin.Context) error {

    tokenString := ExtractToken(c)

    fmt.Println(tokenString)

    _, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {

        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

        }

        return []byte(os.Getenv("API_SECRET")), nil

    })

    if err != nil {

        return err

    }

    return nil

}

// 从请求头中获取token

func ExtractToken(c *gin.Context) string {

    bearerToken := c.GetHeader("Authorization")

    if len(strings.Split(bearerToken, " ")) == 2 {

        return strings.Split(bearerToken, " ")[1]

    }

    return ""

}

// 从jwt中解析出user_id

func ExtractTokenID(c *gin.Context) (uint, error) {

    tokenString := ExtractToken(c)

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {

        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

        }

        return []byte(os.Getenv("API_SECRET")), nil

    })

    if err != nil {

        return 0, err

    }

    claims, ok := token.Claims.(jwt.MapClaims)

    // 如果jwt有效,将user_id转换为浮点数字符串,然后再转换为 uint32

    if ok && token.Valid {

        uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)

        if err != nil {

            return 0, err

        }

        return uint(uid), nil

    }

    return 0, nil

}

middlewares

  • middlewares.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package middlewares

import (

    "gin-jwt/utils/token"

    "net/http"

    "github.com/gin-gonic/gin"

)

func JwtAuthMiddleware() gin.HandlerFunc {

    return func(c *gin.Context) {

        err := token.TokenValid(c)

        if err != nil {

            c.String(http.StatusUnauthorized, err.Error())

            c.Abort()

            return

        }

        c.Next()

    }

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • Go中的Timer和Ticker介绍
    一:简介 在日常开发中,我们可能会遇到需要延迟执行或周期性地执行一些任务。这个时候就需要用到Go语言中的定时器。 在Go语言中,定
  • Golang在gin框架中使用JWT鉴权
    什么是JWT JWT,全称 JSON Web Token,是一种开放标准(RFC 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。JWT 的设计
  • Golang使用Redis与连接池方式
    Golang使用Redis与连接池 使用下载go的redis包go get github.com/gomodule/redigo/redis 如果网不好的话就很费劲了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 2
  • Golang使用embed引入静态文件的代码
    Go 语言从 1.16 版本开始引入了一个新的标准库embed,可以在二进制文件中引入静态文件 指令:/go:embed 通过一个简单的小实例,来演示将静态
  • Golang发送Get和Post请求的实现
    最近在研究钉钉机器人,发现钉钉的第三方接口有时需要用Get或者Post请求访问,虽然说可以通过Apifox直接模拟发送请求,但是我还是想研究
  • Go实现数据脱敏的方案设计
    在一些常见的业务场景中可能涉及到用户的手机号,银行卡号等敏感数据,对于这部分的数据经常需要进行数据脱敏处理,就是将此部分数
  • golang testing使用示例介绍
    testing包服务于自动化测试 基本测试 Table Drvien Test 基于表的测试通过表形式进行测试每种情况的输入和期望输出,从而测试程序的正确性
  • Golang流程控制语句的具体使用

    Golang流程控制语句的具体使用
    顺序控制 顺序控制 默认情况下,Go代码执行顺序是按照从上到下依次执行的,这种按照顺序执行的代码就叫做顺序语句。如下: 1 2 3 4 5 6
  • Golang图片验证码的使用方法介绍

    Golang图片验证码的使用方法介绍
    一、背景 最近在使用到Golang进行原生开发,注册和登录页面都涉及到图片验证码的功能。找了下第三方库的一些实现,发现了这个库用得还
  • Golang中实现类似类与继承的方法
    在Go语言中,并没有像传统面向对象编程语言(Java、C++)中那样的类和继承的概念。Go语言采用了结构体和组合的方式来实现类似的功能。
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计