前后端
阅读指南
本笔记按照”问题驱动 → 机制 → 工程实践”的顺序展开。 建议阅读顺序:HTTP 基础 → RESTful API → 前端三件套 → 后端分层架构 → Spring Boot → JWT 认证
知识依赖链:
HTTP协议→RESTful设计→Controller层→Service层→Repository/ORM→Spring Security + JWT
1. 浏览器是什么
问题动机
你打开 https://github.com,看到了完整的页面。但”页面”到底是什么?是图片?是文字?谁来解析?谁来渲染?
核心洞察
浏览器本质上是一个运行前端程序的操作系统。
它做的事情与 OS 高度类似:
| OS 的职责 | 浏览器的对应物 |
|---|---|
| 运行程序(进程调度) | 运行 JavaScript(JS 引擎,如 V8) |
| 渲染图形界面 | 渲染 HTML/CSS(排版引擎,如 Blink) |
| 网络 I/O | 发起 HTTP 请求 |
| 文件系统访问 | localStorage / IndexedDB |
| 沙箱隔离 | Same-Origin Policy |
浏览器从服务器下载 HTML、CSS、JS 三类文件,然后在本地执行它们,你看到的”页面”是浏览器在本地渲染的结果,而不是服务器直接发来的图像。
2. 前端三件套:HTML / CSS / JS
为什么是三个文件,而不是一个?
朴素方案:把内容、样式、行为都写在一起(早期 HTML 就是这样做的,<font color="red"> 遍地都是)。
问题:内容和样式耦合,改一处,全局受影响;无法复用;不可维护。
解决方案:关注点分离(Separation of Concerns)。
1
2
3
HTML → 内容与结构("是什么")
CSS → 样式与表现("长什么样")
JS → 行为与交互("做什么")
2.1 HTML — 内容与结构
HyperText Markup Language,用标签描述内容的语义。
1
2
3
4
5
6
7
8
<article>
<h1>什么是 REST</h1>
<p>REST 是一种 API 设计风格...</p>
<ul>
<li>无状态</li>
<li>统一接口</li>
</ul>
</article>
HTML 只负责语义(这是标题、这是段落、这是列表),完全不管颜色、字体、位置。
DOM Tree(文档对象模型)
浏览器解析 HTML 后,在内存中建立一棵树——DOM Tree:
1
2
3
4
5
6
7
Document
└── html
├── head
│ └── title
└── body
├── h1 → "什么是 REST"
└── p → "REST 是..."
关键洞察:DOM Tree 是 HTML 的内存表示,JavaScript 通过操作 DOM Tree 来动态改变页面,而不是直接修改 HTML 文件。
2.2 CSS — 样式与表现
Cascading Style Sheets,控制页面外观。”层叠”是指多条规则可以叠加,优先级由选择器特异性决定。
1
2
3
4
5
6
7
8
9
10
/* 选择所有 h1,设置字体颜色 */
h1 {
color: #333;
font-size: 24px;
}
/* 选择 .highlight 类,优先级更高 */
.highlight {
background-color: yellow;
}
CSS 盒模型:每个 HTML 元素都是一个矩形盒子:
1
2
3
4
5
6
7
┌────────────────────────────┐ ← margin(盒子外部空间)
│ ┌──────────────────────┐ │ ← border
│ │ ┌────────────────┐ │ │ ← padding
│ │ │ content │ │ │
│ │ └────────────────┘ │ │
│ └──────────────────────┘ │
└────────────────────────────┘
2.3 JavaScript — 行为与交互
JS 是唯一能在浏览器中运行的编程语言(WebAssembly 除外)。它可以:
- 读取 / 修改 DOM Tree(动态改变页面内容)
- 监听用户事件(点击、输入)
- 发起 HTTP 请求(AJAX / Fetch API)
1
2
3
4
// 点击按钮,修改页面内容
document.getElementById("btn").addEventListener("click", () => {
document.getElementById("title").textContent = "已点击!";
});
3. AJAX 模式
问题动机
早期 Web(纯 HTML 时代):用户每次操作(搜索、翻页、提交表单)都需要整页刷新——浏览器向服务器请求一整个新的 HTML 页面,屏幕白闪一下,体验极差。
问题根源:内容更新的粒度太粗(整页),而用户实际需要更新的只是一小块数据。
核心思想
AJAX(Asynchronous JavaScript and XML):浏览器可以在后台向服务器发起请求,只拿回需要的数据,然后用 JS 局部更新 DOM,页面不刷新。
1
2
3
4
5
传统模式:
用户点击 → 整页请求 → 服务器返回完整HTML → 浏览器重新渲染全页
AJAX模式:
用户点击 → 后台HTTP请求(只要数据) → 服务器返回JSON → JS局部更新DOM
机制细节
现代浏览器使用 fetch() API(或旧版 XMLHttpRequest)发起异步请求:
1
2
3
4
5
6
7
8
// 不刷新页面,从服务器获取用户数据
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`); // 发起HTTP GET
const user = await response.json(); // 解析JSON
// 只更新页面中的用户名,其余内容不变
document.getElementById("username").textContent = user.name;
}
工程连接:现代前端框架(React、Vue)几乎所有数据获取都是 AJAX 模式,配合 RESTful API 或 GraphQL。
4. React 与 Vue:现代前端框架
为什么需要框架?
随着 AJAX 广泛使用,前端逻辑越来越复杂:数据改变后,哪些 DOM 节点需要更新?如果手动写 document.getElementById(...).textContent = ...,代码量会爆炸,且极易出错(忘记更新某处)。
核心矛盾:数据(State)和 UI 的同步问题。
4.1 React:UI = f(data)
核心思想:把 UI 看作数据的纯函数。数据变了,框架自动重新计算 UI,你只需要描述”数据 → UI 的映射”。
1
2
3
4
5
6
7
8
9
// 组件:描述数据如何映射到UI
function UserCard({ user }) {
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Virtual DOM:为什么不直接操作 DOM?
问题:直接操作 DOM(真实DOM)很慢。DOM 操作会触发浏览器的重排(reflow)和重绘(repaint),代价高昂。
朴素解法:每次数据变化,重新渲染整个 DOM —— 太慢,且会丢失焦点、滚动位置等状态。
React 的解法 — Virtual DOM:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
数据变化
↓
生成新 Virtual DOM(纯 JS 对象,快)
↓
Diff 新旧 Virtual DOM(找出差异)
↓
只把差异部分 patch 到真实 DOM(最小化 DOM 操作)
// Virtual DOM 本质上是一个JS对象(简化示意)
{
type: "div",
props: { className: "card" },
children: [
{ type: "h2", props: {}, children: ["张三"] },
{ type: "p", props: {}, children: ["zhang@example.com"] }
]
}
定量直觉:Diff 算法复杂度为 O(n)(React 的启发式算法),相比朴素树 Diff 的 O(n³) 快得多。
组件化
UI 拆分为可复用的独立单元,每个组件管理自己的数据(state)和副作用。
1
2
3
4
5
6
7
8
9
10
11
function App() {
const [count, setCount] = useState(0); // 状态
return (
<div>
<p>点击次数:{count}</p>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
);
}
// 数据(count)变化 → React自动重新渲染 → 页面更新
4.2 Vue.js:MVVM 与响应式系统
核心思想:数据变化 → 自动更新页面(双向绑定)。
MVVM 架构:
1
2
3
Model → 数据(JS对象)
View → 模板(HTML)
ViewModel → Vue 实例,自动同步 Model 和 View
Vue 通过响应式系统(Reactivity)监听数据变化:当你修改数据时,Vue 自动追踪哪些视图依赖了这个数据,并精确更新它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Vue 模板:声明式地描述UI -->
<template>
<div>
<p></p>
<input v-model="message" /> <!-- 双向绑定 -->
</div>
</template>
<script>
export default {
data() {
return { message: "Hello Vue" }
}
}
// 修改 message → 段落和输入框自动同步更新
</script>
| 对比 | React | Vue |
|---|---|---|
| 核心思想 | UI = f(data),单向数据流 | 响应式系统,双向绑定 |
| 学习曲线 | 陡峭(需理解 JSX、Hooks) | 平缓(模板语法接近 HTML) |
| 灵活性 | 高(几乎无约束) | 中(有更多约定) |
| 适用场景 | 大型复杂应用 | 中小型应用、快速开发 |
5. HTTP 协议
问题动机
浏览器和服务器是两台(可能相距很远的)机器,它们需要一种共同语言来通信:以什么格式发送请求?响应如何编码?出错如何表示?
HTTP(HyperText Transfer Protocol)就是这套约定。
5.1 HTTP 的本质
- 应用层协议,跑在 TCP/IP 之上
- C/S 模型(Client-Server):客户端发起请求,服务器响应
- 文本协议:请求和响应都是人类可读的文本(HTTP/1.1)
1
2
3
4
5
6
7
客户端 (Browser) 服务端 (Server)
│ │
│──── HTTP Request ────────────►│
│ │ 解析请求
│ │ 执行业务逻辑
│◄─── HTTP Response ────────────│
│ │
5.2 无状态(Stateless)
关键特性:HTTP 是无状态协议——服务器不记得你上一次请求是谁。
每个请求都是独立的。服务器处理完一个请求后,不保留任何关于这个客户端的信息。
为什么设计成无状态?
- 服务器不需要维护大量连接状态,可以水平扩展(加更多机器)
- 任意一台服务器都可以处理任意请求(无粘性)
那用户登录状态怎么办? → 用 Cookie / Token 在每次请求中携带身份信息(见第 10 节 JWT)。
Keep-Alive vs 短连接
| 模式 | 行为 | 适用场景 |
|---|---|---|
| 短连接(HTTP/1.0 默认) | 每次请求建立新 TCP 连接,响应后关闭 | 极少请求的简单场景 |
| Keep-Alive(HTTP/1.1 默认) | 同一 TCP 连接复用,减少握手开销 | 现代 Web(页面含大量资源) |
5.3 请求结构
1
2
3
4
5
6
GET /users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json
{"name": "张三"} ← Body(GET请求通常无Body)
四个组成部分:
1
2
3
4
HTTP Request
├── 请求行:Method + URL + 协议版本
├── Headers:元数据(谁发的、接受什么格式、认证信息...)
└── Body:实际数据(POST/PUT 有,GET/DELETE 无)
常用 HTTP 方法:
| 方法 | 语义 | 有 Body? | 幂等? |
|---|---|---|---|
| GET | 查询资源 | 否 | 是 |
| POST | 创建资源 | 是 | 否 |
| PUT | 替换资源(全量更新) | 是 | 是 |
| PATCH | 修改资源(部分更新) | 是 | 否 |
| DELETE | 删除资源 | 否 | 是 |
幂等:执行多次与执行一次结果相同。
DELETE /users/1执行 10 次,结果都是”用户1不存在”,所以幂等。POST /orders执行 10 次会创建 10 个订单,不幂等。
5.4 响应结构
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 12 Mar 2026 08:00:00 GMT
{"id": 42, "name": "张三", "email": "zhang@example.com"}
状态码含义:
| 范围 | 含义 | 常见例子 |
|---|---|---|
| 2xx | 成功 | 200 OK, 201 Created, 204 No Content |
| 3xx | 重定向 | 301 Moved Permanently, 302 Found |
| 4xx | 客户端错误 | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found |
| 5xx | 服务器错误 | 500 Internal Server Error, 503 Service Unavailable |
5.5 URL 结构
1
2
https:// api.example.com :8080 /users/42 ?page=1&size=10 #profile
↑协议 ↑域名(主机名) ↑端口 ↑路径 ↑查询参数 ↑片段
端口默认值:HTTP → 80,HTTPS → 443,省略时使用默认值。
6. RESTful API
问题动机
早期 Web API 的混乱:每个公司设计 API 的方式完全不同:
1
2
3
4
5
/getUser?id=42
/createUser
/user_delete/42
/updateUserInfo
/fetchAllUsers
没有统一规范,API 难以理解、难以维护、不符合 HTTP 语义(用 GET 创建数据?)。
REST(Representational State Transfer) 提供了一套设计 API 的风格约定,让 API 可预测、可扩展、语义清晰。
6.1 核心思想
一切都是资源(Everything is a Resource)! 用 URL 表示资源,用 HTTP 方法表示操作。
1
2
3
4
5
6
7
8
9
10
不好的设计(动词 URL):
GET /getUser?id=42
POST /createUser
GET /deleteUser?id=42 ← 用GET删除?!
REST 设计(名词 URL + HTTP动词):
GET /users/42 → 获取用户42
POST /users → 创建新用户
PUT /users/42 → 更新用户42(全量)
DELETE /users/42 → 删除用户42
6.2 URL 设计规范
1
2
3
4
/users → 用户集合
/users/{id} → 特定用户
/users/{id}/posts → 该用户的所有文章(嵌套资源)
/users/{id}/posts/{postId} → 该用户的特定文章
规则:
- URL 使用名词复数(
/users,而非/user或/getUsers) - 资源 ID 放路径中(
/users/42),不放查询参数()/users?id=42 - 查询参数用于过滤、排序、分页:
/users?gender=male&page=2&size=20
6.3 六大约束
① 客户端 / 服务器分离(Client-Server)
前后端独立开发、独立部署。前端只关心 UI,后端只关心数据和逻辑。
② 无状态(Stateless)
每个请求必须包含所有必要信息。服务器不保存客户端会话。
1
2
3
4
5
6
✓ 正确:每次请求携带 Token
GET /users/42
Authorization: Bearer <token>
✗ 错误:依赖服务器记住"你已登录"
GET /users/42 ← 服务器怎么知道你是谁?
好处:服务器可水平扩展(Scale Out)——加机器即可,不需要共享会话状态。
③ 可缓存(Cacheable)
服务器可以在响应头中声明数据可缓存,客户端或中间代理(CDN)缓存结果,减少服务器压力。
1
2
Cache-Control: max-age=3600 → 缓存1小时
ETag: "abc123" → 资源版本标识
④ 统一接口(Uniform Interface)
所有资源都通过一致的方式访问(URL + HTTP方法),不因资源类型不同而异。
⑤ 分层系统(Layered System)
客户端不需要知道是直接连服务器,还是连了负载均衡器、CDN、API网关。各层之间透明。
1
Client → [CDN] → [Load Balancer] → [Server A / Server B / Server C]
⑥ 按需代码(Code on Demand,可选)
服务器可以返回可执行代码(如 JavaScript),客户端运行之。现代 Web 的基础。
6.4 实际 RESTful API 示例
1
2
3
4
5
6
7
8
9
10
11
# 用户管理
GET /api/users → 获取用户列表(支持分页:?page=1&size=20)
GET /api/users/42 → 获取用户42的详情
POST /api/users → 创建新用户(Body:用户信息JSON)
PUT /api/users/42 → 全量更新用户42
PATCH /api/users/42 → 部分更新(如只改密码)
DELETE /api/users/42 → 删除用户42
# 嵌套资源:用户的文章
GET /api/users/42/posts → 获取用户42的所有文章
POST /api/users/42/posts → 为用户42创建文章
7. 后端为什么存在
问题动机
既然浏览器可以运行 JS,可以发 HTTP 请求,为什么不让前端直接读写数据库?
原因 1:安全性
数据库凭证(用户名、密码)不能暴露给前端——任何人打开浏览器开发者工具都能看到。
原因 2:业务逻辑集中
“下单时如果库存不足应该怎么办?”“同一用户能不能重复购买?”这些逻辑必须在服务器端执行,否则客户端可以绕过。
原因 3:数据权限控制
用户 A 不能读取用户 B 的私信。这种权限判断必须在服务器做。
原因 4:数据持久化
浏览器关闭后数据消失。需要数据库持久化存储。
1
2
3
4
5
浏览器(Frontend)
↓ HTTP(只传公开数据,无DB凭证)
后端服务器(Backend)← 数据库凭证保存在这里
↓ SQL(内网,不暴露)
数据库(Database)
8. 后端分层架构
为什么要分层?
朴素方案:把所有逻辑写在一个地方——接收请求、验证数据、查数据库、返回响应。
问题:
- 代码复用差(相同的数据库查询逻辑在多处复制)
- 单元测试困难(业务逻辑和HTTP耦合,测试必须启动HTTP服务器)
- 修改困难(改数据库表结构,要改所有涉及它的处理函数)
解决方案:关注点分离 ——按职责划分层次。
1
2
3
4
5
6
7
8
9
10
11
HTTP请求
↓
Controller(接收HTTP,转换格式)
↓
Service(执行业务逻辑)
↓
Repository(访问数据库)
↓
Entity(数据模型)
↓
Database(MySQL)
每一层只对上一层负责,不越界。
8.1 Entity — 数据模型
职责:描述数据库表结构,是 Java 类到数据库表的映射(ORM)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 对应数据库的 id 列(自增主键)
@Column(nullable = false, unique = true)
private String username; // 对应 username 列
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String passwordHash; // 注意:存储的是哈希,不是明文密码
@CreationTimestamp
private LocalDateTime createdAt;
// Getters & Setters(或使用 Lombok @Data)
}
ORM(对象关系映射):
1
2
3
Java 类 ←→ 数据库表
Java 对象 ←→ 数据库行
Java 字段 ←→ 数据库列
好处:开发者用 Java 对象思考,不需要手写 SQL(大多数情况),切换数据库时改动最小。
8.2 Repository — 数据访问层
职责:与数据库交互,提供 CRUD 操作。屏蔽 SQL 细节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// JpaRepository 自动提供:
// findById(Long id)
// findAll()
// save(User user) → 插入或更新
// deleteById(Long id)
// count()
// 自定义查询:Spring Data JPA 根据方法名自动生成 SQL
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
// 需要复杂查询时,用 @Query 注解
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findUsersCreatedAfter(@Param("date") LocalDateTime date);
}
工程连接:Spring Data JPA(基于 Hibernate)会根据方法名自动生成 SQL。
findByUsername→SELECT * FROM users WHERE username = ?,无需手写。
8.3 Service — 业务逻辑层
职责:实现具体业务规则。Controller 不应包含业务逻辑,Repository 不应包含业务逻辑——它们都在 Service。
为什么 Controller 不能写业务逻辑?
假设”注册用户”的逻辑写在 Controller 里,后来需要在”批量导入用户”的接口也用同一逻辑——你就得复制代码。而 Service 可以被多个 Controller 调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder; // 密码哈希工具
/**
* 注册新用户
* 业务规则:
* 1. 邮箱不能重复
* 2. 密码需要哈希后存储
* 3. 返回创建成功的用户(不含密码)
*/
public UserDTO register(RegisterRequest request) {
// 业务规则1:检查邮箱是否已存在
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException("邮箱已被注册:" + request.getEmail());
}
// 业务规则2:密码哈希
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPasswordHash(passwordEncoder.encode(request.getPassword())); // 不存明文!
// 保存到数据库
User savedUser = userRepository.save(user);
// 返回 DTO(不包含密码哈希)
return new UserDTO(savedUser.getId(), savedUser.getUsername(), savedUser.getEmail());
}
/**
* 根据 ID 获取用户
* 业务规则:用户不存在时抛出有意义的异常
*/
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("用户不存在:" + id));
return new UserDTO(user.getId(), user.getUsername(), user.getEmail());
}
}
DTO(Data Transfer Object):Controller 和 Service 之间传递的数据对象,通常是 Entity 的子集。
好处:
- 不把
passwordHash暴露给前端 - 可以组合多个 Entity 的字段,按需返回
8.4 Controller — HTTP 接入层
职责:接收 HTTP 请求,调用 Service,把结果转换成 HTTP 响应。Controller 不处理业务逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController // = @Controller + @ResponseBody(自动JSON序列化)
@RequestMapping("/api/users") // 所有方法共享这个前缀
public class UserController {
@Autowired
private UserService userService;
/**
* GET /api/users/{id}
* 获取指定用户的信息
*/
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user); // HTTP 200 + JSON Body
}
/**
* POST /api/users
* 注册新用户
*/
@PostMapping
public ResponseEntity<UserDTO> register(@RequestBody @Valid RegisterRequest request) {
// @RequestBody:从HTTP请求Body解析JSON到Java对象
// @Valid:触发Bean Validation(字段校验)
UserDTO created = userService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created); // HTTP 201
}
/**
* DELETE /api/users/{id}
* 删除用户(需要权限,见第10节)
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build(); // HTTP 204,无Body
}
}
常用注解速查:
| 注解 | 作用 |
|---|---|
@RestController | 声明这是一个 REST 控制器,方法返回值自动序列化为 JSON |
@RequestMapping("/path") | 声明控制器的 URL 前缀 |
@GetMapping, @PostMapping 等 | 对应 HTTP 方法的快捷注解 |
@PathVariable | 从 URL 路径中提取参数(如 /users/{id} 中的 id) |
@RequestBody | 从请求 Body 中解析 JSON 到 Java 对象 |
@RequestParam | 从查询参数中提取值(如 ?page=1 中的 page) |
@Valid | 触发字段校验(配合 Bean Validation 注解) |
8.5 层间数据流图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
HTTP Request (JSON)
POST /api/users
{"username":"张三","email":"zhang@e.com","password":"abc123"}
│
▼
┌───────────────────┐
│ Controller │ ← 解析JSON → RegisterRequest 对象
│ @PostMapping │ 调用 userService.register(request)
└────────┬──────────┘
│ RegisterRequest
▼
┌───────────────────┐
│ Service │ ← 检查邮箱是否重复
│ @Service │ 密码哈希
│ │ 构建 User 对象
└────────┬──────────┘
│ User 对象
▼
┌───────────────────┐
│ Repository │ ← userRepository.save(user)
│ JpaRepository │ 生成 INSERT SQL
└────────┬──────────┘
│ SQL
▼
┌───────────────────┐
│ MySQL │ ← 执行 INSERT,返回生成的 id
└────────┬──────────┘
│ 返回保存后的 User
▼ (逐层返回)
HTTP Response (JSON)
HTTP 201 Created
{"id":42,"username":"张三","email":"zhang@e.com"}
9. Spring Boot
问题动机
Spring 框架(Spring MVC)已经提供了 Controller/Service/Repository 的完整支持,但配置极其繁琐:
1
2
3
4
5
6
7
8
<!-- 早期 Spring:需要大量 XML 配置 -->
<bean id="dataSource" class="...">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
...
</bean>
<bean id="sessionFactory" ...>...</bean>
<bean id="transactionManager" ...>...</bean>
启动一个 Spring Web 应用,还需要安装 Tomcat,把应用打成 WAR 包部署上去。
Spring Boot 解决了这个问题。
Spring Boot 做了三件事
① 自动配置(Auto Configuration)
根据 classpath 中有哪些依赖,自动推断需要什么 Bean,自动配置好。
1
2
3
4
5
6
7
8
9
10
11
你引入了 spring-boot-starter-data-jpa + mysql-connector-java
Spring Boot 自动:
- 配置 DataSource(连接池)
- 配置 EntityManagerFactory
- 配置 TransactionManager
你只需要在 application.properties 里写数据库地址和密码
# application.properties(这就是全部需要写的)
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
② Starter 依赖管理
不再需要手动管理每个库的版本,引入一个 Starter,所有相关依赖一起来:
1
2
3
4
5
6
7
8
9
10
11
<!-- pom.xml:引入Web开发所需的全部依赖(Spring MVC + Tomcat + Jackson等) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入数据库访问所需的全部依赖(JPA + Hibernate + 连接池) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
③ 内嵌服务器
Spring Boot 内嵌了 Tomcat(或 Jetty、Undertow)。运行 main() 方法即可启动,不需要单独安装服务器。
1
2
3
4
5
6
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args); // 启动内嵌Tomcat,监听8080端口
}
}
IoC 与 DI:Spring 的核心机制
问题:如果 Controller 里 new UserService(),Controller 和 UserService 就强耦合了——测试时无法替换 UserService 为 Mock 对象。
IoC(控制反转):对象的创建权交给 Spring 容器,而不是由你 new。
DI(依赖注入):你只声明”我需要一个 UserService”,Spring 自动把它”注入”进来。
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {
@Autowired // Spring 自动注入 UserRepository
private UserRepository userRepository;
// 你不需要 new UserRepository()
}
@RestController
public class UserController {
@Autowired // Spring 自动注入 UserService
private UserService userService;
}
好处:
- 松耦合:测试时可以注入 Mock 对象
- 单例管理:Spring 默认一个 Bean 只有一个实例,节省资源
10. Spring Security + JWT 认证
问题动机
HTTP 是无状态的——服务器不记得你上次的请求。那么”用户登录后,后续请求如何证明身份”?
方案 1(Session + Cookie):
1
2
登录 → 服务器创建 Session,返回 SessionID
后续请求携带 SessionID Cookie → 服务器查 Session 表验证
问题:
- Session 存在服务器内存中,服务器水平扩展时需要 Session 共享(Redis)
- Cookie 有跨域限制,移动端 App 使用不便
方案 2(JWT Token):
1
2
登录 → 服务器生成 Token,发给客户端
后续请求携带 Token → 服务器直接验证 Token(无需查数据库)
好处:无状态,天然支持水平扩展;适合前后端分离和移动端。
10.1 JWT 结构
JWT(JSON Web Token)由三部分组成,用 . 分隔:
1
2
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsImlhdCI6MTcwMH0.Abc123XYZ
↑Header ↑Payload(Claims) ↑Signature
- Header:声明算法(
{"alg": "HS256", "typ": "JWT"}),Base64 编码 - Payload:存放用户信息(
{"sub": "42", "username": "张三", "roles": ["USER"], "exp": 1700000000}),Base64 编码 - Signature:
HMACSHA256(base64(Header) + "." + base64(Payload), 密钥)
关键洞察:Header 和 Payload 是 Base64 编码,不是加密——任何人都能解码看到内容。Signature 的作用是防篡改,不是保密。所以 JWT 里不要放密码等敏感信息。
10.2 完整认证流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ 客户端 │ │ Spring Boot │ │ MySQL │
└────┬────┘ └──────┬──────┘ └────┬────┘
│ │ │
│ POST /api/auth/login │ │
│ {"username","pass"} │ │
│─────────────────────►│ │
│ │ SELECT * FROM users │
│ │ WHERE username=? │
│ │─────────────────────►│
│ │◄─────────────────────│
│ │ 验证密码哈希 │
│ │ 生成JWT Token │
│◄─────────────────────│ │
│ {"token":"eyJ..."} │ │
│ │ │
│ GET /api/users/42 │ │
│ Authorization: Bearer eyJ... │
│─────────────────────►│ │
│ │ 验证JWT签名 │
│ │ 解析用户ID和角色 │
│ │ 执行业务逻辑 │
│◄─────────────────────│ │
│ {"id":42,"name":...} │ │
10.3 Spring Security 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtFilter; // 自定义JWT过滤器
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离时关闭CSRF(Token已防护)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,不用Session
.and()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录/注册接口无需认证
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理接口需要ADMIN角色
.anyRequest().authenticated() // 其他接口需要登录
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
// 在用户名密码过滤器之前,先走JWT验证
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt:自动加盐哈希,抗彩虹表攻击
}
}
10.4 JWT 过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头提取 Token
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); // 没有Token,继续(后续由权限配置决定是否拒绝)
return;
}
String token = authHeader.substring(7); // 去掉 "Bearer " 前缀
// 2. 验证 Token(验证签名 + 过期时间)
if (jwtService.isTokenValid(token)) {
Long userId = jwtService.extractUserId(token);
String username = jwtService.extractUsername(token);
List<String> roles = jwtService.extractRoles(token);
// 3. 将认证信息放入 Spring Security 上下文
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null,
roles.stream().map(SimpleGrantedAuthority::new).collect(toList()));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response); // 继续处理请求
}
}
10.5 登录接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtService jwtService;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
// 1. 根据用户名查用户
User user = userService.findByUsername(request.getUsername())
.orElseThrow(() -> new BadCredentialsException("用户名或密码错误"));
// 2. 验证密码(比较明文和哈希)
if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
throw new BadCredentialsException("用户名或密码错误");
}
// 3. 生成 JWT
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new AuthResponse(token));
}
}
11. 完整请求链路串讲
让我们追踪一个完整的请求:“用户A查询自己的订单列表”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
① 用户在浏览器点击"我的订单"
React 发起 AJAX 请求:
fetch('/api/orders', {
headers: { 'Authorization': 'Bearer eyJ...' }
})
② HTTP Request 到达服务器
GET /api/orders HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
③ Spring Security 过滤器链处理
JwtAuthenticationFilter:
- 提取 Bearer Token
- 验证签名(防篡改)
- 验证过期时间
- 解析 userId = 42, roles = ["USER"]
- 写入 SecurityContext
④ OrderController.getOrders() 被调用
@GetMapping("/api/orders")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<List<OrderDTO>> getOrders() {
Long userId = SecurityContextHolder 取当前用户ID (42)
return ResponseEntity.ok(orderService.getOrdersByUser(42));
}
⑤ OrderService.getOrdersByUser(42) 执行业务逻辑
- 调用 orderRepository.findByUserId(42)
- 业务规则:只返回未取消的订单
⑥ OrderRepository 执行数据库查询
JPA 生成 SQL:
SELECT * FROM orders WHERE user_id = 42 AND status != 'CANCELLED'
⑦ MySQL 返回结果,逐层返回
⑧ Controller 返回 JSON 响应
HTTP/1.1 200 OK
Content-Type: application/json
[{"id":1,"product":"iPhone","price":7999},...]
⑨ React 收到响应,更新 Virtual DOM,页面显示订单列表
12. 代码练习题
练习 1(基础):完善 Entity 和 Repository
背景:实现一个简单的文章管理系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 已给出:User Entity(见第8.1节)
// 任务:完成 Post Entity
@Entity
@Table(name = "posts")
public class Post {
// 要求:
// 1. id 字段(自增主键)
// 2. title 字段(不可为空,最大长度200)
// 3. content 字段(TEXT类型)
// 4. author 字段(与 User 的多对一关联:一个用户可有多篇文章)
// 5. createdAt 字段(自动记录创建时间)
// 提示:多对一关联用 @ManyToOne + @JoinColumn(name = "user_id")
}
// 任务:完成 PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
// 1. 根据 author 的 id 查所有文章
// 2. 根据 title 模糊搜索(提示:方法名 findByTitleContaining)
// 3. 统计某用户的文章数量
}
练习 2(中级):实现 Service 业务逻辑
背景:继续文章系统,实现 PostService。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
/**
* 任务1:实现 createPost
* 业务规则:
* a) 作者必须存在,否则抛 UserNotFoundException
* b) 标题不能为空(额外校验)
* c) 返回创建后的 PostDTO(含 id、title、authorName、createdAt)
*/
public PostDTO createPost(Long authorId, CreatePostRequest request) {
// 你的实现
}
/**
* 任务2:实现 deletePost
* 业务规则:
* a) 文章必须存在
* b) 只有作者本人可以删除(否则抛 ForbiddenException)
* c) currentUserId 从参数传入(Controller 从 SecurityContext 获取)
*/
public void deletePost(Long postId, Long currentUserId) {
// 你的实现
}
}
练习 3(进阶):完整 REST Controller
任务:为文章系统实现完整的 PostController,要求:
GET /api/posts— 获取文章列表(支持查询参数?authorId=42)GET /api/posts/{id}— 获取单篇文章详情POST /api/posts— 创建文章(需要登录)DELETE /api/posts/{id}— 删除文章(需要登录,且是作者本人)
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/api/posts")
public class PostController {
@Autowired
private PostService postService;
// 提示:从 SecurityContext 获取当前用户ID:
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// Long currentUserId = (Long) auth.getPrincipal(); // 需要在JWT过滤器中正确设置
// 你的实现
}
思考题(不需要写代码,只需思考):
- 为什么
GET /api/posts不需要登录,而POST /api/posts需要? DELETE /api/posts/{id}中,如果权限校验放在 Controller 而不是 Service,有什么问题?POST /api/posts如果用户在 1 秒内连发 100 次请求,如何在不修改业务逻辑的情况下限制?(提示:考虑过滤器或 AOP)
13. 附录
13.1 常见误区对照表
| 误区 | 正确理解 |
|---|---|
| “GET 请求更安全,敏感数据用 GET 传” | GET 参数在 URL 中明文显示,会被日志记录、浏览器历史保存,更不安全。敏感数据用 POST,通过 Body + HTTPS 传输 |
| “JWT 是加密的,存什么都行” | JWT 的 Header 和 Payload 只是 Base64 编码,不是加密。任何人解码即可看到内容。不要存密码、银行卡号等敏感信息 |
| “Service 层只是对 Repository 的简单包装” | Service 层是业务逻辑的核心,权限校验、事务管理、跨表操作、业务规则都在这里。只是包装就没有必要单独分层 |
| “PUT 和 POST 都能创建资源” | REST 约定:POST 创建资源(URL 不含 ID),PUT 替换/更新特定资源(URL 含 ID)。混用会导致 API 语义混乱 |
| “后端返回数据,直接把 Entity 序列化成 JSON” | 应该使用 DTO。直接返回 Entity 会暴露 passwordHash 等敏感字段,且 Entity 可能包含 ORM 代理对象导致序列化死循环 |
| “Spring Boot = Spring” | Spring Boot 是对 Spring 生态的封装和自动配置,Spring 才是核心框架。Spring Boot 解决的是”开箱即用”问题 |
| “REST 是一种协议” | REST 是一种架构风格(设计约定),不是协议。HTTP 是协议,REST 是用 HTTP 时的最佳实践 |
13.2 关键概念速查表
| 概念 | 一句话定义 | 所在层 |
|---|---|---|
| Entity | Java 类到数据库表的映射对象 | 数据层 |
| Repository | 数据库访问接口,提供 CRUD | 数据层 |
| DTO | 数据传输对象,Controller↔Service 间传递 | 跨层 |
| Service | 业务逻辑实现,调用 Repository | 业务层 |
| Controller | HTTP 接入,调用 Service,序列化响应 | 接入层 |
| JWT | 自包含的无状态认证 Token,Base64编码+签名 | 安全层 |
| ORM | 对象与关系数据库间的映射机制 | 数据层 |
| IoC / DI | 对象创建权交给容器,通过注入解耦 | Spring核心 |
| AJAX | 后台HTTP请求,不刷新整页更新DOM | 前端 |
| Virtual DOM | React 的内存DOM,Diff后最小化真实DOM操作 | 前端 |
13.3 知识依赖图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP 协议(方法、状态码、Header)
└──► RESTful API 设计(URL设计、资源语义)
└──► Controller 层(接收HTTP请求)
└──► Service 层(业务逻辑)
├──► Repository 层(数据访问)
│ └──► Entity / ORM(数据模型)
│ └──► MySQL
└──► Spring Security + JWT(认证)
浏览器 / DOM
└──► HTML / CSS / JS(前端三件套)
└──► AJAX(异步请求)
└──► React / Vue(现代框架)
└──► Virtual DOM / 响应式系统
Spring Boot(自动配置、内嵌服务器)
└──► 整合 Controller / Service / Repository
└──► IoC / DI(Bean 注入)