《大前端与AI实战》第三次课程:认证与核心业务API

master
zephyr 7 months ago
parent 9fe350d667
commit 4ef5e8adb4

@ -1,698 +0,0 @@
好的这是为您的《大前端与AI实战》实训课程设计的第二次课程的详细内容。本次课程承接上一次的 "Hello World" 服务器,正式进入后端核心功能的开发,引入数据库并实现第一个关键业务——用户注册。
---
### **《大前端与AI实战》第二次课程API设计与数据库实战**
**课程主题:** 从“暂存”到“永存”用户系统的API设计与数据库集成
**总时长:** 4学时 (约3-3.5小时教学,半小时答疑与休息)
#### **一、 本次课程目标 (Objectives)**
在本次课程结束后,每位同学都应该能够:
1. **理解** RESTful API 的设计理念和常用 HTTP 动词GET, POST
2. **了解** NoSQL 数据库 `MongoDB` 的基本概念,并**成功创建**一个免费的云数据库MongoDB Atlas
3. **使用 `Mongoose`** 在 Node.js 项目中连接到 MongoDB 数据库。
4. **设计** 用户数据模型Schema并**实现**一个完整的、**安全的**用户注册 API 接口。
5. **掌握 `Postman`** 的基本使用,以测试后端 API 接口。
#### **二、 核心关键词 (Keywords)**
* RESTful API
* HTTP 动词 (GET, POST)
* 数据库 (Database)
* MongoDB / MongoDB Atlas
* Mongoose (ODM)
* Schema (数据模型)
* 密码哈希 (Password Hashing)
* `bcrypt.js`
* Postman
---
### **三、 详细教学流程 (Step-by-Step Guide)**
---
#### **第一部分:理论先行 - 什么是好的 API (约30分钟)**
**教师讲解:**
1. **回顾上次课内容**
* “上节课我们成功搭建了环境,并用 Express 创建了一个能说Hello的服务器。大家的挑战任务完成了吗检查/解答学生关于添加新路由的问题)今天,我们要让我们的服务器变得更强大,能真正地处理和存储数据。”
2. **API 如同餐厅菜单 - RESTful 风格**
* **讲解:** “API应用程序接口就像是前后端之间的合同菜单。前端顾客通过这份菜单点菜后端厨房根据菜单上的菜品准备并上菜。”
* “RESTful 是一种流行的 API 设计风格,它让这个‘菜单’变得非常清晰、规范。”
* **核心原则讲解(简化版):**
* **资源 (Resource):** 把你的数据看作是一种资源。比如“用户”、“知识点”都是资源。API 的 URL 应该表示资源,如 `/users`, `/knowledge-points`
* **动词 (Verb):** 使用 HTTP 动词来表示对资源的操作。
* `GET`: 获取资源 (查) - “服务员,给我看看所有用户列表。”
* `POST`: 创建资源 (增) - “服务员,帮我新注册一个用户。”
* `PUT` / `PATCH`: 更新资源 (改) - “服务员,帮我修改一下这个用户的信息。”
* `DELETE`: 删除资源 (删) - “服务员,帮我注销这个用户。”
* **举例:** “所以,我们要做的‘用户注册’功能,本质上是**创建**一个新的**用户资源**,因此我们应该设计一个 `POST /api/users` 这样的接口。” (加个`/api`前缀是好习惯用于区分API和其他路由)
---
#### **第二部分:数据仓库 - 拥抱 MongoDB (约60分钟)**
**教师引导,学生动手操作:**
“我们的用户数据现在还不能永久保存,服务器一重启就没了。我们需要一个‘数据仓库’——数据库。我们选择 MongoDB它灵活、现代非常适合我们的项目。”
1. **为什么是 MongoDB**
* **讲解:** “它是一个 NoSQL 数据库,存储的是类似 JSON 的 BSON 文档。它不需要预先定义严格的表格结构,非常灵活,开发速度快,非常适合我们这种快速迭代的项目。”
2. **创建免费云数据库 (MongoDB Atlas)**
* **讲解:** “我们不在自己电脑上安装数据库,而是使用云服务。这样更方便,也更接近真实的企业开发环境。”
* **操作指引 (一步步带领学生完成):**
1. 访问 [MongoDB Atlas 官网](https://www.mongodb.com/cloud/atlas)。
2. 注册账号并登录。
3. 创建一个新的项目 (Project)。
4. **构建一个数据库 (Build a Database)**,选择免费的 **M0 Free Tier**
5. 选择云服务商和区域(保持默认即可),然后创建集群 (Create Cluster)。等待几分钟集群部署。
6. **关键步骤 - 设置安全连接:**
* **创建数据库用户:** 在左侧 "Database Access" 下,创建一个用户。**务必记下用户名和密码**,比如 `user: feynman-user`, `password: a_strong_password`
* **配置网络访问:** 在左侧 "Network Access" 下添加一个IP地址。为了方便教学可以选择 `ALLOW ACCESS FROM ANYWHERE` (0.0.0.0/0)。**并强调在真实项目中应该只允许特定IP访问。**
**详细配置步骤:**
1. 登录到您的 MongoDB Atlas账户
2. 选择您的项目Cluster0
3. 在左侧导航栏中,点击"Network Access"(网络访问)
4. 点击"Edit"按钮
5. 在弹出的窗口中,选择"ALLOW ACCESS FROM ANYWHERE"允许从任何地方访问选项这会添加0.0.0.0/0的IP地址
6. 点击"Confirm"(确认)按钮
完成这些步骤后您的数据库将允许来自任何IP地址的连接。配置完成后可以启动服务器并测试MongoDB连接是否成功。
7. **获取连接字符串:** 回到 "Database" 视图,点击 "Connect" -> "Drivers"。Node.js 版本选择最新的。复制提供的**连接字符串 (Connection String)**。它看起来像这样:`mongodb+srv://feynman-user:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority`
---
#### **第三部分:连接世界 - Mongoose 实战 (约75分钟)**
**教师带领学生一步步敲代码:**
“现在,我们有了数据库的地址,接下来就要在我们的代码里建立连接。”
1. **安装 Mongoose 和 dotenv**
* 在 VS Code 终端里,输入:
```bash
npm install mongoose dotenv
```
* **讲解:** "`mongoose` 是连接 MongoDB 的‘桥梁’,它能帮我们更好地组织数据。`dotenv` 是一个能读取 `.env` 配置文件的工具,可以安全地管理我们的敏感信息,比如数据库密码。"
2. **配置环境变量**
* 在项目根目录,新建一个文件,命名为 `.env` (注意,前面有个点)。
* 在 `.env` 文件里写入:
```
MONGO_URI=mongodb+srv://feynman-user:YOUR_PASSWORD@cluster0.xxxxx.mongodb.net/feynman-db?retryWrites=true&w=majority
```
* **重要提示:**
* 把 `YOUR_PASSWORD` 换成你刚才创建的数据库用户的真实密码。
* 把 `cluster0.xxxxx.mongodb.net` 换成你自己的连接字符串。
* 我在末尾添加了数据库名 `feynman-db`Mongoose 会自动创建它。
* 在项目根目录,再新建一个 `.gitignore` 文件,并写入一行:
```
.env
node_modules/
```
* **讲解:** “`.gitignore` 告诉 Git 哪些文件**不要**上传到代码仓库。我们绝不能把包含密码的 `.env` 文件和庞大的 `node_modules` 文件夹提交上去!”
3. **`index.js` 中建立连接并配置中间件**
* 修改 `index.js` 文件,引入并使用我们新安装的工具。
```javascript
// index.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors'); // 1. 引入cors
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000; // 优先使用环境变量中的端口
// --- 核心中间件 ---
// 2. 使用cors中间件 - 解决跨域问题
// 讲解CORS (Cross-Origin Resource Sharing) 是一个必需的步骤。当我们的前端比如运行在localhost:5173
// 尝试请求后端运行在localhost:3000浏览器会出于安全策略阻止它。
// `cors()` 中间件会自动添加必要的响应头,告诉浏览器“我允许那个地址的请求”,从而让前后端可以顺利通信。
app.use(cors());
// 3. 使用express.json()中间件 - 解析请求体
// 讲解这个中间件让我们的Express应用能够识别并处理传入的JSON格式数据比如用户注册时POST的用户名和密码
app.use(express.json());
// --- 数据库连接 ---
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));
// ... (后续的API路由)
app.listen(port, () => {
console.log(`Feynman Platform backend is running at http://localhost:${port}`);
});
```
* **运行与验证:** 保存文件,在终端中**重启服务器** (`Ctrl+C` 停止,然后 `node index.js` 启动)。如果看到 `MongoDB connected successfully!`,恭喜你,代码世界和数据世界已经成功握手!
4. **实现用户注册功能**
* **安装密码加密工具:**
```bash
npm install bcryptjs
```
**讲解:** “**安全第一!我们绝不能明文存储用户密码!**`bcryptjs`会把用户的密码加密成一串谁也看不懂的乱码(哈希值),即使数据库泄露了,用户的密码也是安全的。”
* **创建用户模型 (Schema):** 在项目根目录新建一个文件夹 `models`,在其中新建 `User.js` 文件。
```javascript
// models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
}, { timestamps: true }); // timestamps会自动添加createdAt和updatedAt字段
module.exports = mongoose.model('User', UserSchema);
```
* **创建API路由 (Route):** 回到 `index.js`,添加注册路由。
```javascript
// 在 index.js 的顶部引入
const User = require('./models/User');
const bcrypt = require('bcryptjs');
// ...
// 中间件让Express能解析JSON格式的请求体
app.use(express.json());
// --- API 路由 ---
// POST /api/register - 用户注册
app.post('/api/register', async (req, res) => {
try {
// 1. 从请求体中获取用户名、邮箱、密码
const { username, email, password } = req.body;
// 2. 检查用户或邮箱是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'Email already exists' });
}
user = await User.findOne({ username });
if (user) {
return res.status(400).json({ msg: 'Username already exists' });
}
// 3. 创建新用户实例
user = new User({ username, email, password });
// 4. 对密码进行哈希加密
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// 5. 将新用户保存到数据库
await user.save();
// 6. 返回成功信息 (暂时不返回token)
res.status(201).json({ msg: 'User registered successfully' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// ... (app.listen)
```
---
#### **第四部分测试我们的API - Postman与curl命令 (约20分钟)**
**教师演示,学生模仿:**
“我们的注册接口写好了但是怎么用呢现在还没有前端页面所以我们有两种方式来测试API专业的API测试工具Postman或者使用命令行工具curl。”
##### **方法一使用Postman图形界面工具**
1. 下载并安装 [Postman](https://www.postman.com/downloads/)。
2. 打开 Postman新建一个请求 (New Request)。
3. **配置请求:**
* 方法 (Method) 选择 `POST`
* URL 输入 `http://localhost:3000/api/register`
* 点击 "Body" 标签页。
* 选择 "raw",然后在右侧的下拉菜单中选择 "JSON"。
* 在文本框中输入要注册的用户信息:
```json
{
"username": "test_student",
"email": "student@test.com",
"password": "a_secure_password123"
}
```
4. 点击 "Send" 按钮发送请求。
5. **观察结果:**
* **成功:** 在下方的响应区 (Response),你应该会看到 `Status: 201 Created``{"msg": "User registered successfully"}`
* **失败(重复注册):** 如果你再点一次 "Send",应该会看到 `Status: 400 Bad Request``{"msg": "Email already exists"}`
* **检查数据库:** 回到 MongoDB Atlas 网站,在 "Database" -> "Browse Collections" 中,你会看到一个 `feynman-db` 数据库和 `users` 集合,里面已经有了你刚刚创建的用户数据,并且密码是一长串乱码!
##### **方法二使用curl命令命令行工具**
“如果你不想安装额外的图形界面工具或者更喜欢使用命令行curl是一个很好的选择。curl是一个强大的命令行工具用于发送HTTP请求。”
1. **打开终端**在macOS上可以使用Terminal或iTerm
2. **确保服务器正在运行**
```bash
node index.js
```
**重要提示:** 确保只有一个node进程在运行。如果之前已经启动了服务器请先使用`Ctrl+C`停止它然后再重新启动。多个node进程同时运行可能会导致端口冲突或请求处理异常。
3. **使用curl发送POST请求**
```bash
curl -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"username":"test_student","email":"student@test.com","password":"a_secure_password123"}'
```
**命令解释:**
* `-X POST`: 指定HTTP方法为POST
* `-H "Content-Type: application/json"`: 设置请求头告诉服务器我们发送的是JSON数据
* `-d '{"username":"test_student",...}'`: 设置请求体,包含要注册的用户信息
4. **观察结果:**
* **成功:** 终端会直接显示响应结果:`{"msg":"User registered successfully"}`
* **失败(重复注册):** 如果再次运行相同的命令,会看到:`{"msg":"Email already exists"}`
5. **使用curl进行更多测试**
* **测试GET请求**(获取所有用户,如果实现了这个接口):
```bash
curl http://localhost:3000/api/users
```
* **详细模式**(查看完整的请求和响应信息):
```bash
curl -v -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"username":"another_user","email":"another@test.com","password":"password123"}'
```
`-v`参数会显示详细的连接信息、请求头和响应头,对于调试非常有用。
**两种方法的比较:**
* **Postman**:图形界面友好,适合初学者,可以保存和组织请求,有更丰富的测试功能。
* **curl**无需安装额外工具macOS自带适合快速测试可以轻松集成到脚本中是服务器开发必备技能。
**重要提醒:**
无论使用哪种方法测试API都**确保只有一个node进程在运行**。如果在测试过程中遇到问题首先检查是否有多个node进程同时运行这可能导致请求无法正确处理。可以使用以下命令检查和终止node进程
```bash
# 查看所有node进程
ps aux | grep node
# 终止指定进程将PID替换为实际的进程ID
kill -9 PID
```
---
#### **五、 课堂总结与作业 (15分钟)**
* **总结:**
* “今天我们取得了巨大的进步我们设计了第一个RESTful API连接了云数据库并用工业级的安全标准实现了用户注册功能。大家现在已经是一个初具雏形的后端工程师了
* **课后作业:**
1. **必须完成:** 确保用户注册功能在你的本地可以成功运行,并能通过 Postman 创建用户。
2. **挑战任务(为下次课做准备):**
* **实现用户登录API** 创建一个新的路由 `POST /api/login`
* **逻辑提示:**
1. 接收 `email``password`
2. 根据 `email` 在数据库中查找用户。
3. 如果用户不存在,返回错误。
4. 如果用户存在,使用 `bcrypt.compare(password, user.password)` 来比较用户输入的密码和数据库中存储的哈希密码是否匹配。
5. 如果匹配,返回成功信息;如果不匹配,返回“密码错误”。
* **预告下次课内容:**
* “下次课,我们将在登录成功后,使用 JWT (JSON Web Token) 技术给用户颁发一个通行证实现真正的用户认证。然后我们将开始开发项目的核心功能——知识点的增删改查API
**答疑环节,课程结束。**
---
#### **六、 进阶技巧与最佳实践 (选学内容)**
##### **1. 错误处理和日志记录的最佳实践**
在实际开发中,良好的错误处理和日志记录对于调试和监控应用程序至关重要。我们可以改进当前的错误处理方式:
```javascript
// 创建一个中间件来处理错误
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '服务器内部错误',
error: process.env.NODE_ENV === 'development' ? err.message : {}
});
});
// 在路由中使用更详细的错误处理
app.post('/api/register', async (req, res) => {
try {
// ... 现有代码 ...
} catch (err) {
console.error(`用户注册失败: ${err.message}`);
res.status(500).json({
success: false,
message: '用户注册失败',
error: process.env.NODE_ENV === 'development' ? err.message : {}
});
}
});
```
##### **2. API响应格式标准化**
为了使前端更容易处理各种响应建议使用统一的API响应格式
```javascript
// 创建一个辅助函数来生成标准响应
const createResponse = (success, data = null, message = '', error = null) => {
return {
success,
data,
message,
error
};
};
// 在路由中使用标准响应格式
app.post('/api/register', async (req, res) => {
try {
// ... 现有代码 ...
// 返回成功信息
res.status(201).json(createResponse(true, null, '用户注册成功'));
} catch (err) {
// ... 错误处理代码 ...
// 返回错误信息
res.status(500).json(createResponse(false, null, '用户注册失败', err.message));
}
});
```
##### **3. 环境变量的更多使用**
除了数据库连接字符串,还可以使用环境变量来管理其他配置:
```javascript
// .env 文件
PORT=3000
NODE_ENV=development
MONGO_URI=mongodb+srv://...
JWT_SECRET=your_jwt_secret
LOG_LEVEL=info
BCRYPT_ROUNDS=10
```
然后在代码中使用这些环境变量:
```javascript
const port = process.env.PORT || 3000;
const nodeEnv = process.env.NODE_ENV || 'development';
const jwtSecret = process.env.JWT_SECRET;
const logLevel = process.env.LOG_LEVEL || 'info';
const bcryptRounds = parseInt(process.env.BCRYPT_ROUNDS) || 10;
// 使用bcryptRounds替代硬编码的值
const salt = await bcrypt.genSalt(bcryptRounds);
```
##### **4. 代码组织结构优化**
随着项目规模的增长,将代码组织到不同的文件和文件夹中变得非常重要:
```
feynman-platform-backend/
├── src/
│ ├── controllers/ # 控制器 - 处理请求逻辑
│ │ └── userController.js
│ ├── models/ # 数据模型
│ │ └── User.js
│ ├── routes/ # 路由定义
│ │ └── userRoutes.js
│ ├── middleware/ # 中间件
│ │ └── errorHandler.js
│ └── utils/ # 工具函数
│ └── responseHelper.js
├── .env
├── index.js
└── package.json
```
示例:将用户路由分离到单独文件
```javascript
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
// 用户注册路由
router.post('/register', userController.register);
// 用户登录路由
router.post('/login', userController.login);
module.exports = router;
```
```javascript
// src/controllers/userController.js
const User = require('../models/User');
const bcrypt = require('bcryptjs');
const { createResponse } = require('../utils/responseHelper');
exports.register = async (req, res) => {
try {
// ... 用户注册逻辑 ...
res.status(201).json(createResponse(true, null, '用户注册成功'));
} catch (err) {
// ... 错误处理 ...
res.status(500).json(createResponse(false, null, '用户注册失败', err.message));
}
};
exports.login = async (req, res) => {
// ... 用户登录逻辑 ...
};
```
```javascript
// index.js
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json());
// 路由
app.use('/api/users', require('./src/routes/userRoutes'));
// 数据库连接
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));
// 错误处理中间件
app.use(require('./src/middleware/errorHandler'));
app.listen(port, () => {
console.log(`Feynman Platform backend is running at http://localhost:${port}`);
});
```
##### **5. API文档工具介绍**
Swagger/OpenAPI是一个强大的API文档工具可以帮助你自动生成交互式API文档
1. 安装必要的包:
```bash
npm install swagger-jsdoc swagger-ui-express
```
2. 创建Swagger配置
```javascript
// src/utils/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '费曼学习平台 API',
version: '1.0.0',
description: '费曼学习平台后端API文档',
},
servers: [
{
url: 'http://localhost:3000',
},
],
},
apis: ['./src/routes/*.js'], // 包含API注解的文件路径
};
const specs = swaggerJsdoc(options);
module.exports = {
serve: swaggerUi.serve,
setup: swaggerUi.setup(specs),
};
```
3. 在路由中添加Swagger注解
```javascript
// src/routes/userRoutes.js
/**
* @swagger
* /api/users/register:
* post:
* summary: 用户注册
* description: 创建一个新用户账户
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* username:
* type: string
* email:
* type: string
* password:
* type: string
* responses:
* 201:
* description: 用户注册成功
* 400:
* description: 用户已存在或请求数据无效
* 500:
* description: 服务器内部错误
*/
router.post('/register', userController.register);
```
4. 在index.js中添加Swagger UI路由
```javascript
const { serve, setup } = require('./src/utils/swagger');
// ...
// Swagger UI
app.use('/api-docs', serve, setup);
// ...
```
5. 访问 `http://localhost:3000/api-docs` 查看交互式API文档。
##### **6. 单元测试入门**
单元测试是确保代码质量和功能正确性的重要手段。以下是使用Jest进行API测试的简单示例
1. 安装测试相关包:
```bash
npm install --save-dev jest supertest mongodb-memory-server
```
2. 创建测试配置:
```json
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}
```
3. 创建测试文件:
```javascript
// tests/user.test.js
const request = require('supertest');
const app = require('../index');
const User = require('../src/models/User');
const mongoose = require('mongoose');
describe('User API', () => {
beforeAll(async () => {
// 连接到测试数据库
await mongoose.connect(process.env.MONGO_TEST_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// 断开数据库连接
await mongoose.connection.close();
});
beforeEach(async () => {
// 每次测试前清空用户集合
await User.deleteMany({});
});
describe('POST /api/users/register', () => {
it('应该成功注册一个新用户', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toEqual(201);
expect(res.body.success).toBe(true);
expect(res.body.message).toBe('用户注册成功');
});
it('不应该允许重复的邮箱注册', async () => {
// 先注册一个用户
await request(app)
.post('/api/users/register')
.send({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
});
// 尝试使用相同的邮箱再次注册
const res = await request(app)
.post('/api/users/register')
.send({
username: 'testuser2',
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toEqual(400);
expect(res.body.success).toBe(false);
});
});
});
```
4. 运行测试:
```bash
npm test
```
这些进阶技巧和最佳实践可以帮助你构建更加健壮、可维护和可扩展的后端应用程序。随着项目的发展,你会越来越体会到这些实践的价值。

