Go开发企业级微服务网关(8)登陆功能

从 gin_scaffold 到网关后端 go_gateway

在正式开始代码的编写之前,先对 gin_scaffold 进行处理,完成代码的删除、修改,并为下一步代码的编写做好准备。
删除controller 、dao 、dto 目录下的内容,并删除 /router/route.go 文件中多余的 router,并替换项目中出现的目录内容,完成对正式开发的准备。

大纲:

  • 登陆
  • 登陆信息获取
  • 退出
  • 修改密码

管理员登陆接口

首先编写管理员登陆控制器初步框架

type AdminLoginController struct{}

func AdminLoginRegisterController(router *gin.RouterGroup) {

}

func (adminlogin *AdminLoginController) AdminLogin(c *gin.Context) {

}

接下来需要对请求参数进行校验。需要用户名与密码参数,因此在 dto 中进行参数的定义

package dto

type AdminLoginInput struct {
	Username string `json:"username" form:"username" comment:"姓名" example:"admin" validate:"required"`
	Password string `json:"password" form:"password" comment:"密码" example:"123456" validate:"required"`
}

在 Go 语言中,结构体字段后面可以带一个标签(Tag),用反引号 ` 包裹。标签是键值对的形式,用于为字段附加元信息, 这些元信息可以在运行时通过反射获取,从而被各种库或框架用于不同的目的

在管理员登录输入 AdminLoginInput 结构体中,对定义了上述标签:

其中,json 标签和 form 标签 分别指定对应字段在 JSON 序列化/反序列化时的键名和 HTTP 表单中的参数名。 comment 标签和 example 标签通常在生成 API 文档时使用。 而 validate 标签用于定义字段的验证规则。

在 Go 语言中,结构体的标签不占用结构体实例的内存。标签是类型的一部分,而不是值的一部分,它们存储在程序的只读数据段(.rodata)中,每个类型只会存储一份,与创建了多少个结构体实例无关。

标签字符串(如 json:"username")在编译时被嵌入到二进制文件的只读数据区,作为类型元数据的一部分。Go 的反射包(reflect)通过访问类型的元信息来获取标签,而不是从实例中读取。

创建绑定结构体并校验参数函数:

func (param *AdminLoginInput) BindValidParam(c *gin.Context) error {
	return public.DefaultGetValidParams(c, param)
}

随后完成 controller 注册到 group 的代码:

type AdminLoginController struct{}

func AdminLoginRegisterController(router *gin.RouterGroup) {
	adminLogin := &AdminLoginController{}
	group.POST("/login", adminLogin.AdminLogin)
}

func (adminlogin *AdminLoginController) AdminLogin(c *gin.Context) {
	// 接下来需要对请求参数进行校验。需要用户名与密码参数,因此在 dto 中进行参数的定义
	params := &dto.AdminLoginInput{}
	if err := params.BindValidParam(c); err != nil {
		middleware.ResponseError(c, 1001, err)
		return
	}
	middleware.ResponseSuccess(c, "")
}

随后实现路由注册,将下面代码添加到 route.go 中:

	adminLoginRouter := router.Group("/admin_login")
	store, err := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("secret"))
	if err != nil {
		log.Fatal("sessions.NewRedisStore err:%v", err)
	}
	// 用 Router 设置关于 session 的中间件
	adminLoginRouter.Use(
		sessions.Sessions("mysession", store),
		middleware.RecoveryMiddleware(),
		middleware.RequestLog(),
		middleware.TranslationMiddleware())
	// 为 adminLoginRouter group 注册子 group
	{
		controller.AdminLoginRegister(adminLoginRouter)
	}

最后进行测试,服务器应当正确运行并输出:

2026/03/02 21:53:04 ------------------------------------------------------------------------
2026/03/02 21:53:04 [INFO]  config=./conf/dev/
2026/03/02 21:53:04 [INFO]  start loading resources.
2026/03/02 21:53:04 [INFO]  success loading resources.
2026/03/02 21:53:04 ------------------------------------------------------------------------
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> go_gateway_demo/router.InitRouter.func1 (3 handlers)
[GIN-debug] GET    /swagger/*any             --> github.com/swaggo/gin-swagger.CustomWrapHandler.func1 (3 handlers)
[GIN-debug] POST   /admin_login/login        --> go_gateway_demo/controller.(*AdminLoginController).AdminLogin-fm (7 handlers)
2026/03/02 21:53:04  [INFO] HttpServerRun::8880

使用 curl 进行测试应当响应:

curl.exe -X POST "http://127.0.0.1:8880/admin_login/login" -d "username=1111&password=2222"
{"errno":0,"errmsg":"","data":"","trace_id":"a9fefcc169a596efd22868cc5a802db0","stack":null}

接下来分别通过两种方式完成用户名和密码的教研:写死与数据库校验

首先对验证器 tag 进行扩充,并在 /middleware/translation.go 中对验证器进行实现:

Username string `json:"username" form:"username" comment:"管理员用户名" example:"admin" validate:"required,is_valid_username"`
			//自定义验证方法
			//https://github.com/go-playground/validator/blob/v9/_examples/custom-validation/main.go
			val.RegisterValidation("is_valid_username", func(fl validator.FieldLevel) bool {
				return fl.Field().String() == "admin"
			})

			//自定义翻译器
			//https://github.com/go-playground/validator/blob/v9/_examples/translations/main.go
			val.RegisterTranslation("is_valid_username", trans, func(ut ut.Translator) error {
				return ut.Add("is_valid_username", "{0} 填写不正确哦", true)
			}, func(ut ut.Translator, fe validator.FieldError) string {
				t, _ := ut.T("is_valid_username", fe.Field())
				return t
			})

接口文档生成

在下面的内容中,我们进行接口文档的编写,便于开发过程中的测试。

在 controller/admin_login.go 中新增如下内容,并在 dto/admin_login.go 中新增 Output 结构体:

// AdminLogin godoc
// @Summary 管理员登陆
// @Description 管理员登陆
// @Tags 管理员登录接口
// @ID /admin_login/login
// @Accept json
// @Produce json
// @Param body body dto.AdminLoginInput true "body"
// @Success 200 {object} middleware.Response{data=dto.AdminLoginOutput} "success"
// @Router /admin_login/login [post]
func (adminlogin *AdminLoginController) AdminLogin(c *gin.Context) {
	// 接下来需要对请求参数进行校验。需要用户名与密码参数,因此在 dto 中进行参数的定义
	params := &dto.AdminLoginInput{}
	if err := params.BindValidParam(c); err != nil {
		middleware.ResponseError(c, 1001, err)
		return
	}
	out := &dto.AdminLoginOutput{Token: params.Username}
	fmt.Println(out)
	middleware.ResponseSuccess(c, "")
}
type AdminLoginOutput struct {
	Token string `json:"token" form:"token" comment:"token" example:"token" validate:""`
}

运行 swag init 指令并启动服务器,可以在 Swagger UI 地址查看到接口文档。

登陆接口开发

在定义输入、输出和接口文档后,开始进行登陆业务逻辑的开发。

  1. 通过 params.UserName 取得管理员信息 admininfo
  2. 使用 admininfo.salt 和 params.Password 进行 sha256 加密得到 saltPassword
  3. 校验 if saltPassword ==admininfo.password

在第一步中涉及到数据库查询,因此在 /dao 目录下创建 admin.go ,实现对数据库中 gateway_admin 表格的查询

package dao

import (
	"time"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

type Admin struct {
	Id        int       `json:"id" gorm:"primary_key" description:"自增主键"`
	UserName  string    `json:"user_name" gorm:"column:user_name" description:"管理员用户名"`
	Salt      string    `json:"salt" gorm:"column:salt" description:"盐"`
	Password  string    `json:"password" gorm:"column:password" description:"密码"`
	UpdatedAt time.Time `json:"update_at" gorm:"column:update_at" description:"更新时间"`
	CreatedAt time.Time `json:"create_at" gorm:"column:create_at" description:"创建时间"`
	IsDelete  int       `json:"is_delete" gorm:"column:is_delete" description:"是否删除"`
}

func (t *Admin) TableName() string {
	return "gateway_admin"
}

func (t *Admin) Find(c *gin.Context, tx *gorm.DB, search *Admin) (*Admin, error) {
	out := &Admin{}
	err := tx.WithContext(c).Where(search).Find(out).Error
	if err != nil {
		return nil, err
	}
	return out, nil
}

由此可以实现取得管理员信息,接下来实现加 salt 操作

func (t *Admin) LoginCheck(c *gin.Context, tx *gorm.DB, param *dto.AdminLoginInput) (*Admin, error) {
	adminInfo, err := t.Find(c, tx, (&Admin{UserName: param.UserName, IsDelete: 0}))
	if err != nil {
		return nil, errors.New("用户信息不存在")
	}
	//param.Password
	//adminInfo.Salt
	return nil, nil
}

至此,需要一个新的方法构建 saltpassword ,因此在 public 目录下创建 util.go 进行编写:

package public

import (
	"crypto/sha256"
	"fmt"
)

func GenSaltPassword(salt, password string) string {
	s1 := sha256.New()
	s1.Write([]byte(password))
	str1 := fmt.Sprintf("%x", s1.Sum(nil))

	s2 := sha256.New()
	s2.Write([]byte(str1 + salt))
	return fmt.Sprintf("%x", s2.Sum(nil))
}

由此,就可以实现密码校验的流程:

	//param.Password
	//adminInfo.Salt
	saltPassword := public.GenSaltPassword(adminInfo.Salt, param.Password)
	if adminInfo.Password != saltPassword {
		return nil, errors.New("密码错误,请重新输入")
	}

完成登陆信息的校验后,进行 session 的设置,首先在 /dto/admin_login.go 中创建 session 的结构体:

type AdminSessionInfo struct {
	ID        int       `json:"id"`
	UserName  string    `json:"username"`
	LoginTime time.Time `json:"login_time"`
}

随后实现 session 的设置:

	// 设置session
	sessInfo := &dto.AdminSessionInfo{
		ID:        admin.Id,
		UserName:  admin.UserName,
		LoginTime: time.Now(),
	}
	sessBts, err := json.Marshal(sessInfo)
	if err != nil {
		middleware.ResponseError(c, 2003, err)
		return
	}

	sess := sessions.Default(c)
	sess.Set(public.AdminSessionInfoKey, string(sessBts))
	sess.Save()

至此,登陆接口的开发基本完成,在接下来的开发中继续实现后续的接口开发。

登陆信息获取接口开发

在之前的实现中,登陆采用的是 adminlogin 接口,在注册路由的流程中不对登陆信息进行校验。

获取登陆信息则需要对信息进行校验, 因此需要重新建立一个 controller ,并使用 session 校验中间件(/middleware/session_auth.go)

接口内部流程如下:

  1. 读取 sessionKey 对应 json 转换为结构体
  2. 取出数据然后封装输出结构体

首先定义输出的结构体 AdminInfoOutput:

package dto

import "time"

type AdminInfoOutput struct {
	ID           int       `json:"id"`
	UserName     string    `json:"name"`
	LoginTime    time.Time `json:"login_time"`
	Avatar       string    `json:"avatar"`
	Introduction string    `json:"introduction"`
	Roles        []string  `json:"roles"`
}

随后实现 controller 逻辑并进行注册:

package controller

import (
	"encoding/json"
	"fmt"
	"go_gateway_demo/dto"
	"go_gateway_demo/middleware"
	"go_gateway_demo/public"

	"github.com/gin-gonic/contrib/sessions"
	"github.com/gin-gonic/gin"
)

type AdminController struct{}

func AdminRegister(group *gin.RouterGroup) {
	adminLogin := &AdminController{}
	group.GET("/admin_info", adminLogin.AdminInfo)
}

// AdminInfo godoc
// @Summary 管理员信息
// @Description 管理员信息
// @Tags 管理员登录接口
// @ID /admin/admin_info
// @Accept json
// @Produce json
// @Success 200 {object} middleware.Response{data=dto.AdminInfoOutput} "success"
// @Router /admin/admin_info [get]
func (adminlogin *AdminController) AdminInfo(c *gin.Context) {
	// 1. 读取 sessionKey 对应 json 转换为结构体
	// 2. 取出数据然后封装输出结构体
	sess := sessions.Default(c)
	sessInfo := sess.Get(public.AdminSessionInfoKey)
	adminSessionInfo := &dto.AdminSessionInfo{}
	if err := json.Unmarshal([]byte(fmt.Sprint(sessInfo)), adminSessionInfo); err != nil {
		middleware.ResponseError(c, 2000, err)
		return
	}

	out := &dto.AdminInfoOutput{
		ID:           adminSessionInfo.ID,
		Name:         adminSessionInfo.UserName,
		LoginTime:    adminSessionInfo.LoginTime,
		Avatar:       "",
		Introduction: "I am a super administrator",
		Roles:        []string{"admin"},
	}
	middleware.ResponseSuccess(c, out)
}
	adminRouter := router.Group("/admin")
	adminRouter.Use(
		sessions.Sessions("mysession", store),
		middleware.RecoveryMiddleware(),
		middleware.RequestLog(),
		middleware.SessionAuthMiddleware(),
		middleware.TranslationMiddleware())
	{
		controller.AdminRegister(adminRouter)
	}

中间件 session_auth 的认证逻辑如下:

package middleware

import (
	"errors"
	"go_gateway_demo/public"

	"github.com/gin-gonic/contrib/sessions"
	"github.com/gin-gonic/gin"
)

func SessionAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		session := sessions.Default(c)
		if adminInfo, ok := session.Get(public.AdminSessionInfoKey).(string); !ok || adminInfo == "" {
			ResponseError(c, InternalErrorCode, errors.New("user not login"))
			c.Abort()
			return
		}
		c.Next()
	}
}

退出接口开发

退出接口逻辑相对简单,在登陆接口的基础上进行实现即可:

// AdminLogin godoc
// @Summary 管理员退出
// @Description 管理员退出
// @Tags 管理员接口
// @ID /admin_login/logout
// @Accept json
// @Produce json
// @Success 200 {object} middleware.Response{data=string} "success"
// @Router /admin_login/logout [get]
func (adminlogin *AdminLoginController) AdminLogout(c *gin.Context) {
	sess := sessions.Default(c)
	sess.Delete(public.AdminSessionInfoKey)
	sess.Save()
	middleware.ResponseSuccess(c, "")
}

并为 AdminLoginRegister 添加内容:

func AdminLoginRegister(group *gin.RouterGroup) {
	adminLogin := &AdminLoginController{}
	group.POST("/login", adminLogin.AdminLogin)
	group.GET("/logout", adminLogin.AdminLogout)
}

密码修改接口开发

密码修改的逻辑与登陆逻辑相似,均需要对用户 session 信息进行校验,具体的流程如下:

  1. session 读取用户信息到结构体 sessInfo
  2. sessInfo.ID 读取数据库信息 adminInfo
  3. params.password + adminInfo.salt sha256 saltPassword
  4. adminInfo ==> adminInfo.password 执行数据保存

代码实现及接口文档生成代码如下:

// AdminInfo godoc
// @Summary 修改密码
// @Description 修改密码
// @Tags 管理员接口
// @ID /admin/change_pwd
// @Accept json
// @Produce json
// @Param body body dto.ChangePwdInput true "body"
// @Success 200 {object} middleware.Response{data=string} "success"
// @Router /admin/change_pwd [post]
func (adminlogin *AdminController) ChangePwd(c *gin.Context) {
	params := &dto.ChangePwdInput{}
	if err := params.BindValidParam(c); err != nil {
		middleware.ResponseError(c, 2001, err)
		return
	}

	sess := sessions.Default(c)
	sessInfo := sess.Get(public.AdminSessionInfoKey)
	adminSessionInfo := &dto.AdminSessionInfo{}
	if err := json.Unmarshal([]byte(fmt.Sprint(sessInfo)), adminSessionInfo); err != nil {
		middleware.ResponseError(c, 2000, err)
		return
	}

	// 从数据库中读取 adminInfo
	tx, err := lib.GetGormPool("default")
	if err != nil {
		middleware.ResponseError(c, 2001, err)
		return
	}
	adminInfo := &dao.Admin{}
	adminInfo, err = adminInfo.Find(c, tx, (&dao.Admin{UserName: adminSessionInfo.UserName}))
	if err != nil {
		middleware.ResponseError(c, 2002, err)
		return
	}

	saltPassword := public.GenSaltPassword(adminInfo.Salt, params.Password)
	adminInfo.Password = saltPassword
	if err := adminInfo.UpdatePassword(c, tx, saltPassword); err != nil {
		middleware.ResponseError(c, 2003, err)
		return
	}

	middleware.ResponseSuccess(c, "")
}

UpdatePassword 逻辑如下:

func (t *Admin) UpdatePassword(c *gin.Context, tx *gorm.DB, newPassword string) error {
return tx.WithContext(c).Model(t).Update("password", newPassword).Error
}

结语

至此,登陆管理功能接口开发基本完成,完整实现了登陆、登陆信息获取、退出和修改密码四个功能,接口文档页面如下:

发表评论