每日一Go-20、Go语言实战-利用Gin开发用户注册登录功能

本文综合前面所学的知识,利用Gin开发一个用户注册登录和验证的功能。

1、项目结构

sh 复制代码
$ tree
.
|-- config        <- 配置目录
|   `-- db.go     <- 数据库配置
|-- controllers   <- 控制器目录
|   `-- auth.go   <- 认证控制器
|-- go.mod
|-- go.sum
|-- main.go       <- 程序入口
|-- middlewares   <- 中间件目录
|   `-- auth.go   <- 认证中间件
|-- models        <- 模型目录
|   `-- user.go   <- 用户模型
|-- routers       <- 路由目录
|   |-- auth.go   <- 认证路由
|   `-- init.go   <- 初始化路由注册
`-- utils         <- 工具箱
    `-- jwt.go

6 directories, 10 files

2、开始写代码

2.1 config/db.go

go 复制代码
package config
import (
    "log"
    "os"
    "time"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
    // dsn := "root:123456@tcp(127.0.0.1:3306)/golang_per_day??charset=utf8&parseTime=True&loc=Local&timeout=1000ms"
    dsn := os.Getenv("DATABASE_DSN")
    if dsn == "" {
        panic("请在环境变量里配置【DATABASE_DSN】")
    }
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("连接接数据库失败:", err)
    }
    log.Println("数据库连接成功:", db)
    sqlDb, err := db.DB()
    if err == nil {
        sqlDb.SetMaxIdleConns(10)
        sqlDb.SetMaxOpenConns(100)
        sqlDb.SetConnMaxLifetime(time.Hour)
    }
    // 开启debug模式
    DB = db.Debug()
}

2.2 controllers/auth.go

go 复制代码
package controllers
import (
    "fmt"
    "gindemo2/config"
    "gindemo2/models"
    "gindemo2/utils"
    "net/http"
    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)
type inputUser struct {
    Username string `json:"username" binding:"required,min=3,max=32"`
    Passwd   string `json:"passwd" binding:"required,min=6"`
}
// 注册用户
func Register(c *gin.Context) {
    input := inputUser{}
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            `error`: err.Error(),
        })
        return
    }
    hasedPwd, err := bcrypt.GenerateFromPassword([]byte(input.Passwd), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            `error`: "哈希密码失败",
        })
        return
    }
    user := models.User{
        Username: input.Username,
        PassWd:   string(hasedPwd),
    }
    if err := config.DB.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{`error`: err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{
        `message`: "用户创建成功",
        `user`: gin.H{
            `id`:       user.ID,
            `username`: user.Username,
        },
    })
}
// 登录
func Login(c *gin.Context) {
    input := inputUser{}
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            `error`: err.Error(),
        })
        return
    }
    var user models.User
    err := config.DB.Where(`username = ?`, input.Username).First(&user).Error
    if err != nil {
        if err == gorm.ErrRecordNotFound {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的凭证"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    err = bcrypt.CompareHashAndPassword([]byte(user.PassWd), []byte(input.Passwd))
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的凭证"})
        return
    }
    token, err := utils.GenerateToken(user.ID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "message": "登录成功",
        "token":   token,
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
        },
    })
}
// 查询当前登录者的信息
func Me(c *gin.Context) {
    var user models.User
    uid := c.GetUint(`user_id`)
    fmt.Println(`uid=`, uid)
    err := config.DB.Where(`id=?`, uid).First(&user).Error
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            `error`: err.Error(),
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "message": "查询成功",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
        },
    })
}

2.3 middlewares/auth.go

go 复制代码
package middlewares
import (
    "fmt"
    "gindemo2/utils"
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`: "Authorization header 不能为空",
            })
            return
        }
        parts := strings.Fields(authHeader)
        if len(parts) != 2 || strings.ToLower(parts[0]) != `bearer` {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`: "Authorization header 的格式错误,必须是 Bearer {token}",
            })
            return
        }
        token := parts[1]
        claims, err := utils.ParseToken(token)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                `error`:  "非法token",
                `detail`: err.Error(),
            })
            return
        }
        fmt.Println(`claims=`, claims)
        //设定当前登录的用户id,方便传下去
        c.Set(`user_id`, claims.UserID)
        c.Next()
    }
}

2.4 models/user.go

go 复制代码
package models
import "gorm.io/gorm"
// 用户表
type User struct {
    gorm.Model
    // gorm:"uniqueIndex" 是唯一索引,not null表示不为空
    Username string `json:"username" gorm:"type:varchar(30);uniqueIndex;not null"`
    // json:"-", - 表示不输出到json
    PassWd string `json:"-" gorm:"not null"`
}

2.5 routers/init.go

go 复制代码
package routers
import "github.com/gin-gonic/gin"
// 定义路由函数
type Router func(*gin.Engine)
// 这里放所有的路由
var routers = []Router{}
// 初始化路由
func InitRouter() *gin.Engine {
    r := gin.Default()
    for _, route := range routers {
        route(r)
    }
    return r
}
// 注册路由通用函数
func RegisterRoute(r ...Router) {
    routers = append(routers, r...)
}

2.6 routers/auth.go

go 复制代码
package routers
import (
    "gindemo2/controllers"
    "gindemo2/middlewares"
    "github.com/gin-gonic/gin"
)
// init()会在程序启动的时候自动运行
func init() {
    RegisterRoute(func(e *gin.Engine) {
        // 路由分组
        api := e.Group("/api/v1")
        // 路由分组
        auth := api.Group("/auth")
        auth.POST("/login", controllers.Login)
        auth.POST("/reg", controllers.Register)
        // 路由/api/v1/auth开头的都加上认证
        auth.Use(middlewares.AuthMiddleware())
        {
            auth.GET("/me", controllers.Me)
            //其他路由
        }
    })
}

2.7 utils/jwt.go

go 复制代码
package utils
import (
    "fmt"
    "os"
    "time"
    "github.com/golang-jwt/jwt/v5"
)
type Claims struct {
    UserID uint `json:"user_id"`
    jwt.RegisteredClaims
}
func getSecret() []byte {
    s := os.Getenv("JWT_SECRET")
    if s == "" {
        panic("请在.env文件里配置【JWT_SECRET】")
    }
    return []byte(s)
}
// 生成token
func GenerateToken(userID uint) (string, error) {
    expireHours := 24
    if v := os.Getenv("JWT_EXPIRE_HOURS"); v != "" {
        var parsed int
        if _, err := fmt.Sscanf(v, "%d", &parsed); err == nil {
            expireHours = parsed
        }
    }
    claims := &Claims{
        UserID: userID,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "Codee君",
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(getSecret())
}
// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        // ensure signing method is HMAC
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("签名方法不支持: %v", token.Header["alg"])
        }
        return getSecret(), nil
    })
    if err != nil {
        return nil, err
    }
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    return nil, fmt.Errorf("token校验失败")
}

2.8 .env

go 复制代码
PORT=8080
JWT_SECRET=Codee君
JWT_EXPIRE_HOURS=24
DATABASE_DSN=root:123456@tcp(localhost:3306)/golang_per_day?charset=utf8&parseTime=True&loc=Local&timeout=1000ms

2.9 go.mod

go 复制代码
module gindemo2
go 1.23.0
toolchain go1.24.10
require (
    github.com/gin-gonic/gin v1.11.0
    github.com/golang-jwt/jwt/v5 v5.3.0
    github.com/joho/godotenv v1.5.1
    golang.org/x/crypto v0.40.0
    gorm.io/driver/mysql v1.6.0
    gorm.io/gorm v1.31.1
)
...

2.10 main.go

go 复制代码
package main
import (
    "log"
    "os"
    "github.com/joho/godotenv"
    "gindemo2/config"
    "gindemo2/models"
    "gindemo2/routers"
)
func main() {
    // 加载env变量
    if err := godotenv.Load(); err != nil {
        log.Println("No .env file found --- using environment variables")
    }
    // 连接数据库
    config.ConnectDatabase()
    // 自动迁移
    if err := config.DB.AutoMigrate(&models.User{}); err != nil {
        log.Fatalf("AutoMigrate failed: %v", err)
    }
    // 初始化路由
    r := routers.InitRouter()
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    r.Run(":" + port)
}

3、运行与测试

3.1 初始化模块并下载依赖

go 复制代码
go mod tidy

3.2 启动服务

go 复制代码
go run .

3.3 注册用户

go 复制代码
$ curl -X POST http://localhost:8080/api/v1/auth/reg \                                                                          
  -H "Content-Type: application/json" \                                                                                         
  -d '{"username":"coding_jun","passwd":"golang"}'                                                                              
{"message":"用 户 创 建 成 功 ","user":{"id":1,"username":"coding_jun"}}   

3.4 登录

go 复制代码
$ curl -X POST http://localhost:8080/api/v1/auth/login \                                                                        
  -H "Content-Type: application/json" \                                                                                         
  -d '{"username":"coding_jun","passwd":"golang"}'                                                                              
{"message":"登 录 成 功 ","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJpc3MiOiJDb2RlZeWQmyIsImV4cCI6MTc2MzM1MTcy
MywiaWF0IjoxNzYzMjY1MzIzfQ.GKNcht6PkqzO92tQuTSfy62AO8qSgZZB4AZCULfG12M","user":{"id":1,"username":"coding_jun"}} 

3.5 访问受保护的路由

go 复制代码
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJpc3MiOiJDb2RlZeWQmyIsImV4cCI6MTc2MzM1M 
TcyMywiaWF0IjoxNzYzMjY1MzIzfQ.GKNcht6PkqzO92tQuTSfy62AO8qSgZZB4AZCULfG12M" http://localhost:8080/api/v1/auth/me                 
{"message":"查 询 成 功 ","user":{"id":1,"username":"coding_jun"}} 

4、安全与建议(敲黑板)

JWT_SECRET必须强壮且保密,不要把默认密钥打包进代码;使用HTTPS;生成中建议使用Access Token+ Refresh Token来避免token的长期暴露;严格错误信息,不要在认证失败的时候返回过多细节,例如"用户不存在"就别说了;防止暴力破解,对登录尝试次数做限制;加入token黑名单,例如退出、强制下线等。

5、源码地址

1、公众号"Codee君"回复"源码"获取源码

2、pan.baidu.com/s/1B6pgLWfS...


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!

相关推荐
用户26851612107562 小时前
GMP 三大核心结构体字段详解
后端·go
corpse201014 小时前
FastMonitor - 网络流量监控与威胁检测工具--各种报错!!!
go
源代码•宸3 天前
Leetcode—1929. 数组串联&&Q1. 数组串联【简单】
经验分享·后端·算法·leetcode·go
nil3 天前
记录protoc生成代码将optional改成omitepty问题
后端·go·protobuf
Way2top3 天前
Go语言动手写Web框架 - Gee第五天 中间件
后端·go
Way2top3 天前
Go语言动手写Web框架 - Gee第四天 分组控制
后端·go
Grassto3 天前
从 `go build` 开始:Go 第三方包加载流程源码导读
golang·go·go module
源代码•宸4 天前
Golang基础语法(go语言结构体、go语言数组与切片、go语言条件句、go语言循环)
开发语言·经验分享·后端·算法·golang·go
華勳全栈5 天前
两天开发完成智能体平台
java·spring·go