本文综合前面所学的知识,利用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...
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!