Golang 后端接入 JWT 验证
前言
JWT 介绍
JSON Web Token(JWT)是一种前后端验证身份的协议。
项目相关
完整代码
https://github.com/deadmau5v/example-go-jwt
项目依赖
github.com/gin-gonic/gin v1.10.0 // HTTP框架
github.com/golang-jwt/jwt/v5 v5.2.1 // jwt 验证库
github.com/joho/godotenv v1.5.1 // 读取环境变量
golang.org/x/crypto v0.28.0 // 加密库
gorm.io/driver/postgres v1.5.9 // 数据库驱动
gorm.io/gorm v1.25.12 // 数据库ORM
项目结构
controller 实现用户注册和登录
initializers 实现数据库连接和迁移、读取环境变量
middleware 实现 gin + jwt 鉴权中间件
module 实现 user 模型
代码部分
1.读取环境变量
通过 godotenv.Load()
方法读取环境变量后,在任何地方使用 os.Getenv("Key")
就可以获取 .env
文件中设置的键值对
// initializers/loadEnv.go
func LoadEnv() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
.env
示例
PORT HTTP监听的端口
JWT_SECRET JWT加密密钥
DB_DSN 指定postgresql连接DSN
PORT = 5000
JWT_SECRET = "g734b8X6SvA3a1g734b8X6SvA3a1"
DB_DSN = "host=localhost user=example-go-jwt password=g734b8X6SvA3a1 port=5432 dbname=example-go-jwt"
2. 连接到数据库
使用 Gorm
来做数据库和实体类的映射
通过 .env
设置的 DB_DNS
连接到 Postgresql
连接后其他模块可以通过 initializers.DB
这个全局变量来调用
// initializers/connectToDb.go
var DB *gorm.DB
func ConnectToPostgres() {
var err error
dsn := os.Getenv("DB_DSN")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
}
3. 定义模型并迁移
在 module
包中,定义User
结构体,继承gorm.Module
来获得ID、updateTime等基础字段。
自定义 Email
、Password
字段,并定义unique(唯一,不可重复)
// module/UserModule.go
type User struct {
gorm.Model
Email string `gorm:"unique"`
Password string
}
定义完成结构体后,对模型进行迁移。
// initializers/syncDb.go
import "github.com/deadmau5v/example-go-jwt/module"
func SyncDb() {
DB.AutoMigrate(&module.User{})
// 更多其他模型...
}
4. 在 main
中执行初始化
func init() {
// 读取环境变量
initializers.LoadEnv()
// 连接数据库
initializers.ConnectToPostgres()
// 迁移
initializers.SyncDb()
}
5. 实现HTTP服务
监听 .env
设置的端口
// main.go
func main() {
app := gin.Default()
app.Run(":" + os.Getenv("PORT"))
}
6. 实现注册功能
首先定义用户发过来的结构requestBody
。
通过 ctx.BindJSON
方法,绑定POST的Json数据,如果是Form类型的POST,直接使用ctx.PostForm("xxx")
获取相应的值。
如果绑定失败或者参数不合法就返回错误信息,返回错误信息后记得要return
,防止程序继续往下执行。
如果参数合法并且用户未被注册,则加密密码并创建。(不应该直接保存密码明文)
通过 bcrypt.GenerateFromPassword
加密密码并保存,每次用户登录通过CompareHashAndPassword
方法对比加密后的密码即可。
func SingUp(ctx *gin.Context) {
// 获取请求参数 email 和 password
var requestBody struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
err := ctx.BindJSON(&requestBody)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
// 验证参数
if strings.TrimSpace(requestBody.Email) == "" || strings.TrimSpace(requestBody.Password) == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
// 查询用户是否存在
var user module.User
result := initializers.DB.Where("email = ?", requestBody.Email).First(&user)
if result.RowsAffected > 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "用户已存在"})
return
}
// 加密密码
hash, err := bcrypt.GenerateFromPassword([]byte(requestBody.Password), bcrypt.DefaultCost)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误 uc45"})
return
}
// 创建用户
user = module.User{
Email: requestBody.Email,
Password: string(hash),
}
result = initializers.DB.Create(&user)
if result.Error != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误 uc57"})
return
}
// 返回
ctx.JSON(http.StatusOK, gin.H{"message": "注册成功"})
}
7. 实现登录并创建JWT Token
定义用户请求模型。
通过 CompareHashAndPassword
对比数据库已加密的密码和用户发送的密码。
如果密码正确则将此用户的 user.ID
放到 JWT 中加密 生成Token 发送给用户。
设置 exp
过期时间为30天。
生成后设置到用户的Cookie中,并设置SameSite
为严格模式,防止跨域请求。
设置在Cookie的好处是,浏览器会在过期后自动删除用户Cookie。
用户登陆后,携带这个Token再次访问需要授权的API,通过Gin的中间件实现放行和拦截。
func Login(ctx *gin.Context) {
// 获取请求参数 email 和 password
var requestBody struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
ctx.BindJSON(&requestBody)
// 判断参数是否合法
if strings.TrimSpace(requestBody.Email) == "" || strings.TrimSpace(requestBody.Password) == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
// 判断加密后的密码是否相等
var user module.User
initializers.DB.First(&user, "email = ?", requestBody.Email)
if user.ID == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"})
return
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(requestBody.Password))
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "密码错误"})
return
}
// 生成 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "内部错误 uc103"})
return
}
ctx.SetSameSite(http.SameSiteStrictMode)
ctx.SetCookie("Authorization", tokenString, 3600*24*30, "/", "", false, true)
// 返回
ctx.JSON(http.StatusOK, gin.H{"status": "success"})
return
}
8. 实现Gin JWT 鉴权中间件
首先获取用户携带的Cookie,
如果没有携带则直接使用 ctx.Abort
方法拦截掉请求,返回401错误信息。
通过 jwt.ParseWithClaims
解密token内容,参数是.env
中设置的 JWT_SECRET
,
只有拥有这个 Secret 才能解密这个信息,用户篡改任意字符就会导致直接失效。
解密 token内容后 验证是否过期,或是用户是否存在。
通过验证后通过ctx.Set
设置一个 user 到上下文中。
func RequrieAuth(ctx *gin.Context) {
// 获取请求头的Cookie Authorization
jwt_token, err := ctx.Cookie("Authorization")
if err != nil {
log.Fatal(err)
ctx.JSON(401, gin.H{"error": "内部错误 ra17"})
ctx.Abort()
return
}
// 如果没有直接返回401
if strings.TrimSpace(jwt_token) == "" {
ctx.JSON(401, gin.H{"error": "请登录"})
ctx.Abort()
return
}
// 解析验证token
token, err := jwt.ParseWithClaims(jwt_token, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
}, jwt.WithLeeway(5*time.Second))
if err != nil {
log.Fatal(err)
ctx.JSON(401, gin.H{"error": "内部错误 ra31"})
ctx.Abort()
return
} else if claims, ok := token.Claims.(jwt.MapClaims); ok {
userId := claims["sub"].(float64)
timeStep := claims["exp"].(float64)
if timeStep < float64(time.Now().Unix()) {
ctx.JSON(401, gin.H{"error": "登录过期"})
ctx.Abort()
return
}
var user module.User
initializers.DB.First(&user, userId)
if user.ID == 0 {
ctx.JSON(401, gin.H{"error": "无效的登录"})
ctx.Copy().AbortWithStatus(http.StatusUnauthorized)
}
ctx.Set("user", user)
} else {
ctx.JSON(401, gin.H{"error": "无效的登录"})
ctx.Abort()
return
}
// 放行
ctx.Next()
}
9. 实现权限验证API
实现一个简单的权限验证API,通过 ctx.Get
获取上下文中的 user
数据,
如果用户没有登陆就会直接被 RequrieAuth
拦截掉
指定user类型 _user.(module.User)
func ValiDate(ctx *gin.Context) {
_user, _ := ctx.Get("user")
user := _user.(module.User)
ctx.JSON(http.StatusOK, gin.H{"status": "hello " + user.Email})
}
10. 设置路由
在 main.go
中决定哪些需要使用到鉴定权限中间件。
像注册、登录这种API,并不需要权限验证。
定义 api
里面绑定需要验证的路由
定义 noAuth
里面绑定无须验证的路由
func main() {
app := gin.Default()
api := app.Group("/api")
api.Use(middleware.RequrieAuth)
api.GET("/validate", controller.ValiDate)
noAuth := app.Group("/api")
noAuth.POST("/signup", controller.SingUp)
noAuth.POST("/login", controller.Login)
app.Run(":" + os.Getenv("PORT"))
}
评论