Web 架构的演化:PHP 的回忆与 CORS 的噩梦

📖 6min read

“那时候,一个文件就够了……”

我大学时接触过的 PHP 真的非常方便。一个 index.php 文件里既能写 HTML 标签,也能在中间打开 <?php ?> 直接写数据库查询代码。前端和后端之间几乎没有界线。声明一个变量,HTML 里到处都能拿来用,也几乎不会因为数据通信而报错。

但到了实务里,当技术栈切换成 Vue.js(前端)和 Spring Boot(后端)之后,地狱就开始了。

“明明本地 Spring 服务器已经起了,Vue 页面也起来了,为什么数据就是过不来?”

Chrome 开发者工具的控制台,早已被鲜红的报错信息刷满了。

Access to XMLHttpRequest at ... has been blocked by CORS policy

这个叫做 CORS 的家伙到底是什么,为什么偏偏要拦住我的数据?还有,我们为什么要放弃过去那种一个文件就能搞定的舒服方式,偏要把前端和后端硬生生拆开?

以前住在同一个屋檐下,如今住在不同的房子里,见面之前还得先过安检。

到底谁来做菜?(SSR vs CSR)

要理解这种变化,就得先弄明白:名为网页(HTML)的“料理”到底是由谁做出来的。这正是 SSR(Server Side Rendering)与 CSR(Client Side Rendering)之间的差别。

1. SSR:做好的便当(PHP, JSP)

2. CSR:配送来的料理包(React, Vue)

[Tip] CSR 和 SPA 不是同一个概念

这两个词经常被混着用,但严格来说并不相同。

我们通常是为了做 SPA 才采用 CSR。也就是说,因为我们想通过单页方式避免整个页面闪烁刷新(SPA),所以才选择让浏览器自己去局部重绘的方式(CSR)。

我们之所以用 Vue.js 或 React,本质上是为了用户体验。为了让网站像手机 App 一样顺滑,我们把“做菜”的主体从服务器移到了浏览器。

拦住不速之客的保安:CORS

前端(Vue)和后端(Spring)分离之后,我们实际上拥有了“两栋房子”。

问题就出在这里。出于安全原因,Web 浏览器有一条默认原则:“不要轻易信任来自不同 Origin 的资源。” 这就是 SOP(Same Origin Policy)。

你想想看:如果我正登录着 Naver,而黑客做的网站偷偷向 Naver 服务器发请求说“把这个用户的隐私信息交出来”,那浏览器要是不拦着,我的数据不就全都被偷走了吗?

所以浏览器默认会拦截不同 Origin,也就是不同域名或端口发出的请求。可我们为了开发,偏偏又有意把端口拆开。明明只是想用自己的数据,浏览器这个“保安”却说:“你的出身端口不一样,禁止入内!” 这就是所谓 CORS(Cross-Origin Resource Sharing)错误的真面目。

[Code Verification] 给它发一张通行证

这个问题的解决方法其实很简单。服务器,也就是后端,只要向浏览器这个“保安”说明:“这是我邀请来的客人,请放行。” 相当于给它发一张通行证就行了。

在 Spring Boot 里,只用一个配置文件就能签发这张通行证。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 1. 對所有 API 路徑
                .allowedOrigins("http://localhost:8080") // 2. 允許從此位址發來的請求
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 3. 允許這幾種方法的請求
                .allowCredentials(true); // 4. 可一併包含 Cookie/認證資訊
    }
}

分析:

需要注意的是,别因为嫌麻烦就用 allowedOrigins("*") 把所有来源都放开。那就跟把家门完全敞开,然后贴张纸条写着“欢迎小偷光临”没什么区别。

实务建议:什么时候该用什么?

那么 CSR,也就是 Vue/React,难道就一定是标准答案吗?并不是。实务里还是要根据目标来选。

结语:跨越边界

PHP 时代那种“一个屋檐下的大家庭”虽然舒服,但随着系统变复杂,管理就越来越困难。现在这种“分家后的两个家庭(前端/后端)”虽然因为通信和 CORS 变得麻烦,却也让双方都能更专注在各自的角色上。

所以下次再遇到 CORS 错误时,别只顾着烦躁。那并不是浏览器在故意折腾我们,而是它为了保护我们的家,也就是服务器,而进行的一次严格安检。

好了,现在前端和后端之间的通道已经打通,数据也可以来回流动了。那么这些数据到底是存放在服务器的什么地方呢?以前只要随手写个查询就能拿到数据,可在 Spring 里却多了 JPA、Entity 这些东西,像是在阻止你直接写 SQL。

下一篇,我们就来看看“数据库设计的教科书做法(规范化)”在实务中是怎么反过来拖后腿的,以及为什么 JPA 总想把 SQL 藏起来。

發佈留言