登录状态记录是Web开发的核心需求之一,其本质是在客户端存储用户身份凭证,让用户在一段时间内无需重复登录。HTML本身不直接处理登录逻辑,但可结合浏览器存储(Cookie/LocalStorage/SessionStorage)、HTTP协议和JavaScript实现完整的登录状态管理。本教程从核心原理到实战实现,全面讲解登录状态记录的设计、实现与安全规范。
一、登录状态记录核心原理
1. 登录流程与状态记录逻辑
完整的登录状态记录流程分为3步:
用户输入账号密码 → 前端提交验证 → 后端验证通过后生成身份凭证 → 前端存储凭证 → 后续请求携带凭证 → 后端验证凭证有效性
- 核心载体:浏览器存储(Cookie/LocalStorage)是前端记录登录状态的关键;
- 核心原则 :凭证必须可验证、防篡改、有有效期,避免明文存储敏感信息。
2. 前端存储方案对比(核心选择)
登录状态记录的核心是选择合适的存储方式,以下是3种主流方案的对比:
| 存储方式 | 存储位置 | 生命周期 | 容量 | 自动携带到后端 | 安全性 | 适用场景 |
|---|---|---|---|---|---|---|
| Cookie | 浏览器(磁盘) | 可设置过期时间(expires/max-age),默认会话结束失效 |
4KB | 是(同域名请求自动携带) | 支持HttpOnly/Secure/SameSite | 后端主导的登录状态(推荐) |
| LocalStorage | 浏览器(内存/磁盘) | 永久存储(除非手动清除) | 5MB | 否(需手动携带) | 无内置安全属性 | 前端临时存储非敏感登录信息 |
| SessionStorage | 浏览器(内存) | 会话结束(关闭标签页)失效 | 5MB | 否(需手动携带) | 无内置安全属性 | 临时登录(仅当前会话有效) |
核心结论 :
生产环境优先使用 Cookie(带HttpOnly/Secure) 存储登录凭证(如Token/SessionID),LocalStorage仅作为补充(如存储用户昵称等非敏感信息),SessionStorage仅用于临时会话。
二、登录状态记录核心技术
1. Cookie 详解(后端+前端协作)
Cookie是服务器通过HTTP响应头设置、浏览器存储的小型文本数据,是登录状态记录的标准方案。
(1)Cookie 核心属性(安全关键)
| 属性 | 作用 | 安全意义 |
|---|---|---|
name=value |
键值对(存储SessionID/Token) | 核心凭证载体 |
expires |
过期时间(GMT格式) | 控制登录状态有效期 |
max-age |
有效期(秒),优先级高于expires |
更灵活的有效期设置 |
domain |
生效域名(如.example.com) |
限制Cookie仅在指定域名生效 |
path |
生效路径(如/) |
限制Cookie仅在指定路径生效 |
HttpOnly |
禁止JS访问Cookie | 防止XSS攻击窃取凭证 |
Secure |
仅在HTTPS协议下传输Cookie | 防止HTTP明文传输被劫持 |
SameSite |
限制跨域请求携带Cookie(Strict/Lax/None) | 防止CSRF攻击 |
(2)设置Cookie的两种方式
-
后端设置(推荐) :通过HTTP响应头
Set-Cookie设置(可配置HttpOnly/Secure);示例(Node.js/Express):
javascript// 登录验证通过后设置Cookie app.post('/login', (req, res) => { const { username, password } = req.body; // 模拟验证(实际需查数据库) if (username === 'admin' && password === '123456') { // 生成SessionID/Token(实际需用加密算法) const sessionId = 'xxx-xxx-xxx'; // 设置Cookie:有效期7天,HttpOnly+Secure+SameSite res.cookie('login_token', sessionId, { maxAge: 7 * 24 * 60 * 60 * 1000, // 7天有效期(毫秒) httpOnly: true, // 禁止JS访问 secure: process.env.NODE_ENV === 'production', // 生产环境开启HTTPS sameSite: 'Lax', // 防止CSRF domain: '.example.com', // 主域名生效(子域名共享) path: '/' }); res.json({ code: 200, msg: '登录成功' }); } else { res.json({ code: 401, msg: '账号密码错误' }); } }); -
前端设置(仅用于非敏感信息):通过JS设置(无法配置HttpOnly);
javascript// 前端设置Cookie(无HttpOnly,仅演示) function setCookie(name, value, days) { const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); const expires = `expires=${date.toGMTString()}`; // 注意:前端设置的Cookie无法加HttpOnly document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`; } // 登录成功后调用 setCookie('username', 'admin', 7); // 存储用户名(非敏感)
(3)读取Cookie(前端)
前端可通过document.cookie读取非HttpOnly的Cookie:
javascript
// 获取指定名称的Cookie值
function getCookie(name) {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === name) return decodeURIComponent(value);
}
return '';
}
// 示例:读取用户名
const username = getCookie('username');
if (username) {
console.log('已登录用户:', username);
}
(4)删除Cookie(退出登录)
javascript
// 删除Cookie(设置过期时间为过去)
function removeCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
}
// 退出登录时调用
function logout() {
removeCookie('username');
// 通知后端销毁Session/Token
fetch('/logout', { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.code === 200) {
window.location.href = '/login.html'; // 跳转到登录页
}
});
}
2. LocalStorage/SessionStorage 实现(前端主导)
适用于前端需要自主管理登录状态的场景(如纯前端项目、前后端分离且Token由前端携带)。
(1)存储登录信息(登录成功后)
javascript
// 登录请求
async function login(username, password) {
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.code === 200) {
// 存储Token(核心凭证)和用户信息
localStorage.setItem('token', data.token);
localStorage.setItem('userInfo', JSON.stringify(data.userInfo));
// SessionStorage存储临时状态(关闭标签页失效)
sessionStorage.setItem('isLogin', 'true');
// 跳转到首页
window.location.href = '/index.html';
} else {
alert(data.msg);
}
} catch (err) {
alert('登录失败:' + err.message);
}
}
(2)验证登录状态(页面加载时)
javascript
// 页面初始化时检查登录状态
function checkLogin() {
const token = localStorage.getItem('token');
const isLogin = sessionStorage.getItem('isLogin');
// 无Token则跳转到登录页
if (!token || !isLogin) {
window.location.href = '/login.html';
return false;
}
// 有Token则验证有效性(可选,调用后端接口)
verifyToken(token);
return true;
}
// 验证Token有效性
async function verifyToken(token) {
try {
const res = await fetch('/api/verifyToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // 携带Token到后端
}
});
const data = await res.json();
if (data.code !== 200) {
// Token失效,清除存储并跳转登录
logout();
}
} catch (err) {
logout();
}
}
// 页面加载时执行
window.onload = function() {
checkLogin();
// 渲染用户信息
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
if (userInfo.username) {
document.getElementById('userName').textContent = userInfo.username;
}
};
(3)退出登录(清除存储)
javascript
function logout() {
// 清除LocalStorage
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
// 清除SessionStorage
sessionStorage.clear();
// 通知后端失效Token
fetch('/api/logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
// 跳转登录页
window.location.href = '/login.html';
}
3. Token 认证(现代主流方案)
Token(令牌)是无状态的登录凭证,相比传统Session更适合分布式系统,结合Cookie/LocalStorage使用:
- JWT(JSON Web Token):最常用的Token格式,由Header.Payload.Signature三部分组成,后端签名防篡改;
- Token传输 :
- 方案1:后端将JWT写入HttpOnly Cookie(推荐,防XSS);
- 方案2:前端将JWT存储在LocalStorage,请求时通过
Authorization头携带。
示例:JWT + LocalStorage 实现
javascript
// 登录成功后存储JWT
async function loginWithJWT(username, password) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.code === 200) {
// 存储JWT Token
localStorage.setItem('jwt_token', data.token);
// 解析Token获取用户信息(可选,Payload部分)
const payload = JSON.parse(atob(data.token.split('.')[1]));
localStorage.setItem('expire_time', payload.exp); // 存储过期时间
window.location.href = '/index.html';
}
}
// 检查Token是否过期
function isTokenExpired() {
const expireTime = localStorage.getItem('expire_time');
if (!expireTime) return true;
// 对比当前时间戳(秒)和过期时间戳
return Date.now() / 1000 > expireTime;
}
// 请求拦截:携带Token
async function requestWithToken(url, options = {}) {
const token = localStorage.getItem('jwt_token');
if (token && !isTokenExpired()) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
return fetch(url, options);
}
// 示例:获取用户信息(带Token请求)
requestWithToken('/api/userInfo')
.then(res => res.json())
.then(data => console.log(data));
三、完整登录状态记录实战(前后端分离)
1. 前端登录页面(login.html)
包含表单、验证、登录请求、状态存储:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录页</title>
<style>
.login-form { max-width: 300px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; }
.form-item { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input { width: 100%; padding: 8px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; cursor: pointer; }
.error { color: red; font-size: 12px; margin-top: 5px; display: none; }
</style>
</head>
<body>
<div class="login-form">
<h3>用户登录</h3>
<div class="form-item">
<label for="username">用户名</label>
<input type="text" id="username" required placeholder="请输入用户名">
</div>
<div class="form-item">
<label for="password">密码</label>
<input type="password" id="password" required placeholder="请输入密码">
<div class="error" id="errorTip"></div>
</div>
<div class="form-item">
<label>
<input type="checkbox" id="remember"> 记住我(7天免登录)
</label>
</div>
<button onclick="handleLogin()">登录</button>
</div>
<script>
// 登录处理函数
async function handleLogin() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const remember = document.getElementById('remember').checked;
const errorTip = document.getElementById('errorTip');
// 前端基础验证
if (!username || !password) {
errorTip.textContent = '用户名和密码不能为空';
errorTip.style.display = 'block';
return;
}
try {
// 提交登录请求
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, remember })
});
const data = await res.json();
if (data.code === 200) {
// 存储登录状态(根据"记住我"选择存储方式)
const expires = remember ? 7 : 0; // 7天或会话有效
if (remember) {
// 记住我:LocalStorage存储Token(7天)+ Cookie存储用户名
localStorage.setItem('token', data.token);
setCookie('username', username, 7);
} else {
// 不记住:SessionStorage存储Token
sessionStorage.setItem('token', data.token);
setCookie('username', username, 0);
}
// 跳转到首页
window.location.href = '/index.html';
} else {
errorTip.textContent = data.msg;
errorTip.style.display = 'block';
}
} catch (err) {
errorTip.textContent = '网络错误,请重试';
errorTip.style.display = 'block';
}
}
// Cookie操作工具函数
function setCookie(name, value, days) {
let expires = '';
if (days > 0) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = `expires=${date.toGMTString()}`;
}
document.cookie = `${name}=${encodeURIComponent(value)};${expires};path=/;SameSite=Lax`;
}
// 页面加载时检查是否已登录(防止重复登录)
window.onload = function() {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
if (token) {
window.location.href = '/index.html';
}
};
</script>
</body>
</html>
2. 首页(index.html):验证登录状态 + 展示用户信息
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div id="userPanel">
<span id="userName"></span>
<button onclick="logout()">退出登录</button>
</div>
<div id="content" style="display: none;">
<h1>欢迎访问首页</h1>
<!-- 登录后可见的内容 -->
</div>
<script>
// 页面加载时验证登录状态
window.onload = async function() {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
const username = getCookie('username');
// 无Token或用户名,跳转到登录页
if (!token || !username) {
window.location.href = '/login.html';
return;
}
// 验证Token有效性
const isValid = await verifyToken(token);
if (!isValid) {
logout();
return;
}
// 渲染用户信息和内容
document.getElementById('userName').textContent = `欢迎,${username}`;
document.getElementById('content').style.display = 'block';
};
// 验证Token
async function verifyToken(token) {
try {
const res = await fetch('/api/verifyToken', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
return data.code === 200;
} catch (err) {
return false;
}
}
// 退出登录
function logout() {
// 清除存储
localStorage.removeItem('token');
sessionStorage.removeItem('token');
removeCookie('username');
// 通知后端
fetch('/api/logout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${localStorage.getItem('token') || sessionStorage.getItem('token')}` }
});
// 跳转登录页
window.location.href = '/login.html';
}
// 工具函数:获取Cookie
function getCookie(name) {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === name) return decodeURIComponent(value);
}
return '';
}
// 工具函数:删除Cookie
function removeCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
}
</script>
</body>
</html>
3. 后端核心逻辑(Node.js/Express 示例)
javascript
const express = require('express');
const jwt = require('jsonwebtoken'); // 需安装:npm i jsonwebtoken
const app = express();
app.use(express.json());
// 模拟用户数据库
const users = [{ username: 'admin', password: '123456' }];
// JWT密钥(生产环境需用环境变量)
const JWT_SECRET = 'your-secret-key-123';
// 登录接口
app.post('/api/login', (req, res) => {
const { username, password, remember } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.json({ code: 401, msg: '账号或密码错误' });
}
// 生成JWT Token(根据"记住我"设置有效期)
const expiresIn = remember ? '7d' : '1h'; // 7天或1小时
const token = jwt.sign(
{ username: user.username, exp: Math.floor(Date.now() / 1000) + (remember ? 7*24*60*60 : 60*60) },
JWT_SECRET
);
res.json({ code: 200, msg: '登录成功', token });
});
// 验证Token接口
app.post('/api/verifyToken', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.json({ code: 401, msg: '未登录' });
}
try {
// 验证Token
jwt.verify(token, JWT_SECRET);
res.json({ code: 200, msg: 'Token有效' });
} catch (err) {
res.json({ code: 401, msg: 'Token失效' });
}
});
// 退出登录接口(前端清除存储,后端无需操作JWT,仅记录日志)
app.post('/api/logout', (req, res) => {
res.json({ code: 200, msg: '退出成功' });
});
// 静态文件托管(前端页面)
app.use(express.static('public'));
// 启动服务
app.listen(3000, () => {
console.log('服务运行在 http://localhost:3000/login.html');
});
四、安全规范(重中之重)
登录状态记录涉及用户隐私,必须遵守以下安全规则,避免数据泄露或攻击:
1. 核心安全原则
- 禁止明文存储密码:前端仅传递密码,后端必须哈希存储(如bcrypt),示例中为简化用明文,生产环境需替换;
- Token/密码必须HTTPS传输:HTTP协议下数据易被劫持,生产环境强制开启HTTPS;
- HttpOnly Cookie:敏感凭证(如SessionID/Token)必须通过后端设置HttpOnly Cookie,禁止前端JS访问(防XSS);
- Token必须有有效期:避免永久有效的凭证,即使泄露也能限制危害;
- SameSite Cookie :设置
SameSite=Lax/Strict,防止CSRF攻击; - Secure Cookie :仅在HTTPS下传输Cookie,设置
Secure属性。
2. 常见攻击防护
| 攻击类型 | 防护措施 |
|---|---|
| XSS | 1. 凭证用HttpOnly Cookie;2. 输入输出转义;3. 开启CSP策略 |
| CSRF | 1. SameSite=Strict/Lax;2. 后端验证Referer/Origin;3. 使用CSRF Token |
| 中间人攻击 | 1. 强制HTTPS;2. 配置HSTS头;3. 避免明文传输 |
| 令牌劫持 | 1. 缩短Token有效期;2. 敏感操作二次验证;3. 检测异常IP/设备 |
3. 生产环境优化
-
密钥/敏感配置 :使用环境变量(如
process.env.JWT_SECRET),避免硬编码; -
Token刷新机制:实现无感刷新(Token快过期时自动请求新Token);
-
多端登录管理:记录登录设备,支持踢下线;
-
日志监控:记录登录/退出/Token验证日志,及时发现异常;
-
密码加密 :后端用bcrypt/argon2哈希存储密码,示例:
javascriptconst bcrypt = require('bcrypt'); // 加密密码 const hashPassword = async (pwd) => await bcrypt.hash(pwd, 10); // 验证密码 const verifyPassword = async (pwd, hash) => await bcrypt.compare(pwd, hash);
五、常见问题与解决方案
1. 记住我功能失效
- 检查Cookie/LocalStorage的有效期设置是否正确;
- 确认浏览器未禁用Cookie(隐私模式可能禁用LocalStorage);
- 后端Token有效期需与前端存储有效期一致。
2. Token过期后无法自动刷新
-
解决方案:实现Token刷新接口,前端在Token过期前(如剩余5分钟)请求新Token;
javascript// 刷新Token示例 async function refreshToken() { const oldToken = localStorage.getItem('jwt_token'); const res = await fetch('/api/refreshToken', { method: 'POST', headers: { 'Authorization': `Bearer ${oldToken}` } }); const data = await res.json(); if (data.code === 200) { localStorage.setItem('jwt_token', data.newToken); } }
3. 跨域请求时Cookie无法携带
-
后端配置CORS允许凭证:
javascriptapp.use(cors({ origin: 'http://localhost:8080', // 前端域名 credentials: true // 允许携带Cookie })); -
前端请求时设置
credentials: 'include':javascriptfetch('/api/login', { method: 'POST', credentials: 'include', // 携带Cookie // ...其他配置 });
4. 退出登录后Token仍可使用
- JWT是无状态的,无法主动失效,解决方案:
- 缩短Token有效期(如1小时);
- 后端维护黑名单(Redis存储失效的Token);
- 敏感操作需二次验证(如修改密码)。
六、总结
登录状态记录的核心是安全存储凭证 + 有效验证凭证,关键要点:
- 优先选择HttpOnly Cookie存储敏感凭证(防XSS),LocalStorage仅存储非敏感信息;
- 采用Token(JWT)实现无状态认证,适配分布式系统;
- 必须遵守安全规范(HTTPS、有效期、HttpOnly/SameSite);
- 前端负责存储/携带凭证,后端负责生成/验证凭证;
- 生产环境需优化密码存储、Token刷新、日志监控等细节。