Skip to content

认证与安全

后端核心 认证 6 级安全

一、为什么需要认证

1.1 核心原因

原因说明
数据关联将用户生成的数据(帖子、消息、收藏)与个人账户绑定
隐私保护防止他人查看你的私信、邮件等私密信息
访问控制按用户状态限制访问(如 Netflix/Spotify 付费内容)

1.2 认证的难点

创建账户和登录本身很简单,难的是安全性

Level 1 → 明文密码
Level 2 → 数据库加密
Level 3 → 哈希(MD5)
Level 4 → 加盐哈希(bcrypt)
Level 5 → Cookie 与 Session
Level 6 → OAuth 2.0(第三方登录)

二、Level 1 — 明文用户名 + 密码

2.1 基本流程

javascript
// 用户 Schema
const userSchema = new mongoose.Schema({
  email: String,
  password: String
});

const User = new mongoose.model("User", userSchema);

2.2 注册

javascript
app.post("/register", (req, res) => {
  const newUser = new User({
    email: req.body.username,
    password: req.body.password   // ⚠️ 明文存储!
  });

  newUser.save((err) => {
    if (err) {
      console.log(err);
    } else {
      res.render("secrets");
    }
  });
});

2.3 登录

javascript
app.post("/login", (req, res) => {
  const username = req.body.username;
  const password = req.body.password;

  User.findOne({ email: username }, (err, foundUser) => {
    if (err) {
      console.log(err);
    } else {
      if (foundUser) {
        if (foundUser.password === password) {
          res.render("secrets");
        }
      }
    }
  });
});

2.4 问题

数据库中:
{ email: "user@test.com", password: "123456" }
                                     ↑ 明文!任何人都能看到

致命缺陷

  • 内部员工可以直接看到所有人的密码
  • 数据库被黑客入侵 → 所有密码立即泄露
  • 用户通常在多个网站使用相同密码 → 连锁反应

三、Level 2 — 数据库加密

3.1 加密原理

加密 = 打乱信息 + 密钥 → 只有知道密钥才能还原

明文 "Hello" + 密钥(shift=3) → 密文 "Khoor"(凯撒密码)

现代加密算法 AES 比凯撒密码复杂得多,数学上更难破解。

3.2 使用 mongoose-encryption

bash
npm i mongoose-encryption
javascript
const encrypt = require("mongoose-encryption");

// Schema 必须是 mongoose.Schema 对象(不是普通 JS 对象)
const userSchema = new mongoose.Schema({
  email: String,
  password: String
});

// 定义加密密钥
const secret = "ThisisourLittlesecret.";

// 作为插件添加到 Schema — 只加密 password 字段
userSchema.plugin(encrypt, {
  secret: secret,
  encryptedFields: ["password"]  // 不加密 email,方便查询
});

const User = new mongoose.model("User", userSchema);

3.3 工作原理

调用 .save()  → 自动加密 password 字段
调用 .find()  → 自动解密 password 字段
  • 注册时存入数据库的 password 是 加密后的二进制字符串
  • 登录时 mongoose-encryption 自动解密,对比密码

3.4 问题

仍有漏洞

黑客入侵服务器 → 找到 app.js → 获取加密密钥(secret)→ 解密所有密码

只要密码可以被"还原"为明文,就不够安全。


四、环境变量(dotenv)

4.1 问题:密钥泄露

javascript
// ❌ 密钥硬编码在代码中
const secret = "ThisisourLittlesecret.";
// 提交到 GitHub → 全世界都能看到你的密钥!

真实案例: 开发者将 AWS 密钥提交到 GitHub → 被自动爬虫发现 → 被用于挖矿 → 产生 $3000+ 的账单。

4.2 解决方案:dotenv

bash
npm i dotenv
javascript
// app.js 最顶部(必须在最前面)
require("dotenv").config();

4.3 创建 .env 文件

bash
touch .env   # 在项目根目录创建
env
SECRET=ThisisourLittlesecret.
API_KEY=your-api-key-here

.env 格式规则

  • NAME=VALUE(无空格、无引号、无分号)
  • 变量名使用 全大写 + 下划线(SNAKE_CASE)
  • 每行一个变量

4.4 使用环境变量

javascript
// 通过 process.env 访问
const secret = process.env.SECRET;
console.log(process.env.API_KEY);

4.5 .gitignore — 防止提交

gitignore
# .gitignore 文件
node_modules/
.env          # ← 关键!不提交 .env

注意

如果你在添加 .gitignore 之前已经提交过 .env → Git 历史中仍然有密钥!

最佳实践: 项目创建之初就配置 .gitignore + .env

4.6 部署时的处理

