Appearance
代码审查案例分析
通过真实案例分析,我们可以更好地理解代码审查在实际项目中的应用,以及如何识别和解决常见的代码问题。
案例一:电商网站支付模块审查
背景
电商平台支付模块需要处理用户付款、验证支付信息、更新订单状态等功能。
原始代码 (需要审查)
javascript
// bad-payment.js
function processPayment(userId, orderId, amount, cardInfo) {
// 1. 验证用户
let user = db.getUserById(userId);
if (!user) {
return { error: 'User not found' };
}
// 2. 验证订单
let order = db.getOrderById(orderId);
if (!order) {
return { error: 'Order not found' };
}
// 3. 验证金额
if (amount != order.total) { // 使用 == 而非 ===
return { error: 'Amount mismatch' };
}
// 4. 处理支付
let paymentResult = externalPaymentProvider.charge(cardInfo, amount);
// 5. 更新订单状态
if (paymentResult.success) {
db.updateOrderStatus(orderId, 'paid');
// 6. 发送邮件 - 没有错误处理
emailService.sendConfirmation(user.email, order);
// 7. 更新库存 - 没有错误处理
inventoryService.updateStock(order.items);
return { success: true, transactionId: paymentResult.transactionId };
} else {
return { error: paymentResult.message };
}
}
问题分析
1. 安全问题
- 输入验证不足: 没有验证
userId、orderId、amount、cardInfo的格式和类型 - 权限检查缺失: 没有验证用户是否有权访问指定订单
- 敏感信息泄露: 错误消息可能暴露系统内部信息
2. 错误处理问题
- 异步操作缺乏错误处理: 邮件发送和库存更新没有错误处理
- 弱比较: 使用
!=而非!==
3. 代码质量问题
- 函数职责过多: 单个函数承担了太多职责
- 硬编码逻辑: 缺少配置和常量定义
改进后的代码
javascript
// good-payment.js
const PAYMENT_STATUS = {
PENDING: 'pending',
PAID: 'paid',
FAILED: 'failed',
REFUNDED: 'refunded'
};
const MAX_RETRY_ATTEMPTS = 3;
class PaymentService {
constructor(db, paymentProvider, emailService, inventoryService) {
this.db = db;
this.paymentProvider = paymentProvider;
this.emailService = emailService;
this.inventoryService = inventoryService;
}
async processPayment(userId, orderId, amount, cardInfo) {
try {
// 1. 验证输入
this.validateInputs(userId, orderId, amount, cardInfo);
// 2. 获取用户并验证权限
const user = await this.db.getUserById(userId);
if (!user) {
throw new Error('INVALID_USER');
}
// 3. 获取订单并验证用户权限
const order = await this.db.getOrderById(orderId);
if (!order) {
throw new Error('ORDER_NOT_FOUND');
}
if (order.userId !== userId) {
throw new Error('PERMISSION_DENIED');
}
// 4. 验证金额
if (amount !== order.total) {
throw new Error('AMOUNT_MISMATCH');
}
// 5. 验证订单状态
if (order.status !== PAYMENT_STATUS.PENDING) {
throw new Error('INVALID_ORDER_STATUS');
}
// 6. 处理支付
const paymentResult = await this.executePayment(cardInfo, amount);
if (paymentResult.success) {
// 使用事务确保数据一致性
return await this.completePaymentSuccess(orderId, user, order, paymentResult);
} else {
await this.handlePaymentFailure(orderId, paymentResult);
throw new Error('PAYMENT_FAILED');
}
} catch (error) {
// 记录错误但不泄露敏感信息
console.error('Payment processing error:', error.message);
throw new Error('PAYMENT_PROCESSING_ERROR');
}
}
validateInputs(userId, orderId, amount, cardInfo) {
if (!userId || typeof userId !== 'string' || !/^[a-zA-Z0-9-_]+$/.test(userId)) {
throw new Error('INVALID_USER_ID');
}
if (!orderId || typeof orderId !== 'string' || !/^[a-zA-Z0-9-_]+$/.test(orderId)) {
throw new Error('INVALID_ORDER_ID');
}
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('INVALID_AMOUNT');
}
if (!cardInfo || typeof cardInfo !== 'object') {
throw new Error('INVALID_CARD_INFO');
}
// 更多卡片信息验证...
}
async executePayment(cardInfo, amount) {
let attempts = 0;
let lastError;
while (attempts < MAX_RETRY_ATTEMPTS) {
try {
return await this.paymentProvider.charge(cardInfo, amount);
} catch (error) {
attempts++;
lastError = error;
if (attempts < MAX_RETRY_ATTEMPTS) {
// 指数退避策略
await this.delay(Math.pow(2, attempts) * 1000);
}
}
}
throw lastError;
}
async completePaymentSuccess(orderId, user, order, paymentResult) {
// 使用数据库事务
const transaction = await this.db.beginTransaction();
try {
// 更新订单状态
await this.db.updateOrderStatus(orderId, PAYMENT_STATUS.PAID, transaction);
// 记录支付信息
await this.db.recordPayment({
orderId,
amount: order.total,
transactionId: paymentResult.transactionId,
status: PAYMENT_STATUS.PAID
}, transaction);
await transaction.commit();
// 异步操作 - 发送邮件和更新库存
this.performPostPaymentActions(user, order, paymentResult.transactionId);
return {
success: true,
transactionId: paymentResult.transactionId
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
async handlePaymentFailure(orderId, paymentResult) {
// 记录失败的支付尝试
await this.db.recordPayment({
orderId,
amount: order.total,
status: PAYMENT_STATUS.FAILED,
errorMessage: paymentResult.message
});
}
async performPostPaymentActions(user, order, transactionId) {
try {
// 异步发送确认邮件
await this.emailService.sendConfirmation(user.email, order, transactionId);
} catch (emailError) {
// 邮件发送失败不应影响主要流程
console.error('Failed to send confirmation email:', emailError);
// 可以安排重试或记录到队列稍后处理
}
try {
// 异步更新库存
await this.inventoryService.updateStock(order.items);
} catch (inventoryError) {
console.error('Failed to update inventory:', inventoryError);
// 记录错误,可能需要人工干预
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
审查要点总结
- 安全增强: 添加了输入验证和权限检查
- 错误处理: 使用try-catch和事务确保数据一致性
- 职责分离: 将大函数拆分为小的、专注的方法
- 异步处理: 正确处理异步操作和错误
- 配置管理: 使用常量和配置参数
案例二:API接口审查
背景
RESTful API接口,用于管理用户信息。
原始代码
javascript
// bad-user-api.js
app.get('/users/:id', (req, res) => {
let userId = req.params.id;
let sql = `SELECT * FROM users WHERE id = ${userId}`; // SQL注入风险
db.query(sql, (err, result) => {
if (err) {
res.status(500).send('Server error'); // 泄露内部错误信息
} else {
res.json(result); // 没有数据验证,返回所有字段
}
});
});
app.put('/users/:id', (req, res) => {
let userId = req.params.id;
let userData = req.body;
let sql = `UPDATE users SET name='${userData.name}', email='${userData.email}' WHERE id=${userId}`;
db.query(sql, (err, result) => {
if (err) {
res.status(500).send('Update failed');
} else {
res.json({message: 'Updated successfully'});
}
});
});
问题分析
- SQL注入: 直接拼接SQL语句
- XSS风险: 没有清理用户输入
- 数据泄露: 返回所有用户字段(包括密码等敏感信息)
- 权限缺失: 没有验证用户权限
- 错误处理: 泄露内部错误信息
- 输入验证: 缺少对输入数据的验证
改进后的代码
javascript
// good-user-api.js
import { body, param, validationResult } from 'express-validator';
import bcrypt from 'bcryptjs';
// 用户数据验证规则
const userValidationRules = () => [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Name must be between 2 and 50 characters'),
body('email')
.normalizeEmail()
.isEmail()
.withMessage('Must be a valid email address'),
body('password')
.optional()
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
];
// ID参数验证规则
const idValidationRules = () => [
param('id')
.isNumeric()
.withMessage('User ID must be a number')
];
// 验证中间件
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
// 权限中间件
const requireAdmin = (req, res, next) => {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
// 数据脱敏函数
const sanitizeUser = (user) => {
const { password, salt, ...sanitizedUser } = user;
return sanitizedUser;
};
// 获取用户信息
app.get(
'/users/:id',
idValidationRules(),
validate,
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const userId = parseInt(req.params.id, 10);
// 验证用户权限
if (req.user.id !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
// 使用参数化查询防止SQL注入
const query = 'SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?';
const user = await db.query(query, [userId]);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// 脱敏处理
const sanitizedUser = sanitizeUser(user);
res.json(sanitizedUser);
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
// 更新用户信息
app.put(
'/users/:id',
idValidationRules(),
userValidationRules(),
validate,
async (req, res) => {
try {
const userId = parseInt(req.params.id, 10);
const userData = req.body;
// 验证权限
if (req.user.id !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
// 准备更新数据
const updateData = {
name: userData.name.trim(),
email: userData.email.toLowerCase()
};
// 如果提供了密码,则加密
if (userData.password) {
const salt = await bcrypt.genSalt(10);
updateData.password_hash = await bcrypt.hash(userData.password, salt);
}
updateData.updated_at = new Date();
// 构建参数化更新查询
const fields = Object.keys(updateData);
const values = Object.values(updateData);
const setClause = fields.map(field => `${field} = ?`).join(', ');
const query = `UPDATE users SET ${setClause}, updated_at = ? WHERE id = ?`;
values.push(new Date(), userId);
const result = await db.query(query, values);
if (result.affectedRows === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
message: 'User updated successfully',
userId: userId
});
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
);
案例三:前端组件审查
背景
React组件,用于显示用户列表并支持搜索和排序功能。
原始代码
jsx
// bad-user-list.jsx
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState('name');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetch('/api/users')
.then(response => response.json())
.then(data => {
setUsers(data);
setIsLoading(false);
});
}, []);
// 有问题的搜索和排序实现
const filteredAndSortedUsers = users
.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => {
if (a[sortField] < b[sortField]) return -1;
if (a[sortField] > b[sortField]) return 1;
return 0;
});
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const handleSortChange = (field) => {
setSortField(field);
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<input
type="text"
placeholder="Search users..."
value={searchTerm}
onChange={handleSearchChange}
/>
<div>
<button onClick={() => handleSortChange('name')}>Sort by Name</button>
<button onClick={() => handleSortChange('email')}>Sort by Email</button>
<button onClick={() => handleSortChange('created_at')}>Sort by Date</button>
</div>
<ul>
{filteredAndSortedUsers.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
问题分析
- 性能问题: 搜索和排序在每次渲染时重新执行
- 安全性: 没有处理XSS风险
- 用户体验: 缺少防抖搜索
- 错误处理: 没有处理API错误
- 可访问性: 缺少ARIA标签和键盘支持
改进后的代码
jsx
// good-user-list.jsx
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { debounce } from 'lodash';
function UserList() {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState('name');
const [sortDirection, setSortDirection] = useState('asc');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
// 获取用户数据
useEffect(() => {
const fetchUsers = async () => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/users', {
signal: controller.signal
});
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
if (!controller.signal.aborted) {
setUsers(data);
setIsLoading(false);
}
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setIsLoading(false);
}
}
};
fetchUsers();
// 清理函数
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
// 防抖搜索处理
const debouncedSearchTerm = useMemo(() => {
return debounce((callback) => callback(), 300);
}, []);
const [debouncedSearch, setDebouncedSearch] = useState(searchTerm);
useEffect(() => {
debouncedSearchTerm(() => {
setDebouncedSearch(searchTerm);
});
}, [searchTerm, debouncedSearchTerm]);
// 记忆化的过滤和排序逻辑
const filteredAndSortedUsers = useMemo(() => {
let result = [...users];
// 过滤
if (debouncedSearch) {
const searchLower = debouncedSearch.toLowerCase();
result = result.filter(user =>
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
);
}
// 排序
result.sort((a, b) => {
let aValue = a[sortField];
let bValue = b[sortField];
// 处理日期类型
if (sortField === 'created_at' || sortField === 'updated_at') {
aValue = new Date(aValue);
bValue = new Date(bValue);
} else {
// 转换为字符串进行比较
aValue = String(aValue).toLowerCase();
bValue = String(bValue).toLowerCase();
}
let comparison = 0;
if (aValue < bValue) {
comparison = -1;
} else if (aValue > bValue) {
comparison = 1;
}
return sortDirection === 'desc' ? comparison * -1 : comparison;
});
return result;
}, [users, debouncedSearch, sortField, sortDirection]);
// 安全的渲染函数
const renderUser = useCallback((user) => {
// 防止XSS,React默认会转义内容
return (
<li key={user.id} className="user-item">
<span className="user-name">{user.name}</span>
<span className="user-email">{user.email}</span>
<span className="user-date">{new Date(user.created_at).toLocaleDateString()}</span>
</li>
);
}, []);
const handleSearchChange = useCallback((event) => {
setSearchTerm(event.target.value);
}, []);
const handleSortChange = useCallback((field) => {
if (sortField === field) {
// 切换排序方向
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
// 切换排序字段,默认升序
setSortField(field);
setSortDirection('asc');
}
}, [sortField]);
// 当前排序指示器
const getSortIndicator = (field) => {
if (sortField !== field) return '';
return sortDirection === 'asc' ? ' ↑' : ' ↓';
};
if (error) {
return (
<div className="error-container" role="alert">
<h3>Error Loading Users</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>Try Again</button>
</div>
);
}
return (
<div className="user-list-container">
<div className="controls">
<div className="search-section">
<label htmlFor="user-search" className="sr-only">Search users</label>
<input
id="user-search"
type="text"
placeholder="Search users..."
value={searchTerm}
onChange={handleSearchChange}
aria-label="Search users"
/>
</div>
<div className="sort-section" role="group" aria-label="Sort controls">
<button
onClick={() => handleSortChange('name')}
aria-label={`Sort by name${getSortIndicator('name')}`}
className={sortField === 'name' ? 'active-sort' : ''}
>
Sort by Name{getSortIndicator('name')}
</button>
<button
onClick={() => handleSortChange('email')}
aria-label={`Sort by email${getSortIndicator('email')}`}
className={sortField === 'email' ? 'active-sort' : ''}
>
Sort by Email{getSortIndicator('email')}
</button>
<button
onClick={() => handleSortChange('created_at')}
aria-label={`Sort by date${getSortIndicator('created_at')}`}
className={sortField === 'created_at' ? 'active-sort' : ''}
>
Sort by Date{getSortIndicator('created_at')}
</button>
</div>
</div>
{isLoading ? (
<div className="loading" role="status" aria-live="polite">
Loading users...
</div>
) : (
<ul className="user-list" aria-label="User list">
{filteredAndSortedUsers.length > 0 ? (
filteredAndSortedUsers.map(renderUser)
) : (
<li className="no-results">No users found</li>
)}
</ul>
)}
<div className="results-info" aria-live="polite">
Showing {filteredAndSortedUsers.length} of {users.length} users
</div>
</div>
);
}
export default UserList;
审查要点总结
- 性能优化: 使用useMemo避免不必要的计算,防抖搜索
- 错误处理: 添加API错误处理和请求取消
- 可访问性: 添加ARIA标签和键盘支持
- 用户体验: 加载状态和结果计数
- 安全性: 防止XSS(React默认转义)
案例四:数据库查询优化审查
背景
一个博客系统的文章列表页面,需要显示文章标题、作者、摘要和评论数。
原始代码
javascript
// bad-blog-queries.js
async function getArticleList(page = 1, limit = 10) {
// 1. 获取文章列表
const articles = await db.query(`
SELECT * FROM articles
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [limit, (page - 1) * limit]);
// 2. 为每篇文章单独查询作者信息 - N+1 查询问题
for (const article of articles) {
const author = await db.query(
'SELECT * FROM users WHERE id = ?',
[article.author_id]
);
article.author = author[0];
}
// 3. 为每篇文章单独查询评论数 - N+1 查询问题
for (const article of articles) {
const commentCount = await db.query(`
SELECT COUNT(*) as count
FROM comments
WHERE article_id = ?
`, [article.id]);
article.comment_count = commentCount[0].count;
}
return articles;
}
问题分析
- N+1查询问题: 为每篇文章单独查询作者和评论数
- 性能问题: 大量数据库往返
- 资源浪费: 多余的数据传输
改进后的代码
javascript
// good-blog-queries.js
async function getArticleList(page = 1, limit = 10) {
// 使用JOIN一次性获取所有需要的数据
const articles = await db.query(`
SELECT
a.id,
a.title,
a.summary,
a.created_at,
a.updated_at,
u.id as author_id,
u.name as author_name,
u.avatar_url as author_avatar,
COALESCE(c.comment_count, 0) as comment_count
FROM articles a
LEFT JOIN users u ON a.author_id = u.id
LEFT JOIN (
SELECT
article_id,
COUNT(*) as comment_count
FROM comments
GROUP BY article_id
) c ON a.id = c.article_id
WHERE a.status = 'published'
ORDER BY a.created_at DESC
LIMIT ? OFFSET ?
`, [limit, (page - 1) * limit]);
// 数据格式化
return articles.map(article => ({
id: article.id,
title: article.title,
summary: article.summary,
createdAt: article.created_at,
updatedAt: article.updated_at,
author: {
id: article.author_id,
name: article.author_name,
avatarUrl: article.author_avatar
},
commentCount: article.comment_count || 0
}));
}
// 如果需要更复杂的查询,可以考虑使用缓存
class ArticleService {
constructor(db, cache) {
this.db = db;
this.cache = cache;
}
async getArticleList(page = 1, limit = 10, includeStats = true) {
const cacheKey = `articles:${page}:${limit}:${includeStats}`;
// 尝试从缓存获取
let articles = await this.cache.get(cacheKey);
if (!articles) {
articles = await this.fetchArticlesFromDB(page, limit, includeStats);
// 缓存结果(缓存1小时)
await this.cache.set(cacheKey, articles, 3600);
}
return articles;
}
async fetchArticlesFromDB(page, limit, includeStats) {
// 使用上述优化的查询
return await getArticleList(page, limit);
}
async invalidateArticleCache(articleId) {
// 当文章更新时,清除相关缓存
await this.cache.deleteByPattern('articles:*');
}
}
审查要点总结
- 查询优化: 使用JOIN消除N+1查询
- 数据聚合: 在数据库层面聚合统计数据
- 缓存策略: 适当使用缓存减少数据库压力
- 性能监控: 考虑添加查询性能监控
审查总结
通过这些案例分析,我们可以看到代码审查在识别和解决各种问题方面的重要性:
- 安全问题: 输入验证、权限控制、数据脱敏
- 性能问题: N+1查询、防抖处理、数据缓存
- 错误处理: 适当的错误处理和用户友好的错误消息
- 代码质量: 职责分离、可维护性、可测试性
- 用户体验: 加载状态、可访问性、响应性能
在实际的代码审查过程中,我们应该重点关注这些方面,确保代码既安全又高效,同时具有良好的可维护性。