登录页的“记住密码”和“保持登录”是两件事:密码可以交给浏览器或密码管理器保存,登录状态由服务端会话管理。不要把可解密的密码写入 Cookie 或 localStorage;如果私钥也随前端代码发布,拿到代码的人就能解密。

让浏览器填写密码

使用标准的 name 和 autocomplete,让浏览器识别登录表单。是否保存、自动填充,仍由用户和浏览器设置决定。参见 MDN 密码输入框。

<form id="login-form">
<label for="username">账号</label>
<input id="username" name="username" autocomplete="username" required>
<label for="password">密码</label>
<input id="password" name="password" type="password"
autocomplete="current-password" required>
<label><input name="rememberMe" type="checkbox">保持登录</label>
<button type="submit">登录</button>
<p id="login-status" role="status"></p>
</form>

提交表单

下面约定同源的 /api/login 接收 JSON,成功返回 2xx,并由服务端设置会话 Cookie。接口需要在后端实现;密码放在请求体中,经 HTTPS 传输,不拼进 URL。

const form = document.querySelector('#login-form');
const status = document.querySelector('#login-status');
const button = form.querySelector('button');
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button.disabled || !form.reportValidity()) return;
button.disabled = true;
status.textContent = '';
const fields = new FormData(form);
try {
const response = await fetch('/api/login', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: fields.get('username'),
password: fields.get('password'),
rememberMe: fields.get('rememberMe') === 'on'
})
});
if (!response.ok) throw new Error('登录失败,请检查账号密码后重试');
form.elements.namedItem('password').value = '';
window.location.assign('/');
} catch (error) {
status.textContent = error.message || '请求失败,请稍后重试';
} finally {
button.disabled = false;
}
});

服务端保存登录状态

sequenceDiagram
participant U as 浏览器
participant S as 服务端
participant D as 会话存储
U->>S: HTTPS 提交账号、密码和保持登录选项
S->>S: 校验密码
S->>D: 创建可过期、可撤销的会话
S-->>U: Set-Cookie 会话标识
U->>S: 后续请求自动携带 Cookie
S->>D: 校验会话及权限
S-->>U: 返回业务结果

Cookie 中只放会话标识。HttpOnly 由服务器通过 Set-Cookie 设置,前端 JavaScript 不能创建或读取这类 Cookie;Secure 限制其经安全连接发送,SameSite 则约束跨站携带行为。实际策略还需与跨域部署和 CSRF 防护配合。参见 MDN Set-Cookie。

“保持登录”决定会话保存多久,不代表会话永久有效。退出登录、修改密码和撤销设备时,服务端都应使相应会话失效。