goframe框架签到系统项目开发(用户认证、基于 JWT 实现认证、携带access token获取用户信息)

文章目录

用户认证

HTTP 是一个无状态的协议,一次请求结束后,下次再发送服务器就不知道这个请求是谁发来的了(同一个 IP 不代表同一个用户),在 Web 应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用方案,并且不同的方案各有千秋。

在 Web 应用发展的初期,大部分网站采用基于 Cookie-Session 的会话管理方式,逻辑如下:

  1. 客户端输入用户名、密码进行认证
  2. 服务端验证用户名、密码正确后生成并存储 Session,将 SessionID 通过 Cookie 返回给客户端
  3. 客户端访问需要认证的接口时在 Cookie 中携带 SessionID
  4. 服务端通过 SessionID 查找 Session 并进行鉴权,返回给客户端需要的数据

基于 Session 的方式存在多种问题。

  • 服务端需要存储 Session,并且由于 Session 需要经常快速查找,通常存储在内存或内存数据库中 ,同时在线用户较多时需要占用大量的服务器资源
  • 当需要扩展时,创建 Session 的服务器可能不是验证 Session 的服务器,所以还需要将所有 Session 单独存储并共享
  • 由于客户端使用 Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范 CSRF 攻击。

Token 认证模式

鉴于基于 Session 的会话管理方式存在上述多个缺点,基于 Token 的无状态会话管理方式 诞生了。所谓无状态,就是服务端可以不再存储信息,甚至是不再存储 Session,逻辑如下:

  1. 客户端使用用户名、密码进行认证
  2. 服务端验证用户名、密码正确后生成 Token 返回给客户端
  3. 客户端保存 Token,访问需要认证的接口时在 URL 参数或 HTTP Header 中加入 Token
  4. 服务端通过解码 Token 进行鉴权,鉴权通过后返回给客户端需要的数据

基于 Token 的会话管理方式有效解决了基于 Session 的会话管理存在的问题。

  • 服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到 Token 中,服务端只需要读取 Token 中包含的鉴权信息即可
  • 避免了共享 Session 导致的系统不易扩展问题
  • 不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
  • 使用 CORS 可以快速解决跨域问题

JWT 介绍

JWT 是 JSON Web Token 的缩写 ,是为了在网络应用环境间传递声明而流行的一种基于 JSON 的开放标准(RFC 7519)。JWT 本身没有定义任何技术实现,它只是定义了一种基于 Token 的会话管理的规则,涵盖 Token 需要包含的标准内容和 Token 的生成过程,特别适用于分布式站点的单点登录(SSO)场景。

一个 JWT Token 就像这样:

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjE5MjYxOTkwNTg1NTMxMTQ2MjQsIlVzZXJuYW1lIjoidTFtaSIsImlzcyI6Imxpd2uemhvdS5jb20iLCJzdWIiOiJkZW1vIiwiaXhwIjoxNzQ4MjEzNjEzfQ.NHFkKk2e87CYDqp3NmDEzI6UNAg5K6Tv3IuyDf8_jlw


JWT 的 Header 中存储了所使用的加密算法和 Token 类型。

json 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

Payload 表示负载,也是一个 JSON 对象,JWT 规定了 7 个官方字段供选用:

json 复制代码
{
  iss (issuer): 签发人
  exp (expiration time): 过期时间
  sub (subject): 主题
  aud (audience): 受众
  nbf (Not Before): 生效时间
  iat (Issued At): 签发时间
  jti (JWT ID): 编号
}

除了官方字段,开发者也可以自己指定字段和内容,例如下面的内容:

json 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意:
JWT 默认是不加密的,任何人都可以读取,所以不要把机密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名:

复制代码
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWT 优缺点

JWT 拥有基于 Token 的会话管理方式所拥有的一切优势,不依赖 Cookie ,使得其可以防止 CSRF 攻击,也能在禁用 Cookie 的浏览器环境中正常运行。

JWT 的最大优势是服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展,避免存储 Session 所需要引入的 Redis 等组件,降低了系统架构复杂度。

