vault backup: 2026-04-25 16:06:25
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
---
|
||||
tags: [go, security, bcrypt, password-hashing, cost-factor]
|
||||
create time: 2026-04-25 14:45
|
||||
---
|
||||
|
||||
# bcrypt 成本因子(Cost Factor)详解
|
||||
|
||||
## 概述
|
||||
|
||||
成本因子是 bcrypt 算法的核心参数,控制哈希计算的"难度"。它是平衡安全性和性能的关键旋钮。
|
||||
|
||||
## 什么是成本因子?
|
||||
|
||||
bcrypt 不是做一次哈希就完事,而是通过 **多次迭代** 来增加暴力破解的计算成本。成本因子(cost)就是这个迭代次数的对数指数:
|
||||
|
||||
```
|
||||
实际迭代次数 = 2^cost
|
||||
```
|
||||
|
||||
所以在 Go 中:
|
||||
|
||||
```go
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// bcrypt.DefaultCost = 10 → 2^10 = 1024 次迭代
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
```
|
||||
|
||||
## 不同成本因子的对比
|
||||
|
||||
| 成本值 | 迭代次数 | 单次哈希耗时 | 适用场景 |
|
||||
|--------|---------|------------|---------|
|
||||
| 4 | 16 | ~1ms | 本地开发、测试 |
|
||||
| 8 | 256 | ~50ms | 低流量内部系统 |
|
||||
| **10** | 1,024 | ~100-300ms | **生产环境默认** |
|
||||
| 12 | 4,096 | ~500ms | 高安全需求 |
|
||||
| 14 | 16,384 | ~2s | 极低频登录场景 |
|
||||
|
||||
> 💡 **想一想**:成本因子是不是越高越好?为什么?
|
||||
>
|
||||
> 答案:不是。成本越高,合法用户登录时服务器也要做同等次数的计算。过高的成本会导致登录延迟,影响用户体验,也给服务器带来不必要的负载。
|
||||
|
||||
## 如何选择合适的成本?
|
||||
|
||||
### 选择原则
|
||||
|
||||
```
|
||||
目标:让单次哈希耗时在 100ms ~ 300ms 之间
|
||||
```
|
||||
|
||||
这个区间意味着:
|
||||
- **对合法用户**:几乎无感,登录流畅
|
||||
- **对攻击者**:暴力破解速度被大幅压制(每秒只能尝试 3~10 次)
|
||||
|
||||
### 调整方式
|
||||
|
||||
```go
|
||||
// 生产环境手动调高
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), 12)
|
||||
|
||||
// 也可以手动降低(仅开发环境)
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.MinCost) // MinCost = 4
|
||||
```
|
||||
|
||||
## 成本因子的"升级陷阱"
|
||||
|
||||
一个容易被忽视的问题:当硬件变快后,原来合适的成本因子会变得不够安全。
|
||||
|
||||
```go
|
||||
// 2024 年的机器比 2018 年快很多
|
||||
// 当年 cost=10 是 200ms,现在可能只要 80ms
|
||||
|
||||
// 解决方案:用户登录时检查并升级哈希
|
||||
func Login(username, password string) error {
|
||||
user := db.GetUser(username)
|
||||
bcrypt.CompareHashAndPassword(user.Hash, []byte(password))
|
||||
|
||||
// 检测当前哈希的成本是否偏低
|
||||
currentCost, _ := bcrypt.Cost(user.Hash)
|
||||
if currentCost < bcrypt.DefaultCost {
|
||||
// 用更高的成本重新哈希并保存
|
||||
newHash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
db.UpdateHash(username, newHash)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **想一想**:为什么要在登录时"顺便"升级哈希,而不是批量扫描全量用户?
|
||||
>
|
||||
> 答案:惰性升级(lazy upgrade)避免一次性计算压力过大,且只升级活跃用户的哈希,不活跃用户的旧哈希影响较小。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
✅ **推荐**
|
||||
- 生产环境默认 `bcrypt.DefaultCost`(10)
|
||||
- 定期评估并逐步提升成本因子
|
||||
- 登录时惰性升级低于最低成本的哈希
|
||||
|
||||
❌ **避免**
|
||||
- 在生产环境使用 `< 10` 的成本
|
||||
- 为每个用户设置不同的成本(增加复杂度且收益有限)
|
||||
- 为了追求极致安全而盲目调高到 14+
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[金山办公作业/Week06/登录安全流程]] - 登录安全全流程
|
||||
- [[CS/DB/SQL安全]] - 密码安全存储
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
tags: [go, frontend, security, pbkdf2, salt-challenge, client-side-derivation]
|
||||
create time: 2026-04-25 14:45
|
||||
---
|
||||
|
||||
# 客户端密钥派生:盐值挑战方案详解
|
||||
|
||||
## 概述
|
||||
|
||||
本文为「登录安全流程」的进阶篇,讲解金融类高安全场景下采用的**客户端密钥派生**方案(盐值挑战 / Salt Challenge)。核心思路:前端在发送密码之前,先用后端下发的随机盐值做 PBKDF2 派生,让传输层上永远不出现原始密码。
|
||||
|
||||
## 核心类比:餐厅里的"一次性餐巾纸"
|
||||
|
||||
想象你去一家**高安全级别的餐厅**点餐——点餐的过程就等同于"登录"。
|
||||
|
||||
### 普通餐厅(标准 HTTPS 登录)
|
||||
|
||||
```
|
||||
你 → 把订单写在纸条上 → 服务员拿走 → 厨房照着做
|
||||
```
|
||||
|
||||
对应到代码:用户输入密码 → 浏览器直接发 `{username, password}` → 后端验证。
|
||||
|
||||
HTTPS 就像餐厅的监控摄像头 + 玻璃隔断,服务员看不到纸条内容。但如果有坏蛋偷偷装了针孔摄像头(**HTTPS 被攻破**),他拍到的就是你**完整的订单**,可以直接拿去厨房冒充你点餐。
|
||||
|
||||
### 高级餐厅(盐值挑战方案)
|
||||
|
||||
现在来看完整的盐值挑战流程——这就是高级餐厅的做法。
|
||||
|
||||
---
|
||||
|
||||
## 完整流程拆解
|
||||
|
||||
### 第一步:前端先"要盐"
|
||||
|
||||
```go
|
||||
func (h *Handler) GetChallenge(w http.ResponseWriter, r http.Request) {
|
||||
salt := generateRandomSalt(32) // 随机生成 32 位盐值
|
||||
cache.Set("challenge:"+r.RemoteAddr, salt, 1*time.Minute) // 存到缓存,1 分钟后自动清除
|
||||
json.NewEncoder(w).Encode(LoginChallenge{
|
||||
Salt: salt,
|
||||
ExpireAt: time.Now().Add(time.Minute).Unix(), // 告诉前端什么时候过期
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**生活类比:服务员递给你一张"一次性餐巾纸"**
|
||||
|
||||
- 你刚落座(`r.RemoteAddr` = 你的桌号),服务员就递给你一张**随机印了乱码的餐巾纸**(`salt`)
|
||||
- 这张纸**只有效 1 分钟**(`ExpireAt`),过期作废
|
||||
- 餐厅后台会记住:哪张纸给了哪桌,什么时候过期
|
||||
|
||||
| 代码 | 类比 | 为什么重要 |
|
||||
|---|---|---|
|
||||
| `generateRandomSalt(32)` | 餐巾纸上随机印的乱码 | 每次都不一样,两个不同桌的人拿到的纸完全不同 |
|
||||
| `cache.Set(..., 1*time.Minute)` | 1 分钟后餐厅收回这张纸 | 过期后就算有人偷了这张纸也没用 |
|
||||
| `"challenge:"+r.RemoteAddr` | 按桌号记录哪张纸给了谁 | 防止有人拿别人桌的纸来冒充 |
|
||||
|
||||
---
|
||||
|
||||
### 第二步:前端拿到盐后"做记号"
|
||||
|
||||
```go
|
||||
// 前端拿到 salt = "a7xK9mZp..." 后:
|
||||
// 用 PBKDF2 算法,把 用户密码 + 盐值 混合运算
|
||||
// 得到一个派生密钥,比如 "e4f8b2d1c9a3..."
|
||||
// 把这个派生密钥发给后端登录
|
||||
```
|
||||
|
||||
**生活类比:你在餐巾纸上用"暗语"写订单**
|
||||
|
||||
- 你在餐巾纸的乱码旁边,**用只有你和厨房能懂的暗语**写下你要点的菜(密码)
|
||||
- 你不是直接写"红烧肉",而是写暗语"8 号食材 + 3 号做法"
|
||||
- 厨房拿到纸条后,用同样的暗语表**反查**,知道你点了什么
|
||||
|
||||
**PBKDF2 在这里的角色:** 是一套**单向暗语转换规则**。
|
||||
- 你(前端)用这套规则把"红烧肉"变成"8 号食材 + 3 号做法"
|
||||
- 厨房(后端)用同样的规则验证这个暗语对不对
|
||||
- **但反过来——有人只看到"8 号食材 + 3 号做法",猜不出原菜名是什么**(因为 PBKDF2 是单向的)
|
||||
|
||||
---
|
||||
|
||||
### 第三步:后端"对暗语" + 撕纸
|
||||
|
||||
```go
|
||||
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. 前端先用盐值做 PBKDF2 → 得到派生密钥
|
||||
// 2. 后端用同样的盐 + 数据库里的密码做 PBKDF2 → 对比两者是否一致
|
||||
// 3. 用完这张盐,立即作废(一次性使用)
|
||||
}
|
||||
```
|
||||
|
||||
**生活类比:厨房验证 + 用完即撕**
|
||||
|
||||
- 厨房拿到餐巾纸上的暗语
|
||||
- 同时从自己的菜单上,用同样的规则算一遍 → 看暗语是否匹配
|
||||
- **算完这张纸直接撕掉**(盐值一次性使用),哪怕有人偷了这张已经撕掉的纸也没用
|
||||
|
||||
---
|
||||
|
||||
## 全程对比
|
||||
|
||||
```
|
||||
【普通登录 — 普通餐厅】
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ 你(用户) │────▶│ 服务员 │────▶│ 厨房 │
|
||||
│ 密码:1234 │ │ (HTTPS) │ │ 验证 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
▲
|
||||
如果有摄像头
|
||||
拍到完整密码 ❌
|
||||
|
||||
【盐值挑战 — 高级餐厅】
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ 你(用户) │────▶│ 盐值纸条 │────▶│ 厨房 │
|
||||
│ 先拿盐→ │ │ (1分钟 │ │ 对暗语 │
|
||||
│ 再做变换→ │ │ 一次性) │ │ 用完撕掉 │
|
||||
│ 发密钥 │────▶│ │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
▲
|
||||
如果有摄像头
|
||||
只拍到"暗语" + "废纸" ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全性总结
|
||||
|
||||
> "即使 HTTPS 被绕过(极少见),攻击者也只能看到客户端派生的密钥,无法还原原始密码。"
|
||||
|
||||
**翻译:** 就算餐厅的摄像头被拆了(HTTPS 被攻破),坏蛋拍到的也只是你写在餐巾纸上的"暗语"和"废纸"。没有那套 PBKDF2 转换规则和原始盐值,他**无法反推出你最初输入的密码**,更不可能在 1 分钟后再次使用。
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[金山办公作业/Week06/登录安全流程]] - 登录安全流程总览,本文为其进阶篇
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
tags: [go, frontend, security, authentication, login, encryption, assignment]
|
||||
create time: 2026-04-25 14:30
|
||||
---
|
||||
|
||||
# 登录安全流程:前端与后端的加密协作
|
||||
|
||||
## 概述
|
||||
|
||||
登录是 Web 应用最敏感的操作之一。本文深入探讨前端和后端各自的安全职责,以及 HTTPS 协议下的标准登录认证流程。
|
||||
|
||||
## 核心问题:两端都需要加密吗?
|
||||
|
||||
**不完全是。** 更准确的说法是——**传输层加密 + 密码侧哈希**。
|
||||
|
||||
> 💡 **想一想**:如果前端把密码用 SHA-256 哈希后再发送,这样后端收到的是哈希值而不是明文密码,是不是更安全?
|
||||
|
||||
答案是否定的。因为**哈希后的值本身就是凭证**。攻击者截获这个哈希值后,可以直接重放给后端完成登录——哈希值被截获等于密码被截获。所以业界最主流的做法是:**前端直接发送明文密码,由 HTTPS 保护传输安全**。
|
||||
|
||||
## 登录全流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant F as 前端(React)
|
||||
participant S as 后端(Go)
|
||||
participant DB as 数据库
|
||||
|
||||
U->>F: 输入用户名 + 密码
|
||||
F->>S: POST /api/login (HTTPS)
|
||||
Note over S: 传输层 TLS 加密
|
||||
S->>DB: SELECT * FROM users WHERE username=?
|
||||
DB-->>S: 用户记录 (hashed_password)
|
||||
S->>S: bcrypt.Compare(password, stored_hash)
|
||||
|
||||
alt 密码正确
|
||||
S->>S: 生成 JWT Token
|
||||
S-->>F: {access_token, expires_in}
|
||||
F->>F: 保存 Token (Memory/LocalStorage)
|
||||
F-->>U: 跳转首页 ✓
|
||||
else 密码错误
|
||||
S-->>F: 401 Unauthorized
|
||||
F-->>U: 提示"用户名或密码错误"
|
||||
end
|
||||
```
|
||||
|
||||
## 三层安全模型
|
||||
|
||||
登录安全分为三个层次,每层由不同方负责:
|
||||
|
||||
### 第一层:传输加密(HTTPS/TLS)
|
||||
|
||||
**谁负责**:服务器端配置,前端透明使用
|
||||
|
||||
```
|
||||
HTTP (明文) → HTTPS (TLS 加密)
|
||||
password = "123456" → TLS 密文传输
|
||||
```
|
||||
|
||||
- 所有请求自动加密,中间人无法窃取
|
||||
- 前端只需使用 `https://` 前缀的 API 地址
|
||||
- 这是**必须的**,没有替代方案
|
||||
|
||||
### 第二层:密码存储加密(后端)
|
||||
|
||||
**谁负责**:后端
|
||||
|
||||
```go
|
||||
package auth
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// 注册时:密码哈希后存入数据库
|
||||
func Register(username, password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(password), bcrypt.DefaultCost,
|
||||
)
|
||||
// 默认成本因子 = 10(详见 [[金山办公作业/Week06/bcrypt成本因子]])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 存入数据库的是 hash,不是 password
|
||||
return db.InsertUser(username, string(hash))
|
||||
}
|
||||
|
||||
// 登录时:比对输入的密码与存储的哈希
|
||||
func Login(username, password string) (string, error) {
|
||||
user, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return "", ErrUserNotFound
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(
|
||||
[]byte(user.HashedPassword), []byte(password),
|
||||
); err != nil {
|
||||
return "", ErrWrongPassword
|
||||
}
|
||||
// 验证通过 → 签发 Token
|
||||
return generateJWT(user), nil
|
||||
}
|
||||
```
|
||||
|
||||
默认成本因子 = 10(详见 [[金山办公作业/Week06/bcrypt成本因子]])
|
||||
**为什么用 bcrypt?**
|
||||
- 自动加盐,相同密码在不同用户哈希结果不同
|
||||
- 计算成本可调,抵御暴力破解
|
||||
- 比 MD5/SHA-256 更适合密码哈希
|
||||
|
||||
### 第三层:认证凭据管理(前后端协作)
|
||||
|
||||
**谁负责**:前后端共同
|
||||
|
||||
```go
|
||||
// 后端签发 JWT
|
||||
func generateJWT(userID string, username string) string {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"exp": time.Now().Add(2 * time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secretKey)
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 前端登录后存储 Token
|
||||
async function handleLogin(username: string, password: string) {
|
||||
const resp = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
// 方式1:Memory(更安全,刷新后失效)
|
||||
sessionStorage.setItem('token', data.access_token);
|
||||
// 方式2:LocalStorage(持久登录)
|
||||
localStorage.setItem('token', data.access_token);
|
||||
|
||||
router.push('/dashboard');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端是否需要额外加密?
|
||||
|
||||
| 方案 | 前端做哈希? | 安全性 | 复杂度 | 推荐度 |
|
||||
|------|------------|--------|--------|--------|
|
||||
| **标准方案** ✅ | ❌ 不加密,直接发 | 高(HTTPS 保护) | 低 | ⭐⭐⭐⭐⭐ |
|
||||
| 前端 SHA-256 | ✅ 哈希后发送 | 中(哈希值可重放) | 中 | ⭐⭐ |
|
||||
| 客户端密钥派生 | ✅ PBKDF2/Argon2 | 更高 | 高 | ⭐⭐⭐ |
|
||||
|
||||
**为什么标准方案最优?**
|
||||
|
||||
```
|
||||
方案A(前端不加密):
|
||||
前端明文 → HTTPS加密传输 → 后端验证 bcrypt 哈希
|
||||
✅ 中间人看不到密码
|
||||
|
||||
方案B(前端加 SHA-256):
|
||||
前端 SHA-256(password) → 密文传输 → 后端比对 SHA-256
|
||||
❌ 中间人截获哈希值 → 直接重放登录(哈希=密码)
|
||||
```
|
||||
|
||||
**结论**:前端额外哈希不仅不能提升安全性,反而引入了"重放攻击"风险。
|
||||
|
||||
## 进阶:高安全场景下的前端处理
|
||||
|
||||
对于金融类应用等高安全需求场景,可以采用客户端密钥派生方案——前端在发送密码之前,先用后端下发的随机盐值做 PBKDF2 派生,让传输层上永远不出现原始密码。
|
||||
|
||||
详细流程请见:[[金山办公作业/Week06/客户端密钥派生]]
|
||||
|
||||
## 最佳实践清单
|
||||
|
||||
### 前端
|
||||
|
||||
✅ **推荐**
|
||||
- 使用 HTTPS 地址调用 API
|
||||
- 登录后 Token 存储在内存(sessionStorage)
|
||||
- 密码输入框使用 `<input type="password">`(防误看)
|
||||
- 登录失败后清空密码输入框
|
||||
- 设置合理的 Token 过期时间(2h ~ 24h)
|
||||
|
||||
❌ **避免**
|
||||
- 在 URL 参数中传递密码
|
||||
- 用 localStorage 存明文密码
|
||||
- 登录失败后暴露具体原因("用户名不存在" vs "密码错误")——统一提示"用户名或密码错误"
|
||||
- 前端对密码做 SHA-256 哈希
|
||||
|
||||
### 后端
|
||||
|
||||
✅ **推荐**
|
||||
- 使用 bcrypt/argon2 哈希存储密码
|
||||
- 统一返回模糊错误信息(防止用户名枚举)
|
||||
- 添加登录频率限制(防暴力破解)
|
||||
- JWT 设置短期过期 + Refresh Token 机制
|
||||
- 密钥通过环境变量管理,不写死代码
|
||||
|
||||
❌ **避免**
|
||||
- 明文存储密码
|
||||
- 使用 MD5/SHA-256 哈希密码
|
||||
- 错误信息暴露过多细节
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[金山办公作业/Week05/用户认证.md]] - Go 用户认证完整实现
|
||||
- [[金山办公作业/Week06/客户端密钥派生]] - 客户端密钥派生(盐值挑战)详解
|
||||
- [[CS/NET/HTTPS]] - HTTPS/TLS 原理
|
||||
- [[CS/DB/SQL安全]] - 密码安全存储
|
||||
@@ -1,5 +1,7 @@
|
||||
# 项目概述
|
||||
|
||||
> 验收任务清单:[[验收任务清单]]
|
||||
|
||||
👋
|
||||
|
||||
本作业要求实现一个在线学习管理平台的部分内容,涵盖前端与后端两部分。前端基于 React + TypeScript 技术栈构建,后端采用 Koa 开发。系统实现了用户登录认证、工作台数据可视化、课程管理、学生管理及学习总结展示等功能,支持数据的增删改查等操作。
|
||||
@@ -11,6 +13,8 @@
|
||||

|
||||
|
||||
- 实现用户登录表单(用户名 + 密码),调用后端接口进行认证。
|
||||
|
||||
**相关**:[[登录安全流程]] - 前端与后端的加密协作、登录全流程、最佳实践
|
||||
- 未登录状态下访问其他页面自动跳转到登录页。
|
||||
- 固定测试账号/密码:`admin` / `admin123`
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# 验收任务清单
|
||||
|
||||
> 来源:[[题目]]
|
||||
|
||||
## 一、前端功能验收
|
||||
|
||||
### 1. 登录认证
|
||||
- [ ] 实现用户登录表单(用户名 + 密码),调用后端接口进行认证
|
||||
- [ ] 未登录状态下访问其他页面自动跳转到登录页
|
||||
- [ ] 固定测试账号/密码:`admin` / `admin123`
|
||||
|
||||
### 2. 工作台(Dashboard)
|
||||
- [ ] 展示 4 个统计卡片:课程总数、学生总数、发布率、活跃率
|
||||
- [ ] 柱状图:课程选课人数排行
|
||||
- [ ] 折线图:近 7 天学习活跃度(学习人数 + 学习时长)
|
||||
- [ ] 饼图:学生状态分布(活跃 / 非活跃)
|
||||
- [ ] 饼图:课程分类分布
|
||||
|
||||
### 3. 课程管理
|
||||
- [ ] 课程列表展示,支持分页(默认 10 条每页,可调整)
|
||||
- [ ] 按课程名/讲师搜索、按状态和分类筛选
|
||||
- [ ] 选课人数列的服务端排序
|
||||
- [ ] 新增、编辑、删除课程
|
||||
- [ ] 课程发布/下架状态切换
|
||||
- [ ] 删除操作气泡确认框
|
||||
|
||||
### 4. 学生管理
|
||||
- [ ] 学生列表展示,支持分页(默认 10 条每页,可调整)
|
||||
- [ ] 按姓名/学号搜索、按班级和状态筛选
|
||||
- [ ] 新增、编辑、删除学生
|
||||
- [ ] 支持多选课程
|
||||
|
||||
### 5. 学习总结
|
||||
- [ ] 从服务端获取 Markdown 格式的学习总结内容,前端解析渲染展示
|
||||
- [ ] 修改 `server/data/summary.md` 内容为真实的前端学习总结
|
||||
|
||||
## 二、技术要求验收
|
||||
|
||||
### 前端技术栈
|
||||
- [ ] Vite + React + TypeScript
|
||||
- [ ] Ant Design + Tailwind CSS
|
||||
- [ ] 未使用第三方脚手架(UmiJS、飞冰、Ant Design Pro 等)
|
||||
|
||||
### 后端技术栈
|
||||
- [ ] Node.js + Koa
|
||||
- [ ] @koa/router + better-sqlite3 + jsonwebtoken
|
||||
- [ ] 完善个别缺失的后端代码
|
||||
|
||||
### 开发与生产配置
|
||||
- [ ] 前端监听 5173 端口,Vite 代理到 3000
|
||||
- [ ] 前端 `npm run build` 打包到 `client/dist`
|
||||
- [ ] 生产环境中 Koa 托管静态资源,`http://localhost:3000` 可直接访问
|
||||
|
||||
## 三、作业提交验收
|
||||
|
||||
- [ ] 作业放置于 `week07/homework/course` 目录
|
||||
- [ ] `client` 目录包含 package.json(`npm run dev` / `npm run build`)
|
||||
- [ ] `server` 目录包含后端代码
|
||||
- [ ] 视频要求:
|
||||
- [ ] 时长控制在 5 分钟内
|
||||
- [ ] 展示学生管理的增删改查、工作台和学习总结界面
|
||||
- [ ] 核心技术点讲解或优化思考
|
||||
- [ ] 视频开头说明身份信息
|
||||
- [ ] 命名格式:`姓名_学号_在线学习后台.mp4`
|
||||
- [ ] README.md 包含:
|
||||
- [ ] 项目基本信息(姓名、学校、学号)
|
||||
- [ ] 开发任务索引(任务清单)
|
||||
- [ ] 核心技术实现说明
|
||||
Reference in New Issue
Block a user