一、Cookie安全签名:防止数据篡改的小妙招

在Web开发中,Cookie就像快递员手里的包裹,谁都能看到。为了防止坏人中途调包,我们需要给Cookie加上"防伪标签"。Openresty提供了hmac_sha1等加密算法,可以轻松实现这个功能。

下面是一个完整的Cookie签名实现示例(技术栈:Openresty+Lua):

-- 生成带签名的Cookie
local function set_signed_cookie(name, value, secret_key)
    -- 使用HMAC_SHA1生成签名
    local signature = ngx.encode_base64(ngx.hmac_sha1(secret_key, value))
    -- 替换base64中的特殊字符使其适合Cookie传输
    signature = string.gsub(signature, "[+/=]", {
        ["+"] = "-",
        ["/"] = "_",
        ["="] = ""
    })
    -- 设置格式为"值.签名"的Cookie
    ngx.header["Set-Cookie"] = name.."="..value.."."..signature.."; Path=/"
end

-- 验证签名的Cookie
local function verify_signed_cookie(name, secret_key)
    local cookie = ngx.var["cookie_"..name]
    if not cookie then return nil end
    
    -- 分离值和签名
    local value, signature = string.match(cookie, "^(.*)%.(.*)$")
    if not value or not signature then return nil end
    
    -- 恢复签名中的特殊字符
    signature = string.gsub(signature, "[-_]", {
        ["-"] = "+",
        ["_"] = "/"
    })
    -- 补全base64的等号
    signature = signature .. string.rep("=", 4 - (string.len(signature) % 4))
    
    -- 重新计算签名并验证
    local expected_signature = ngx.encode_base64(ngx.hmac_sha1(secret_key, value))
    expected_signature = string.gsub(expected_signature, "[+/=]", {
        ["+"] = "-",
        ["/"] = "_",
        ["="] = ""
    })
    
    if signature == expected_signature then
        return value
    else
        return nil
    end
end

-- 使用示例
local secret_key = "my_super_secret_key_123!"
set_signed_cookie("user_token", "user123_session456", secret_key)
local verified_value = verify_signed_cookie("user_token", secret_key)

这个方案特别适合存储会话令牌、用户ID等敏感信息。即使黑客截获了Cookie,没有密钥也无法伪造有效签名。但要注意定期更换密钥,就像定期更换门锁一样重要。

二、跨域共享Cookie:解决单点登录难题

现代应用经常需要跨多个子域共享登录状态,比如主站(example.com)和后台(admin.example.com)。通过简单设置,Openresty可以让Cookie在不同子域间安全共享。

下面是实现跨域Cookie的完整示例(技术栈:Openresty+Lua):

-- 设置跨子域共享的Cookie
local function set_cross_domain_cookie(name, value, domain, max_age)
    -- 设置Domain属性为顶级域名
    local cookie_str = string.format("%s=%s; Domain=%s; Path=/", name, value, domain)
    
    -- 设置有效期(秒)
    if max_age then
        cookie_str = cookie_str .. "; Max-Age=" .. max_age
    end
    
    -- 安全相关设置
    cookie_str = cookie_str .. "; HttpOnly; Secure"
    
    -- 添加到响应头
    ngx.header["Set-Cookie"] = cookie_str
end

-- 使用示例
set_cross_domain_cookie(
    "shared_token", 
    "abcdef123456", 
    ".example.com",  -- 注意前面的点表示所有子域
    3600 * 24 * 7    -- 7天有效期
)

实际项目中,我们经常结合签名和跨域功能:

-- 安全的跨域Cookie设置
local secret_key = "cross_domain_secret_321!"
local user_data = "user_id=123|expire="..os.time()+604800  -- 7天后过期

-- 先加密数据
local encrypted = ngx.encode_base64(ngx.hmac_sha1(secret_key, user_data))
encrypted = string.gsub(encrypted, "[+/=]", {
    ["+"] = "-",
    ["/"] = "_",
    ["="] = ""
})

-- 再设置跨域Cookie
set_cross_domain_cookie("app_auth", user_data.."."..encrypted, ".example.com")

这种方案完美解决了单点登录问题,但要注意:

  1. 不要过度共享Cookie,仅限必要域名
  2. 敏感操作仍需二次验证
  3. HTTPS是必须的,否则Secure属性无效

三、SameSite属性:防御CSRF攻击的利器

SameSite是Cookie的新属性,能有效防止CSRF攻击。它有三个模式:

  • Strict:最严格,完全禁止跨站携带
  • Lax:宽松模式,允许部分安全请求
  • None:关闭SameSite保护(需要配合Secure)

Openresty中设置SameSite非常简单:

-- 设置SameSite属性的Cookie
local function set_samesite_cookie(name, value, mode)
    -- 参数检查
    if mode ~= "Strict" and mode ~= "Lax" and mode ~= "None" then
        mode = "Lax"  -- 默认使用Lax模式
    end
    
    local cookie_str = string.format("%s=%s; Path=/; SameSite=%s", name, value, mode)
    
    -- None模式必须配合Secure
    if mode == "None" then
        cookie_str = cookie_str .. "; Secure"
    end
    
    ngx.header["Set-Cookie"] = cookie_str
