Gin 脚手架项目实战:JWT、日志封装与认证中间件

来自AI助手的总结
文章介绍脚手架与JWT,并演示用logrus和端口配置搭建脚手架项目。
Gin 脚手架项目实战:JWT、日志封装与认证中间件

一、什么是脚手架?

脚手架是项目开发的基础框架,脚手架包含了基本了项目结构、依赖管理、构建工具、测试框架等基本功能和配置,脚手架可以使开发者能够非常迅速的展开工作,避免重复造轮了,可以大大提高项目开发的效率和质量。

使用脚手架可以快速生成一个包含众多功能的项目,比如:

  • 日志框架封装

  • 程序配置加载

  • 路由注册功能

  • 登录认证功能

  • 日志输出规范

  • 以及其它通用的功能

二、JWT

2.1 什么是JWT?

JWT:JSON Web Token,是一种用于身份验证和授权的开放标准,JWT可以在网络应用间安全的传输。

JWT具有可扩展性、简单、轻量级、跨语言等优点,是前后端分离框架中最常用的验证方式。JWT工作流程大致如下:

  1. 当用户成功登录后,服务器会生成一个JWT并返回给客户端

  2. 客户端将JWT储存在本地

  3. 之后每次向服务器请求时都会在请求头中携带JWT

  4. 服务器会验证JWT的合法性,并根据其中的信息判断用户的身份和权限,从而决定是否允许用户访问请求的资源

2.2 JWT构成

JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

1、头部(Header)

作用:描述令牌的元数据,如签名算法和令牌类型。

结构:一个 JSON 对象,包含两个字段:

  • alg:签名算法(如 HS256RS256ES256 等),不可为空。

  • typ:令牌类型,固定为 JWT(可选,但通常包含)。

示例

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

处理:通过 Base64Url 编码(URL安全的Base64,替换 +-/_,并删除末尾的 =)生成头部字符串。

2、载荷(Payload)

作用:携带实际数据(即“声明”),包含用户信息或其他业务数据。

结构:一个 JSON 对象,声明分为三类:

  • 注册声明(Registered Claims):预定义的标准字段(非强制但推荐):

  • iss(Issuer):签发者。

  • exp(Expiration Time):过期时间(Unix时间戳)。

  • sub(Subject):主题(如用户ID)。

  • aud(Audience):受众(接收方)。

  • iat(Issued At):签发时间。

  • nbf(Not Before):生效时间。

  • 公共声明(Public Claims):自定义字段,需在 IANA JSON Web Token Registry 注册或避免冲突。

  • 私有声明(Private Claims):双方协商的自定义字段(如 userIdrole)。

示例

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

处理:通过 Base64Url 编码生成载荷字符串。

3、签名(Signature)

作用:验证令牌的完整性和来源真实性,防止篡改。

生成方式

  1. 将编码后的 HeaderPayload. 连接:base64UrlEncode(Header) + "." + base64UrlEncode(Payload)

  2. 使用 Header 中指定的算法和密钥(或私钥)对拼接后的字符串进行签名。

示例(HS256算法)

HMACSHA256(
  base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
  "your-secret-key"
)

处理:签名结果为二进制数据,需通过 Base64Url 编码后作为签名部分。

完整 JWT 示例

将三部分用 . 连接,形成最终 Token:


<your-jwt-token>

2.3 JWT工作流程-认证逻辑

JWT工作流程

1、用户登录(前端 → 后端)

  • 前端操作:用户在前端界面输入用户名和密码,点击登录按钮。

  • 请求发送:前端将登录请求(含用户名、密码)发送至后端认证接口(如 POST /api/login)。

  • 安全建议:必须通过 HTTPS 加密传输,防止明文密码被窃取。

2、验证凭证(后端处理)

  • 后端验证
  1. 接收请求后,后端从数据库查询对应用户信息。

  2. 对比密码哈希值(如使用 bcrypt)验证用户身份。

  3. 检查账户状态(如是否被封禁、是否已激活)。

  • 验证失败:返回 401 Unauthorized,提示用户名或密码错误。

3、生成并返回 JWT(后端 → 前端)

  • 生成 JWT
  1. 验证成功后,后端生成 JWT,包含以下关键声明:

    • sub(用户唯一标识,如用户ID)。

    • exp(过期时间,如 当前时间 + 1小时)。

    • role(用户角色,用于权限控制)。

  2. 使用密钥(如 HS256)或私钥(如 RS256)对 JWT 进行签名。

  • 返回 Token:通过 HTTP 响应 Body 或 Header 返回 JWT(常见格式:{ "token": "xxx.yyy.zzz" })。

4、前端存储JWT

  • 存储方式

  • 推荐方案:使用 HttpOnly + Secure Cookie(防 XSS 读取,需配合 CSRF Token 防御跨站请求伪造)。

  • 替代方案:LocalStorage/SessionStorage(需防范 XSS 攻击)。

  • 示例代码

“`javascript

// 登录成功后保存 Token

localStorage.setItem(‘jwt’, response.data.token);

“`

5、携带 JWT 发起接口请求(前端 → 后端)

  • 请求头设置:前端在后续请求的 Authorization 头中附加 JWT:

“`http

GET /api/protected-data HTTP/1.1

Authorization: Bearer

“`

  • 其他方式

  • Cookie:自动携带(需设置 SameSite=Strict 防御 CSRF)。

  • URL 参数:不推荐(可能被日志记录导致泄露)。