@ -0,0 +1,436 @@
好的这是为您的《大前端与AI实战》实训课程设计的第三次课程的详细内容。本次课程将完成后端认证闭环并构建项目核心的CRUD功能为后续的前端开发和AI集成打下坚实的基础。
---
### **《大前端与AI实战》第三次课程认证与核心业务API**
**课程主题:** 颁发“通行证”JWT认证与知识点CRUD实战
**总时长:** 4学时 (约3-3.5小时教学,半小时答疑与休息)
#### **一、 本次课程目标 (Objectives)**
在本次课程结束后,每位同学都应该能够:
1. **理解** 基于 Token 的认证流程,特别是 JWT (JSON Web Token) 的工作原理。
2. **完成** 用户登录API并在登录成功后生成并返回 JWT。
3. **创建** 一个认证中间件Middleware用于保护需要登录才能访问的API路由。
4. **设计** 知识点Knowledge Point的数据模型Schema
5. **实现** 针对知识点的完整 CRUD (Create, Read, Update, Delete) API。
6. **熟练使用 Postman** 发送带有认证信息的请求Bearer Token
#### **二、 核心关键词 (Keywords)**
* 认证 (Authentication)
* JWT (JSON Web Token)
* 中间件 (Middleware)
* CRUD (Create, Read, Update, Delete)
* 路由模块化 (Router)
* Bearer Token
---
### **三、 详细教学流程 (Step-by-Step Guide)**
---
#### **第一部分登录与JWT认证 (约75分钟)**
**教师讲解与带领编码:**
1. **回顾与挑战任务检查**
* “上节课我们实现了用户注册,并留了一个挑战任务——实现用户登录。有同学完成了吗?(邀请同学分享思路或代码,进行点评和引导)”
* “今天我们就来完善这个登录功能并引入一个至关重要的概念——JWT给登录成功的用户颁发一个数字身份证。”
2. **为什么需要 JWT**
* **讲解:** “HTTP协议是无状态的。这意味着服务器不会记住你上一次是谁。你登录了一次下次再请求别的接口服务器又不知道你是谁了。”
* “JWT就像一张有时效性的电影票。你登录买票成功后服务器给你一张票JWT。之后你看电影的任何环节访问其他需要登录的接口只需要出示这张票检票员服务器验证票是真的、没过期就让你通过而不需要你每次都重新出示身份证买票。”
* **JWT结构简述** Header头部、Payload载荷存放用户信息如用户ID、Signature签名防伪标识
3. **安装 JWT 工具包**
* 在 VS Code 终端里,输入:
```bash
npm install jsonwebtoken
```
4. **`.env` 文件中添加 JWT 密钥**
* **讲解:** “为了生成独一无二且无法伪造的签名,我们需要一个‘私钥’,只有我们的服务器知道。”
* 打开 `.env` 文件,添加一行:
```
JWT_SECRET=this_is_a_very_secret_string_for_feynman_platform
```
* **强调:** 这个密钥在真实项目中应该是一个更长、更随机的字符串。
5. **安全加固:安装与讲解密码加密库 `bcryptjs`**
* **讲解(重要):** “在上一节课,我们只是简单地创建了用户,但并没有处理最敏感的数据——密码。**在任何时候,我们都绝对不能将用户的原始密码直接存入数据库!** 这是最严重、最不可原谅的安全漏洞。一旦数据库泄露,所有用户的密码将曝光。”
* “我们将使用 `bcryptjs` 这个库来解决问题。它会对用户的密码进行哈希处理把它变成一串谁也看不懂的乱码。这个过程是单向的无法从乱码反推出原始密码但它却可以验证用户输入的密码是否正确。这是现代Web应用保护用户密码的标准做法。”
* **安装:**
```bash
npm install bcryptjs
```
6. **完善用户认证API (注册与登录)**
* 现在,我们将把密码加密集成到我们的注册和登录流程中。
* **创建API路由文件** (如果尚未创建) 为了让 `index.js` 不那么臃肿,我们把用户相关的路由分离出去。
* 新建文件夹 `routes`
* 在 `routes` 文件夹下新建 `users.js` 文件。
* **编写完整的 `routes/users.js`:**
```javascript
// routes/users.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs'); // 引入加密库
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// --- 用户注册 API (已集成密码加密) ---
// @route POST /api/users/register
// @desc 注册一个新用户
// @access Public
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// 1. 检查用户是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
// 2. 创建新用户实例
user = new User({
name,
email,
password
});
// 3. 【安全核心】对密码进行哈希加密
// 讲解:我们使用 bcrypt 库。它会先生成一个“盐”salt这是一个随机字符串
// 然后将盐和原始密码混合在一起进行哈希计算。
// 这样做可以确保即使两个用户设置了相同的密码,它们在数据库中的哈希值也完全不同。
const salt = await bcrypt.genSalt(10); // 10是安全强度数值越大越安全但越耗时
user.password = await bcrypt.hash(password, salt); // 生成加密后的密码
// 4. 保存用户到数据库
await user.save();
// 5. 注册成功直接生成JWT并返回实现注册后自动登录
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// --- 用户登录 API ---
// @route POST /api/users/login
// @desc 用户登录并获取token
// @access Public
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// 1. 检查用户是否存在
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 2. 【安全核心】比较密码
// 这里使用 bcrypt.compare 来安全地比较客户端传来的原始密码和数据库中存储的哈希密码。
// 它会自动处理盐值我们无需关心。只有密码匹配才会返回true。
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 3. 登录成功生成JWT
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
```
* **在 `index.js` 中使用用户路由:**
```javascript
// index.js
// ... (数据库连接等代码)
app.use(express.json());
// 使用路由文件
app.use('/api/users', require('./routes/users'));
// ... (app.listen)
```
**讲解:** "`app.use('/api/users', ...)` 的意思是,所有发往 `/api/users` 前缀的请求,都交给 `users.js` 这个路由文件去处理。所以 `users.js` 里的 `/register` 完整路径就是 `/api/users/register`。"
---
#### **第二部分保护API - 认证中间件 (约45分钟)**
**教师讲解与带领编码:**
“现在任何人都可以访问我们未来的知识点API这显然不行。我们需要一个保安——认证中间件来检查每个请求是否带有合法的电影票JWT。”
1. **中间件概念讲解**
* “中间件Middleware是 Express 的一个核心概念。它是一个函数,可以在请求到达最终处理函数**之前**执行一些操作。就像流水线上的一个工序,可以检查、修改请求。”
* “我们的认证中间件要做的事:检查请求头里有没有 JWT验证它是否有效如果有效就把用户信息附加到请求上然后放行如果无效就直接拒绝。”
2. **创建认证中间件**
* 新建文件夹 `middleware`
* 在 `middleware` 文件夹下新建 `auth.js` 文件。
```javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
// 1. 从请求头中获取token
const token = req.header('x-auth-token');
// 2. 检查token是否存在
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' }); // 401: 未授权
}
// 3. 验证token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 将解码后的用户信息特别是user.id附加到请求对象上
req.user = decoded.user;
// 调用next(),将控制权交给下一个中间件或路由处理器
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
```
**讲解:** “`req.header('x-auth-token')` 是一个约定俗成的标准前端会把Token放在这个请求头里发送过来。`req.user = decoded.user;` 这一步非常关键,它让后续的路由能直接从 `req.user.id` 中知道当前是哪个用户在操作。”
---
#### **第三部分:核心业务 - 知识点CRUD (约75分钟)**
**教师带领学生一步步敲代码:**
“认证系统完成了现在开始构建我们平台的核心——知识点管理功能。这部分是典型的后端业务开发我们会实现增、删、改、查全套API。”
1. **设计知识点数据模型 (Schema)**
* 在 `models` 文件夹下新建 `KnowledgePoint.js` 文件。
```javascript
// models/KnowledgePoint.js
const mongoose = require('mongoose');
const KnowledgePointSchema = new mongoose.Schema({
user: { // 关联到用户
type: mongoose.Schema.Types.ObjectId,
ref: 'User' // 引用User模型
},
title: {
type: String,
required: true
},
content: { // 存放Markdown, LaTeX, Mermaid等原始内容
type: String,
required: true
},
status: { // 学习状态: 'not_started', 'in_progress', 'mastered'
type: String,
default: 'not_started'
},
reviewList: { // 是否在复习列表中
type: Boolean,
default: false
}
}, { timestamps: true });
module.exports = mongoose.model('KnowledgePoint', KnowledgePointSchema);
```
2. **创建知识点路由文件**
* 在 `routes` 文件夹下新建 `knowledgePoints.js` 文件。
3. **实现CRUD API**
* 在 `routes/knowledgePoints.js` 中编写代码。
```javascript
// routes/knowledgePoints.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth'); // 引入认证中间件
const KnowledgePoint = require('../models/KnowledgePoint');
// @route POST /api/knowledge-points
// @desc 创建一个新的知识点
// @access Private (需要登录)
router.post('/', auth, async (req, res) => { // 在这里使用auth中间件
try {
const { title, content } = req.body;
const newKp = new KnowledgePoint({
title,
content,
user: req.user.id // 从auth中间件附加的req.user中获取用户ID
});
const kp = await newKp.save();
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points
// @desc 获取当前用户的所有知识点
// @access Private
router.get('/', auth, async (req, res) => {
try {
const kps = await KnowledgePoint.find({ user: req.user.id }).sort({ createdAt: -1 });
res.json(kps);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points/:id
// @desc 获取单个知识点详情
// @access Private
router.get('/:id', auth, async (req, res) => {
// ... (学生可以作为练习,实现获取单个知识点的逻辑)
});
// @route PUT /api/knowledge-points/:id
// @desc 更新一个知识点
// @access Private
router.put('/:id', auth, async (req, res) => {
try {
let kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
// 确保是该用户自己的知识点
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
const { title, content, status, reviewList } = req.body;
kp = await KnowledgePoint.findByIdAndUpdate(
req.params.id,
{ $set: { title, content, status, reviewList } },
{ new: true } // 返回更新后的文档
);
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route DELETE /api/knowledge-points/:id
// @desc 删除一个知识点
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
let kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
await KnowledgePoint.findByIdAndRemove(req.params.id);
res.json({ msg: 'Knowledge point removed' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
```
4. **`index.js` 中使用知识点路由**
```javascript
// index.js
// ...
app.use('/api/users', require('./routes/users'));
app.use('/api/knowledge-points', require('./routes/knowledgePoints')); // 新增
// ...
```
---
#### **第四部分:使用 Postman 进行带认证的测试 (15分钟)**
**教师演示,学生模仿:**
“现在我们的知识点API有了保安我们用 Postman 模拟登录用户,来测试这些新接口。”
1. **登录并获取 Token**
* 在 Postman 中,发送一个 `POST` 请求到 `http://localhost:3000/api/users/login`Body中提供正确的邮箱和密码。
* 成功后,你会从响应中得到一个很长的 `token` 字符串。**复制这个 token**。
2. **测试受保护的路由 (创建知识点):**
* 新建一个请求。
* 方法 `POST`URL `http://localhost:3000/api/knowledge-points`
* **关键步骤:添加认证头**
* 点击 "Authorization" (或 "Headers") 标签页。
* Type 选择 "Bearer Token"。
* 在右侧的 Token 输入框中,**粘贴你刚才复制的 token**。
* 点击 "Body" 标签页,选择 "raw" 和 "JSON",输入内容:
```json
{
"title": "什么是JWT",
"content": "# JWT (JSON Web Token)\n是一种开放标准..."
}
```
* 点击 "Send"。如果一切正常,你会收到 `200 OK` 和新创建的知识点数据。
* **尝试不带Token发送请求** 去掉 "Authorization" 头,再发送一次,你会收到 `401 Unauthorized``{"msg": "No token, authorization denied"}`。这证明我们的“保安”中间件生效了!
3. **测试其他CRUD接口** 鼓励学生自己尝试用 Postman 测试 `GET` (获取所有)、`PUT` (更新)、`DELETE` (删除) 接口。
---
#### **五、 课堂总结与作业 (15分钟)**
* **总结:**
* “今天我们构建了后端应用的龙骨——完整的认证系统和核心业务的CRUD。我们学会了如何使用JWT保护API如何用中间件来简化代码以及如何将业务逻辑模块化。后端的基础设施已经基本完备下一次课我们将正式进军前端领域
* **课后作业:**
1. **必须完成:** 确保所有用户和知识点的API都能在Postman中成功测试通过。
2. **代码完善:** 完成 `GET /api/knowledge-points/:id` 这个获取单个知识点详情的API。逻辑与更新和删除类似需要先查找再验证用户权限。
3. **预习:** 了解一下 React 的基本概念比如什么是组件Component、JSX语法。
* **预告下次课内容:**
* “后端准备就绪,‘弹药’已经装填好了!下次课,我们将使用强大的前端框架 React 和构建工具 Vite开始搭建费曼学习平台的用户界面让我们的应用第一次拥有面孔
**答疑环节,课程结束。**

