Cookies and Sessions
Finch 提供两种在 HTTP 请求之间持久化数据的机制:cookie(客户端)和 session(服务端)。理解两者的区别有助于你选择正确的工具:
- Cookie 存储在浏览器中,并随每个请求一起发送。它适合存储可以安全放在客户端的轻量级用户偏好(例如主题、语言)。
- Session 存储在服务器上,只有一个 session ID 会被发送到浏览器。它适合存储诸如当前登录用户之类的敏感数据。
两者都可以在任何控制器或路由处理程序中通过 rq 对象访问。
Cookies
Cookie 是存储在浏览器中的键值字符串对。Finch 从传入的 Cookie 请求头中读取它们,并通过 Set-Cookie 响应头写入它们。
Read a Cookie
使用 rq.getCookie(key, def: defaultValue) 读取一个 cookie。def 参数指定当该 cookie 不存在时应返回的值。
getCookie和addCookie的safe参数都默认为true。 除非你另行指定,否则 Finch 读取或写入的每一个 cookie 都会被透明地加密/解密。如果要存储或读取真正的明文值(可以在浏览器的开发者工具中查看),必须显式传入safe: false:
// 加密(默认)——使用 getCookie 读回时会自动解密
var token = rq.getCookie('auth_token', def: '');
// 明文——存储和返回的值与传入的完全一致
var theme = rq.getCookie('theme', def: 'light', safe: false);
Write a Cookie
使用 rq.addCookie(key, value) 设置一个 cookie。该 cookie 会被附加到响应中:
// 加密(默认)
rq.addCookie('auth_token', 'abc123');
// 明文
rq.addCookie('theme', 'dark', safe: false);
addCookie 还为底层的 Set-Cookie 响应头提供了更细粒度的选项:
rq.addCookie(
'session_pref',
'compact',
safe: false,
duration: Duration(days: 30), // 设置 Max-Age
path: '/', // 默认值为 '/'
domain: '.example.com',
secure: true, // 仅通过 HTTPS 发送
httpOnly: true, // 无法从 JavaScript 中读取
sameSite: SameSite.lax,
);
Remove a Cookie
要删除一个 cookie,Finch 会以一个已过期的 max-age 重新设置它:
rq.removeCookie('theme');
Complete Example
class CookieController extends Controller {
Future<String> showCookie() async {
var value = rq.getCookie('test', def: 'not set', safe: false);
rq.addParam('cookieValue', value);
return rq.renderView(path: 'example/cookie');
}
Future<String> addCookie() async {
var key = rq.get<String>('key', def: '');
var value = rq.get<String>('value', def: '');
var safe = rq.get<bool>('safe', def: false);
if (key.isNotEmpty) {
rq.addCookie(key, value, safe: safe);
}
return rq.redirect('/example/cookie');
}
}
Sessions
Session 由 Dart 内置的 HttpSession 管理。每个访客都会得到一个唯一的 session ID,存储在浏览器 cookie 中,而实际数据则保存在服务器的内存中。
重要提示: 默认的 Dart
HttpSession是基于内存的。服务器重启后 session 数据会丢失,并且在负载均衡器后面的多个服务器实例之间不会共享。若要在生产环境中大规模使用,请考虑使用持久化/共享的 session 存储,或者将关键数据存储在数据库中。
Read a Session Value
使用 rq.getSession(key, def: defaultValue) 读取一个值。如果该键不存在,它会返回 def。请注意其返回类型是 Object,而不是 String——如果需要特定类型,请自行进行类型转换:
var userEmail = rq.getSession('user', def: '') as String;
Write a Session Value
使用 rq.addSession(key, value) 在当前 session 中存储一个值。该值可以是任何 JSON 友好的类型(String、num、bool、Map、List),不仅限于字符串:
rq.addSession('user', '[email protected]');
rq.addSession('cart', {'items': 3, 'total': 42.5});
Remove a Session Value
rq.session.remove('user');
Complete Example
一个使用 session 的典型登录/登出/仪表盘流程:
class SessionController extends Controller {
Future<String> login() async {
var email = rq.get<String>('email', def: '');
var password = rq.get<String>('password', def: '');
if (isValidUser(email, password)) {
// 认证成功后,将用户的邮箱存储到 session 中
rq.addSession('user', email);
return rq.redirect('/dashboard');
}
return rq.renderError(401);
}
Future<String> logout() async {
// 从 session 中移除用户,使其登出
rq.session.remove('user');
return rq.redirect('/login');
}
Future<String> dashboard() async {
var email = rq.getSession('user', def: '') as String;
// 如果没有 session,说明用户尚未登录——重定向到登录页
if (email.isEmpty) return rq.redirect('/login');
rq.addParam('email', email);
return rq.renderView(path: 'pages/dashboard');
}
}
关于带权限和路由守卫的完整身份验证流程,请参阅 Auth Controller——它正是基于本文中展示的这些 getSession/addSession 调用构建的。
How Cookie Encryption Works
加密密钥是 FinchConfigs.cookiePassword:
FinchConfigs configs = FinchConfigs(
cookiePassword: env.get('COOKIE_SECRET', 'change-this-in-production'),
);
// 写入——以加密形式存储在浏览器中
rq.addCookie('prefs', '{"theme":"dark"}');
// 读回——Finch 会自动解密
var prefs = rq.getCookie('prefs', def: '{}');
由于加密是以 cookiePassword 为密钥的,更改该值会使访客浏览器中已存储的所有 cookie 失效——getCookie 将无法解密它们,并静默地回退到 def。
安全提示: 使用一个足够长且随机的
cookiePassword。将其存储为环境变量。切勿将其硬编码或提交到版本控制中。
Reading Cookies in Templates
你也可以直接在 Jinja 模板内部读取某个 cookie 的值:
{{ $e.getCookie('theme', 'light') }}
关于 $e 在模板中可用的其他所有内容,请参阅 Template。