6、后端验证 JWT(核心步骤)

(1) 解析 JWT

  • 将 Token 按 . 分割为 HeaderPayloadSignature 三部分。

  • Base64Url 解码 HeaderPayload,获取原始 JSON 数据。

(2) 验证签名

  • 根据 Header.alg 指定的算法(如 HS256),使用预共享密钥或公钥重新计算签名。

  • 对比计算的签名与 JWT 中的 Signature,确保未被篡改。

(3) 校验声明(Claims)

  • 时间有效性

  • 检查当前时间是否在 exp(过期时间)之前。

  • 验证 nbf(生效时间)是否已过。

  • 业务逻辑

  • 检查 iss(签发者)是否为可信服务。

  • 确认 aud(受众)匹配当前服务地址。

  • 验证用户权限(如 role: "admin" 是否允许访问该接口)。

(4) 黑名单检查(可选)

  • 若实现 Token 吊销机制,需查询数据库或缓存,确认该 JWT 未被加入黑名单。

7、响应处理

  • 验证通过

  • 后端处理请求,返回所需数据(如用户信息、业务数据)。

    “`json

    HTTP/1.1 200 OK

    { “data”: “Protected content” }

    “`

  • 验证失败

  • 返回 401 Unauthorized(Token 无效或过期)或 403 Forbidden(权限不足)。

  • 前端收到 401 后,跳转至登录页并要求用户重新认证。

8、Token 过期续期(可选)

  • 短期 Token + 长期 Refresh Token
  1. 登录时返回两个 Token:

    • access_token:短期有效(如 15 分钟)。

    • refresh_token:长期有效(如 7 天),存储于数据库。

  2. access_token 过期后,前端使用 refresh_token 调用续期接口(如 POST /api/refresh)。

  3. 后端验证 refresh_token 有效性,若合法则颁发新的 access_token

三、脚手架案例一

3.1 日志输出logrus封装

1、复制之前的project-demo项目并将其更名为scaffold-demo项目

2、使用vscode软件打开scaffold-demo文件夹,点击【搜索】,将project-demo替换为scaffold-demo

image-20250517110501697

3、执行下面命令安装logrus包和viper包,并执行go mod tidy命令自动解决依赖关系


$ go get github.com/sirupsen/logrus

$ go get github.com/spf13/viper

$ go mod tidy

4、修改main.go文件


// 项目的总入口

package main

import (

    _ "scaffold-demo/config"

    "scaffold-demo/utils/logs"

    "github.com/gin-gonic/gin"

)

func main() {

    // 1.加载程序的配置

    // 2.配置gin

    r := gin.Default()

    logs.Info(nil, "启动程序成功")

    r.Run()

}

5、在utils文件夹下面新建一个名为logs的文件夹,定义一个名为logs.go的文件


package logs

import "github.com/sirupsen/logrus"

// 打印debug类型的日志

func Debug(fields map[string]interface{}, msg string) {

    logrus.WithFields(fields).Debug(msg)

}

func Info(fields map[string]interface{}, msg string) {

    logrus.WithFields(fields).Info(msg)

}

func Error(fields map[string]interface{}, msg string) {

    logrus.WithFields(fields).Error(msg)

}

func Warning(fields map[string]interface{}, msg string) {

    logrus.WithFields(fields).Warning(msg)

}

6、修改config.go文件


// 存放程序的配置信息

package config

import (

    "scaffold-demo/utils/logs"

    "github.com/sirupsen/logrus"

    "github.com/spf13/viper"

)

const (

    TimeFormat string = "2006-01-02 15:04:05"

)

func initLogConfig(logLevel string) {

    //配置程序的日志输出级别

    if logLevel == "debug" {

        logrus.SetLevel(logrus.DebugLevel)

    } else {

        logrus.SetLevel(logrus.InfoLevel)

    }

    // 文件名和行号加进去

    logrus.SetReportCaller(true)

    // 日志格式改为json

    logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: TimeFormat})

}

func init() {

    logs.Debug(nil, "开始加载程序配置")

    // 环境变量加载我们的程序配置

    viper.SetDefault("LOG_LEVEL", "debug")

    viper.AutomaticEnv()

    logLevel := viper.GetString("LOG_LEVEL") //获取程序的配置

    // 加载日志输出格式

    initLogConfig(logLevel)

}

7、执行下面命令运行程序,观察到日志输出logrus已封装完成


$ go run .\main.go

// 回显信息

{"file":"C:/zq/宽哥/云原生全栈开发/云原生开发-Go、Gin入门到项目实战/课程笔记/Day013-项目开发实战-脚手架项目/Day014-项目开发

实战-脚手架项目-代码/scaffold-demo/utils/logs/logs.go:11","func":"scaffold-demo/utils/logs.Info","level":"info","msg":"启动

程序成功","time":"2025-05-17 11:47:20"}

3.2 自定义程序启动的端口号

1、修改config文件夹下的config.go文件

新增内容


var (

    Port string

)

    Port = viper.GetString("PORT")

完整代码文件内容


// 存放程序的配置信息

package config

import (

    "scaffold-demo/utils/logs"

    "github.com/sirupsen/logrus"

    "github.com/spf13/viper"

)

const (

    TimeFormat string = "2006-01-02 15:04:05"

)

var (

    Port string

)

func initLogConfig(logLevel string) {

    //配置程序的日志输出级别

    if logLevel == "debug" {

        logrus.SetLevel(logrus.DebugLevel)

    } else {

        logrus.SetLevel(logrus.InfoLevel)

    }

    // 文件名和行号加进去

    logrus.SetReportCaller(true)

    // 日志格式改为json

    logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: TimeFormat})

}

func init() {

    logs.Debug(nil, "开始加载程序配置")

    // 环境变量加载我们的程序配置

    viper.SetDefault("LOG_LEVEL", "debug")

    // 获取程序启动端口号的配置

    viper.SetDefault("PORT", "8080")

    viper.AutomaticEnv()

    logLevel := viper.GetString("LOG_LEVEL") //获取程序的配置

    Port = viper.GetString("PORT")

    // 加载日志输出格式

    initLogConfig(logLevel)

}

2、修改main.go文件

修改代码文件内容


    // 3.自定义程序启动的端口号

    r.Run(config.Port)

完整代码文件内容


// 项目的总入口

package main

import (

    "scaffold-demo/config"

    "scaffold-demo/utils/logs"

    "github.com/gin-gonic/gin"

)

func main() {

    // 1.加载程序的配置

    // 2.配置gin

    r := gin.Default()

    logs.Info(nil, "启动程序成功")

    // 3.自定义程序启动的端口号

    r.Run(config.Port)

}

3、定义PORT变量为:8888

右键【此电脑】,点击【属性】-【高级系统设置】-【环境变量】,点击【新建】设置变量名为PORT,变量值为:8888

image-20250517125334570

4、重新使用vscode软件打开scaffold-demo文件夹,执行下面命令运行程序,观察到已成功自定义程序启动的端口号


$ go run .\main.go

// 回显信息

...

...

[GIN-debug] Listening and serving HTTP on :8888

3.3 使用gitee管理项目源码

3.3 新建仓库

1、点击【+】-【新建仓库】

新建仓库-1

2、输入仓库名称scaffold-demo后,点击【创建】

image-20250517130958020

3.3 上传代码到仓库

1、重新定义README.md文件


## 项目信息

```

这是一个脚手架项目,可以根据这个项目去生成一个基础的框架

```

2、使用vscode软件打开scaffold-demo文件夹,执行下面命令提交代码到gitee仓库

git init
git add -A
git commit -m "first commit"
git remote add origin https://gitee.com/jeckjohn/scaffold-demo.git
git push -u origin "master"

3、gitee刷新页面查看,观察到已成功上传

image-20250517131354512

四、脚手架案例二

参考链接:https://pkg.go.dev/github.com/golang-jwt/jwt/v5#section-readme

4.1 封装生成jwt token函数

参考链接:https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-New-Hmac

image-20250517140440497

1、下载v5版本的jwt包


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

2、在utils文件夹下面新建一个名为jwtutil的文件夹,定义一个名为jwtutil.go的文件


package jwtutil

import (

    "scaffold-demo/config"

    "time"

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

)

var jwtSignKey = []byte("config.JwtSignKey")

// 1.自定义声明类型

type MyCustomClaims struct {

    Username string `json:"username"`

    jwt.RegisteredClaims

}

// 2.封装生成token的函数

func GenToken(username string) (string, error) {

    claims := MyCustomClaims{

        "bar",

        jwt.RegisteredClaims{

            // A usual scenario is to set the expiration time relative to the current time

            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * time.Duration(config.JwtExpTime))),

            IssuedAt:  jwt.NewNumericDate(time.Now()),

            NotBefore: jwt.NewNumericDate(time.Now()),

            Issuer:    "test",

            Subject:   "zq",

        },

    }

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

    ss, err := token.SignedString(jwtSignKey)

    return ss, err

}

3、修改config文件夹下面config.go的文件


package jwtutil

import (

    "scaffold-demo/config"

    "time"

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

)

var jwtSignKey = []byte("config.JwtSignKey")

// 1.自定义声明类型

type MyCustomClaims struct {

    Username string `json:"username"`

    jwt.RegisteredClaims

}

// 2.封装生成token的函数

func GenToken(username string) (string, error) {

    claims := MyCustomClaims{

        "bar",

        jwt.RegisteredClaims{

            // A usual scenario is to set the expiration time relative to the current time

            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * time.Duration(config.JwtExpTime))),

            IssuedAt:  jwt.NewNumericDate(time.Now()),

            NotBefore: jwt.NewNumericDate(time.Now()),

            Issuer:    "test",

            Subject:   "zq",

        },

    }

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

    ss, err := token.SignedString(jwtSignKey)

    return ss, err

}

4、修改main.go的文件


// 项目的总入口

package main

import (

    "fmt"

    "scaffold-demo/config"

    "scaffold-demo/utils/jwtutil"

    "scaffold-demo/utils/logs"

    "github.com/gin-gonic/gin"

)

func main() {

    // 1.加载程序的配置

    // 2.配置gin

    r := gin.Default()

    logs.Info(nil, "启动程序成功")

    // 测试生成jwt token是否可用

    ss, _ := jwtutil.GenToken("ddd")

    fmt.Println("测试是否能生成token", ss)

    // 自定义程序启动的端口号

    r.Run(config.Port)

}

5、运行程序,查看到生成的jwt token


$ go run .\main.go

#回显内容

测试是否能生成token <your-jwt-token>

