欢迎继续我们的 Go 语言学习与项目实战系列。上一回,我们已经把网络请求的封装和诸多细节都处理到位了,今天直接进入正题:一是进一步优化项目目录结构,二是把 token 系统打磨得更加完善。
事情还要从一个小插曲说起。
先聊聊那个关于 Token 的小插曲
在动手完善 token 系统之前,我特意给 AI 出了个“考题”。问题是:管理员身份令牌的值,我打算用 UUID v7 来生成,那么入库的时候,还有必要再加密一次吗?
结果你猜AI怎么答的?它很干脆地告诉我“不需要”。理由是 UUID v7 本身的结构(48位时间戳 + 74位随机数)已经足够安全,想要暴力枚举一个128位的UUID,在计算上基本是不可行的。它甚至还给我列了一堆关于“哈希的代价”和“真正需要加哈希的场景”的分析。
看完这个答案,我当时就愣住了。这不扯淡吗?
安全最大的敌人是谁?是“拖库”。数据库一旦泄露,黑客拿到的是明文 token,那人家不就直接可以登录后台了?但如果你对 token 做了哈希加密,那么黑客即便拿到了数据库,也得再费劲去破解你的加密,甚至还得同时拿到你的程序源码,才能模拟出一个有效的 token 来发起攻击。这相当于加了一把安全锁,怎么能说不加密呢?
所以,这事坚定了我的判断:在安全这件事上,不能完全迷信 AI 的结论,必须有自己的坚持。最终我们的 token 入库,必须用 SHA256 加密。
目录结构的调整:该分家时就分家
好了,说回正题。先来调整一下项目结构。之前我们的数据库初始化函数放在 internal/database/database.go 里。但接下来,token、captcha、upload 这些模块都会陆续加进来。如果继续按原来的规划,目录会变得很杂:
├── internal/
这个方案最大的问题是,像 database、captcha、upload 这些东西,非常偏底层,属于基础设施。而 handler、model 这些东西,是业务层。硬把它们塞在一起,总觉得不太对劲。
所以,我决定再加一层,叫做 infra,专门用来存放这些基础设施。调整后的结构看起来就清爽多了:
├── internal/│ ├── infra/
│ │ ├── token/
│ │ │ ├── driver/
│ │ │ │ ├── database.go
│ │ │ │ └── redis.go
│ │ │ └── token.go
│ │ ├── captcha/
│ │ └── upload/
│ ├── handler/
│ ├── model/
│ ├── repository/
│ ├── router/
│ ├── response/
│ ├── middleware/
│ └── service/
这里稍微解释一下:router 和 response 我没有移进去。原因很简单,router 是业务的入口,跟业务层放一起天经地义;response 可以看作是业务的出口,也有点类似于中间件,可移可不移,我选择不移。
另外,之前项目的配置解析逻辑(config/config.go 文件,注意不是 yaml 配置文件)本身也属于基础设施的一部分,最理想的归宿自然就是 internal/infra/config/config.go。既然有了 infra 目录,顺手就把它迁过去了。在 AI 时代,这种需求真的只需要一句话,让Claude Code(简称 cc)帮你搞定就行,基本不会出问题。
对了,友情提示一下:config/*.yaml 这些运行时配置文件不用动,放在项目根目录的 /config 下,这是符合社区习惯的做法,别挪错了。
Token 系统的正式搭建
结构搭好了,接下来就是重头戏——完善 token 系统。我的规划如下:
- 模型定义:在
internal/model/common.go里定义一个 Token 模型,包含 token值、类型(字符串)、用户ID、创建时间、过期时间这些字段,所有字段都要带中文注释。 - 多驱动架构:在
internal/infra/token/token.go里建立一个 token 管理接口和结构体。所有具体驱动放在internal/infra/token/driver/目录下,一个驱动一个文件。目前只实现 database 驱动,未来可以无缝扩展 Redis 等。 - 动态加载机制:增加一个
token.driver配置项,默认值是database。管理器根据配置自动加载对应的驱动实例。 - 接口方法:每个驱动需要实现
Create、Get、Delete、Clear(清除指定用户指定类型的所有 token)这四个方法。管理器则额外暴露一个Check方法,用于检查 token 是否存在且未过期。 - 强制加密:入库的 token 值必须使用 SHA256 加密。
目前这个版本,还没有考虑加全局秘钥,也没搞“SHA256索引 + bcrypt校验”这种双保险。但配合后端的接口节流,即便 token 的 SHA256 值泄露了,想要暴力破解也是相当困难的。先把底子打好,未来再迭代优化。
把上面这些规划丢给 cc 之后,它生成的代码基本上就是我要的样子。我们来看看核心代码:
首先是配置文件增加驱动选项:
# config/config.yamltoken:
driver: database
然后是模型定义:
// internal/model/common.go// 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.gopackage token
// ... 导入必要的包
// Driver 令牌存储驱动接口
type Driver interface { ... }
// Manager 令牌管理器,封装了加解密逻辑和 Check 方法
type Manager struct {
driver Driver
}
// NewManager 创建令牌管理器
func NewManager(driver Driver) *Manager { ... }
// Create 创建令牌,入库前自动对 Token 做 SHA256
func (m *Manager) Create(ctx context.Context, token *model.Token) error { ... }
// Get 获取令牌信息,返回时自动解密? 不,我们只存密文,拿密文去查
func (m *Manager) Get(ctx context.Context, token string) (*model.Token, error) { ... }
// Check 检查令牌是否存在且未过期
func (m *Manager) Check(ctx context.Context, token string) bool { ... }
// ... 其他方法
// 全局单例
func Instance() *Manager { ... }
// newDriver 根据配置创建存储驱动
func newDriver(name string) Driver { ... }
最后是数据库驱动的具体实现:
// internal/infra/token/driver/database.gopackage driver
// Database 基于关系型数据库的令牌驱动
type Database struct{}
// NewDatabase 创建数据库令牌驱动
func NewDatabase() *Database { ... }
// Create / Get / Delete / Clear 的具体实现
// 其中 Get 方法如果查不到记录,返回 nil, nil,方便上层判断
// Clear 方法会删除指定用户所有指定类型的 token
// ...
代码的具体实现细节,大家可以对照源码来看,这里就不逐行解释了。重要的是这套“多驱动 + 强制加密 + 灵活校验”的设计思路,可以很好地应对未来的变化。
今天的任务到这就算搞定了。目录结构更清晰了,token 系统也支棱起来了,而且我们用实际行动给 AI上了一课:安全,不能只靠理论上的不可行性,还得靠实践上的加固。
下一期,我们该聊点什么?评论区见。