但这也是 JWT 最大的劣势,由于有效期存储在 Token 中,JWT Token 一旦签发,就会在有效期内一直可用,无法在服务端废止 。当用户进行登出操作,只能依赖客户端删除本地存储的 JWT Token。如果需要禁用用户,单纯使用 JWT 是无法做到的。(可以在前置增加一个黑名单过滤,在黑名单中的 token 直接拒绝)

基于 JWT 实现认证的实践

前面的 Token 都是 Access Token,也就是访问资源接口时所需要的 Token,还有另外一种 Token:Refresh Token,通常情况下,Refresh Token 的有效期较长,Access Token 的有效期较短,当 Access Token 因为过期而失效时,可以使用 Refresh Token 获取新的 Access Token;如果 Refresh Token 也失效了,用户就只能重新登录。

在 JWT 的实践中,引入 Refresh Token 后的会话管理流程如下:

  • 客户端使用用户名、密码进行认证
  • 服务端生成有效时间较短的 Access Token(例如 10 分钟)
    和有效时间较长的 Refresh Token(例如 7 天)
  • 客户端访问需要认证的接口时,携带 Access Token
  • 如果 Access Token 没有过期,服务端鉴权通过后返回给客户端需要的数据
  • 如果携带 Access Token 访问需要认证的接口时返回鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token
  • 如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token,客户端使用新的 Access Token 访问需要认证的接口

后端需要对外提供一个刷新 Token 的接口,前端需要实现一个当 Access Token 过期时自动请求刷新 Token 接口获取新 Access Token 的处理器。

Go 语言 Jwt 库

<github.com/golang-jwt/jwt/v5>

下载依赖

bash 复制代码
go get -u github.com/golang-jwt/jwt/v5

引入依赖

bash 复制代码
import "github.com/golang-jwt/jwt/v5"

创建 Token

go 复制代码
mySigningKey := []byte("AllYourBase")

type MyCustomClaims struct {
  Foo string `json:"foo"`
  jwt.RegisteredClaims
}

// Create claims with multiple fields populated
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(24 * time.Hour)),
    IssuedAt:  jwt.NewNumericDate(time.Now()),
    NotBefore: jwt.NewNumericDate(time.Now()),
    Issuer:    "test",
    Subject:   "somebody",
    ID:        "1",
    Audience:  []string{"somebody_else"},
  },
}

fmt.Printf("foo: %v\n", claims.Foo)

// Create claims while leaving out some of the optional fields
claims = MyCustomClaims{
  "bar",
  jwt.RegisteredClaims{
    // Also fixed dates can be used for the NumericDate
    ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
    Issuer:    "test",
  },
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Println(ss, err)

解析 Token

go 复制代码
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

type MyCustomClaims struct {
  Foo string `json:"foo"`
  jwt.RegisteredClaims
}

token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
  return []byte("AllYourBase"), nil
})

if err != nil {
  log.Fatal(err)
}

if claims, ok := token.Claims.(*MyCustomClaims); ok {
  fmt.Println(claims.Foo, claims.RegisteredClaims.Issuer)
} else {
  log.Fatal("unknown claims type, cannot proceed")
}

实践------签到服务登录返回JWT

定义常量

go 复制代码
package consts

const (
	JWTAccessTokenSecret  = "You're making my blood run beyond dimensions" // JWT 访问令牌密钥
	JWTRefreshTokenSecret = "You're making my blood run out of emissions"  // JWT 刷新令牌密钥
	JWTTokenExpireDay     = 24                                             // JWT 令牌过期时间(小时)
	JWTRefreshExpireWeek  = 7 * 24                                         // JWT 刷新令牌过期时间(小时)
)

定义model下的Token结构

go 复制代码
package model

import (
	"github.com/golang-jwt/jwt/v5"
)

// 定义 controller 层 与 service 层 之间交互的数据
// gf 框架推荐使用 input/output 结构体封装交互的数据

type CreateUserInput struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Email    string `json:"email"`
}

type CreateUserOutput struct {
	UserId   uint64 `json:"userId"`
	Username string `json:"username"`
}

type LoginInput struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// LoginOutput 用于登录响应,包含访问令牌和刷新令牌
type LoginOutput struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken"`
}

// TokenOutput 用于返回 token 信息,包含访问令牌和刷新令牌
type TokenOutput struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken"`
}