平台环境变量设置方式
HerokuDashboard → Settings → Config Vars
VercelProject Settings → Environment Variables
AWSParameter Store / Secrets Manager

五、Level 3 — 哈希(Hash)

5.1 哈希 vs 加密

特征加密(Encryption)哈希(Hash)
可逆性✅ 可解密❌ 不可逆
需要密钥✅ 是❌ 否
安全性密钥泄露 → 全部暴露无密钥可泄露
类比上锁的箱子(有钥匙就能开)绞肉机(不可能还原)

5.2 哈希函数的数学原理

正向计算(极快):  13 × 29 = 377        ← 毫秒级
反向计算(极慢):  377 的因子是? → 逐个试    ← 耗时很长

💡 真正的哈希函数远比因式分解复杂,使反向计算在当前算力下 不可行

5.3 使用 MD5

bash
npm i md5
javascript
const md5 = require("md5");

// 注册:哈希密码后存储
app.post("/register", (req, res) => {
  const newUser = new User({
    email: req.body.username,
    password: md5(req.body.password)  // "123456" → "e10adc..."
  });
  newUser.save(/* ... */);
});

// 登录:哈希输入的密码,与数据库中的哈希比较
app.post("/login", (req, res) => {
  User.findOne({ email: username }, (err, foundUser) => {
    if (foundUser.password === md5(password)) {
      res.render("secrets");  // 哈希匹配 → 密码正确
    }
  });
});

5.4 核心特性

相同输入 → 永远产生相同哈希
md5("123456") → 总是 "e10adc3949ba59abbe56e057f20f883e"

六、黑客如何破解哈希

6.1 哈希表攻击(Hash Table Attack)

如果数据库中多个用户有相同的哈希
→ 说明他们使用了相同的密码
→ 黑客构建"密码→哈希"对照表
→ 反查就能知道原始密码

6.2 构建哈希表

来源组合数量
字典中所有单词~150,000
电话号码簿~数百万
所有 ≤6 位字符组合~数十亿
总计~19.8 亿

用最新 GPU 计算 19.8 亿个 MD5 哈希需要多久?

0.9 秒。 (最新 GPU 每秒可计算 200 亿 个 MD5 哈希)

6.3 常见弱密码 Top 5

  1. 123456
  2. password
  3. qwerty
  4. 111111
  5. 123456789

6.4 密码强度的关键

6 位随机密码  → 标准 PC 3 秒破解
12 位随机密码 → 标准 PC 31 年,快速 GPU 2 年

💡 密码长度 >> 密码复杂度。 增加字符数的效果是 指数级 的。

6.5 实用工具

工具用途
haveibeenpwned.com检查你的邮箱是否在数据泄露中
plaintextoffenders.com曝光以明文存储密码的网站

七、Level 4 — 加盐哈希(bcrypt)

7.1 盐(Salt)是什么

普通哈希:password → hash(password) → 存入数据库
加盐哈希:password + 随机盐 → hash(password + salt) → 存入数据库
  • = 随机生成的字符串,每个用户不同
  • 即使两个用户密码相同,因为盐不同 → 哈希也不同
  • 盐存储在数据库中(不需要保密,但需要保存)

7.2 Salt Rounds(加盐轮数)

第 1 轮:hash(password + salt) → hash1
第 2 轮:hash(hash1 + salt)    → hash2
第 3 轮:hash(hash2 + salt)    → hash3
...
第 N 轮:hash(hashN-1 + salt)  → 最终哈希

每增加 1 轮 → 计算时间 翻倍

7.3 MD5 vs bcrypt

指标MD5bcrypt
每秒哈希数(最新 GPU)200 亿17,000
构建加盐哈希表耗时~3 秒~8 个月
设计目的快速校验故意设计得慢

7.4 使用 bcrypt

bash
npm i bcrypt
javascript
const bcrypt = require("bcrypt");
const saltRounds = 10;  // 2024-2025 推荐值

// 注册:生成盐 + 哈希
app.post("/register", (req, res) => {
  bcrypt.hash(req.body.password, saltRounds, (err, hash) => {
    // hash 已包含盐信息
    const newUser = new User({
      email: req.body.username,
      password: hash          // 存储哈希(内含盐)
    });
    newUser.save(/* ... */);
  });
});

// 登录:用 bcrypt.compare 对比
app.post("/login", (req, res) => {
  User.findOne({ email: username }, (err, foundUser) => {
    if (foundUser) {
      bcrypt.compare(password, foundUser.password, (err, result) => {
        if (result === true) {
          res.render("secrets");
        }
      });
    }
  });
});

7.5 Salt Rounds 与时间

