流程:
Set-Cookie
给浏览器。优点: 简单易实现,安全性较高(前提是 HTTPS + HttpOnly Cookie)。
缺点: 需要服务端存储 Session,分布式/微服务架构下需要共享存储(Redis 等)。
流程:
localStorage /
sessionStorage / Cookie。Authorization: Bearer <token>
头里传递。优点:
缺点:
场景: 用微信/Google/GitHub 登录网站。
流程:
Authorization Code。Code 去换取 Access Token。Access Token 去 Google
获取用户信息,并在自己网站创建/绑定账号。优点: 用户不用记密码,安全性高。
缺点: 实现稍复杂。
例如:
优点: 强安全。
缺点: 部署和管理复杂。
Session + Cookie(简单) 或
JWT Token(前后端分离)。OAuth2.0 / OpenID Connect。2FA / MFA。session-cookie-demo/
├─ app.js
├─ package.json
└─ .env{
"name": "session-cookie-demo",
"type": "module",
"dependencies": {
"bcryptjs": "^2.4.3",
"connect-redis": "^7.1.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.17.3",
"ioredis": "^5.4.1"
}
}SESSION_SECRET=dev_only_change_me
REDIS_URL=redis://localhost:6379
NODE_ENV=development
import 'dotenv/config';
import express from 'express';
import session from 'express-session';
import connectRedis from 'connect-redis';
import Redis from 'ioredis';
import bcrypt from 'bcryptjs';
const app = express();
app.use(express.json());
// ⭐ 使用 Redis 存储 Session(生产环境推荐)
const RedisStore = connectRedis(session);
const redisClient = new Redis(process.env.REDIS_URL);
app.set('trust proxy', 1); // 如果有反向代理(如 Nginx/Cloudflare)
app.use(
session({
store: new RedisStore({ client: redisClient }),
name: 'sid', // Cookie 名字
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax', // 为第三方跳转登录可改为 'none' 并配合 secure
secure: process.env.NODE_ENV === 'production',
maxAge: 1000 * 60 * 60 * 2 // 2 小时
}
})
);
// 假用户数据库(示例)
const users = [
{ id: 1, username: 'alice', passwordHash: bcrypt.hashSync('password123', 10) }
];
function requireAuth(req, res, next) {
if (req.session.user) return next();
res.status(401).json({ message: '未登录' });
}
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
return res.status(401).json({ message: '用户名或密码错误' });
}
req.session.user = { id: user.id, username: user.username };
res.json({ message: '登录成功' });
});
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('sid');
res.json({ message: '已登出' });
});
});
app.get('/me', requireAuth, (req, res) => {
res.json({ user: req.session.user });
});
app.listen(3000, () => console.log('Session server http://localhost:3000'));npm i
node app.js
# 登录: POST http://localhost:3000/login {"username":"alice","password":"password123"}生产注意:强制 HTTPS;cookie.sameSite/secure 正确配置;考虑 CSRF 保护(例如使用 csurf 或双重 Cookie 策略)。
jwt-demo/
├─ app.js
├─ package.json
└─ .env{
"name": "jwt-demo",
"type": "module",
"dependencies": {
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"ioredis": "^5.4.1"
}
}ACCESS_TOKEN_SECRET=access_secret_change_me
REFRESH_TOKEN_SECRET=refresh_secret_change_me
NODE_ENV=development
REDIS_URL=redis://localhost:6379
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import Redis from 'ioredis';
import crypto from 'crypto';
import 'dotenv/config';
const app = express();
app.use(express.json());
// 模拟用户数据(实际应是数据库)
const users = [
{ id: 1, username: 'alice', passwordHash: bcrypt.hashSync('123456', 10) },
{ id: 2, username: 'bob', passwordHash: bcrypt.hashSync('abcdef', 10) }
];
// Redis 连接
const redis = new Redis();
// ====== JWT 工具函数 ======
function signAccessToken(payload) {
return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
}
function signRefreshToken(payload, jti) {
return jwt.sign({ ...payload, jti }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
}
// 保存 refresh token(带过期时间)
async function saveRefreshToken(userId, jti, ttl) {
await redis.set(`rt:${userId}:${jti}`, '1', 'EX', ttl);
}
async function revokeRefreshToken(userId, jti) {
await redis.del(`rt:${userId}:${jti}`);
}
async function isRefreshTokenActive(userId, jti) {
return (await redis.exists(`rt:${userId}:${jti}`)) === 1;
}
// ====== Auth 中间件 ======
function authMiddleware(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null;
if (!token) return res.status(401).json({ message: '缺少访问令牌' });
try {
const payload = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
req.user = payload;
next();
} catch (e) {
return res.status(401).json({ message: '无效或过期的访问令牌' });
}
}
// ====== 登录 ======
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
return res.status(401).json({ message: '用户名或密码错误' });
}
const payload = { sub: user.id, username: user.username };
const accessToken = signAccessToken(payload);
const jti = cryptoRandom();
const refreshToken = signRefreshToken(payload, jti);
// 保存 refreshToken 到 redis
const { exp } = jwt.decode(refreshToken);
await saveRefreshToken(user.id, jti, exp - Math.floor(Date.now() / 1000));
res.json({ accessToken, refreshToken });
});
// ====== 刷新 token ======
app.post('/token/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(400).json({ message: '缺少 refreshToken' });
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
const { sub: userId, username, jti } = payload;
if (!(await isRefreshTokenActive(userId, jti))) {
return res.status(401).json({ message: 'refreshToken 已失效' });
}
// 作废旧的 refresh token
await revokeRefreshToken(userId, jti);
// 生成新的 refresh token
const newJti = cryptoRandom();
const newRefresh = signRefreshToken({ sub: userId, username }, newJti);
const { exp } = jwt.decode(newRefresh);
await saveRefreshToken(userId, newJti, exp - Math.floor(Date.now() / 1000));
// 生成新的 access token
const newAccess = signAccessToken({ sub: userId, username });
res.json({ accessToken: newAccess, refreshToken: newRefresh });
} catch (e) {
return res.status(401).json({ message: '无效或过期的 refreshToken' });
}
});
// ====== 登出 ======
app.post('/logout', async (req, res) => {
const { refreshToken } = req.body;
try {
const { sub: userId, jti } = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
await revokeRefreshToken(userId, jti);
} catch (_) { }
res.json({ message: '已登出' });
});
// ====== 获取用户信息 ======
app.get('/me', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
app.listen(3001, () => console.log('JWT server http://localhost:3001'));
// ====== 生成随机 jti ======
function cryptoRandom(size = 16) {
return crypto.randomBytes(size).toString('hex');
}生产注意:ACCESS_TOKEN_SECRET/REFRESH_TOKEN_SECRET 使用足够强的随机值;refresh token 放在 HttpOnly+Secure Cookie 更安全;为受保护接口加速率限制。
用途:用于客户端在访问受保护资源(API、用户数据等)时,作为身份凭证。
特点:
Authorization: Bearer <token>)。风险:
用途:当 AccessToken 过期后,用来获取新的 AccessToken。
特点:
安全性:
AccessToken = 门票 🎫
RefreshToken = VIP 卡 💳
使用 passport + passport-google-oauth20,成功后在本地写 Session(也可换成 JWT)。
OAuth 2.0 是一种开放标准的 授权框架(Authorization Framework),主要用于 让第三方应用在不暴露用户账号和密码的情况下,获取受保护资源的有限访问权限。
它不是一个认证协议(authentication),而是一个 授权协议(authorization)。 👉 简单来说:OAuth2.0 解决的是 “某个应用想用你的数据,但你不想给它你的账号密码” 的问题。
你在一个网站上想用 Google 登录:
这样就实现了 安全授权。
OAuth2 有几种授权模式,常见的有:
授权码模式(Authorization Code)
简化模式(Implicit)
密码模式(Resource Owner Password Credentials)
客户端凭证模式(Client Credentials)
✅ 总结一句: OAuth2.0 就是一个安全的授权机制,让第三方应用可以在不拿到你密码的情况下,访问你的数据(有限范围、有限时间)。
npm init -y
npm install express passport passport-oauth2 express-sessionconst express = require("express");
const session = require("express-session");
const passport = require("passport");
const OAuth2Strategy = require("passport-oauth2").Strategy;
const app = express();
// ⚙️ session 配置
app.use(
session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
})
);
app.use(passport.initialize());
app.use(passport.session());
// ⚙️ 用户序列化(这里简单处理)
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));
// ⚙️ OAuth2 配置(替换为你实际的OAuth2服务端地址)
passport.use(
new OAuth2Strategy(
{
authorizationURL: "http://localhost:4000/oauth/authorize", // 授权端点
tokenURL: "http://localhost:4000/oauth/token", // token 端点
clientID: "your-client-id",
clientSecret: "your-client-secret",
callbackURL: "http://localhost:3000/auth/callback",
},
function (accessToken, refreshToken, profile, cb) {
// 这里可以去请求用户信息 API
const user = { accessToken, refreshToken };
return cb(null, user);
}
)
);
// 首页
app.get("/", (req, res) => {
res.send(`
<h1>OAuth2 Demo</h1>
<a href="/auth">使用 OAuth2 登录</a>
`);
});
// 登录
app.get("/auth", passport.authenticate("oauth2"));
// 回调
app.get(
"/auth/callback",
passport.authenticate("oauth2", { failureRedirect: "/" }),
(req, res) => {
res.send(`
<h2>登录成功 🎉</h2>
<p>Access Token: ${req.user.accessToken}</p>
<a href="/profile">查看 Profile</a>
`);
}
);
// 模拟受保护资源
app.get("/profile", (req, res) => {
if (!req.user) {
return res.redirect("/");
}
res.json({
message: "这是受保护的用户信息",
user: req.user,
});
});
app.listen(3000, () => {
console.log("🚀 Node.js OAuth2 Demo 运行在 http://localhost:3000");
});[用户浏览器] ---> (1) /auth 请求 ---> [你的服务器(Node.js)]
|
| (2) 重定向到授权服务器
v
[OAuth2 授权服务器(第三方)] <--- 用户登录并同意授权
|
| (4) 回调带 code
v
[用户浏览器] ---> (5) /auth/callback?code=xxx ---> [你的服务器(Node.js)]
|
| (5) 后端用 code 换 token
v
[OAuth2 授权服务器] ---> 返回 access_token
|
v
[你的服务器] 保存 access_token 建立会话
|
v
[用户浏览器] ---> (7) 请求 /profile ---> [你的服务器 验证用户的Session信息或AccessToken] ---> 返回用户信息