输入验证
输入验证的重要性
输入验证是确保应用程序安全性和稳定性的关键环节。未经处理的用户输入可能导致SQL注入、跨站脚本攻击(XSS)或其他安全漏洞。Node.js作为服务端JavaScript运行时,需要特别注意对传入数据的严格校验。
基础验证方法
最简单的验证方式是检查输入是否存在以及是否符合预期类型:
function validateUsername(username) {
if (!username || typeof username !== 'string') {
throw new Error('用户名必须为非空字符串');
}
return username.trim();
}
对于更复杂的场景,可以使用正则表达式进行模式匹配:
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('无效的邮箱格式');
}
return email;
}
使用验证库
手动编写验证逻辑容易出错且难以维护。流行的验证库如Joi可以简化这个过程:
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{8,30}$')),
email: Joi.string().email({ minDomainSegments: 2 })
});
function validateUser(user) {
const { error, value } = userSchema.validate(user);
if (error) throw error;
return value;
}
中间件验证
在Express应用中,可以创建验证中间件集中处理输入验证:
const validateLogin = (req, res, next) => {
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
app.post('/login', validateLogin, (req, res) => {
// 处理登录逻辑
});
文件上传验证
文件上传需要特殊处理,包括检查文件类型、大小等:
const multer = require('multer');
const upload = multer({
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
},
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
return cb(new Error('只允许上传图片文件'));
}
cb(null, true);
}
});
app.post('/upload', upload.single('avatar'), (req, res) => {
// 处理上传文件
});
深度对象验证
嵌套对象需要递归验证:
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/)
});
const userSchema = Joi.object({
name: Joi.string().required(),
addresses: Joi.array().items(addressSchema)
});
自定义验证规则
Joi允许创建自定义验证规则:
const Joi = require('joi');
const customValidation = Joi.extend((joi) => {
return {
type: 'string',
base: joi.string(),
rules: {
noSpaces: {
validate(value, helpers) {
if (/\s/.test(value)) {
return helpers.error('string.noSpaces');
}
return value;
}
}
},
messages: {
'string.noSpaces': '{{#label}} 不能包含空格'
}
};
});
const schema = customValidation.string().noSpaces();
验证性能优化
对于高频验证场景,可以预编译验证函数:
const Joi = require('joi');
const userSchema = Joi.object({
// 定义schema
});
// 预编译
const validateUser = userSchema.validate.bind(userSchema);
// 后续直接使用
function processUser(user) {
const { error, value } = validateUser(user);
// ...
}
客户端与服务端双重验证
虽然客户端验证能提升用户体验,但服务端验证必不可少:
// 客户端验证示例 (React)
function UserForm() {
const [errors, setErrors] = useState({});
const validate = (values) => {
const errs = {};
if (!values.username) errs.username = '用户名必填';
if (values.password?.length < 8) errs.password = '密码至少8位';
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate(formData)) {
// 提交到服务端
}
};
// 表单渲染...
}
验证错误处理
良好的错误处理应该提供清晰的错误信息:
app.use((err, req, res, next) => {
if (err instanceof Joi.ValidationError) {
return res.status(422).json({
message: '验证失败',
details: err.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
});
}
next(err);
});
数据库层面的验证
除了应用层验证,数据库约束也很重要:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: props => `${props.value} 不是有效的邮箱地址`
}
},
// 其他字段...
});
验证与业务逻辑分离
保持验证逻辑独立有利于代码维护:
// validators/userValidator.js
exports.validateCreateUser = (user) => {
const schema = Joi.object({ /* ... */ });
return schema.validate(user);
};
// controllers/userController.js
const { validateCreateUser } = require('../validators/userValidator');
exports.createUser = async (req, res) => {
const { error, value } = validateCreateUser(req.body);
if (error) return res.status(400).json({ error: error.message });
// 业务逻辑...
};
测试验证逻辑
为验证逻辑编写测试用例:
const { validateEmail } = require('./validators');
describe('邮箱验证', () => {
test('接受有效邮箱', () => {
expect(validateEmail('test@example.com')).toBe('test@example.com');
});
test('拒绝无效邮箱', () => {
expect(() => validateEmail('invalid')).toThrow('无效的邮箱格式');
});
});
验证与类型系统结合
使用TypeScript可以增强验证效果:
interface User {
username: string;
email: string;
age?: number;
}
function validateUser(user: unknown): user is User {
// 运行时验证逻辑
return Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().optional()
}).validate(user).error === undefined;
}
function processUser(user: unknown) {
if (!validateUser(user)) throw new Error('无效用户数据');
// 现在user被类型推断为User接口
}
验证与安全头
设置适当的安全头可以防止某些攻击:
const helmet = require('helmet');
app.use(helmet());
app.use(express.json({ limit: '10kb' })); // 限制请求体大小
验证与速率限制
防止暴力破解需要限制请求频率:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP限制100次请求
});
app.use('/login', limiter);
验证与内容安全策略
设置CSP防止XSS攻击:
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:']
}
})
);
验证与CSRF防护
防止跨站请求伪造:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// 验证CSRF令牌
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn