一、什么是脚手架?

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

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

  • 日志框架封装
  • 程序配置加载
  • 路由注册功能
  • 登录认证功能
  • 日志输出规范
  • 以及其它通用的功能

二、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、验证凭证(后端处理)

  • 后端验证
  • 接收请求后,后端从数据库查询对应用户信息。
  • 对比密码哈希值(如使用 bcrypt)验证用户身份。
  • 检查账户状态(如是否被封禁、是否已激活)。
  • 验证失败:返回 401 Unauthorized,提示用户名或密码错误。

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

  • 生成 JWT
  • 验证成功后,后端生成 JWT,包含以下关键声明:
    • sub(用户唯一标识,如用户ID)。
    • exp(过期时间,如 当前时间 + 1小时)。
    • role(用户角色,用于权限控制)。
  • 使用密钥(如 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 <your-jwt-token>

  • 其他方式

  • 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
  • 登录时返回两个 Token:
    • access_token:短期有效(如 15 分钟)。
    • refresh_token:长期有效(如 7 天),存储于数据库。
  • access_token 过期后,前端使用 refresh_token 调用续期接口(如 POST /api/refresh)。
  • 后端验证 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、点击【+】-【新建仓库】

新建仓库-1

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

image-20250517130958020

3.3.2 上传代码到仓库

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加密后的数据:<encrypted-credential>
  • 密码zq加密后的数据:<encrypted-credential>

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刷新页面查看,观察到已成功上传