欢迎您访问新疆栾骏商贸有限公司,公司主营电子五金轴承产品批发业务!
全国咨询热线: 400-8878-609

新闻资讯

技术学院

Golang实现用户登录校验的基础逻辑

作者:P粉6029986702026-01-07 00:00:00
密码校验须用 bcrypt.CompareHashAndPassword 比对哈希值,禁用明文比较;登录接口需统一错误提示、限流防爆破、禁打明文密码日志;Session/JWT 要绑定 IP、UA、过期时间并安全签名;数据库操作必须参数化查询且处理 sql.ErrNoRows。

用户密码校验别直接比对明文

Go 里最常见错误是把前端传来的 password 和数据库存的明文(或简单 base64)直接用 == 比较。这既不安全,也不符合实际存储方式。真实场景中,密码必须经哈希(如 bcrypt)处理后存储,校验时要用专用比对函数。

  • 永远不要在代码里出现 user.Password == input.Password 这类逻辑
  • 使用 golang.org/x/crypto/bcryptbcrypt.CompareHashAndPassword,它自带时序攻击防护
  • 如果数据库里存的是 bcrypt 哈希值(形如 $2a$10$...),就直接传进去比;别自己解码或截断
  • 注意:CompareHashAndPassword 第一个参数是哈希值([]byte),第二个是原始密码([]byte),顺序反了会始终返回错误

登录接口要防暴力尝试和信息泄露

一个裸奔的 POST /login 接口,只要返回 {"error": "wrong password"} 就等于告诉攻击者“用户名存在”。更糟的是,没限流时,脚本可每秒试几百次。

  • 统一返回 {"error": "invalid credentials"},不区分“用户不存在”和“密码错误”
  • 对同一 IP 或同一 username 做请求频控,可用 golang.org/x/time/rate 或简单内存计数(上线前换 Redis)
  • 首次失败后引入指数退避:第二次失败延时 1s,第三次 2s,最多封禁 15 分钟(记在 Redis 里)
  • 别在日志里打印原始密码,哪怕只是 debug 级别——用 log.Printf("login failed for user %s", username)

Session 或 Token 生成要绑定关键上下文

只生成一个随机字符串当 session ID 或 JWT,却不绑定 IP、User-Agent、过期时间,等于给攻击者留后门。

  • 若用内存 session(如 gorilla/sessions),设置 Options.HttpOnly = trueOptions.Secure = true(HTTPS 环境下)
  • 若发 JWT,payload 至少包含:sub(用户 ID)、exp(短时效,如 30m)、ip(客户端真实 IP)、ua_hashsha256(UserAgent) 前 16 字节)
  • 签名密钥(SigningKey)绝不能硬编码在代码里,从环境变量或 secret manager 加载
  • 每次登录成功后,旧 token 必须失效——要么服务端维护黑名单(Redis key with TTL),要么在 JWT 中加入 jti 并检查是否被撤销

数据库查询必须防 SQL 注入和空指针 panic

database/sql 查用户时,拼接 "SELECT * FROM users WHERE username = '" + username + "'" 是高危操作;而查不到用户就直接取 rows[0] 会 panic。

  • 强制使用参数化查询:db.QueryRow("SELECT id, password_hash FROM users WHERE username = ?", username)
  • err := row.Scan(&id, &hash) 后,必须检查 err == sql.ErrNoRows,而不是忽略或直接 if err != nil 就返回 500
  • 字段名别写死:如果数据库里密码字段叫 pwd_hash,代码里就该用 pwd_hash,别想当然写成 password
  • username 做基础过滤:拒绝空格、控制字符、长度超 64 的输入,避免后续环节异常
func loginHandler(w http.ResponseWriter, r *http.Request) {
    var user struct {
        ID          int64  `json:"id"`
        PasswordHash []byte `json:"-"` // 不暴露
    }
    err := db.QueryRow("SELECT id, password_hash FROM users WHERE username = ?", r.FormValue("username")).Scan(&user.ID, &user.PasswordHash)
    if err == sql.ErrNoRows {
        http.Error(w, `{"error": "invalid credentials"}`, http.StatusUnauthorized)
        return
    }
    if err != nil {
        http.Error(w, `{"error": "server error"}`, http.StatusInternalServerError)
        return
    }
    if err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(r.FormValue("password"))); err != nil {
        http.Error(w, `{"error": "invalid credentials"}`, http.StatusUnauthorized)
        return
    }
    // 此处签发 token 或写 session...
}

真正难的不是写完登录,而是让每个分支都处理到边界情况:查无此人、哈希格式错、token 签发失败、客户端没带 Cookie、IP 被临时封禁……这些地方漏一个,就可能变成线上事故。