为什么Go中的认证不是儿戏?
\n\nGo——一种因速度和可靠性而被选用的语言。但即使是最快的服务器,如果密码能被窃取,也会变得毫无价值。Go应用中的安全认证不仅仅是“安装一个库就完事了”。这是一门完整的科学:如何存储密码,如何不搞砸JWT,如何在一切已经紧急时防御CSRF。
\n\n在本文中,我们不会进行理论讲座。我们将编写代码。真实、可运行的代码,可以直接插入到您的项目中。我们将剖析安全的三大支柱:密码哈希(bcrypt)、JWT令牌处理以及跨站请求伪造(CSRF)防护。
\n\n正确哈希密码:为什么md5是邪恶的
\n\n迈向安全的第一步——永远,听好了,永远不要以明文形式存储密码。即使您信任自己的数据库。黑客攻击可能发生在任何人身上(问问Yahoo或Adobe就知道了)。正确的哈希处理就是您的盾牌。
\n\n在Go中,标准选择是golang.org/x/crypto/bcrypt。为什么是bcrypt?它能抵抗暴力攻击(包含“盐”并减慢计算速度)。MD5或SHA-1可以在微秒内计算完成。Bcrypt则需要数百毫秒。对于攻击者来说这是一场灾难,对用户来说则毫无察觉。
示例:用户注册
\n\npackage main\n\nimport (\n "fmt"\n "golang.org/x/crypto/bcrypt"\n)\n\nfunc hashPassword(password string) (string, error) {\n bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n return string(bytes), err\n}\n\nfunc checkPasswordHash(password, hash string) bool {\n err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))\n return err == nil\n}\n\nfunc main() {\n password := "mySecretPass123"\n hash, _ := hashPassword(password)\n\n fmt.Println("密码:", password)\n fmt.Println("哈希值: ", hash)\n\n match := checkPasswordHash(password, hash)\n fmt.Println("匹配:", match)\n}\n\n请注意:bcrypt.DefaultCost 是10次迭代。对于生产环境,可以增加到12-14,但不要过度,否则服务器会在负载下崩溃。请在您的硬件上进行测试。
登录时密码验证
\n\n很简单:从表单获取密码,从数据库获取哈希值,通过bcrypt.CompareHashAndPassword进行比较。如果返回nil,则用户身份正确。如果出错,则返回401。
func loginHandler(w http.ResponseWriter, r *http.Request) {\n // 从表单获取密码\n storedHash := getUserHashFromDB(r.FormValue("email"))\n if !checkPasswordHash(r.FormValue("password"), storedHash) {\n http.Error(w, "密码错误", http.StatusUnauthorized)\n return\n }\n // 创建会话或令牌\n}\n\nJWT:带有陷阱的轻量级认证
\n\n用户输入密码后,需要发放一个“通行证”。一种流行的方法是JWT(JSON Web Token)。这是一个紧凑的令牌,包含用户数据和签名。但魔鬼在细节中:不正确的JWT实现可能会使您的应用变得脆弱。
\n\n在Go中正确生成JWT
\n\n使用库github.com/golang-jwt/jwt/v5。永远不要自己编写加密相关的轮子——只使用经过验证的库。
import (\n "time"\n "github.com/golang-jwt/jwt/v5"\n)\n\nvar jwtKey = []byte("my_secret_key_which_must_be_long_and_random")\n\ntype Claims struct {\n Username string `json:"username"`\n jwt.RegisteredClaims\n}\n\nfunc generateToken(username string) (string, error) {\n expirationTime := time.Now().Add(15 * time.Minute)\n claims := &Claims{\n Username: username,\n RegisteredClaims: jwt.RegisteredClaims{\n ExpiresAt: jwt.NewNumericDate(expirationTime),\n },\n }\n\n token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n return token.SignedS