不安全;Go原生map非线程安全,并发读写会panic,须用sync.RWMutex或sync.Map保护,且密码必须哈希存储、注册需原子性校验、登录须用bcrypt.CompareHashAndPassword验证。
不安全,直接用 map 存用户在并发场景下会 panic:Go 的原生 map 非线程安全,多个 goroutine 同时读写会触发 fatal error: concurrent map read and map write。即使只读操作混着写,也会崩溃。
常见错误写法:
var users = make(map[string]*User)
// 在 HTTP handler 里直接 users[name] = &User{...} 或 users[name].Password = "xxx"
正确做法是加锁或换并发安全结构:
sync.RWMutex 包裹读写(推荐,轻量、可控)sync.Map(适合读多写少,但不支持遍历、类型擦除、无法直接存结构体指针)bcrypt.GenerateFromPassword
最小可用的注册登录结构体要覆盖验证、状态、安全三类需求,不是越全越好。字段过多会增加序列化/存储负担,也容易暴露敏感信息。
建议基础字段:
ID:int64 或 string(如 UUID),用于唯一标识,避免用用户名当主键(用户名可改)Username:string,唯一索引,校验长度(3–20)、字符范围(alphanum + underscore)PasswordHash:[]byte,永远不存明文,用 bcrypt 或 argon2 哈希Email:string,可选,用于找回密码,需校验格式和唯一性CreatedAt:time.Time,便于审计和清理僵尸账号IsActive:bool,支持禁用账号,比删库更安全不要放:SessionToken(应存在独立 session store)、RefreshToken(同理)、PlainPassword(任何阶段都不该存在内存中)。
注册流程本质是「检查 + 插入」两个原子操作,单纯靠 map 查再写,必然有竞态:两个请求同时查到用户名不存在,然后都写入,导致重复。
解决方式取决于你用什么后端:
sync.Mutex 锁住整个注册逻辑块,不是只锁 map 操作UNIQUE(username))+ 捕获 sql.ErrNoRows 或具体驱动的 dupli
23505)示例(内存版,带锁):
var mu sync.Mutex
var users = make(map[string]*User)
func Register(username, password string) error {
mu.Lock()
defer mu.Unlock()
if _, exists := users[username]; exists {
return errors.New("username already taken")
}
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
users[username] = &User{
Username: username,
PasswordHash: hash,
CreatedAt: time.Now(),
IsActive: true,
}
return nil
}
PasswordHash 是 bcrypt 生成的带 salt 的字符串(如 $2a$10$...),它本身不是可逆哈希值,不能用 == 比较。必须用 bcrypt.CompareHashAndPassword —— 它会自动提取 salt 并重算哈希。
典型错误:
PasswordHash 字符串做 == 对比(永远失败)bytes.Equal 比对 []byte(同样错,因为没做 salt-aware 校验)user != nil && user.IsActive,导致禁用账号也能登录正确验证片段:
func Login(username, password string) (*User, error) {
mu.RLock()
user, ok := users[username]
mu.RUnlock()
if !ok || !user.IsActive {
return nil, errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)); err != nil {
return nil, errors.New("invalid credentials")
}
return user, nil
}
注意:这里用了 RWMutex 的读锁,比全锁更高效;但若注册/登出等写操作频繁,仍需评估读写比例是否适合。
真正难的不是结构怎么搭,而是锁粒度怎么控、错误路径是否全覆盖、密码是否真没进日志——这些细节漏一个,上线就成漏洞。