end

-- 使用示例
set_samesite_cookie("csrf_token", "random_value_987", "Strict")

实际业务中,我们通常这样组合使用:

-- 登录Cookie使用Lax模式,允许从邮件链接跳转
set_samesite_cookie("session_id", "sess_123456", "Lax")

-- 敏感操作使用Strict模式
set_samesite_cookie("admin_token", "adm_654321", "Strict")

-- 需要嵌入到iframe的Cookie使用None模式
set_samesite_cookie("embed_auth", "emb_987654", "None")

SameSite是现代浏览器防御CSRF攻击的第一道防线,但要注意:

  1. 旧版浏览器可能不支持
  2. None模式必须配合HTTPS
  3. 关键操作仍需CSRF Token双重保护

四、实战案例:电商平台的Cookie安全方案

让我们看一个电商平台的完整实现(技术栈:Openresty+Lua):

-- 电商平台Cookie管理中心
local cookie_util = {}

-- 配置参数
cookie_util.config = {
    secret_key = "ecommerce_secret_2023!",
    domain = ".myshop.com",
    session_expire = 3600 * 24 * 30,  -- 30天会话
    csrf_expire = 3600 * 2            -- 2小时CSRF令牌
}

-- 生成随机字符串
local function random_string(length)
    local charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    local result = ""
    for i = 1, length do
        local pos = math.random(1, #charset)
        result = result .. string.sub(charset, pos, pos)
    end
    return result
end

-- 设置用户会话Cookie
function cookie_util.set_user_session(user_id)
    -- 生成会话ID和随机数
    local session_id = "usr_" .. user_id .. "_" .. random_string(16)
    local nonce = os.time() .. "_" .. random_string(8)
    
    -- 组合数据
    local session_data = session_id .. "|" .. nonce
    
    -- 生成签名
    local signature = ngx.encode_base64(ngx.hmac_sha1(
        cookie_util.config.secret_key, 
        session_data
    ))
    signature = string.gsub(signature, "[+/=]", {
        ["+"] = "-",
        ["/"] = "_",
        ["="] = ""
    })
    
    -- 设置跨域安全Cookie
    local cookie_value = session_data .. "." .. signature
    ngx.header["Set-Cookie"] = string.format(
        "user_session=%s; Domain=%s; Path=/; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
        cookie_value,
        cookie_util.config.domain,
        cookie_util.config.session_expire
    )
end

-- 设置CSRF防护Cookie
function cookie_util.set_csrf_token()
    local token = "csrf_" .. random_string(32)
    ngx.header["Set-Cookie"] = string.format(
        "csrf_token=%s; Path=/; Max-Age=%d; HttpOnly; Secure; SameSite=Strict",
        token,
        cookie_util.config.csrf_expire
    )
    return token
end

-- 验证用户会话
function cookie_util.verify_user_session()
    local cookie = ngx.var.cookie_user_session
    if not cookie then return nil end
    
    -- 分离数据和签名
    local data, signature = string.match(cookie, "^(.*)%.(.*)$")
    if not data or not signature then return nil end
    
    -- 验证签名
    local expected_sig = ngx.encode_base64(ngx.hmac_sha1(
        cookie_util.config.secret_key, 
        data
    ))
    expected_sig = string.gsub(expected_sig, "[+/=]", {
        ["+"] = "-",
        ["/"] = "_",
        ["="] = ""
    })
    
    if signature ~= expected_sig then return nil end
    
    -- 解析会话数据
    local session_id, nonce = string.match(data, "^(.*)|(.*)$")
    if not session_id or not nonce then return nil end
    
    -- 返回用户ID
    return string.match(session_id, "^usr_(%d+)_")
end

return cookie_util

使用这个工具类:

local cookie = require "cookie_util"

-- 用户登录成功后
cookie.set_user_session(12345)
local csrf_token = cookie.set_csrf_token()

-- 验证请求时
local user_id = cookie.verify_user_session()
if not user_id then
    ngx.status = ngx.HTTP_UNAUTHORIZED
    ngx.say("请先登录")
    return
end

五、Cookie安全最佳实践总结

  1. 签名验证是基础:给所有重要Cookie加上HMAC签名,防止篡改
  2. 合理设置作用域:跨域共享要谨慎,仅限必要子域
  3. SameSite按需配置:平衡安全性和用户体验
  4. 安全标志不能少:HttpOnly、Secure是标配
  5. 生命周期要合理:会话Cookie设置合理过期时间
  6. 敏感操作双重验证:即使有安全Cookie,关键操作仍需二次确认

记住,Cookie安全是一个系统工程,需要:

  • 定期更换加密密钥
  • 监控异常Cookie使用
  • 及时处理安全漏洞
  • 保持Openresty版本更新

通过合理组合这些技术,你的Web应用将建立起坚固的Cookie安全防线。