一、什么是脚手架?¶
脚手架是项目开发的基础框架,脚手架包含了基本了项目结构、依赖管理、构建工具、测试框架等基本功能和配置,脚手架可以使开发者能够非常迅速的展开工作,避免重复造轮了,可以大大提高项目开发的效率和质量。
使用脚手架可以快速生成一个包含众多功能的项目,比如:
- 日志框架封装
- 程序配置加载
- 路由注册功能
- 登录认证功能
- 日志输出规范
- 以及其它通用的功能
二、JWT¶
2.1 什么是JWT?¶
JWT:JSON Web Token,是一种用于身份验证和授权的开放标准,JWT可以在网络应用间安全的传输。
JWT具有可扩展性、简单、轻量级、跨语言等优点,是前后端分离框架中最常用的验证方式。JWT工作流程大致如下:
- 当用户成功登录后,服务器会生成一个JWT并返回给客户端
- 客户端将JWT储存在本地
- 之后每次向服务器请求时都会在请求头中携带JWT
- 服务器会验证JWT的合法性,并根据其中的信息判断用户的身份和权限,从而决定是否允许用户访问请求的资源
2.2 JWT构成¶
JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
1、头部(Header)
作用:描述令牌的元数据,如签名算法和令牌类型。
结构:一个 JSON 对象,包含两个字段:
alg:签名算法(如HS256、RS256、ES256等),不可为空。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):双方协商的自定义字段(如
userId、role)。
示例:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
处理:通过 Base64Url 编码生成载荷字符串。
3、签名(Signature)
作用:验证令牌的完整性和来源真实性,防止篡改。
生成方式:
- 将编码后的
Header和Payload用.连接:base64UrlEncode(Header) + "." + base64UrlEncode(Payload)。 - 使用
Header中指定的算法和密钥(或私钥)对拼接后的字符串进行签名。
示例(HS256算法):
HMACSHA256(
base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
"your-secret-key"
)
处理:签名结果为二进制数据,需通过 Base64Url 编码后作为签名部分。
完整 JWT 示例
将三部分用 . 连接,形成最终 Token:
<your-jwt-token>
2.3 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+SecureCookie(防 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 按
.分割为Header、Payload、Signature三部分。 - Base64Url 解码
Header和Payload,获取原始 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

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

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后,点击【创建】

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

四、脚手架案例二¶
参考链接: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

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

说明:本实验依然基于上面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"
}

命令行界面回显内容如下:
...
...
[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】

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"
}

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"
}

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>"
}

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>"
}

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】

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>"
}

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 |

Postman工具回显内容如下:
{
"message": "Token验证未通过",
"status": 401
}
7、使用Postman工具进行POST请求测试,模拟请求未携带Token
填写Uri内容:http://127.0.0.1:8888/api/auth/login后,选择【Body】
填写空的内容后,点击【Send】

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】

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