回显内容解释说明:

  • 第一部分:Head头部

  • eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

  • 第二部分:Payload(载荷)

  • eyJ1c2VybmFtZSI6ImJhciIsImlzcyI6InRlc3QiLCJzdWIiOiJ6cSIsImV4cCI6MTc0NzQ3MDgwOSwibmJmIjoxNzQ3NDYzNjA5LCJpYXQiOjE3NDc0NjM2MDl9

  • Signature(签名)

  • _1wq-IRR5ml1ovifcOgkjMiPvXnpRjslX1smSH3hJjs

4.2 封装解析jwt token函数

参考链接:https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-New-Hmac

image-20250517144154477

说明:本实验依然基于上面4.1的基础上进行的

1、重新修改main.go文件

// 项目的总入口
package main
import (
    "fmt"
    "scaffold-demo/config"
    "scaffold-demo/utils/jwtutil"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
func main() {
    // 1.加载程序的配置
    // 2.配置gin
    r := gin.Default()
    logs.Info(nil, "启动程序成功")
    // 测试生成jwt token是否可用
    ss, _ := jwtutil.GenToken("ddd")
    fmt.Println("测试是否能生成token", ss)
    // 验证解析token的方法
    claims, err := jwtutil.ParseToken("<your-jwt-token>")
    if err != nil {
        // 说明解析失败
        fmt.Println("解析token失败:", err.Error())
    } else {
        // fmt.Println(claims)
        fmt.Println("✅ Token 解析成功!")
        fmt.Printf("解析结果:\n"+
            "  用户名: %s\n"+
            "  签发者: %s\n"+
            "  主题: %s\n"+
            "  过期时间: %v\n",
            claims.Username,
            claims.Issuer,
            claims.Subject,
            claims.ExpiresAt.Time.Format("2006-01-02 15:04:05"),
        )
    }
    // 自定义程序启动的端口号
    r.Run(config.Port)
}

2、重新修改jwtutil.go的文件


package jwtutil

import (

    "errors"

    "scaffold-demo/config"

    "scaffold-demo/utils/logs"

    "time"

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

)

var jwtSignKey = []byte("config.JwtSignKey")

// 1.自定义声明类型

type MyCustomClaims struct {

    Username string `json:"username"`

    jwt.RegisteredClaims

}

// 2.封装生成token的函数

func GenToken(username string) (string, error) {

    claims := MyCustomClaims{

        username,

        jwt.RegisteredClaims{

            // A usual scenario is to set the expiration time relative to the current time

            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * time.Duration(config.JwtExpTime))),

            IssuedAt:  jwt.NewNumericDate(time.Now()),

            NotBefore: jwt.NewNumericDate(time.Now()),

            Issuer:    "test",

            Subject:   "zq",

        },

    }

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

    ss, err := token.SignedString(jwtSignKey)

    return ss, err

}

// 3. 解析token

func ParseToken(ss string) (*MyCustomClaims, error) {

    token, err := jwt.ParseWithClaims(ss, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {

        return jwtSignKey, nil

    })

    if err != nil {

        // 解析token失败

        logs.Error(nil, "解析Token失败")

        return nil, err

    }

    if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {

        // 说明token合法

        return claims, nil

    } else {

        // token不合法

        logs.Warning(nil, "Token不合法")

        return nil, errors.New("token不合法: invalid token")

    }

}

3、运行程序

赋予一个错误token进行解析


#错误token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJhciIsImlzcyI6InRlc3QiLCJzdWIiOiJ6cSIsImV4cCI6MTc0NzQ3MDgwOSwibmJmIjoxNzQ3NDYzNjA5LCJpYXQiOjE3NDc0NjM2MDl9

$ go run .\main.go

#回显内容

解析token失败: token is malformed: token contains an invalid number of segments

赋予一个正确的token进行解析


#正确token

<your-jwt-token>

$ go run .\main.go

#回显内容

✅ Token 解析成功!

解析结果:

  用户名: bar

  签发者: test

  主题: zq

  过期时间: 2025-05-17 16:33:29

4.3 上传代码到仓库

1、重新定义README.md文件


## 项目信息

```

这是一个脚手架项目,可以根据这个项目去生成一个基础的框架

```

2、使用vscode软件打开scaffold-demo文件夹,执行下面命令提交代码到gitee仓库

git init
git add -A
git commit -m "first commit"
git remote add origin https://gitee.com/jeckjohn/scaffold-demo.git
git push -u origin "master"

3、gitee刷新页面查看,观察到已成功上传

4.4 针对不同控制器实现路由的拆分和注册

1、重新定义main.go文件


// 项目的总入口

package main

import (

    "scaffold-demo/config"

    "scaffold-demo/routers"

    "scaffold-demo/utils/logs"

    "github.com/gin-gonic/gin"

)

func main() {

    // 1.加载程序的配置

    // 2.配置gin

    r := gin.Default()

    logs.Info(nil, "启动程序成功")

    // // 测试生成jwt token是否可用

    // ss, _ := jwtutil.GenToken("ddd")

    // fmt.Println("测试是否能生成token", ss)

    // // 验证解析token的方法

    // claims, err := jwtutil.ParseToken("<your-jwt-token>")

    // if err != nil {

    //  // 说明解析失败

    //  fmt.Println("解析token失败:", err.Error())

    // } else {

    //  // fmt.Println(claims)

    //  fmt.Println("✅ Token 解析成功!")

    //  fmt.Printf("解析结果:\n"+

    //      "  用户名: %s\n"+

    //      "  签发者: %s\n"+

    //      "  主题: %s\n"+

    //      "  过期时间: %v\n",

    //      claims.Username,

    //      claims.Issuer,

    //      claims.Subject,

    //      claims.ExpiresAt.Time.Format("2006-01-02 15:04:05"),

    //  )

    // }

    r.GET("/debug/routes", func(c *gin.Context) {

        c.JSON(200, r.Routes())

    })

    routers.RegisterRouters(r)

    // 自定义程序启动的端口号

    r.Run(config.Port)

}

