加密与安全:让你的 Go 程序固若金汤
安全不是事后补救,而是从一开始就应该考虑的事情。
当你存储用户密码时,你有没有想过:如果数据库泄露了,用户的密码会不会被直接看到?当你传输敏感数据时,你有没有想过:中间人会不会窃听你的通信?当用户登录时,你有没有想过:怎么确保这个请求真的来自他本人?
这些问题都需要密码学来解决。好消息是,Go 的标准库提供了强大的 crypto 系列包,覆盖了从哈希到加密、从签名到 TLS 的各种场景。
今天我们就来学习 Go 的安全编程。
哈希函数
哈希函数把任意长度的数据映射为固定长度的"指纹"。好的哈希函数有以下特点:
- 确定性:相同输入总是产生相同输出
- 不可逆:无法从哈希值恢复原始数据
- 雪崩效应:输入的微小变化导致输出的巨大变化
- 抗碰撞:很难找到两个不同输入产生相同哈希值
MD5 和 SHA-1(不推荐)
package main
import (
"crypto/md5"
"crypto/sha1"
"fmt"
)
func main() {
data := []byte("Hello, World!")
// MD5(128位,已不安全)
md5Hash := md5.Sum(data)
fmt.Printf("MD5: %x\n", md5Hash)
// SHA-1(160位,已不安全)
sha1Hash := sha1.Sum(data)
fmt.Printf("SHA-1: %x\n", sha1Hash)
}
⚠️ 重要:MD5 和 SHA-1 已经被证明存在碰撞漏洞,不要用于安全敏感的场景。它们只能用于非安全用途,比如文件校验和。
SHA-256 和 SHA-512(推荐)
package main
import (
"crypto/sha256"
"crypto/sha512"
"fmt"
)
func main() {
data := []byte("Hello, World!")
// SHA-256(256位)
hash256 := sha256.Sum256(data)
fmt.Printf("SHA-256: %x\n", hash256)
// SHA-512(512位)
hash512 := sha512.Sum512(data)
fmt.Printf("SHA-512: %x\n", hash512)
}
文件完整性校验
package main
import (
"crypto/sha256"
"fmt"
"io"
"os"
)
func hashFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func main() {
hash, err := hashFile("example.txt")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("SHA-256:", hash)
}
密码存储:bcrypt
永远不要用 MD5 或 SHA-256 直接存储密码! 因为:
- 哈希速度太快,攻击者可以暴力破解
- 没有加盐,相同密码的哈希值一样
bcrypt 是专门为密码设计的哈希算法,它内置了盐值和计算成本参数:
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(password string) (string, error) {
// cost=10 表示 2^10 次迭代(默认值)
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func main() {
password := "mySuperSecretPassword123"
// 哈希密码
hash, err := hashPassword(password)
if err != nil {
fmt.Println("哈希失败:", err)
return
}
fmt.Println("哈希值:", hash)
// $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
// 验证密码
fmt.Println("正确密码:", checkPassword(password, hash)) // true
fmt.Println("错误密码:", checkPassword("wrongPassword", hash)) // false
// 即使同一个密码,每次哈希的结果都不同(因为盐值不同)
hash2, _ := hashPassword(password)
fmt.Println("哈希值2:", hash2) // 和第一次不同!
fmt.Println("验证仍然通过:", checkPassword(password, hash2)) // true
}
安装:
go get golang.org/x/crypto/bcrypt
对称加密:AES
对称加密使用同一个密钥来加密和解密数据。AES(Advanced Encryption Standard)是目前最广泛使用的对称加密算法。
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)
func encrypt(plaintext []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
// 使用 GCM 模式(认证加密)
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// 生成随机 nonce
nonce := make([]byte, aesGCM.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密(nonce 会附加在密文前面)
ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
return hex.EncodeToString(ciphertext), nil
}
func decrypt(ciphertextHex string, key []byte) ([]byte, error) {
ciphertext, err := hex.DecodeString(ciphertextHex)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := aesGCM.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("密文太短")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return aesGCM.Open(nil, nonce, ciphertext, nil)
}
func main() {
// 密钥必须是 16、24 或 32 字节(对应 AES-128、AES-192、AES-256)
key := []byte("my-32-byte-secret-key-1234567890")
plaintext := "这是一条秘密消息"
// 加密
encrypted, err := encrypt([]byte(plaintext), key)
if err != nil {
fmt.Println("加密失败:", err)
return
}
fmt.Println("密文:", encrypted)
// 解密
decrypted, err := decrypt(encrypted, key)
if err != nil {
fmt.Println("解密失败:", err)
return
}
fmt.Println("明文:", string(decrypted))
}
⚠️ 安全提示:
- 密钥不要硬编码在代码中
- 使用
crypto/rand生成随机密钥,不要用math/rand - 使用 GCM 模式(认证加密),不要用 ECB 模式
非对称加密:RSA
非对称加密使用一对密钥:公钥(加密)和私钥(解密)。
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"fmt"
)
func main() {
// 生成 RSA 密钥对
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Println("生成密钥失败:", err)
return
}
publicKey := &privateKey.PublicKey
// 加密
plaintext := []byte("这是用 RSA 加密的消息")
ciphertext, err := rsa.EncryptOAEP(
sha256.New(),
rand.Reader,
publicKey,
plaintext,
nil,
)
if err != nil {
fmt.Println("加密失败:", err)
return
}
fmt.Printf("密文: %x...\n", ciphertext[:20])
// 解密
decrypted, err := rsa.DecryptOAEP(
sha256.New(),
rand.Reader,
privateKey,
ciphertext,
nil,
)
if err != nil {
fmt.Println("解密失败:", err)
return
}
fmt.Println("明文:", string(decrypted))
}
数字签名
RSA 还可以用于数字签名——用私钥签名,用公钥验证:
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"fmt"
)
func sign(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
hash := sha256.Sum256(data)
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
}
func verify(data []byte, signature []byte, publicKey *rsa.PublicKey) bool {
hash := sha256.Sum256(data)
err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], signature)
return err == nil
}
func main() {
// 生成密钥对
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
data := []byte("这是一份重要的合同")
// 签名
signature, err := sign(data, privateKey)
if err != nil {
fmt.Println("签名失败:", err)
return
}
fmt.Printf("签名: %x...\n", signature[:20])
// 验证
fmt.Println("签名有效:", verify(data, signature, publicKey)) // true
// 篡改数据后验证
data[0] = 'X'
fmt.Println("篡改后:", verify(data, signature, publicKey)) // false
}
HMAC:消息认证码
HMAC(Hash-based Message Authentication Code)用于验证消息的完整性和来源:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func generateHMAC(message, key []byte) string {
h := hmac.New(sha256.New, key)
h.Write(message)
return hex.EncodeToString(h.Sum(nil))
}
func verifyHMAC(message, key []byte, expectedMAC string) bool {
actualMAC := generateHMAC(message, key)
return hmac.Equal([]byte(actualMAC), []byte(expectedMAC))
}
func main() {
key := []byte("my-secret-key")
message := []byte("转账 1000 元给张三")
// 生成 HMAC
mac := generateHMAC(message, key)
fmt.Println("HMAC:", mac)
// 验证
fmt.Println("验证:", verifyHMAC(message, key, mac)) // true
// 篡改消息
message[0] = 'X'
fmt.Println("篡改后:", verifyHMAC(message, key, mac)) // false
}
TLS/HTTPS
创建 HTTPS 服务器
package main
import (
"crypto/tls"
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, HTTPS!")
})
// 配置 TLS
server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12, // 最低 TLS 1.2
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
},
}
// 启动 HTTPS 服务器
err := server.ListenAndServeTLS("cert.pem", "key.pem")
if err != nil {
fmt.Println("启动失败:", err)
}
}
生成自签名证书
go run $(go env GOROOT)/src/crypto/tls/generate_cert.go --host=localhost
HTTPS 客户端
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
)
func main() {
// 跳过证书验证(仅开发环境使用!)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Get("https://localhost:443")
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
安全最佳实践
1. 使用 crypto/rand 生成随机数
// ❌ 不要用 math/rand(可预测)
import "math/rand"
token := rand.Int()
// ✅ 用 crypto/rand(安全的随机数)
import "crypto/rand"
token := make([]byte, 32)
rand.Read(token)
2. 防范 SQL 注入
// ❌ 字符串拼接(有 SQL 注入风险)
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)
// ✅ 使用参数化查询
db.Query("SELECT * FROM users WHERE name = ?", name)
3. 防范 XSS 攻击
// ❌ 直接输出用户输入
fmt.Fprintf(w, "<h1>%s</h1>", userInput)
// ✅ 使用 html/template 自动转义
import "html/template"
tmpl, _ := template.New("page").Parse("<h1>{{.}}</h1>")
tmpl.Execute(w, userInput)
4. 防范 CSRF 攻击
// 使用 CSRF token
func generateCSRFToken() string {
token := make([]byte, 32)
rand.Read(token)
return hex.EncodeToString(token)
}
// 在表单中包含 CSRF token
// <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
// 验证 CSRF token
func validateCSRF(r *http.Request) bool {
sessionToken := getSessionCSRFToken(r)
formToken := r.FormValue("csrf_token")
return hmac.Equal([]byte(sessionToken), []byte(formToken))
}
5. 设置安全的 HTTP 头
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
next.ServeHTTP(w, r)
})
}
实战:JWT 认证
JSON Web Token(JWT)是一种常用的认证方式:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
var jwtSecret = []byte("your-256-bit-secret")
type Claims struct {
UserID int `json:"user_id"`
Name string `json:"name"`
Exp int64 `json:"exp"`
}
func generateJWT(userID int, name string) (string, error) {
header := base64.RawURLEncoding.EncodeToString(
[]byte(`{"alg":"HS256","typ":"JWT"}`),
)
claims := Claims{
UserID: userID,
Name: name,
Exp: time.Now().Add(24 * time.Hour).Unix(),
}
claimsJSON, _ := json.Marshal(claims)
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
// 签名
mac := hmac.New(sha256.New, jwtSecret)
mac.Write([]byte(header + "." + payload))
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return header + "." + payload + "." + signature, nil
}
func verifyJWT(tokenString string) (*Claims, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("无效的 token 格式")
}
// 验证签名
mac := hmac.New(sha256.New, jwtSecret)
mac.Write([]byte(parts[0] + "." + parts[1]))
expectedSig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
return nil, fmt.Errorf("签名验证失败")
}
// 解析 claims
claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
var claims Claims
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
return nil, err
}
// 检查过期
if time.Now().Unix() > claims.Exp {
return nil, fmt.Errorf("token 已过期")
}
return &claims, nil
}
func main() {
// 生成 token
token, err := generateJWT(42, "张三")
if err != nil {
fmt.Println("生成失败:", err)
return
}
fmt.Println("JWT:", token)
// 验证 token
claims, err := verifyJWT(token)
if err != nil {
fmt.Println("验证失败:", err)
return
}
fmt.Printf("用户: ID=%d, Name=%s\n", claims.UserID, claims.Name)
}
小结
今天我们全面学习了 Go 的安全编程:
- 哈希函数:SHA-256/512、bcrypt 密码哈希
- 对称加密:AES-GCM 认证加密
- 非对称加密:RSA 加密和数字签名
- HMAC:消息认证码
- TLS/HTTPS:安全的网络通信
- 安全实践:随机数、SQL 注入、XSS、CSRF 防范
- JWT:基于 Token 的认证
安全是一个持续的话题。永远不要自己发明加密算法,使用经过验证的标准库和第三方库。
练习时间
- 密码管理器:实现一个密码哈希和验证的工具
- 文件加密:实现一个文件加密/解密工具(AES-GCM)
- API 签名验证:用 HMAC 实现 API 请求签名
- 安全中间件:实现包含 CSRF 保护和安全头的 HTTP 中间件
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。