注入
首先,关闭注入漏洞。用户输入不应该被信任,也不应该直接传递给另一个子系统。最好的办法是在旁通道中传递用户数据;在 SQL 中,这意味着使用准备好的查询。例如,您可以使用connection.execute,而不是connection.query:
connection.execute(
'SELECT Count(*) AS usernameCount FROM adminuser WHERE username=?',
[username],
(err, data) => {...}
)
可用的 MySQL 驱动程序可能只支持转义参数和模拟预准备语句,但这总比没有好(但如果是这种情况,您应该认真考虑切换到支持预准备语句的驱动程序):
connection.query(
mysql.format('SELECT Count(*) AS usernameCount FROM adminuser WHERE username=?', [username]),
...
逻辑错误
仔细查看多语句示例中的块:
if(password === confirm){
if(error_mssg.lenght > 0){
*******query to insert user information*******
}else{
res.send(error_mssg)
}
...
注意如果有错误信息则插入用户信息,如果没有则返回(空)错误信息列表。这与您想要的相反。调试器可以帮助捕捉这种逻辑错误。
如果密码不匹配,您不仅可以发送存在错误消息,还可以发送所有错误消息。这样,用户可以一次更正所有错误,而不必只提交以发现其他错误。这也将更好地将请求验证与请求处理分开。
if (password !== confirm) {
error_msgs.push("passwords do not match");
}
if (error_msgs.length) {
res.send(error_msgs);
} else {
// create new user
...
}
存在性测试
查询现有记录的另一种方法是使用单个查询来检查adminuser 数据库中的现有用户名和电子邮件地址。这比链式查询或awaits 具有效率优势,因为一个查询不需要等待另一个查询完成(尽管后两者的性能成本可能可以忽略不计)。
由于这两个字段是同一数据库中的不同列,因此查询很简单:
SELECT username, email FROM adminuser WHERE username=? OR email=?;
用户名和电子邮件应唯一的数据要求应通过使用UNIQUE 索引反映在架构中。这会将结果行限制为最多 2 行(每个值的最大出现次数为 1),然后可以检查是否存在唯一值。
connection.execute(
'SELECT username, email FROM adminuser WHERE username=? OR email=?;',
[username, email],
(err, data) => {
if (err) throw err;
/*** Validate user creation request ***/
/* There should be at most 2 rows, with at most 1 occurrence each of `username` and `email`. */
for (var iRow=0; iRow < data.length; ++iRow) {
if (username == data[iRow].username) {
error_msgs.push("username already exists");
}
if (email == data[iRow].email) {
error_msgs.push("email already exists");
}
}
if (password !== confirm) {
error_msgs.push("passwords do not match");
}
/*** Handle user creation request ***/
if (error_msgs.length) {
res.send(error_msgs);
} else {
// create new user
...
}
}
);
以更复杂的 SQL 语句为代价,可以使存在性检查更加健壮(以防数据库出现问题并返回多次出现的用户名或电子邮件)。如果驱动程序支持,命名占位符(而不是“?”)可以用于清晰和减少冗余。
// namedPlaceholders can also be an `execute` option
connection.config.namedPlaceholders = true
...
connection.execute(
'SELECT Sum(If(:username=username, 1, 0) As usernameCount,
Sum(If(:email=email, 1, 0) AS emailCount
FROM adminuser
WHERE username=:username OR email=:email;',
{ username, email }, // you could instead pass req.body, assuming it's been checked for 'username' and 'email' properties
(err, data) => {
if (err) throw err;
/*** Validate user creation request ***/
if (data[0].usernameCount) {
error_msgs.push("username already exists");
}
if (data[0].emailCount) {
error_msgs.push("email already exists");
}
if (password !== confirm) {
error_msgs.push("passwords do not match");
}
/*** Handle user creation request ***/
if (error_msgs.length) {
res.send(error_msgs);
} else {
// create new user
...
}
});
其他设计注意事项
示例代码混合了几个不同的问题:数据库访问和请求处理(验证等)。换句话说,示例代码承担了多重责任,这是不可取的。代码单元的职责应尽可能少;一个类应该有一个单一的职责。单一职责最流行的定义是Single Responsibility Principle:“一个类应该只有一个改变的理由”。此定义并不完全适用于大于类的代码单元(取决于如何表达更改原因)。
在示例中,考虑可能出现的维护任务:
- 添加/切换 DBMS 支持
- 更新用户字段验证(包括添加字段)
- 在创建用户时发送消息(例如管理员、模组、新用户)
其中每一个都是更改代码的不同原因,因此是不同的责任。
Separating concerns 将导致类的职责减少。一个模块,数据访问层,应该处理管理数据库中的用户(例如检查现有记录中的用户名或电子邮件,添加新用户),另一个应该通过进行适当的调用来处理用户创建请求(验证和用户创建)到 DAL(可能是间接的)。特别是,SQL 语句和数据库调用不应出现在请求处理程序中。请求处理程序可能类似于:
async function validateUserCreationRequest(req) {
const { username, email, password, confirm } = req.body;
var error_msgs = [],
userExists = await User.exists({ username, email }); // here, User is responsible for accessing the DAL
if (userExists) {
if (userExists.usernameCount) {
error_msgs.push("username already exists");
}
if (userExists.emailCount) {
error_msgs.push("email already exists");
}
}
if (password !== confirm) {
error_msgs.push("passwords do not match");
}
if (error_msgs.length) {
return error_msgs;
}
return null;
}
function createUser(req) {
return User.create(req); // again, User is responsible for accessing the DAL
}
function handleUserCreationRequest(req) {
var errors = validateUserCreationRequest(req);
if (errors) {
res.send(errors);
} else {
createUser(req);
}
}
当代码承担多重责任时,一般是高度coupled(处理不同任务的代码之间存在高度的相互依赖关系),这使得维护更加困难并导致更多的错误。分离这些职责减少了耦合并增加了cohesion(模块中的代码属于一起,因为它们只关注它们完成的任务),因为耦合和内聚是反向相关的。
concerns and responsibilities 之间有相当多的重叠,但它们是 distinct 原则。
在某些情况下可能会违反这些原则,但这应该源于软件需求并且是有意识的决定。