2、重新定义routers文件夹下面auth文件夹下面的auth.go的文件


package auth

import (

    "scaffold-demo/controllers/auth"

    "github.com/gin-gonic/gin"

)

// 实现登录接口

func login(authGroup *gin.RouterGroup) {

    authGroup.POST("/login", auth.Login)

}

// 实现退出接口

func logout(authGroup *gin.RouterGroup) {

    authGroup.GET("/logout", auth.Logout)

}

func RegisterSubRouter(g *gin.RouterGroup) {

    // 配置登录功能的路由策略

    authGroup := g.Group("/auth")

    // 登录功能

    login(authGroup)

    logout(authGroup)

}

3、重新定义routers文件夹下面的routers,go文件


// 路由层 配置程序的路由规则

package routers

import (

    "scaffold-demo/routers/auth"

    "github.com/gin-gonic/gin"

)

// 注册路由的方法

func RegisterRouters(r *gin.Engine) {

    // 登录的路由配置

    // 1.登录:login

    // 2.退出:logout

    // 3./api/auth/login   /api/auth/logout

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

    auth.RegisterSubRouter(apiGroup)

}

4、在controllers文件夹下面新增auth文件夹,并在auth文件夹下面新建auth.go的文件


package auth

import (

    "fmt"

    "scaffold-demo/utils/logs"

    "github.com/gin-gonic/gin"

)

type UserInfo struct {

    Username string `json:"username"`

    Password string `json:"password"`

}

// 登录的逻辑

func Login(r *gin.Context) {

    // 1.获取前端传递用户名和密码

    userInfo := UserInfo{}

    if err := r.ShouldBindJSON(&userInfo); err != nil {

        r.JSON(200, gin.H{

            "message": err.Error(),

            "status":  401,

        })

        return

    }

    fmt.Println("用户已经成功登录")

    // logs.Debug(map[string]interface{}{"用户名": userInfo.Username, "密码": userInfo.Password}, "开始验证登录信息")

}

// 登出的逻辑

func Logout(r *gin.Context) {

    // 退出

    r.JSON(200, gin.H{

        "message": "退出成功",

        "status":  200,

    })

    logs.Debug(nil, "用户已退出")

}

5、运行程序


$ go run .\main.go

6、使用Postman工具进行POST请求测试,模拟登录

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "zq",
    "password": "xxx"
}

image-20250517175741781

命令行界面回显内容如下:

...
...
[GIN-debug] Listening and serving HTTP on :8888
用户已经成功登录
[GIN] 2025/05/17 - 17:50:12 | 200 |         516µs |       127.0.0.1 | POST     "/api/auth/login"

7、使用Postman工具进行GET请求测试,模拟登出

填写Uri内容:http://127.0.0.1:8888/api/auth/logout后,点击【Send】

image-20250517180023774

Postman工具回显内容

{
    "message": "退出成功",
    "status": 200
}

命令行界面回显内容


[GIN] 2025/05/17 - 17:59:34 | 200 |            0s |       127.0.0.1 | GET      "/api/auth/logout"

4.5 实现登录且生成JWT Token返回给前端

1、重新定义controllers文件夹下面的auth.go

package auth
import (
    "scaffold-demo/config"
    "scaffold-demo/utils/jwtutil"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
type UserInfo struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
// 登录的逻辑
func Login(r *gin.Context) {
    // 1.获取前端传递用户名和密码
    userInfo := UserInfo{}
    if err := r.ShouldBindJSON(&userInfo); err != nil {
        r.JSON(200, gin.H{
            "message": err.Error(),
            "status":  401,
        })
        return
    }
    logs.Info(map[string]interface{}{"用户名": userInfo.Username, "密码": userInfo.Password}, "开始验证登录信息")
    // 验证用户名和密码是否正确
    // 数据库 环境变量
    if userInfo.Username == config.Username && userInfo.Password == config.Password {
        ss, err := jwtutil.GenToken(userInfo.Username)
        if err != nil {
            logs.Error(map[string]interface{}{"用户名": userInfo.Username, "错误信息": err.Error()}, "用户名和密码正确,但生成token失败")
            r.JSON(200, gin.H{
                "status":  401,
                "message": "生成token失败",
            })
            return
        }
        // token正常生成,返回给前端
        logs.Info(map[string]interface{}{"用户名": userInfo.Username}, "登录成功")
        data := make(map[string]interface{})
        data["token"] = ss
        r.JSON(200, gin.H{
            "status":  200,
            "message": "登录成功",
            "data":    data,
        })
        return
    } else {
        // 用户名和密码错误
        r.JSON(200, gin.H{
            "status":  401,
            "message": "用户名或密码错误",
        })
        return
    }
}
// 登出的逻辑
func Logout(r *gin.Context) {
    // 退出
    r.JSON(200, gin.H{
        "message": "退出成功",
        "status":  200,
    })
    logs.Debug(nil, "用户已退出")
}

2、重新定义config文件夹下面的config.go文件


// 存放程序的配置信息

package config

import (

    "scaffold-demo/utils/logs"

    "github.com/sirupsen/logrus"

    "github.com/spf13/viper"

)

const (

    TimeFormat string = "2006-01-02 15:04:05"

)

var (

    Port       string

    JwtSignKey string

    JwtExpTime int64 //JWT token过期时间,单位:分钟

    Username   string

    Password   string

)

func initLogConfig(logLevel string) {

    //配置程序的日志输出级别

    if logLevel == "debug" {

        logrus.SetLevel(logrus.DebugLevel)

    } else {

        logrus.SetLevel(logrus.InfoLevel)

    }

    // 文件名和行号加进去

    logrus.SetReportCaller(true)

    // 日志格式改为json

    logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: TimeFormat})

}

