Golang中存储Postgres凭据的最佳实践

Golang中存储Postgres凭据的最佳实践 你好

我正在为一个服务编写一个命令行工具,我们需要直接与Postgres数据库通信。有什么方法可以将登录凭据存储到(客户端的)磁盘上吗?有没有一些第三方包之类的?

// Micke
3 回复

通常使用环境变量,例如:

DATABASE_URL=postgres://...

更多信息:

更多关于Golang中存储Postgres凭据的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


要么每次向命令行界面的用户询问凭据,要么确保其权限设置为600(或在访问控制列表下设置更严格的权限)。

在这里进行加密是相当没有意义的,因为任何加密要么需要在客户端上提供解密密钥,要么需要实现与服务器的交换机制,而针对该服务器又需要进行身份验证,这又需要公开可用的密钥或在每次访问时由用户提供……

或者,可以使用 HashiCorps Vault。(这仍然是一个基于服务器的解决方案,需要一些本地密钥,尽管在紧急情况下这些密钥很容易撤销。)

在Golang中存储Postgres凭据,推荐使用以下两种主要方式:

1. 使用操作系统密钥环(推荐)

macOS - 使用keychain

import (
    "github.com/zalando/go-keyring"
)

func storeCredentials() {
    service := "myapp"
    user := "db_user"
    password := "secret_password"
    
    err := keyring.Set(service, user, password)
    if err != nil {
        // 处理错误
    }
}

func getCredentials() (string, error) {
    service := "myapp"
    user := "db_user"
    
    password, err := keyring.Get(service, user)
    if err != nil {
        return "", err
    }
    return password, nil
}

Windows - 使用Credential Manager

import (
    "github.com/danieljoos/wincred"
)

func storeWindowsCreds() {
    cred := wincred.NewGenericCredential("myapp_postgres")
    cred.CredentialBlob = []byte("secret_password")
    cred.UserName = "db_user"
    cred.Persist = wincred.PersistLocalMachine
    err := cred.Write()
    if err != nil {
        // 处理错误
    }
}

2. 加密本地配置文件

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "io"
    "os"
    "path/filepath"
)

type Config struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Username string `json:"username"`
    Password string `json:"password"`
    Database string `json:"database"`
}

func encryptConfig(key []byte, config Config) error {
    configPath := filepath.Join(os.Getenv("HOME"), ".myapp", "config.enc")
    
    // 序列化配置
    data, err := json.Marshal(config)
    if err != nil {
        return err
    }
    
    // 创建AES cipher
    block, err := aes.NewCipher(key)
    if err != nil {
        return err
    }
    
    // 创建GCM
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }
    
    // 生成nonce
    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return err
    }
    
    // 加密数据
    encrypted := gcm.Seal(nonce, nonce, data, nil)
    
    // Base64编码存储
    encoded := base64.StdEncoding.EncodeToString(encrypted)
    
    // 确保目录存在
    os.MkdirAll(filepath.Dir(configPath), 0700)
    
    // 写入文件
    return os.WriteFile(configPath, []byte(encoded), 0600)
}

func decryptConfig(key []byte) (Config, error) {
    configPath := filepath.Join(os.Getenv("HOME"), ".myapp", "config.enc")
    var config Config
    
    // 读取文件
    encoded, err := os.ReadFile(configPath)
    if err != nil {
        return config, err
    }
    
    // Base64解码
    encrypted, err := base64.StdEncoding.DecodeString(string(encoded))
    if err != nil {
        return config, err
    }
    
    // 创建AES cipher
    block, err := aes.NewCipher(key)
    if err != nil {
        return config, err
    }
    
    // 创建GCM
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return config, err
    }
    
    // 提取nonce
    nonceSize := gcm.NonceSize()
    nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
    
    // 解密数据
    data, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return config, err
    }
    
    // 反序列化配置
    err = json.Unmarshal(data, &config)
    return config, err
}

3. 使用pgpass文件(PostgreSQL标准)

import (
    "bufio"
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

func readPgpassFile(host string, port int, database string, username string) (string, error) {
    pgpassPath := filepath.Join(os.Getenv("HOME"), ".pgpass")
    
    file, err := os.Open(pgpassPath)
    if err != nil {
        return "", err
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
            continue
        }
        
        parts := strings.Split(line, ":")
        if len(parts) != 5 {
            continue
        }
        
        lineHost := parts[0]
        linePort := parts[1]
        lineDatabase := parts[2]
        lineUsername := parts[3]
        password := parts[4]
        
        // 支持通配符匹配
        if (lineHost == "*" || lineHost == host) &&
           (linePort == "*" || linePort == fmt.Sprintf("%d", port)) &&
           (lineDatabase == "*" || lineDatabase == database) &&
           (lineUsername == "*" || lineUsername == username) {
            return password, nil
        }
    }
    
    return "", fmt.Errorf("no matching password found in pgpass file")
}

4. 集成示例

import (
    "database/sql"
    "fmt"
    _ "github.com/lib/pq"
    "github.com/zalando/go-keyring"
)

func getDBConnection() (*sql.DB, error) {
    // 从密钥环获取密码
    password, err := keyring.Get("myapp_postgres", "production")
    if err != nil {
        return nil, fmt.Errorf("failed to get credentials: %v", err)
    }
    
    connStr := fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        "localhost", 5432, "postgres", password, "mydb",
    )
    
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, err
    }
    
    return db, nil
}

这些方法提供了不同级别的安全性和便利性。操作系统密钥环提供了最高级别的安全性,而加密配置文件则提供了更多的灵活性。pgpass文件是PostgreSQL生态系统的标准方式,与其他PostgreSQL工具兼容。

回到顶部