Post

前后端

前后端

阅读指南

本笔记按照”问题驱动 → 机制 → 工程实践”的顺序展开。 建议阅读顺序:HTTP 基础 → RESTful API → 前端三件套 → 后端分层架构 → Spring Boot → JWT 认证

知识依赖链:HTTP协议RESTful设计Controller层Service层Repository/ORMSpring 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>
对比ReactVue
核心思想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。findByUsernameSELECT * 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 编码
  • SignatureHMACSHA256(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,要求:

  1. GET /api/posts — 获取文章列表(支持查询参数 ?authorId=42
  2. GET /api/posts/{id} — 获取单篇文章详情
  3. POST /api/posts — 创建文章(需要登录)
  4. 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 关键概念速查表

概念一句话定义所在层
EntityJava 类到数据库表的映射对象数据层
Repository数据库访问接口,提供 CRUD数据层
DTO数据传输对象,Controller↔Service 间传递跨层
Service业务逻辑实现,调用 Repository业务层
ControllerHTTP 接入,调用 Service,序列化响应接入层
JWT自包含的无状态认证 Token,Base64编码+签名安全层
ORM对象与关系数据库间的映射机制数据层
IoC / DI对象创建权交给容器,通过注入解耦Spring核心
AJAX后台HTTP请求,不刷新整页更新DOM前端
Virtual DOMReact 的内存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 注入)
This post is licensed under CC BY 4.0 by the author.