@ -0,0 +1,298 @@
好的这是为您的《大前端与AI实战》实训课程设计的第四次课程的详细内容。从这次课开始我们将正式踏入前端开发的世界将之前构建的强大后端能力通过用户界面呈现出来。
---
### **《大前端与AI实战》第四次课程前端起航与界面搭建**
**课程主题:** 从“黑盒”到“视界”React项目初始化与基础布局
**总时长:** 4学时 (约3-3.5小时教学,半小时答疑与休息)
#### **一、 本次课程目标 (Objectives)**
在本次课程结束后,每位同学都应该能够:
1. **理解** 现代前端框架React的核心思想组件化和数据驱动视图。
2. **使用 `Vite`** 快速初始化一个现代化的 `React` 前端项目。
3. **掌握** `React` 的基础核心概念:`JSX` 语法、组件(函数式组件)、`Props` 和 `State`
4. **使用 `react-router-dom`** 为应用设置客户端路由实现单页面应用SPA的页面跳转。
5. **搭建** “费曼学习平台”的整体页面布局Layout包括导航栏、侧边栏和主内容区。
6. **创建** 登录Login和注册Register两个基础页面组件。
#### **二、 核心关键词 (Keywords)**
* React
* Vite
* 组件 (Component)
* JSX (JavaScript XML)
* Props (属性)
* State (状态) & `useState` Hook
* 单页面应用 (SPA - Single Page Application)
* `react-router-dom`
---
### **三、 详细教学流程 (Step-by-Step Guide)**
---
#### **第一部分:前端新世界 - 拥抱 React (约45分钟)**
**教师讲解:**
1. **回顾与承接**
* “在过去的三次课里,我们已经用 Node.js 构建了一个强大的‘引擎’——我们的后端服务。它能处理用户注册、登录,还能管理知识点。但目前,它还是一个‘黑盒子’,只能通过 Postman 和它交互。从今天起我们要为这个引擎装上车身仪表盘——也就是用户界面UI。”
2. **为什么是 React告别“刀耕火种”**
* **讲解:** “大家可能都写过原生的 HTML/CSS/JS甚至用过 jQuery。当页面变得复杂时手动操作 DOM文档对象模型会变得非常混乱和低效就像是在用锄头耕地。”
* “React 是一个由 Facebook现Meta推出的前端库它彻底改变了我们构建用户界面的方式。它就像是给我们配备了一台智能拖拉机。”
* **核心思想(通俗讲解):**
* **组件化 (Component-Based):** 把复杂的界面拆分成一个个独立、可复用的积木块(组件)。比如一个导航栏是一个组件,一个按钮也是一个组件。我们可以像搭积木一样组合它们,构建出整个应用。
* **数据驱动视图 (Data-Driven View):** 在 React 中我们不再直接去操作哪个div的颜色改成红色。我们只需要关心数据我们称之为`State`。当数据变化时React 会自动、高效地帮我们更新界面。我们只需要改变数据界面就会响应。这就是React这个名字的由来。
3. **介绍 Vite - 闪电般的构建工具**
* **讲解:** “要让 React 代码在浏览器里跑起来,需要一个构建工具。传统的工具有些慢。`Vite`(法语‘快’的意思)是一个新生代的构建工具,它的开发服务器启动速度极快,能提供极致的开发体验。我们将用它来创建我们的项目。”
---
#### **第二部分:创建你的第一个 React 应用 (约60分钟)**
**教师引导,学生动手操作:**
“理论听完了,让我们立刻动手,感受一下 Vite 和 React 带来的‘速度与激情’。”
1. **创建项目文件夹**
* 在你的电脑上,与 `feynman-platform-backend` **同级**的位置,创建一个新的文件夹,命名为 `feynman-platform-frontend`
* **讲解:** “前后端项目分离是一个非常好的工程实践,这让它们可以独立开发、独立部署。”
2. **使用 Vite 初始化项目**
* 用 VS Code 打开 `feynman-platform-frontend` 这个新文件夹。
* 在 VS Code 的集成终端里,运行命令:
```bash
npm create vite@latest
```
* Vite 会提出几个问题,请按如下方式回答:
* `✔ Project name: …` -> `.` (表示在当前文件夹下创建)
* `✔ Select a framework: ` -> 选择 `React`
* `✔ Select a variant: ` -> 选择 `JavaScript` (或 `JavaScript + SWC`)
* Vite 会生成项目文件,然后提示你运行:
```bash
npm install
npm run dev
```
* **运行与验证:** 当你运行 `npm run dev` 后,终端会显示一个本地地址,如 `http://localhost:5173`。在浏览器中打开它,你会看到一个旋转的 React Logo 和一个计数器。恭喜你,你的第一个 React 应用已经成功运行了!
3. **项目结构概览**
* **讲解(带着学生看文件):**
* `index.html`: 应用的入口 HTML 文件,注意里面只有一个 `<div id="root"></div>`,我们的整个 React 应用都会被挂载到这里。
* `src/`我们未来99%的代码都会在这里编写。
* `src/main.jsx`: 项目的 JavaScript 入口文件,它告诉 React 把我们的主组件 (`<App />`) 渲染到 `#root` div 里。
* `src/App.jsx`: 项目的根组件。我们现在看到的内容就是在这里定义的。
* `package.json`: 前端项目的“身份证”,记录了依赖和脚本命令。
---
#### **第三部分React 核心 - 组件与路由 (约90分钟)**
**教师带领学生一步步敲代码:**
“现在我们要清理掉 Vite 的默认模板,开始搭建我们自己的‘费曼学习平台’。”
1. **清理项目**
* 删除 `src/App.css`, `src/assets/react.svg`
* 清空 `src/index.css` 里的所有内容。
* 修改 `src/App.jsx` 为最简单的结构:
```jsx
// src/App.jsx
function App() {
return (
<div>
<h1>欢迎来到费曼学习平台</h1>
</div>
);
}
export default App;
```
**讲解 JSX** “这种在 JavaScript 里写 HTML 标签的语法就叫 JSX。它不是 HTML但非常相似最终会被编译成普通的 JavaScript。这是 React 的一大特色。”
2. **安装路由工具 `react-router-dom`**
* 在终端里(确保在前端项目目录下),运行:
```bash
npm install react-router-dom
```
* **讲解:** “单页面应用SPA并不会在切换页面时真的向服务器请求新的 HTML。它只是在客户端用 JavaScript 动态地切换显示的组件,`react-router-dom` 就是帮我们实现这个‘假跳转’的工具。”
3. **配置路由**
* 修改 `src/main.jsx` 来包裹我们的应用:
```jsx
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';
import { BrowserRouter } from 'react-router-dom'; // 1. 引入
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter> {/* 2. 用BrowserRouter包裹App */}
<App />
</BrowserRouter>
</React.StrictMode>,
);
```
4. **创建页面组件**
* 在 `src/` 下新建一个文件夹 `pages`
* 在 `src/pages` 中创建两个文件:
* `LoginPage.jsx`:
```jsx
function LoginPage() {
return <h1>登录页面</h1>;
}
export default LoginPage;
```
* `RegisterPage.jsx`:
```jsx
function RegisterPage() {
return <h1>注册页面</h1>;
}
export default RegisterPage;
```
* 再创建一个 `DashboardPage.jsx` 作为登录后的主页:
```jsx
function DashboardPage() {
return <h1>仪表盘 - 学习主页</h1>;
}
export default DashboardPage;
```
5. **`App.jsx` 中定义路由规则**
* 修改 `src/App.jsx`
```jsx
// src/App.jsx
import { Routes, Route } from 'react-router-dom';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import DashboardPage from './pages/DashboardPage';
function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<DashboardPage />} />
{/* 根路径暂时指向Dashboard */}
</Routes>
);
}
export default App;
```
* **验证:** 现在,在浏览器中分别访问 `http://localhost:5173/login``http://localhost:5173/register` 和 `http://localhost:5173/`,你会看到页面内容随之切换,但浏览器没有刷新!这就是客户端路由。
6. **搭建通用布局 (Layout Component)**
* **讲解:** “我们发现很多页面都有共同的部分,比如导航栏。我们可以把这些共同部分抽成一个‘布局’组件。”
* 在 `src/` 下新建 `components` 文件夹。
* 在 `src/components` 中新建 `Layout.jsx`
```jsx
// src/components/Layout.jsx
import { Link, Outlet } from 'react-router-dom';
function Layout() {
return (
<div className="app-layout">
<nav style={{ background: '#eee', padding: '1rem' }}>
<Link to="/" style={{ marginRight: '1rem' }}>主页</Link>
<Link to="/login" style={{ marginRight: '1rem' }}>登录</Link>
<Link to="/register">注册</Link>
</nav>
<main style={{ padding: '1rem' }}>
{/* Outlet 是一个占位符,子路由匹配的组件会在这里显示 */}
<Outlet />
</main>
</div>
);
}
export default Layout;
```
* **修改 `App.jsx` 以使用布局:**
```jsx
// src/App.jsx
// ... imports
import Layout from './components/Layout'; // 引入Layout
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}> {/* Layout作为父路由 */}
{/* 嵌套在Layout中的子路由 */}
<Route index element={<DashboardPage />} /> {/* index表示父路径'/'的默认子路由 */}
<Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} />
</Route>
</Routes>
);
}
export default App;
```
* **验证:** 现在无论你访问哪个页面,顶部的导航栏都会一直存在。点击导航链接,下面的内容会切换。
7. **引入 `useState` - 让组件拥有记忆**
* **讲解:** “现在我们的组件都是静态的。如果想让组件响应用户的输入,比如在输入框里打字,我们就需要 `State`。”
* 以 `LoginPage.jsx` 为例,引入表单和状态:
```jsx
// src/pages/LoginPage.jsx
import { useState } from 'react'; // 1. 引入 useState
function LoginPage() {
// 2. 使用 useState 创建状态变量
// email 是状态值setEmail 是更新这个值的函数
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault(); // 阻止表单默认的提交刷新行为
console.log('正在尝试登录:', { email, password });
// 下节课我们会在这里调用API
};
return (
<div>
<h1>登录</h1>
<form onSubmit={handleSubmit}>
<div>
<label>邮箱:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)} // 3. 监听输入并用setEmail更新状态
/>
</div>
<div>
<label>密码:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">登录</button>
</form>
</div>
);
}
export default LoginPage;
```
* **验证:** 打开登录页面在输入框里打字打开浏览器的开发者工具F12看 Console 面板。点击登录按钮,你会看到 `console.log` 打印出了你输入的内容。这证明我们成功地用 `State` 捕获了用户输入。
---
#### **四、 课堂总结与作业 (15分钟)**
* **总结:**
* “今天,我们从零开始,用 Vite 和 React 搭建起了前端项目的骨架。我们学会了组件化的思想,掌握了 JSX 语法,并用 `react-router-dom` 实现了页面间的导航。最重要的是,我们通过 `useState` 让组件‘活’了起来,能够响应用户的操作。我们的‘费曼学习平台’终于有了看得见的界面!”
* **课后作业:**
1. **必须完成:** 确保前端项目能成功运行所有路由都能正确跳转并且通用布局Layout能够正常显示。
2. **模仿与实践:** 模仿 `LoginPage.jsx` 的写法,为 `RegisterPage.jsx` 添加表单,包含 `username`, `email`, `password` 三个输入框,并用 `useState` 来管理它们的状态。
* **预告下次课内容:**
* “前端的‘骨架’已经搭好,下次课,我们将打通前后端的‘任督二脉’!我们会学习如何使用 `axios` 从前端发送网络请求调用我们之前写好的后端API真正实现用户的注册和登录功能并学习如何管理全局的登录状态。”
**答疑环节,课程结束。**

