使用 go 搭建用户认证系统

环境配置

  • WSL2 Ubuntu24.04
  • go 1.25.1
  • MySql

用户信息和密码的存储

作为一个最简化的系统,数据库里只储存用户名、加密过的密码和用户邮箱三个值。

+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| Id | int | NO | PRI | NULL | auto_increment |
| username | varchar(255) | NO | UNI | | |
| password | varchar(255) | NO | | | |
| email | varchar(255) | NO | | | |
+----------+--------------+------+-----+---------+----------------+

数据库里任何时候都绝对不能存储用户的密码明文,而是应该使用摘要算法进行加密处理和验证。

bcrypt 是一个设计用于密码存储的哈希函数,它使用盐(salt)和迭代来抵抗暴力破解和彩虹表攻击。在 Go 中,可以使用官方的扩展库 golang.org/x/crypto/bcrypt 来实现 bcrypt 功能。

password := []byte("password")
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
err := bcrypt.CompareHashAndPassword(storedHash, inputPassword)
if err != nil {
fmt.Println("密码验证失败:", err) // 如果不匹配,会返回错误
} else {
fmt.Println("密码验证成功!")
}

盐值嵌入了哈希字符串中,所以无需单独存储盐值,非常安全,可以避免彩虹表攻击。

登录、注册路由函数

// 注册路由函数
func (a *App) register(c *fiber.Ctx) error {
type RegisterInput struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"` // Corrected struct tag
}
var input RegisterInput
// 使用c.BodyParser解析body
// fiber.StatusBadRequest 代表 http 错误码 400
if err := c.BodyParser(&input); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "message": "Error on register request", "data": err})
}
var count int
// SQL查询
err := a.db.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", input.Username).Scan(&count)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "message": "Database error", "data": err})
}
if count > 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "message": "Username already exists", "data": nil})
}
// 对用户的密码进行加密
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
// Use the injected database connection: a.db
_, err = a.db.Exec("INSERT INTO users (username, password, email) VALUES (?, ?, ?)", input.Username, hashedPassword, input.Email)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "message": "Database error", "data": err})
}
return c.JSON(fiber.Map{"status": "success", "message": "Registration successful", "data": nil})
}
// 登录路由函数
func (a *App) login(c *fiber.Ctx) error {
type LoginInput struct {
Username string `json:"username"`
Password string `json:"password"`
}
var input LoginInput
if err := c.BodyParser(&input); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "error", "message": "Error on login request", "data": err})
}
var hashed_correct_password string
err := a.db.QueryRow("SELECT password FROM users WHERE username = ?", input.Username).Scan(&hashed_correct_password)
if err != nil {
if err == sql.ErrNoRows {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "error", "message": "User not found", "data": nil})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"status": "error", "message": "Database error", "data": err})
}
// 验证密码
err = bcrypt.CompareHashAndPassword([]byte(hashed_correct_password), []byte(input.Password))
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "error", "message": "Invalid password", "data": nil})
}
// 创建 JWT claims
claims := jwt.MapClaims{
"username": input.Username,
"exp": time.Now().Add(time.Hour * 72).Unix(), // 令牌有效期 72 小时
}

// 创建 JWT 令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// 签署令牌并获取完整的编码字符串
t, err := token.SignedString(jwtSecret)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}

return c.JSON(fiber.Map{"status": "success", "message": "Success login", "data": t})
}

JWT

JWT 全称是 JSON Web Token,经常用在 身份认证 和 信息交换 场景中。比如,用户登录成功后,服务端生成一个 JWT 返回给客户端,之后客户端发请求时带上这个 JWT,服务端就能验证用户身份。

一个典型的 JWT 由三个部分组成,每一部分用 “.” 分隔:

  • Header(头部)

    • 描述 JWT 的元数据,比如使用了什么算法来生成签名。
    • 一般是类似:{"alg": "HS256", "typ": "JWT"}
  • Payload(负载)

    • 存放实际传递的数据,比如用户 ID、签发时间、过期时间等等。
    • 注意:JWT 本身不会加密 Payload,只是编码(Base64URL),所以不能直接把敏感数据放在里面。
  • Signature(签名)

    • 用来确保消息没有被篡改。
      算法通常是:
      HMACSHA256(
      base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
      secret
      )
      密钥 secret 储存在服务器端并保密,用于验证 Token 的有效性。

curl -H "Authorization: Bearer <你的令牌>" http://localhost:3000/restricted

JWT 认证中间件

func jwtMiddleware(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "error", "message": "Missing or malformed JWT", "data": nil})
}

// 检查 "Bearer " 前缀
const bearerSchema = "Bearer "
if !strings.HasPrefix(authHeader, bearerSchema) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "error", "message": "Missing or malformed JWT", "data": nil})
}

// 提取令牌
tokenString := authHeader[len(bearerSchema):]

// 解析和验证令牌
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 验证签名方法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return jwtSecret, nil
})

if err != nil || !token.Valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"status": "error", "message": "Invalid or expired JWT", "data": nil})
}

// 将 claims 存储在 c.Locals 中,以便后续处理函数使用
claims := token.Claims.(jwt.MapClaims)
c.Locals("claims", claims)

return c.Next()
}