Salt Rounds每次哈希耗时
10~100ms
12~400ms
15~3s
20~1.5min
31~2-3 天

💡 推荐值:10(2024-2025),随硬件提升逐年增加。


Cookie = 网站存储在 浏览器 上的小数据,类似 幸运饼干(里面有一条信息)。

Day 1:
浏览器 → GET amazon.com → 服务器返回首页
浏览器 → POST "加入购物车:电脑" → 服务器创建 Cookie → 返回给浏览器保存

Day 2:
浏览器 → GET amazon.com(Cookie 随请求发送)→ 服务器读取 Cookie → "这个用户昨天想买电脑"
→ 返回页面 + 购物车中已有电脑

8.2 Session 是什么

Session = 浏览器与服务器的一次 交互周期

登录 → Session 开始 → Cookie 创建(包含 Session ID)
浏览网站 → Cookie 随每次请求发送 → 服务器验证 Session
登出 → Session 结束 → Cookie 销毁

8.3 使用 Passport.js

安装

bash
npm i passport passport-local passport-local-mongoose express-session

注意

express-session(单数),不是 express-sessions(复数)!

配置(顺序很重要!)

javascript
// 1. 引入包
const session = require("express-session");
const passport = require("passport");
const passportLocalMongoose = require("passport-local-mongoose");

// 2. 配置 Session(在 mongoose.connect 之前)
app.use(session({
  secret: "Our little secret.",
  resave: false,
  saveUninitialized: false
}));

// 3. 初始化 Passport(紧跟 session 之后)
app.use(passport.initialize());
app.use(passport.session());

// 4. Schema 添加插件
userSchema.plugin(passportLocalMongoose);

// 5. 创建 Model 后,配置序列化/反序列化
passport.use(User.createStrategy());
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

代码顺序

session 配置 → passport.initialize → passport.session
→ Schema 插件 → Model 创建 → serialize/deserialize
→ 路由定义

顺序错误 = 无法工作!

序列化与反序列化

概念作用类比
Serialize将用户信息 塞入 Cookie把信息塞进幸运饼干
Deserialize从 Cookie 中 提取 用户信息掰开饼干读取信息

注册

javascript
app.post("/register", (req, res) => {
  User.register(
    { username: req.body.username },  // 用户名
    req.body.password,                 // 密码
    (err, user) => {
      if (err) {
        console.log(err);
        res.redirect("/register");
      } else {
        passport.authenticate("local")(req, res, () => {
          res.redirect("/secrets");  // 认证成功 → 重定向
        });
      }
    }
  );
});

登录

javascript
app.post("/login", (req, res) => {
  const user = new User({
    username: req.body.username,
    password: req.body.password
  });

  req.login(user, (err) => {
    if (err) {
      console.log(err);
    } else {
      passport.authenticate("local")(req, res, () => {
        res.redirect("/secrets");
      });
    }
  });
});

受保护的路由

javascript
app.get("/secrets", (req, res) => {
  if (req.isAuthenticated()) {
    res.render("secrets");     // ✅ 已登录 → 显示页面
  } else {
    res.redirect("/login");    // ❌ 未登录 → 强制登录
  }
});

登出

javascript
app.get("/logout", (req, res) => {
  req.logout();
  res.redirect("/");
});

8.4 Session 生命周期

注册/登录 → passport.authenticate("local")
  → 创建 Session → 序列化用户 → Cookie 保存到浏览器
  → 后续请求自动携带 Cookie → 服务器反序列化 → 验证身份

关闭浏览器 → Cookie 过期 → Session 结束 → 需要重新登录
服务器重启 → Session 丢失 → 需要重新登录

九、Level 6 — OAuth 2.0

9.1 什么是 OAuth

OAuth = Open Authorization — 开放授权标准。

允许用户通过第三方(Google/Facebook)登录你的网站,无需向你透露密码

9.2 为什么用 OAuth

优势说明
安全委托密码管理交给 Google/Facebook,他们有更强的安全团队
用户体验一键登录,无需注册新账户
数据获取经用户授权,可访问第三方数据(联系人、好友列表等)
减少责任不存储用户密码 = 泄露风险大幅降低

9.3 OAuth 三大特性

特性说明
细粒度权限可指定需要的数据范围(profile/email/friends)
读写控制只读(获取信息)或读写(代发帖子)
可撤销用户可随时在第三方平台取消授权

9.4 OAuth 2.0 流程

1. 开发者注册应用
   在 Google Developer Console 注册 → 获取 Client ID + Client Secret

2. 用户点击"使用 Google 登录"
   → 跳转到 Google 登录页面(用户在 Google 的界面上操作)

