用户鉴权


Token(JWT)方式(前后端分离/移动端常用)


OAuth2.0 / OpenID Connect(第三方登录)


双因素认证(2FA / MFA)


基于证书/密钥的 Auth(少数场景)


总结

Session+Cookie实例

  1. 目录结构
session-cookie-demo/
├─ app.js
├─ package.json
└─ .env
  1. package.json
{
    "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"
    }
}
  1. .env
SESSION_SECRET=dev_only_change_me
REDIS_URL=redis://localhost:6379
NODE_ENV=development
  1. app.js
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'));
  1. 启动
npm i
node app.js
# 登录: POST http://localhost:3000/login {"username":"alice","password":"password123"}

生产注意:强制 HTTPS;cookie.sameSite/secure 正确配置;考虑 CSRF 保护(例如使用 csurf 或双重 Cookie 策略)。

JWT(Access Token + Refresh Token)

  1. 目录结构
jwt-demo/
├─ app.js
├─ package.json
└─ .env
  1. package.json
{
    "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"
    }
}
  1. .env
ACCESS_TOKEN_SECRET=access_secret_change_me
REFRESH_TOKEN_SECRET=refresh_secret_change_me
NODE_ENV=development
REDIS_URL=redis://localhost:6379
  1. app.js
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 更安全;为受保护接口加速率限制。

AcessToken和RefreshToken区别

JWT 中 AccessToken 与 RefreshToken 的区别

Access Token(访问令牌)


Refresh Token(刷新令牌)


配合使用流程

  1. 用户登录成功 → 服务器颁发 AccessToken + RefreshToken
  2. 客户端调用 API 时 → 使用 AccessToken
  3. 当 AccessToken 过期 → 使用 RefreshToken 向认证服务器请求新的 AccessToken。
  4. 服务器验证 RefreshToken 有效后 → 颁发新的 AccessToken(有时也会返回新的 RefreshToken)。
  5. 如果 RefreshToken 也过期 → 用户需要重新登录。

类比理解

OAuth 2.0与OpenID Connect

使用 passport + passport-google-oauth20,成功后在本地写 Session(也可换成 JWT)。


🌐 什么是 OAuth2.0

OAuth 2.0 是一种开放标准的 授权框架(Authorization Framework),主要用于 让第三方应用在不暴露用户账号和密码的情况下,获取受保护资源的有限访问权限

它不是一个认证协议(authentication),而是一个 授权协议(authorization)。 👉 简单来说:OAuth2.0 解决的是 “某个应用想用你的数据,但你不想给它你的账号密码” 的问题。


📱 生活中的例子

你在一个网站上想用 Google 登录

  1. 你点了「用 Google 登录」。
  2. 系统跳转到 Google 授权页面,问你是否允许这个网站访问你的基本信息(比如邮箱)。
  3. 你同意后,Google 不会把密码给网站,而是颁发一个 Access Token 给这个网站。
  4. 网站用这个 Token 去 Google 获取你的邮箱地址,而不是直接登录你的 Google 账号。

这样就实现了 安全授权


🔄 OAuth2.0 的核心角色

  1. 资源所有者(Resource Owner):用户(你)。
  2. 客户端(Client):想访问你数据的第三方应用。
  3. 授权服务器(Authorization Server):负责验证身份和颁发 Token(比如 Google 的 OAuth 服务)。
  4. 资源服务器(Resource Server):存放受保护数据的服务器(比如 Google 的用户信息 API)。

🔑 常见的授权流程(Grant Types)

OAuth2 有几种授权模式,常见的有:

  1. 授权码模式(Authorization Code)

  2. 简化模式(Implicit)

  3. 密码模式(Resource Owner Password Credentials)

  4. 客户端凭证模式(Client Credentials)


✅ 总结一句: OAuth2.0 就是一个安全的授权机制,让第三方应用可以在不拿到你密码的情况下,访问你的数据(有限范围、有限时间)。

npm init -y
npm install express passport passport-oauth2 express-session
const 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] ---> 返回用户信息