@ -1,24 +1,14 @@
// index.js
const User = require('./models/User');
const bcrypt = require('bcryptjs');
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors'); // 1. 引入cors
const cors = require('cors');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000; // 优先使用环境变量中的端口
const port = process.env.PORT || 3000;
// --- 核心中间件 ---
// 2. 使用cors中间件 - 解决跨域问题
// 讲解CORS (Cross-Origin Resource Sharing) 是一个必需的步骤。当我们的前端比如运行在localhost:5173
// 尝试请求后端运行在localhost:3000浏览器会出于安全策略阻止它。
// `cors()` 中间件会自动添加必要的响应头,告诉浏览器“我允许那个地址的请求”,从而让前后端可以顺利通信。
app.use(cors());
// 3. 使用express.json()中间件 - 解析请求体
// 讲解这个中间件让我们的Express应用能够识别并处理传入的JSON格式数据比如用户注册时POST的用户名和密码
app.use(express.json());
// --- 数据库连接 ---
@ -26,42 +16,9 @@ mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));
// --- API 路由 ---
// POST /api/register - 用户注册
app.post('/api/register', async (req, res) => {
try {
// 1. 从请求体中获取用户名、邮箱、密码
const { username, email, password } = req.body;
// 2. 检查用户或邮箱是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'Email already exists' });
}
user = await User.findOne({ username });
if (user) {
return res.status(400).json({ msg: 'Username already exists' });
}
// 3. 创建新用户实例
user = new User({ username, email, password });
// 4. 对密码进行哈希加密
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// 5. 将新用户保存到数据库
await user.save();
// 6. 返回成功信息 (暂时不返回token)
res.status(201).json({ msg: 'User registered successfully' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// ... (后续的API路由)
// --- API 路由 ---
app.use('/api/users', require('./routes/users'));
app.use('/api/knowledge-points', require('./routes/knowledgePoints')); // 新增
app.listen(port, () => {
console.log(`Feynman Platform backend is running at http://localhost:${port}`);

@ -0,0 +1,35 @@
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
// 1. 从请求头中获取token
// 兼容两种传递方式:
// - 自定义头: x-auth-token: <token>
// - 标准头: Authorization: Bearer <token>
let token = req.header('x-auth-token');
if (!token) {
const authHeader = req.header('authorization') || req.header('Authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.slice(7).trim();
}
}
// 2. 检查token是否存在
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' }); // 401: 未授权
}
// 3. 验证token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 将解码后的用户信息特别是user.id附加到请求对象上
req.user = decoded.user;
// 调用next(),将控制权交给下一个中间件或路由处理器
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};

@ -0,0 +1,27 @@
// models/KnowledgePoint.js
const mongoose = require('mongoose');
const KnowledgePointSchema = new mongoose.Schema({
user: { // 关联到用户
type: mongoose.Schema.Types.ObjectId,
ref: 'User' // 引用User模型
},
title: {
type: String,
required: true
},
content: { // 存放Markdown, LaTeX, Mermaid等原始内容
type: String,
required: true
},
status: { // 学习状态: 'not_started', 'in_progress', 'mastered'
type: String,
default: 'not_started'
},
reviewList: { // 是否在复习列表中
type: Boolean,
default: false
}
}, { timestamps: true });
module.exports = mongoose.model('KnowledgePoint', KnowledgePointSchema);

113
package-lock.json generated

@ -13,6 +13,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.20.0",
"mongoose": "^8.18.2"
}
@ -92,6 +93,12 @@
"node": ">=16.20.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -234,6 +241,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -511,6 +527,49 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
@ -520,6 +579,48 @@
"node": ">=12.0.0"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -917,6 +1018,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",

@ -14,6 +14,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.20.0",
"mongoose": "^8.18.2"
}

@ -0,0 +1,87 @@
// routes/knowledgePoints.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth'); // 引入认证中间件
const KnowledgePoint = require('../models/KnowledgePoint');
// @route POST /api/knowledge-points
// @desc 创建一个新的知识点
// @access Private (需要登录)
router.post('/', auth, async (req, res) => { // 在这里使用auth中间件
try {
const { title, content } = req.body;
const newKp = new KnowledgePoint({
title,
content,
user: req.user.id // 从auth中间件附加的req.user中获取用户ID
});
const kp = await newKp.save();
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points
// @desc 获取当前用户的所有知识点
// @access Private
router.get('/', auth, async (req, res) => {
try {
const kps = await KnowledgePoint.find({ user: req.user.id }).sort({ createdAt: -1 });
res.json(kps);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route GET /api/knowledge-points/:id
// @desc 获取单个知识点详情
// @access Private
router.get('/:id', auth, async (req, res) => {
// ... (学生可以作为练习,实现获取单个知识点的逻辑)
});
// @route PUT /api/knowledge-points/:id
// @desc 更新一个知识点
// @access Private
router.put('/:id', auth, async (req, res) => {
try {
let kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
// 确保是该用户自己的知识点
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
const { title, content, status, reviewList } = req.body;
kp = await KnowledgePoint.findByIdAndUpdate(
req.params.id,
{ $set: { title, content, status, reviewList } },
{ new: true } // 返回更新后的文档
);
res.json(kp);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// @route DELETE /api/knowledge-points/:id
// @desc 删除一个知识点
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
let kp = await KnowledgePoint.findById(req.params.id);
if (!kp) return res.status(404).json({ msg: 'Knowledge point not found' });
if (kp.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'Not authorized' });
}
await KnowledgePoint.findByIdAndRemove(req.params.id);
res.json({ msg: 'Knowledge point removed' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;

@ -0,0 +1,107 @@
// routes/users.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs'); // 引入加密库
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// --- 用户注册 API (已集成密码加密) ---
// @route POST /api/users/register
// @desc 注册一个新用户
// @access Public
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// 1. 检查用户是否已存在
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
// 2. 创建新用户实例
user = new User({
name,
email,
password
});
// 3. 【安全核心】对密码进行哈希加密
// 讲解:我们使用 bcrypt 库。它会先生成一个“盐”salt这是一个随机字符串
// 然后将盐和原始密码混合在一起进行哈希计算。
// 这样做可以确保即使两个用户设置了相同的密码,它们在数据库中的哈希值也完全不同。
const salt = await bcrypt.genSalt(10); // 10是安全强度数值越大越安全但越耗时
user.password = await bcrypt.hash(password, salt); // 生成加密后的密码
// 4. 保存用户到数据库
await user.save();
// 5. 注册成功直接生成JWT并返回实现注册后自动登录
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
// --- 用户登录 API ---
// @route POST /api/users/login
// @desc 用户登录并获取token
// @access Public
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// 1. 检查用户是否存在
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 2. 【安全核心】比较密码
// 这里使用 bcrypt.compare 来安全地比较客户端传来的原始密码和数据库中存储的哈希密码。
// 它会自动处理盐值我们无需关心。只有密码匹配才会返回true。
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid Credentials' });
}
// 3. 登录成功生成JWT
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '5h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
Loading…
Cancel
Save