func init() {

    logs.Debug(nil, "开始加载程序配置")

    // 环境变量加载我们的程序配置

    viper.SetDefault("LOG_LEVEL", "debug")

    // 获取程序启动端口号的配置

    viper.SetDefault("PORT", "8080")

    // 获取jwt加密的secret

    viper.SetDefault("JWT_SIGN_KEY", "zq")

    // 获取jwt过期时间的配置

    viper.SetDefault("JWT_EXPIRE_TIME", 120)

    // 配置用户名密码的默认值

    viper.SetDefault("USERNAME", "zq")

    viper.SetDefault("PASSWORD", "zq")

    viper.AutomaticEnv()

    logLevel := viper.GetString("LOG_LEVEL") //获取程序的配置

    Port = viper.GetString("PORT")

    JwtSignKey = viper.GetString("JWT_SIGN_KEY")

    JwtExpTime = viper.GetInt64("JWT_EXPIRE_TIME")

    // 获取用户名和密码

    Username = viper.GetString("USERNAME")

    Password = viper.GetString("PASSWORD")

    // 加载日志输出格式

    initLogConfig(logLevel)

}

3、运行程序


$ go run .\main.go

4、使用Postman工具进行POST请求测试,模拟登录

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "zq",
    "password": "xxx"
}

image-20250517175741781

Postman工具回显内容如下:

{
    "message": "用户名或密码错误",
    "status": 401
}

5、使用Postman工具进行POST请求测试,模拟登录

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "zq",
    "password": "zq"
}

image-20250518090246202

Postman工具回显内容如下:

{
    "data": {
        "token": "<your-jwt-token>"
    },
    "message": "登录成功",
    "status": 200
}

4.6 实现登录信息的加密传输和验证

1、登录MD5在线加密/解密/破解—MD5在线网站,对用户名和密码进行加密

  • 用户名zq加密后的数据:

  • 密码zq加密后的数据:

2、重新定义config文件夹下面的config.go文件


// 存放程序的配置信息

package config

import (

    "scaffold-demo/utils/logs"

    "github.com/sirupsen/logrus"

    "github.com/spf13/viper"

)

const (

    TimeFormat string = "2006-01-02 15:04:05"

)

var (

    Port       string

    JwtSignKey string

    JwtExpTime int64 //JWT token过期时间,单位:分钟

    Username   string

    Password   string

)

func initLogConfig(logLevel string) {

    //配置程序的日志输出级别

    if logLevel == "debug" {

        logrus.SetLevel(logrus.DebugLevel)

    } else {

        logrus.SetLevel(logrus.InfoLevel)

    }

    // 文件名和行号加进去

    logrus.SetReportCaller(true)

    // 日志格式改为json

    logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: TimeFormat})

}

func init() {

    logs.Debug(nil, "开始加载程序配置")

    // 环境变量加载我们的程序配置

    viper.SetDefault("LOG_LEVEL", "debug")

    // 获取程序启动端口号的配置

    viper.SetDefault("PORT", "8080")

    // 获取jwt加密的secret

    viper.SetDefault("JWT_SIGN_KEY", "zq")

    // 获取jwt过期时间的配置

    viper.SetDefault("JWT_EXPIRE_TIME", 120)

    // 配置用户名密码的默认值

    // 加密用户名和密码 md5

    // 默认值为zq

    // viper.SetDefault("USERNAME", "zq")

    viper.SetDefault("USERNAME", "<encrypted-credential>")

    viper.SetDefault("PASSWORD", "<encrypted-credential>")

    viper.AutomaticEnv()

    logLevel := viper.GetString("LOG_LEVEL") //获取程序的配置

    // 加载日志输出格式

    initLogConfig(logLevel)

    Port = viper.GetString("PORT")

    JwtSignKey = viper.GetString("JWT_SIGN_KEY")

    JwtExpTime = viper.GetInt64("JWT_EXPIRE_TIME")

    // 获取用户名和密码

    Username = viper.GetString("USERNAME")

    Password = viper.GetString("PASSWORD")

}

3、运行程序


$ go run .\main.go

4、使用Postman工具进行POST请求测试,模拟登录

第一次测试

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "<encrypted-credential>",
    "password": "<encrypted-credential>"
}

image-20250518092251576

Postman工具回显内容如下:

{
    "message": "用户名或密码错误",
    "status": 401
}

之所以报错是因为windows用户本身就有USERNAME这个变量,windows中的USERNAME这个变量会直接替代我们自定义的USERNAME变量


C:\Users\zq>echo %USERNAME%

zq

第二次测试

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "zq",
    "password": "<encrypted-credential>"
}

image-20250518092430035

Postman工具回显内容如下:

{
    "data": {
        "token": "<your-jwt-token>"
    },
    "message": "登录成功",
    "status": 200
}

