注册与登录本身并不难,但是登录后如何维持登录的状态?我如何知道是否已经登录?是否时一个合法的用户?如何实现有些页面只有登录之后的用户才能查看?这就要求有一些东西可以用来记录用户的状态,现在有两种主流的解决方案——基于服务器的Session和基于客户端的JWT。
Sessions实现
工作原理
- 用户登录后,服务器创建一个session对象并存储用户信息
- 服务器向客户端发送一个session ID,通常存储在cookie中
- 客户端后续请求都携带这个session ID
- 服务器根据session ID识别用户并维护会话状态
node.js实现服务器端
const http = require('http');
const crypto = require('crypto');
const url = require('url');
const sessions = {};
// 一般这是从数据库读取的,但这里就直接写死了。
const users = {
'user1': 'password1',
'user2': 'password2'
};
http.createServer((req, res) => {
let sessionId = req.headers.cookie?.split('=')[1];
// 如果没有session ID,创建一个新的
if (!sessionId) {
sessionId = crypto.randomBytes(16).toString('hex');
res.setHeader('Set-Cookie', `session=${sessionId}`);
sessions[sessionId] = {};
}
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === '/login' && req.method === 'POST') {
// 模拟接收POST数据
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const { username, password } = JSON.parse(body);
if (users[username] === password) {
sessions[sessionId].user = username;
res.end('Login successful');
} else {
res.end('Login failed');
}
});
} else if (parsedUrl.pathname === '/profile') {
if (sessions[sessionId].user) {
res.end(`Welcome, ${sessions[sessionId].user}`);
} else {
res.end('Please login first');
}
} else {
res.end('Hello, world!');
}
}).listen(3000);
解释下cookie相关代码:
res.setHeader('Set-Cookie', `session=${sessionId}`);
这行代码的作用是设置一个HTTP响应头,用于在客户端创建一个cookie:
- res.setHeader() 是一个设置HTTP响应头的方法。
- 'Set-Cookie' 是响应头的名称,用于指示浏览器设置cookie。
session=${sessionId}
是cookie的内容,包括:- cookie的名称: session
- cookie的值: sessionId变量的值
- 当浏览器收到这个响应头时,会在客户端创建一个名为"session"的cookie,其值为sessionId。
- 在后续的请求中,浏览器会自动将这个cookie附加在请求头中发送给服务器。
- 服务器可以通过读取这个cookie来识别用户会话。
补充:Cookie
Cookie的基本概念和特性
- Cookie的定义
Cookie是由服务器发送到用户浏览器并保存在本地的一小块数据。它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。 - Cookie的用途
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
- 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等)
- Cookie的结构
Cookie通常包含以下属性:- 名称(Name)
- 值(Value)
- 域(Domain)
- 路径(Path)
- 过期时间(Expires)或有效期(Max-Age)
- 安全标志(Secure)
- HttpOnly标志
例如:Set-Cookie: SessionID=31d4d96e407aad42; Domain=example.com; Path=/; Expires=Wed, 21 Oct 2023 07:28:00 GMT; Max-Age=86400; Secure; HttpOnly; SameSite=Strict
Cookie与域名和路径的关联
- 域(Domain)属性
- 指定哪些域名可以接收Cookie。如果不指定,默认为设置cookie服务器的域名(不包含子域名)。
- 如果指定了Domain,则一般包含子域名。例如,如果设置Domain=mozilla.org,则Cookie也包含在子域名如developer.mozilla.org中。
- 路径(Path)属性
- 指定Cookie的发送范围的URL路径。子路径也会被匹配。
- 例如,设置Path=/docs,则以下地址都会匹配:
- /docs
- /docs/Web/
- /docs/Web/HTTP
- 自动发送机制
- 当Domain和Path都匹配时,浏览器才会发送Cookie。
- 这个机制确保了Cookie只被发送到适当的服务器和URL。
Cookie的设置和传输
- Set-Cookie响应头
- 服务器通过Set-Cookie HTTP响应头部来设置Cookie。
- 例如:
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
- Cookie请求头
- 浏览器通过Cookie HTTP请求头部将Cookie发送回服务器。
- 安全考虑
- Secure标志:只通过HTTPS协议加密传输。
- HttpOnly标志:防止客户端脚本访问Cookie,减少XSS攻击风险。
Cookie的生命周期管理
- 会话期Cookie
- 浏览器关闭时Cookie会被删除。
- 持久性Cookie
- 通过Expires或Max-Age属性设置特定的过期时间。
- Expires设置具体的过期日期。
- Max-Age设置从现在开始Cookie多少秒后过期。
Cookie的大小和数量限制
- 大小限制
- 通常每个Cookie的大小限制在4KB左右。
- 数量限制
- 每个域名下的Cookie数量也有限制,具体取决于浏览器。
- 一般每个域名限制为20-50个Cookie。
SameSite属性
- 定义
- 允许服务器指定Cookie是否/何时通过跨站请求发送。
- 可能的值
- Strict:只在同一站点请求中发送Cookie。
- Lax:允许部分跨站请求(如导航到目标网址的GET请求)发送Cookie。
- None:允许跨站请求发送Cookie,但需要配合Secure属性使用。
- 默认行为
- 大多数现代浏览器默认设置为Lax。
- 安全性
- 主要用于防御CSRF(跨站请求伪造)攻击。
工作流程(关键)
综上所述,我们可以总结出一个流程来。
- 当浏览器准备发送 HTTP 请求时,它会检查其存储的所有 cookie。
- 对每个 cookie,浏览器会检查:
- 请求的域名是否匹配 cookie 的 Domain。
- 请求的路径是否匹配 cookie 的 Path。
- 如果设置了 Secure,是否是 HTTPS 请求。
- SameSite 设置是否允许发送。
- cookie 是否未过期。
- 如果所有条件都满足,该 cookie 会被添加到请求头中。
例如:
Set-Cookie: session=abc123; Domain=example.com; Path=/app; Secure
这个 cookie 只会在以下情况下发送:
- 请求的域名是 example.com 或其子域名。
- 请求的路径以 /app 开头。
- 使用 HTTPS 协议。
JWT(json Web Token)
JWT是一种基于令牌的无状态认证方式
工作原理
- 用户登录后,服务器创建一个JWT,包含用户信息和签名
- 服务器将JWT发送给客户端
- 客户端存储JWT(如localStorage)并在后续请求中携带
- 服务器通过验证JWT签名来认证用户
node.js实现服务器端
const http = require('http');
const crypto = require('crypto');
const url = require('url');
const SECRET_KEY = 'your-secret-key';
const PORT = 3000;
// 模拟用户数据库
const users = [
{ id: 1, username: 'user1', password: 'password1' },
{ id: 2, username: 'user2', password: 'password2' }
];
// 生成 JWT
// 这个函数接受一个payload参数,这通常是一个包含用户信息的对象。
function generateJWT(payload) {
// 在JWT中,我们需要将header和payload从JSON字符串转换为Base64Url编码的格式,
// 所以要先生成一个Buffer实例。
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(`${header}.${encodedPayload}`)
.digest('base64url');
return `${header}.${encodedPayload}.${signature}`;
}
// 验证 JWT
function verifyJWT(token) {
const [header, payload, signature] = token.split('.');
// 使用header中指定的算法和密钥,对header和payload部分进行签名,
// 然后与JWT中的signature部分进行比对。
const expectedSignature = crypto
.createHmac('sha256', SECRET_KEY)
.update(`${header}.${payload}`)
.digest('base64url');
if (signature !== expectedSignature) {
return null;
}
return JSON.parse(Buffer.from(payload, 'base64url').toString());
}
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
// 处理登录请求
if (parsedUrl.pathname === '/login' && req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const { username, password } = JSON.parse(body);
const user = users.find(u => u.username === username && u.password === password);
if (user) {
const token = generateJWT({ id: user.id, username: user.username });
res.writeHead(200, { 'Content-Type': 'application/json' });
// 将JWT发送回客户端
res.end(JSON.stringify({ token }));
} else {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Invalid credentials' }));
}
});
}
// 处理受保护的资源请求
else if (parsedUrl.pathname === '/protected' && req.method === 'GET') {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'No token provided' }));
return;
}
const token = authHeader.split(' ')[1];
const payload = verifyJWT(token);
if (!payload) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Invalid token' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Protected resource accessed', user: payload }));
}
// 处理其他请求
else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Not found' }));
}
});
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
对生成和验证JWT的部分代码进行解释:
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(`${header}.${encodedPayload}`)
.digest('base64url');
- 初始化 HMAC
- 调用
crypto.createHmac('sha256', SECRET_KEY)
- 这一步创建了一个 HMAC 对象,配置为使用 SHA-256 算法
- SECRET_KEY 作为密钥输入,它决定了签名的唯一性
- 调用
- 准备数据
${header}.${encodedPayload}
将编码后的 JWT 头部和载荷连接起来- 这个字符串是我们要签名的实际数据
- 更新 HMAC 对象
- 调用
.update(${header}.${encodedPayload})
- 这一步将准备好的数据输入到 HMAC 对象中
- HMAC 对象现在包含了要处理的完整数据
- 调用
- 生成摘要
- 调用
.digest('base64url');
- 这一步触发 HMAC 算法的计算过程:
- 使用 SHA-256 算法对输入的数据(也就是update()方法提供的
${header}.${encodedPayload}
)进行哈希处理,SHA-256会将这个输入转换成一个256位(32字节)的哈希值; - 将哈希结果与密钥(SECRET_KEY)结合,生成 HMAC,结合过程比较复杂,这里省略;
- 使用 SHA-256 算法对输入的数据(也就是update()方法提供的
- 最后,将 HMAC 结果编码为 base64url 格式:
- 将32字节的HMAC二进制值转换为base64字符串;
- 修改标准base64,替换'+'为'-', '/'为'_';
- 移除所有的填充字符'=';
- 调用
- 输出结果
- 返回 base64url 编码的 HMAC 签名;
- 这个签名就是 JWT 的第三部分;
客户端使用该服务
// 客户端代码示例 (可以在浏览器控制台中运行)
// 登录函数
async function login(username, password) {
const response = await fetch('http://localhost:3000/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
// 存储 JWT 到 localStorage
localStorage.setItem('token', data.token);
console.log('Login successful');
} else {
console.error('Login failed:', data.message);
}
}
// 访问受保护资源的函数
async function accessProtectedResource() {
const token = localStorage.getItem('token');
if (!token) {
console.error('No token found, please login first');
return;
}
const response = await fetch('http://localhost:3000/protected', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
const data = await response.json();
if (response.ok) {
console.log('Protected resource:', data);
} else {
console.error('Access denied:', data.message);
}
}
// 使用示例
login('user1', 'password1').then(() => {
accessProtectedResource();
});
注意:真正使用时,不仅要验证JWT的完整性(签名验证),还有JWT的有效性(时间验证),用户的权限(自定义声明验证)等。