3. 用户登录并授权
   → 用户同意授予你的应用所请求的权限(profile, email)

4. Google 返回 Authorization Code
   → 你的服务器收到授权码 → 确认用户身份

5. 交换 Access Token(可选)
   → 用授权码换取 Access Token → 可长期访问用户授权的数据
Authorization Code = 🎫 单次入场券(一次性,只用于认证)
Access Token       = 🪪 年卡(长期有效,可反复获取数据)

9.5 实现 Google OAuth

步骤 1:Google Developer Console 配置

  1. 创建项目 → 设置 OAuth Consent Screen
  2. 创建 OAuth 2.0 凭据 → 获取 Client IDClient Secret
  3. 设置 Authorized Redirect URI:http://localhost:3000/auth/google/secrets
  4. 将凭据保存到 .env
env
CLIENT_ID=your-google-client-id
CLIENT_SECRET=your-google-client-secret

步骤 2:安装与配置

bash
npm i passport-google-oauth20 mongoose-findorcreate
javascript
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const findOrCreate = require("mongoose-findorcreate");

// Schema 中添加 Google ID 字段
const userSchema = new mongoose.Schema({
  email: String,
  password: String,
  googleId: String    // ← 新增:关联 Google 账户
});

// 添加插件
userSchema.plugin(passportLocalMongoose);
userSchema.plugin(findOrCreate);

const User = new mongoose.model("User", userSchema);

步骤 3:配置通用序列化(支持所有策略)

javascript
// 替换 passport-local-mongoose 的简化版本
// 使用 passport 通用版本(兼容所有认证策略)
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, user) => {
    done(err, user);
  });
});

步骤 4:配置 Google 策略

javascript
passport.use(new GoogleStrategy({
    clientID: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
    callbackURL: "http://localhost:3000/auth/google/secrets",
    userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
    // ↑ 使用 userinfo 端点(Google+ API 已弃用)
  },
  (accessToken, refreshToken, profile, cb) => {
    // profile.id = Google 用户唯一 ID
    User.findOrCreate({ googleId: profile.id }, (err, user) => {
      return cb(err, user);
    });
  }
));

步骤 5:路由设置

javascript
// 发起 Google 登录
app.get("/auth/google",
  passport.authenticate("google", { scope: ["profile"] })
);

// Google 回调(认证后重定向)
app.get("/auth/google/secrets",
  passport.authenticate("google", { failureRedirect: "/login" }),
  (req, res) => {
    res.redirect("/secrets");
  }
);

步骤 6:前端按钮

html
<a class="btn btn-social btn-google" href="/auth/google" role="button">
  <i class="fab fa-google"></i> Sign Up with Google
</a>

9.6 数据库中的 OAuth 用户

json
{
  "_id": "ObjectId(...)",
  "googleId": "104721833...",    // Google 唯一标识
  "username": "user@gmail.com"
  // 没有 password!密码由 Google 管理
}

💡 同一 Google 用户再次登录 → findOrCreate 找到已有记录 → 不会创建重复用户。


十、六级安全对比

级别方法密码存储形式主要弱点
L1明文123456任何人可直接读取
L2加密(AES)二进制密文密钥泄露 → 全部解密
L3哈希(MD5)e10adc3949ba...哈希表/字典攻击可破解
L4加盐哈希(bcrypt)$2b$10$xyz...理论上可破解但耗时极长
L5Cookie/Session不直接暴露Session 管理复杂
L6OAuth 2.0不存储密码依赖第三方服务可用性

十一、核心包速查

包名用途
mongoose-encryptionAES 加密/解密 mongoose 字段
dotenv环境变量管理
md5MD5 哈希(不推荐生产使用)
bcrypt加盐哈希(推荐 saltRounds=10)
express-sessionSession 管理
passport认证框架
passport-local本地策略(用户名 + 密码)
passport-local-mongoosePassport + Mongoose 集成
passport-google-oauth20Google OAuth 2.0 策略
mongoose-findorcreatefindOrCreate 方法实现

十二、完整项目结构

secrets-project/
├── .env                    # 密钥(不提交到 Git)
├── .gitignore              # 忽略 .env 和 node_modules
├── app.js                  # 主服务器
├── package.json
├── public/
│   └── css/
│       ├── styles.css
│       └── bootstrap-social.css
└── views/
    ├── partials/
    │   ├── header.ejs
    │   └── footer.ejs
    ├── home.ejs
    ├── login.ejs            # 本地登录 + Google 登录按钮
    ├── register.ejs         # 本地注册 + Google 注册按钮
    ├── secrets.ejs          # 需要认证才能访问
    └── submit.ejs

← 返回 Web 开发研究