上一期我们探讨了优化细节与网络请求封装的方法,本期继续推进——重点介绍目录结构更新和token系统的完善。在此之前,先分享一个有意思的小插曲。
对AI的小考验
在动手完善token系统之前,我特意给AI出了一道题:token作为管理员身份令牌,计划使用UUID v7生成值,那么入库时还有必要加密吗?
AI的回答很干脆:没必要。它搬出一套理论——UUID v7结构是48位时间戳加74位随机数,每秒能生成2^74个不重复值,暴力枚举128位UUID在计算上不现实。还列举了一堆“哈希的代价”、“真正该加哈希的场景”之类的说辞。
这有点不太对劲。加密最大的意义从来不是防暴力枚举,而是防拖库。token一旦泄露,黑客就能直接登录后台;但如果我们加了加密,黑客必须同时拿到程序源码和token密文才能渗透进来。怎么能省掉这道安全屏障?
所以最终拍板:token入库必须走SHA256加密。
目录结构更新
当前数据库初始化函数放在internal/database/database.go里。接下来要陆续加入token(多驱动设计)、captcha、upload(也是多驱动设计)等模块。按照原来的计划,目录结构会变成这样:
├── internal/
│ ├── handler/
│ ├── model/
│ ├── repository/
│ ├── router/
│ ├── database/
│ ├── captcha/
│ ├── upload/
│ ├── response/
│ ├── middleware/
│ └── service/
问题来了——database、token、captcha、upload这些模块偏向底层基础设施,跟handler、model这些业务层放在一起,确实有些违和。所以决定加一层infra目录,专门存放基础设施。调整后的结构如下:
├── internal/
│ ├── infra/
│ │ ├── token/
│ │ │ ├── driver
│ │ │ │ ├── database.go
│ │ │ │ └── redis.go
│ │ │ └─── token.go
│ │ ├── captcha/
│ │ └── upload/
│ ├── handler/
│ ├── model/
│ ├── repository/
│ ├── router/
│ ├── response/
│ ├── middleware/
│ └── service/
把database、token、captcha、upload这类底层模块移入infra。至于router和response——router是业务入口,response算是业务出口,都是可移可不移的类型,最终选择留在原地不动。
另外,配置解析逻辑(config/config.go文件,不是yaml配置文件)也跟着移入了infra目录,最终路径是internal/infra/config/config.go。这种操作在AI时代就是一句话的事儿,基本不会翻车,最多全项目搜一下/config确认就行。不过config/*.yaml文件不用动——运行时配置放在项目根目录的/config下,是社区标准的做法。
完善token系统
token系统的规划如下:
- 在
internal/model/common.go建立Token模型,字段包括token、type(字符串)、user_id、创建时间、过期时间,每个字段带中文注释 - 在
internal/infra/token/token.go建立token管理接口和结构体,采用多驱动模式。所有驱动放在internal/infra/token/driver目录下,一个驱动一个文件。目前只实现database一种驱动 - 增加
token.driver配置项,默认值database。token.go读取驱动配置并返回对应的驱动实例 - 驱动需要实现
Create、Get、Delete、Clear(删除指定会员指定类型的所有token)四个方法。管理器结构体额外实现Check方法——通过Get读取token信息后检查是否过期 - token入库前走SHA256加密
目前还没引入额外的全局秘钥(后续可能会考虑),也没用“SHA256索引+bcrypt校验”双字段方案。不过配合验权接口的节流逻辑,即便token的SHA256泄露了,抗爆破能力依然够用。
把这些规划交给Claude Code(cc),最终生成的核心代码如下:
# config/config.yaml 增加 token 驱动配置,目前只实现了 database 驱动
# 未来可以增加 redis 等驱动,得益于 AI 的帮助,加驱动基本上只需要一句话
token:
driver: database
// internal/model/common.go 文件,用于存放 captcha、area 等公共模型
// Token 令牌模型,用于存储各类用户令牌
type Token struct {
Token string `gorm:"comment:令牌;type:varchar(64);primaryKey" json:"-"`
Type string `gorm:"comment:令牌类型;type:varchar(32);not null" json:"type"`
UserID uint `gorm:"comment:用户ID;not null;index" json:"user_id"`
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
ExpiredAt time.Time `gorm:"comment:过期时间;not null;index" json:"expired_at"`
}
// internal/infra/token/token.go 文件,用于存放 token 管理接口和结构体
package token
import (
"context"
"crypto/sha256"
"fmt"
"sync"
"time"
"ai-go-mall/internal/infra/config"
"ai-go-mall/internal/infra/token/driver"
"ai-go-mall/internal/model"
)
// Driver 令牌存储驱动接口
type Driver interface {
Create(ctx context.Context, token *model.Token) error
Get(ctx context.Context, token string) (*model.Token, error)
Delete(ctx context.Context, token string) error
Clear(ctx context.Context, userID uint, tokenType string) error
}
// Manager 令牌管理器
type Manager struct {
driver Driver
}
// NewManager 创建令牌管理器
func NewManager(driver Driver) *Manager {
return &Manager{driver: driver}
}
// Create 创建令牌,入库前自动对 Token 做 SHA256
func (m *Manager) Create(ctx context.Context, token *model.Token) error {
token.Token = sha256Hex(token.Token)
return m.driver.Create(ctx, token)
}
// Get 获取令牌信息
func (m *Manager) Get(ctx context.Context, token string) (*model.Token, error) {
return m.driver.Get(ctx, sha256Hex(token))
}
// Check 检查令牌是否存在且未过期
func (m *Manager) Check(ctx context.Context, token string) bool {
t, err := m.Get(ctx, token)
if err != nil || t == nil {
return false
}
return time.Now().Before(t.ExpiredAt)
}
// Delete 删除令牌
func (m *Manager) Delete(ctx context.Context, token string) error {
return m.driver.Delete(ctx, sha256Hex(token))
}
// Clear 清除指定用户指定类型的所有令牌
func (m *Manager) Clear(ctx context.Context, userID uint, tokenType string) error {
return m.driver.Clear(ctx, userID, tokenType)
}
// sha256Hex 返回 raw 的 SHA256 十六进制字符串
func sha256Hex(raw string) string {
sum := sha256.Sum256([]byte(raw))
return fmt.Sprintf("%x", sum)
}
// ==================== 全局单例 ====================
var (
instance *Manager
once sync.Once
)
// Instance 返回全局令牌管理器实例,首次调用时根据配置自动初始化
func Instance() *Manager {
once.Do(func() {
instance = NewManager(newDriver(config.Get().Token.Driver))
})
return instance
}
// newDriver 根据配置创建存储驱动
func newDriver(name string) Driver {
switch name {
default:
return driver.NewDatabase()
}
}
// internal/infra/token/driver/database.go 文件,token 数据库驱动
package driver
import (
"context"
"errors"
"ai-go-mall/internal/infra/database"
"ai-go-mall/internal/model"
"gorm.io/gorm"
)
// Database 基于关系型数据库的令牌驱动
type Database struct{}
// NewDatabase 创建数据库令牌驱动
func NewDatabase() *Database {
return &Database{}
}
// Create 创建令牌
func (d *Database) Create(ctx context.Context, t *model.Token) error {
return gorm.G[model.Token](database.DB()).Create(ctx, t)
}
// Get 获取令牌信息
func (d *Database) Get(ctx context.Context, token string) (*model.Token, error) {
t, err := gorm.G[model.Token](database.DB()).
Where("token = ?", token).
First(ctx)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &t, nil
}
// Delete 删除令牌
func (d *Database) Delete(ctx context.Context, token string) error {
_, err := gorm.G[model.Token](database.DB()).Where("token = ?", token).Delete(ctx)
return err
}
// Clear 清除指定用户指定类型的所有令牌
func (d *Database) Clear(ctx context.Context, userID uint, tokenType string) error {
_, err := gorm.G[model.Token](database.DB()).
Where("user_id = ? AND type = ?", userID, tokenType).
Delete(ctx)
return err
}
一段小总结:目录结构本次调整幅度不算大,主要是为后续多个基础设施模块铺平了道路。token系统的核心逻辑已经跑通,尤其是SHA256加密入库和Check过期检查,这两个点是后续所有鉴权流程的基础。下一期会聊中间件整合和验权接口的联调,感兴趣的朋友可以先思考一下:token管理器已经配好了,验权中间件该怎么做才能既安全又不显得笨重?