4.7 使用中间件拦截请求并验证请求合法性

1、重新定义middlewares文件夹下面的middlewares.go文件

// 实现路由的处理逻辑
package middlewares
import (
    "scaffold-demo/utils/jwtutil"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
func JWTAuth(r *gin.Context) {
    // 1.除了login和logout之外的所有的接口,都要验证请求是否携带token,并且token是否合法
    requestUrl := r.FullPath()
    logs.Debug(map[string]interface{}{"请求路径": requestUrl}, "")
    if requestUrl == "/api/auth/login" || requestUrl == "/api/auth/logout" {
        logs.Debug(map[string]interface{}{"请求路径": requestUrl}, "登录和退出不需要验证token")
        r.Next()
        return
    }
    // token
    // 其他接口需要验证token
    // 获取是否携带token
    tokenString := r.Request.Header.Get("Authorization")
    if tokenString == "" {
        // 说明请求没有携带token
        r.JSON(200, gin.H{
            "status":  401,
            "message": "请求未携带Token, 请登录后尝试",
        })
        r.Abort()
        return
    }
    // token不为空,要去验证token是否合法
    claims, err := jwtutil.ParseToken(tokenString)
    if err != nil {
        r.JSON(200, gin.H{
            "status":  401,
            "message": "Token验证未通过",
        })
        r.Abort()
        return
    }
    // 验证通过
    r.Set("claims", claims)
    r.Next()
}

2、重新定义main.go文件

// 项目的总入口
package main
import (
    "scaffold-demo/config"
    "scaffold-demo/middlewares"
    "scaffold-demo/routers"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
func main() {
    // 1.加载程序的配置
    // 2.配置gin
    r := gin.Default()
    r.Use(middlewares.JWTAuth)
    logs.Info(nil, "启动程序成功")
    // // 测试生成jwt token是否可用
    // ss, _ := jwtutil.GenToken("ddd")
    // fmt.Println("测试是否能生成token", ss)
    // // 验证解析token的方法
    // claims, err := jwtutil.ParseToken("<your-jwt-token>")
    // if err != nil {
    //  // 说明解析失败
    //  fmt.Println("解析token失败:", err.Error())
    // } else {
    //  // fmt.Println(claims)
    //  fmt.Println("✅ Token 解析成功!")
    //  fmt.Printf("解析结果:\n"+
    //      "  用户名: %s\n"+
    //      "  签发者: %s\n"+
    //      "  主题: %s\n"+
    //      "  过期时间: %v\n",
    //      claims.Username,
    //      claims.Issuer,
    //      claims.Subject,
    //      claims.ExpiresAt.Time.Format("2006-01-02 15:04:05"),
    //  )
    // }
    r.GET("/debug/routes", func(c *gin.Context) {
        c.JSON(200, r.Routes())
    })
    routers.RegisterRouters(r)
    // 自定义程序启动的端口号
    r.Run(config.Port)
}

3、运行程序


$ go run .\main.go

4、使用Postman工具进行GET请求测试,模拟登出,观察到不受影响

填写Uri内容:http://127.0.0.1:8888/api/auth/logout后,点击【Send】

image-20250518095124097

Postman工具回显内容如下:

{
    "message": "退出成功",
    "status": 200
}

5、使用Postman工具进行POST请求测试,模拟登录,观察到不受影响

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】-【raw】

填写json内容后,点击【Send】

{
    "username": "zq",
    "password": "<encrypted-credential>"
}

image-20250518092430035

Postman工具回显内容如下:

{
    "data": {
        "token": "<your-jwt-token>"
    },
    "message": "登录成功",
    "status": 200
}

6、使用Postman工具进行POST请求测试,模拟Token验证未通过

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Headers】

填写下面内容后,点击【Send】

Key Value
Authorization xxx

image-20250518095525968

Postman工具回显内容如下:

{
    "message": "Token验证未通过",
    "status": 401
}

7、使用Postman工具进行POST请求测试,模拟请求未携带Token

填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】

填写空的内容后,点击【Send】

image-20250518095648344

Postman工具回显内容如下:

{
    "message": "请求未携带Token, 请登录后尝试",
    "status": 401
}

4.8 封装和规范数据返回格式

1、重新定义config文件夹下面的config.go文件


// 存放程序的配置信息

package config

import (

    "scaffold-demo/utils/logs"

    "github.com/sirupsen/logrus"

    "github.com/spf13/viper"

)

const (

    TimeFormat string = "2006-01-02 15:04:05"

)

var (

    Port       string

    JwtSignKey string

    JwtExpTime int64 //JWT token过期时间,单位:分钟

    Username   string

    Password   string

)

type ReturnData struct {

    Status  int                    `json:"status"`

    Message string                 `json:"message"`

    Data    map[string]interface{} `json:"data"`

}

// 构造函数

func NewReturnData() ReturnData {

    returnData := ReturnData{}

    returnData.Status = 200

    data := make(map[string]interface{})

    returnData.Data = data

    return returnData

}

func initLogConfig(logLevel string) {

    //配置程序的日志输出级别

    if logLevel == "debug" {

        logrus.SetLevel(logrus.DebugLevel)

    } else {

        logrus.SetLevel(logrus.InfoLevel)

    }

    // 文件名和行号加进去

    logrus.SetReportCaller(true)

    // 日志格式改为json

    logrus.SetFormatter(&logrus.JSONFormatter{TimestampFormat: TimeFormat})

}

func init() {

    logs.Debug(nil, "开始加载程序配置")

    // 环境变量加载我们的程序配置

    viper.SetDefault("LOG_LEVEL", "debug")

    // 获取程序启动端口号的配置

    viper.SetDefault("PORT", "8080")

    // 获取jwt加密的secret

    viper.SetDefault("JWT_SIGN_KEY", "zq")

    // 获取jwt过期时间的配置

    viper.SetDefault("JWT_EXPIRE_TIME", 120)

    // 配置用户名密码的默认值

    // 加密用户名和密码 md5

    // 默认值为zq

    // viper.SetDefault("USERNAME", "zq")

    viper.SetDefault("USERNAME", "<encrypted-credential>")

    viper.SetDefault("PASSWORD", "<encrypted-credential>")

    viper.AutomaticEnv()

    logLevel := viper.GetString("LOG_LEVEL") //获取程序的配置

    // 加载日志输出格式

    initLogConfig(logLevel)

    Port = viper.GetString("PORT")

    JwtSignKey = viper.GetString("JWT_SIGN_KEY")

    JwtExpTime = viper.GetInt64("JWT_EXPIRE_TIME")

    // 获取用户名和密码

    Username = viper.GetString("USERNAME")

    Password = viper.GetString("PASSWORD")

}

2、重新定义middlewares文件夹下面的middlewares.go文件

// 实现路由的处理逻辑
package middlewares
import (
    "scaffold-demo/config"
    "scaffold-demo/utils/jwtutil"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
func JWTAuth(r *gin.Context) {
    // 1.除了login和logout之外的所有的接口,都要验证请求是否携带token,并且token是否合法
    requestUrl := r.FullPath()
    logs.Debug(map[string]interface{}{"请求路径": requestUrl}, "")
    if requestUrl == "/api/auth/login" || requestUrl == "/api/auth/logout" {
        logs.Debug(map[string]interface{}{"请求路径": requestUrl}, "登录和退出不需要验证token")
        r.Next()
        return
    }
    returnData := config.NewReturnData()
    // token
    // 其他接口需要验证token
    // 获取是否携带token
    tokenString := r.Request.Header.Get("Authorization")
    if tokenString == "" {
        // 说明请求没有携带token
        returnData.Status = 401
        returnData.Message = "请求未携带Token, 请登录后尝试"
        r.JSON(200, returnData)
        r.Abort()
        return
    }
    // token不为空,要去验证token是否合法
    claims, err := jwtutil.ParseToken(tokenString)
    if err != nil {
        returnData.Status = 401
        returnData.Message = "Token验证未通过"
        r.JSON(200, returnData)
        r.Abort()
        return
    }
    // 验证通过
    r.Set("claims", claims)
    r.Next()
}

3、重新定义controllers文件夹下面auth文件夹下面的auth.go文件

package auth
import (
    "scaffold-demo/config"
    "scaffold-demo/utils/jwtutil"
    "scaffold-demo/utils/logs"
    "github.com/gin-gonic/gin"
)
type UserInfo struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
// 登录的逻辑
func Login(r *gin.Context) {
    // 1.获取前端传递用户名和密码
    userInfo := UserInfo{}
    returnData := config.NewReturnData()
    if err := r.ShouldBindJSON(&userInfo); err != nil {
        returnData.Status = 401
        returnData.Message = err.Error()
        r.JSON(200, returnData)
        return
    }
    logs.Info(map[string]interface{}{"用户名": userInfo.Username, "密码": userInfo.Password}, "开始验证登录信息")
    // 验证用户名和密码是否正确
    // 数据库 环境变量
    if userInfo.Username == config.Username && userInfo.Password == config.Password {
        ss, err := jwtutil.GenToken(userInfo.Username)
        if err != nil {
            logs.Error(map[string]interface{}{"用户名": userInfo.Username, "错误信息": err.Error()}, "用户名和密码正确,但生成token失败")
            r.JSON(200, gin.H{
                "status":  401,
                "message": "生成token失败",
            })
            return
        }
        // token正常生成,返回给前端
        logs.Info(map[string]interface{}{"用户名": userInfo.Username}, "登录成功")
        returnData.Status = 401
        returnData.Message = "登录成功"
        returnData.Data["token"] = ss
        r.JSON(200, returnData)
        return
    } else {
        // 用户名和密码错误
        r.JSON(200, gin.H{
            "status":  401,
            "message": "用户名或密码错误",
        })
        return
    }
}
// 登出的逻辑
func Logout(r *gin.Context) {
    // 退出
    r.JSON(200, gin.H{
        "message": "退出成功",
        "status":  200,
    })
    logs.Debug(nil, "用户已退出")
}

3、运行程序


$ go run .\main.go

4、使用Postman工具进行GET请求测试,模拟登出,观察到不受影响

填写Uri内容:http://127.0.0.1:8888/api/auth/logout后,点击【Send】

image-20250518095124097

Postman工具回显内容如下:

{
    "message": "退出成功",
    "status": 200
}

5、重新定义README.md文件


## 项目信息

```

这是一个脚手架项目,可以根据这个项目去生成一个基础的框架

```

6、使用vscode软件打开scaffold-demo文件夹,执行下面命令提交代码到gitee仓库

git init
git add -A
git commit -m "two commit"
git remote add origin https://gitee.com/jeckjohn/scaffold-demo.git
git push -u origin "master"

7、gitee刷新页面查看,观察到已成功上传

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容