一、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")
这种方案完美解决了单点登录问题,但要注意:
- 不要过度共享Cookie,仅限必要域名
- 敏感操作仍需二次验证
- 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攻击的第一道防线,但要注意:
- 旧版浏览器可能不支持
- None模式必须配合HTTPS
- 关键操作仍需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安全最佳实践总结
- 签名验证是基础:给所有重要Cookie加上HMAC签名,防止篡改
- 合理设置作用域:跨域共享要谨慎,仅限必要子域
- SameSite按需配置:平衡安全性和用户体验
- 安全标志不能少:HttpOnly、Secure是标配
- 生命周期要合理:会话Cookie设置合理过期时间
- 敏感操作双重验证:即使有安全Cookie,关键操作仍需二次确认
记住,Cookie安全是一个系统工程,需要:
- 定期更换加密密钥
- 监控异常Cookie使用
- 及时处理安全漏洞
- 保持Openresty版本更新
通过合理组合这些技术,你的Web应用将建立起坚固的Cookie安全防线。
评论