// JWTClaims 自定义声明结构体并嵌入 jwt.RegisteredClaims
type JWTClaims struct {
	UserId   uint64 `json:"userId"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

修改api接口文件

go 复制代码
package v1

import (
	"github.com/gogf/gf/v2/frame/g"
)

// CreateReq 创建用户请求结构体
type CreateReq struct {
	g.Meta          `path:"/users" method:"post" tags:"用户模块" sm:"创建用户"`
	Username        string `p:"username" v:"required|length:3,20" dc:"用户名"`
	Email           string `p:"email" v:"required|email" dc:"邮箱"`
	Password        string `p:"password" v:"required|length:6,20" dc:"密码"`
	ConfirmPassword string `p:"confirmPassword" v:"required|same:Password#确认密码必须传|两次密码需保持一致" dc:"确认密码"`
}

// CreateRes 创建用户返回结构体
type CreateRes struct {
	// mime:"application/json" 表示:这个接口的响应类型是 JSON
	g.Meta   `mime:"application/json"`
	UserId   uint64 `json:"userID" dc:"用户ID"`
	Username string `json:"username" dc:"用户名"`
}

type LoginReq struct {
	g.Meta   `path:"/auth/login" method:"post" tags:"用户模块" sm:"登录"`
	Username string `p:"username" v:"required|length:3,20" dc:"用户名"`
	Password string `p:"password" v:"required|length:6,20" dc:"密码"`
}

type LoginRes struct {
	AccessToken  string `json:"accessToken" dc:"访问令牌"`
	RefreshToken string `json:"refreshToken" dc:"刷新令牌"`
}

修改control层下的实现代码

go 复制代码
package userinfo

import (
	"context"

	// "github.com/gogf/gf/v2/errors/gcode"
	// "github.com/gogf/gf/v2/errors/gerror"

	v1 "backend/api/userinfo/v1"
	"backend/internal/model"

	"github.com/gogf/gf/errors/gerror"
)

func (c *ControllerV1) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) {
	input := &model.LoginInput{
		Username: req.Username,
		Password: req.Password,
	}

	output, err := c.svc.Login(ctx, input)
	if err != nil {
		return nil, gerror.New("用户名或密码错误")
	}

	// 登录成功,更新token
	return &v1.LoginRes{
		AccessToken:  output.AccessToken,
		RefreshToken: output.RefreshToken,
	}, err
	// return nil, gerror.NewCode(gcode.CodeNotImplemented)
}

完善impl下的userinfo.go登录功能

go 复制代码
package impl

import (
	"backend/internal/consts"
	"backend/internal/dao"
	"backend/internal/model"
	"backend/internal/model/entity"
	"backend/internal/service/userinfo"
	"backend/utility/injection"
	"context"
	"time"

	"github.com/gogf/gf/crypto/gmd5"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/golang-jwt/jwt/v5"
	"github.com/sony/sonyflake/v2"
)

// 用户相关业务逻辑
// 定义一个结构体, 实现UserInfoService接口
type UserInfo struct {
	snowflack *sonyflake.Sonyflake
}

// encryptPassword 加密密码
func (u *UserInfo) encryptPassword(password string) string {
	return gmd5.MustEncryptString(password)
}

// func New() *UserInfo {
// 	return &UserInfo{}
// }

func New() userinfo.UserInfoService {
	return &UserInfo{
		snowflack: injection.MustInvoke[*sonyflake.Sonyflake](),
	}
}

// 建议:在包级别初始化一次(比如 init() 或 main 启动时)
// var sf *sonyflake.Sonyflake

const (
	defaultAvatar = "https://avatars.githubusercontent.com/u/51045272?v=4"
)

// func init() {
// 	st, err := time.Parse(time.DateOnly, "2025-11-01") // 用过去时间
// 	if err != nil {
// 		panic(err) // 这里只在启动阶段 panic 可以接受
// 	}
// 	sf, err = sonyflake.New(sonyflake.Settings{StartTime: st})
// 	if err != nil {
// 		panic(err)
// 	}
// }

// Create 创建用户
// func (u *UserInfo) Create(ctx context.Context, username, password, email string) error {
func (u *UserInfo) Create(ctx context.Context, input *model.CreateUserInput) (*model.CreateUserOutput, error) {
	// 1. 判断用户名是否已经存在(根据用户名查重)
	exist, err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().Username, input.Username).
		Exist()
	if err != nil {
		g.Log().Errorf(ctx, "查询用户是否存在失败: %v", err)
		return nil, err
	}

	if exist {
		return nil, gerror.New("用户已存在")
	}

	// 2. 生成唯一 id
	userId, err := u.snowflack.NextID()
	if err != nil {
		g.Log().Errorf(ctx, "生成用户ID失败: %v", err)
		return nil, gerror.Wrap(err, "生成用户ID失败")
	}

	// 3. 创建用户
	// 创建用户,入库
	newUserInfo := entity.Userinfo{
		UserId:   uint64(userId), // 使用雪花算法生成唯一ID
		Username: input.Username,
		// Password: req.Password, // 是不是需要对用户输入的密码进行加密
		Password: u.encryptPassword(input.Password), // 对字符串 s 做 MD5, 返回 MD5 的十六进制字符串
		// 练习可用;生产换 bcrypt

		Email:  input.Email,
		Avatar: defaultAvatar, // 简化注册流程,一般使用默认头像,后续支持用户在个人中心上传头像
	}

	// id, err := dao.Userinfo.Ctx(ctx).InsertAndGetId(newUserInfo)

	_, err = dao.Userinfo.Ctx(ctx).Insert(newUserInfo)
	if err != nil {
		g.Log().Errorf(ctx, "创建用户失败:%v", err)
		return nil, gerror.Wrap(err, "创建用户失败")
	}
	// 4. 返回结果
	return &model.CreateUserOutput{
		UserId:   uint64(userId),
		Username: input.Username,
	}, nil
}

// Login 登录
func (u *UserInfo) Login(ctx context.Context, input *model.LoginInput) (*model.LoginOutput, error) {
	// 拿用户输入的用户名和密码,去数据库查询
	var user entity.Userinfo
	err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().Username, input.Username).
		Where(dao.Userinfo.Columns().Password, u.encryptPassword(input.Password)).
		Scan(&user)

	if err != nil {
		g.Log().Errorf(ctx, "查询用户失败: %v", err)
		return nil, gerror.Wrapf(err, "查询用户失败")
	}

	// 生成 JWT Token
	tokenObj, err := genJwtByUserInfo(ctx, user.UserId, user.Username)
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, gerror.Wrap(err, "生成 JWT Token 失败")
	}

	// 返回结果
	return &model.LoginOutput{
		AccessToken:  tokenObj.AccessToken,
		RefreshToken: tokenObj.RefreshToken,
	}, nil
}

// type JWTClaims struct {
// 	UserId   uint64 `json:"userId"`
// 	Username string `json:"username"`
// 	jwt.RegisteredClaims
// }

// genJwtByUserInfo 根据用户信息生成 JWT
func genJwtByUserInfo(ctx context.Context, userID uint64, username string) (*model.TokenOutput, error) {
	// 生成 Access Token
	claims := &model.JWTClaims{
		UserId:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "Simon",
			Subject:   "check-in-system",
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(consts.JWTTokenExpireDay * time.Hour)), // 设置过期时间为1天
		},
	}

	// 创建一个新的 JWT 对象
	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// SignedString:把 Header + Payload 按 JWT 规则 base64url 编码后,使用 secret 生成签名,最后拼成标准 JWT 字符串
	// []byte(...):HMAC 需要字节数组形式的 key
	signedAccessToken, err := accessToken.SignedString([]byte(consts.JWTAccessTokenSecret))
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, err
	}

	// 生成 refresh Token
	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &model.JWTClaims{
		UserId:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "Simon",
			Subject:   "check-in-system",
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(consts.JWTRefreshExpireWeek * time.Hour)), // 设置过期时间为1周
		},
	})

	signedRefreshToken, err := refreshToken.SignedString([]byte(consts.JWTRefreshTokenSecret))
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, err
	}

	return &model.TokenOutput{
		AccessToken:  signedAccessToken,
		RefreshToken: signedRefreshToken,
	}, nil
}

启动服务测试

bash 复制代码
{
    "username": "Jackson",
    "password": "1234567"
}
bash 复制代码
{
    "code": 0,
    "message": "OK",
    "data": {
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDg2NDM5Mzk4MjE4MTksInVzZXJuYW1lIjoiSmFja3NvbiIsImlzcyI6IlNpbW9uIiwic3ViIjoiY2hlY2staW4tc3lzdGVtIiwiZXhwIjoxNzY2NzU3NDE1fQ.XNaYyiJQSZwP7mHuIVxbUGh1A6opqi1n3SDtytWIitU",
        "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDg2NDM5Mzk4MjE4MTksInVzZXJuYW1lIjoiSmFja3NvbiIsImlzcyI6IlNpbW9uIiwic3ViIjoiY2hlY2staW4tc3lzdGVtIiwiZXhwIjoxNzY3Mjc1ODE1fQ.HsMgGrsmUsbFqsWF1Q6sZ9l12Kf_UQSM9LU1BmqKB8c"
    }
}
复制代码
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDg2NDM5Mzk4MjE4MTksInVzZXJuYW1lIjoiSmFja3NvbiIsImlzcyI6IlNpbW9uIiwic3ViIjoiY2hlY2staW4tc3lzdGVtIiwiZXhwIjoxNzY2NzYwMDk0fQ.tJwBpbK1ZJlO7k32bxQZ6sdk1rjg36cuuy0PbKUbuLU

因为写死了userid,所以没有逻辑处理header里面的Authorization,所以结果都是Jackson

复制代码
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDgzMTU4MTEwMzEyOTEsInVzZXJuYW1lIjoiRGllZ28iLCJpc3MiOiJTaW1vbiIsInN1YiI6ImNoZWNrLWluLXN5c3RlbSIsImV4cCI6MTc2Njc1OTg5MX0.8O1NEsCgHblw3IWEH9lYo4LfvYT8-imqWquqKEMlOvc

实践------获取用户信息

定义接口api文件

go 复制代码
package v1

import (
	"github.com/gogf/gf/v2/frame/g"
)

// CreateReq 创建用户请求结构体
type CreateReq struct {
	g.Meta          `path:"/users" method:"post" tags:"用户模块" sm:"创建用户"`
	Username        string `p:"username" v:"required|length:3,20" dc:"用户名"`
	Email           string `p:"email" v:"required|email" dc:"邮箱"`
	Password        string `p:"password" v:"required|length:6,20" dc:"密码"`
	ConfirmPassword string `p:"confirmPassword" v:"required|same:Password#确认密码必须传|两次密码需保持一致" dc:"确认密码"`
}

// CreateRes 创建用户返回结构体
type CreateRes struct {
	// mime:"application/json" 表示:这个接口的响应类型是 JSON
	g.Meta   `mime:"application/json"`
	UserId   uint64 `json:"userID" dc:"用户ID"`
	Username string `json:"username" dc:"用户名"`
}

type LoginReq struct {
	g.Meta   `path:"/auth/login" method:"post" tags:"用户模块" sm:"登录"`
	Username string `p:"username" v:"required|length:3,20" dc:"用户名"`
	Password string `p:"password" v:"required|length:6,20" dc:"密码"`
}

type LoginRes struct {
	AccessToken  string `json:"accessToken" dc:"访问令牌"`
	RefreshToken string `json:"refreshToken" dc:"刷新令牌"`
}

type MeReq struct {
	// sm即 summary	接口/参数概要描述
	g.Meta `path:"/users/me" method:"get" tags:"用户模块" sm:"获取当前登录的用户信息"`
}

type MeRes struct {
	Username string  `json:"username"`
	Email    string `json:"email"`
	Avatar   string `json:"avatar"`
}

g.Meta 里的sm

生成controller层实现代码

bash 复制代码
root@GoLang:~/proj/proj2/goframProj/backend# gf gen ctrl
generated: /root/proj/proj2/goframProj/backend/api/hello/hello.go
generated: /root/proj/proj2/goframProj/backend/api/userinfo/userinfo.go
generated: /root/proj/proj2/goframProj/backend/internal/controller/userinfo/userinfo_v1_me.go
done!

service下定义获取用户信息的方法

go 复制代码
package userinfo

import (
	// "backend/internal/dao"
	"backend/internal/model"
	"backend/internal/model/entity"

	// "backend/internal/model/entity"
	"context"
	// "time"
	// "github.com/gogf/gf/crypto/gmd5"
	// "github.com/gogf/gf/v2/errors/gerror"
	// "github.com/gogf/gf/v2/frame/g"
	// "github.com/sony/sonyflake/v2"
)

// 把用户服务抽象成一个接口, 列出来所需要实现的方法
type UserInfoService interface {
	Create(ctx context.Context, input *model.CreateUserInput) (*model.CreateUserOutput, error)
	Login(ctx context.Context, input *model.LoginInput) (*model.LoginOutput, error)
	GetInfo(ctx context.Context, userId string) (*entity.Userinfo, error)
}

在impl下定义真正实现方法

go 复制代码
package impl

import (
	"backend/internal/consts"
	"backend/internal/dao"
	"backend/internal/model"
	"backend/internal/model/entity"
	"backend/internal/service/userinfo"
	"backend/utility/injection"
	"context"
	"time"

	"github.com/gogf/gf/crypto/gmd5"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/golang-jwt/jwt/v5"
	"github.com/sony/sonyflake/v2"
)

// 用户相关业务逻辑
// 定义一个结构体, 实现UserInfoService接口
type UserInfo struct {
	snowflack *sonyflake.Sonyflake
}

// encryptPassword 加密密码
func (u *UserInfo) encryptPassword(password string) string {
	return gmd5.MustEncryptString(password)
}

// func New() *UserInfo {
// 	return &UserInfo{}
// }

func New() userinfo.UserInfoService {
	return &UserInfo{
		snowflack: injection.MustInvoke[*sonyflake.Sonyflake](),
	}
}

// 建议:在包级别初始化一次(比如 init() 或 main 启动时)
// var sf *sonyflake.Sonyflake

const (
	defaultAvatar = "https://avatars.githubusercontent.com/u/51045272?v=4"
)

// func init() {
// 	st, err := time.Parse(time.DateOnly, "2025-11-01") // 用过去时间
// 	if err != nil {
// 		panic(err) // 这里只在启动阶段 panic 可以接受
// 	}
// 	sf, err = sonyflake.New(sonyflake.Settings{StartTime: st})
// 	if err != nil {
// 		panic(err)
// 	}
// }

// Create 创建用户
// func (u *UserInfo) Create(ctx context.Context, username, password, email string) error {
func (u *UserInfo) Create(ctx context.Context, input *model.CreateUserInput) (*model.CreateUserOutput, error) {
	// 1. 判断用户名是否已经存在(根据用户名查重)
	exist, err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().Username, input.Username).
		Exist()
	if err != nil {
		g.Log().Errorf(ctx, "查询用户是否存在失败: %v", err)
		return nil, err
	}

	if exist {
		return nil, gerror.New("用户已存在")
	}

	// 2. 生成唯一 id
	userId, err := u.snowflack.NextID()
	if err != nil {
		g.Log().Errorf(ctx, "生成用户ID失败: %v", err)
		return nil, gerror.Wrap(err, "生成用户ID失败")
	}

	// 3. 创建用户
	// 创建用户,入库
	newUserInfo := entity.Userinfo{
		UserId:   uint64(userId), // 使用雪花算法生成唯一ID
		Username: input.Username,
		// Password: req.Password, // 是不是需要对用户输入的密码进行加密
		Password: u.encryptPassword(input.Password), // 对字符串 s 做 MD5, 返回 MD5 的十六进制字符串
		// 练习可用;生产换 bcrypt

		Email:  input.Email,
		Avatar: defaultAvatar, // 简化注册流程,一般使用默认头像,后续支持用户在个人中心上传头像
	}

	// id, err := dao.Userinfo.Ctx(ctx).InsertAndGetId(newUserInfo)

	_, err = dao.Userinfo.Ctx(ctx).Insert(newUserInfo)
	if err != nil {
		g.Log().Errorf(ctx, "创建用户失败:%v", err)
		return nil, gerror.Wrap(err, "创建用户失败")
	}
	// 4. 返回结果
	return &model.CreateUserOutput{
		UserId:   uint64(userId),
		Username: input.Username,
	}, nil
}

// Login 登录
func (u *UserInfo) Login(ctx context.Context, input *model.LoginInput) (*model.LoginOutput, error) {
	// 拿用户输入的用户名和密码,去数据库查询
	var user entity.Userinfo
	err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().Username, input.Username).
		Where(dao.Userinfo.Columns().Password, u.encryptPassword(input.Password)).
		Scan(&user)

	if err != nil {
		g.Log().Errorf(ctx, "查询用户失败: %v", err)
		return nil, gerror.Wrapf(err, "查询用户失败")
	}

	// 生成 JWT Token
	tokenObj, err := genJwtByUserInfo(ctx, user.UserId, user.Username)
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, gerror.Wrap(err, "生成 JWT Token 失败")
	}

	// 返回结果
	return &model.LoginOutput{
		AccessToken:  tokenObj.AccessToken,
		RefreshToken: tokenObj.RefreshToken,
	}, nil
}

// type JWTClaims struct {
// 	UserId   uint64 `json:"userId"`
// 	Username string `json:"username"`
// 	jwt.RegisteredClaims
// }

// genJwtByUserInfo 根据用户信息生成 JWT
func genJwtByUserInfo(ctx context.Context, userID uint64, username string) (*model.TokenOutput, error) {
	// 生成 Access Token
	claims := &model.JWTClaims{
		UserId:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "Simon",
			Subject:   "check-in-system",
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(consts.JWTTokenExpireDay * time.Hour)), // 设置过期时间为1天
		},
	}

	// 创建一个新的 JWT 对象
	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// SignedString:把 Header + Payload 按 JWT 规则 base64url 编码后,使用 secret 生成签名,最后拼成标准 JWT 字符串
	// []byte(...):HMAC 需要字节数组形式的 key
	signedAccessToken, err := accessToken.SignedString([]byte(consts.JWTAccessTokenSecret))
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, err
	}

	// 生成 refresh Token
	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &model.JWTClaims{
		UserId:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "Simon",
			Subject:   "check-in-system",
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(consts.JWTRefreshExpireWeek * time.Hour)), // 设置过期时间为1周
		},
	})

	signedRefreshToken, err := refreshToken.SignedString([]byte(consts.JWTRefreshTokenSecret))
	if err != nil {
		g.Log().Errorf(ctx, "生成 JWT Token 失败: %v", err)
		return nil, err
	}

	return &model.TokenOutput{
		AccessToken:  signedAccessToken,
		RefreshToken: signedRefreshToken,
	}, nil
}

func (u *UserInfo) GetInfo(ctx context.Context, userId string) (*entity.Userinfo, error) {
	var user entity.Userinfo
	err := dao.Userinfo.Ctx(ctx).
		Where(dao.Userinfo.Columns().UserId, userId).
		Scan(&user)

	if err != nil {
		g.Log().Errorf(ctx, "查询用户失败: %v", err)
		return nil, gerror.Wrapf(err, "查询用户失败")
	}
	return &user, nil
}

修改controller层实现代码(写死userid的情况)

go 复制代码
package userinfo

import (
	"context"

	"github.com/gogf/gf/v2/errors/gerror"

	v1 "backend/api/userinfo/v1"
)

func (c *ControllerV1) Me(ctx context.Context, req *v1.MeReq) (res *v1.MeRes, err error) {
	// 获取用户id
	// 根据用户id获取用户信息
	userInfo, err := c.svc.GetInfo(ctx, "7848643939821819")
	if err != nil {
		return nil, gerror.New("获取用户信息失败")
	}

	// 返回用户信息
	return &v1.MeRes{
		Username: userInfo.Username,
		Avatar:   userInfo.Avatar,
		Email:    userInfo.Email,
	}, err
}

测试启动服务


修改controller层实现代码(不再写死userid,而是从请求中获取用户id,即携带access token获取用户信息)

go 复制代码
package userinfo

import (
	"backend/internal/consts"
	"backend/internal/model"
	"context"
	"strconv"
	"strings"

	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/golang-jwt/jwt/v5"

	v1 "backend/api/userinfo/v1"
)

func (c *ControllerV1) Me(ctx context.Context, req *v1.MeReq) (res *v1.MeRes, err error) {
	// 从请求中获取用户id(根据Access token获取用户id)
	// 从请求头中获取 jwt access token
	r := g.RequestFromCtx(ctx) // 从 ctx 中获取请求对象

	// 从请求头中获取 Authorization 字段的值
	authorizationValue := r.GetHeader("Authorization")

	// HTTP 请求头里通常会带:Authorization: Bearer <access_token>
	// 检查 Authorization 头值是否以 Bearer 开头(Bearer 是 OAuth2.0 中用于表示携带 JWT 的标准方式)。
	if len(authorizationValue) == 0 || !strings.HasPrefix(authorizationValue, "Bearer ") {
		return nil, gerror.New("缺少授权信息")
	}

	accessToken := strings.TrimPrefix(authorizationValue, "Bearer ")

	// 解析token获取用户id
	var claim model.JWTClaims

	// 解析 JWT:将 JWT 字符串(即 accessToken)分解为 Header、Payload、Signature
	// 验证签名:通过密钥 consts.JWTAccessTokenSecret 来验证 Signature 是否正确,确保 Token 没有被篡改。
	// 填充 claim:将解析出来的 Payload 内容(例如用户的 UserId、Username)填充到 claim 变量中
	// keyFunc:这个是一个回调函数,用来返回签名验证所需的密钥。jwt.ParseWithClaims 会使用它来验证 JWT 的 Signature 是否有效
	token, err := jwt.ParseWithClaims(accessToken, &claim, func(token *jwt.Token) (any, error) {
		return []byte(consts.JWTAccessTokenSecret), nil
	})

	if err != nil || !token.Valid {
		return nil, gerror.New("无效的token")
	}

	g.Log().Debugf(ctx, "claim: %v", claim)

	// 根据用户id获取用户信息
	// FormatUint 用来将 uint64 类型的整数转换为字符串
	userInfo, err := c.svc.GetInfo(ctx, strconv.FormatUint(claim.UserId, 10))

	// userInfo, err := c.svc.GetInfo(ctx, "7848643939821819")

	if err != nil {
		return nil, gerror.New("获取用户信息失败")
	}

	// 返回用户信息
	return &v1.MeRes{
		Username: userInfo.Username,
		Avatar:   userInfo.Avatar,
		Email:    userInfo.Email,
	}, err
}

启动服务测试

复制代码
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDg2NDM5Mzk4MjE4MTksInVzZXJuYW1lIjoiSmFja3NvbiIsImlzcyI6IlNpbW9uIiwic3ViIjoiY2hlY2staW4tc3lzdGVtIiwiZXhwIjoxNzY2NzYwMDk0fQ.tJwBpbK1ZJlO7k32bxQZ6sdk1rjg36cuuy0PbKUbuLU
bash 复制代码
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjc4NDgzMTU4MTEwMzEyOTEsInVzZXJuYW1lIjoiRGllZ28iLCJpc3MiOiJTaW1vbiIsInN1YiI6ImNoZWNrLWluLXN5c3RlbSIsImV4cCI6MTc2Njc1OTg5MX0.8O1NEsCgHblw3IWEH9lYo4LfvYT8-imqWquqKEMlOvc

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
布谷歌5 分钟前
开发笔记:如何消除秘钥数据对RPC负荷、日志、系统安全的伤害?
网络·笔记·网络协议·rpc
heartbeat..6 分钟前
零基础学 SQL:DQL/DML/DDL/DCL 核心知识点汇总(附带连接云服务器数据库教程)
java·服务器·数据库·sql
咒法师无翅鱼7 分钟前
【西电计网学习笔记】网络层【RIP,OSPF,ARP,ICMP,IGMP,逻辑寻址(ABCD四类)】
网络
Ares-Wang7 分钟前
网络》》以太网交换安全
网络·安全
编程武士7 分钟前
Python 各版本主要变化速览
开发语言·python
hqwest9 分钟前
码上通QT实战29--系统设置04-用户操作管理
开发语言·qt·模态窗体·addbindvalue·bindvalue
那些年的笔记10 分钟前
Linux屏幕旋转方法
linux·运维·服务器
XiaoHu020711 分钟前
Linux网络编程套接字
linux·服务器·网络·git
专注于大数据技术栈29 分钟前
java学习--LinkedHashSet
java·开发语言·学习
这个图像胖嘟嘟31 分钟前
前端开发的基本运行环境配置
开发语言·javascript·vue.js·react.js·typescript·npm·node.js