You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Go/Node.js多用户命令行应用中安全持久化用户登录状态数据?

Great question—storing session state securely in a CLI app is trickier than in web apps since you don’t have a browser’s built-in secure storage APIs, but there are robust, tamper-resistant approaches for both Go and Node.js that address the security gap you’ve identified. The core idea is to avoid storing plaintext or unauthenticated data; instead, use encrypted storage or signed session tokens so tampering will be immediately detectable.

Go Implementation Approaches

Use Go's built-in crypto/aes package with GCM mode (which provides both encryption and tamper detection) to store session data in a local file. This ensures that even if the file is modified, decryption will fail, alerting your app to tampering.

First, define a session structure to hold user data and expiration:

package main

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

type SessionData struct {
    UserID   string `json:"user_id"`
    Username string `json:"username"`
    Expires  int64  `json:"expires"` // Unix timestamp
}

Then implement functions to save/load encrypted sessions:

// SaveSecureSession encrypts session data and writes it to a user-specific directory
func SaveSecureSession(session SessionData, encryptionKey []byte) error {
    // Serialize session data to JSON
    data, err := json.Marshal(session)
    if err != nil {
        return err
    }

    // Initialize AES-GCM cipher
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return err
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }

    // Generate a random nonce (required for GCM)
    nonce := make([]byte, gcm.NonceSize())
    if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
        return err
    }

    // Encrypt the data (includes authentication tag for tamper detection)
    ciphertext := gcm.Seal(nonce, nonce, data, nil)

    // Create a secure directory (only accessible by the current user)
    sessionDir := filepath.Join(os.Getenv("HOME"), ".yourapp")
    if err := os.MkdirAll(sessionDir, 0700); err != nil {
        return err
    }

    // Write encrypted data to file with restrictive permissions
    sessionPath := filepath.Join(sessionDir, "session.enc")
    return os.WriteFile(sessionPath, ciphertext, 0600)
}

// LoadSecureSession decrypts and validates stored session data
func LoadSecureSession(encryptionKey []byte) (*SessionData, error) {
    sessionPath := filepath.Join(os.Getenv("HOME"), ".yourapp", "session.enc")
    ciphertext, err := os.ReadFile(sessionPath)
    if err != nil {
        return nil, errors.New("no active session found")
    }

    // Initialize cipher and split nonce from ciphertext
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return nil, err
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
        return nil, errors.New("invalid session data (tampered)")
    }
    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

    // Decrypt and verify authenticity (fails if data was modified)
    data, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, errors.New("session data is tampered or invalid")
    }

    // Parse and validate session expiration
    var session SessionData
    if err := json.Unmarshal(data, &session); err != nil {
        return nil, err
    }
    if time.Now().Unix() > session.Expires {
        os.Remove(sessionPath) // Clean up expired session
        return nil, errors.New("session expired")
    }

    return &session, nil
}

2. System Keychain Integration

For even better security, store a short-lived session token (like a JWT) in the system keychain instead of a local file. The OS handles encryption and access control, so other users can't read the token. Use the keybase/go-keychain library for cross-platform support:

import "github.com/keybase/go-keychain"

func SaveSessionToKeychain(userID, token string) error {
    item := keychain.NewItem()
    item.SetSecClass(keychain.SecClassGenericPassword)
    item.SetService("YourAppName") // Unique identifier for your app
    item.SetAccount(userID)
    item.SetData([]byte(token))
    item.SetAccessible(keychain.AccessibleWhenUnlocked) // Restrict access to unlocked device
    return keychain.AddItem(item)
}

func GetSessionFromKeychain(userID string) (string, error) {
    query := keychain.NewItem()
    query.SetSecClass(keychain.SecClassGenericPassword)
    query.SetService("YourAppName")
    query.SetAccount(userID)
    query.SetMatchLimit(keychain.MatchLimitOne)
    query.SetReturnData(true)
    
    results, err := keychain.QueryItem(query)
    if err != nil || len(results) == 0 {
        return "", errors.New("no active session")
    }
    return string(results[0].Data), nil
}

Node.js Implementation Approaches

1. Encrypted Local Storage (Built-in crypto Module)

Use Node.js's native crypto module with AES-GCM to encrypt session data, similar to the Go approach:

const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');

class SessionManager {
    constructor(encryptionKey) {
        this.encryptionKey = encryptionKey; // Load from env or keychain
        this.sessionPath = path.join(process.env.HOME, '.yourapp', 'session.enc');
    }

    async saveSession(userID, username) {
        const sessionData = {
            user_id: userID,
            username: username,
            expires: Date.now() + 7 * 24 * 60 * 60 * 1000 // 1 week expiration
        };
        const data = JSON.stringify(sessionData);

        // Generate nonce and initialize AES-GCM cipher
        const nonce = crypto.randomBytes(12);
        const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, nonce);
        
        let encrypted = cipher.update(data, 'utf8', 'hex');
        encrypted += cipher.final('hex');
        const authTag = cipher.getAuthTag().toString('hex');

        // Create secure directory and write encrypted data
        await fs.mkdir(path.dirname(this.sessionPath), { recursive: true, mode: 0o700 });
        await fs.writeFile(this.sessionPath, `${encrypted}:${authTag}:${nonce.toString('hex')}`, { mode: 0o600 });
    }

    async loadSession() {
        try {
            const content = await fs.readFile(this.sessionPath, 'utf8');
            const [encrypted, authTagHex, nonceHex] = content.split(':');
            
            // Initialize decipher and verify auth tag
            const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, Buffer.from(nonceHex, 'hex'));
            decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));

            let decrypted = decipher.update(encrypted, 'hex', 'utf8');
            decrypted += decipher.final('utf8');
            const sessionData = JSON.parse(decrypted);

            // Check expiration
            if (Date.now() > sessionData.expires) {
                await fs.unlink(this.sessionPath);
                throw new Error('Session expired');
            }
            return sessionData;
        } catch (err) {
            if (err.code === 'ENOENT') {
                throw new Error('No active session found');
            }
            throw new Error('Session data is tampered or invalid');
        }
    }
}

2. System Keychain Integration

Use the keytar package to store session tokens in the system keychain. This avoids manual encryption and leverages OS-level security:

const keytar = require('keytar');

const SERVICE_NAME = 'YourAppName';

async function saveSessionToKeychain(username, token) {
    await keytar.setPassword(SERVICE_NAME, username, token);
}

async function getSessionFromKeychain(username) {
    const token = await keytar.getPassword(SERVICE_NAME, username);
    if (!token) {
        throw new Error('No active session found');
    }
    // Optional: Validate JWT token signature here
    return token;
}

General Best Practices

  • Restrict File Permissions: Always set session files to 0600 (read/write only for the current user) to prevent unauthorized access.
  • Use Short-Lived Sessions: Include an expiration time in your session data and clean up expired sessions automatically.
  • Avoid Storing Sensitive Data: Never store passwords or API keys in session data. Use short-lived tokens instead that can be invalidated server-side if needed.
  • Rotate Encryption Keys: If using encryption, periodically rotate your encryption key to minimize risk if it's ever compromised.

内容的提问来源于stack exchange,提问作者Nafees Nehar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 22:12:45