Skip to content
On this page

代码审查案例分析

通过真实案例分析,我们可以更好地理解代码审查在实际项目中的应用,以及如何识别和解决常见的代码问题。

案例一:电商网站支付模块审查

背景

电商平台支付模块需要处理用户付款、验证支付信息、更新订单状态等功能。

原始代码 (需要审查)

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. 安全问题

  • 输入验证不足: 没有验证 userIdorderIdamountcardInfo 的格式和类型
  • 权限检查缺失: 没有验证用户是否有权访问指定订单
  • 敏感信息泄露: 错误消息可能暴露系统内部信息

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));
  }
}

审查要点总结

  1. 安全增强: 添加了输入验证和权限检查
  2. 错误处理: 使用try-catch和事务确保数据一致性
  3. 职责分离: 将大函数拆分为小的、专注的方法
  4. 异步处理: 正确处理异步操作和错误
  5. 配置管理: 使用常量和配置参数

案例二: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;

审查要点总结

  1. 性能优化: 使用useMemo避免不必要的计算,防抖搜索
  2. 错误处理: 添加API错误处理和请求取消
  3. 可访问性: 添加ARIA标签和键盘支持
  4. 用户体验: 加载状态和结果计数
  5. 安全性: 防止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:*');
  }
}

审查要点总结

  1. 查询优化: 使用JOIN消除N+1查询
  2. 数据聚合: 在数据库层面聚合统计数据
  3. 缓存策略: 适当使用缓存减少数据库压力
  4. 性能监控: 考虑添加查询性能监控

审查总结

通过这些案例分析,我们可以看到代码审查在识别和解决各种问题方面的重要性:

  1. 安全问题: 输入验证、权限控制、数据脱敏
  2. 性能问题: N+1查询、防抖处理、数据缓存
  3. 错误处理: 适当的错误处理和用户友好的错误消息
  4. 代码质量: 职责分离、可维护性、可测试性
  5. 用户体验: 加载状态、可访问性、响应性能

在实际的代码审查过程中,我们应该重点关注这些方面,确保代码既安全又高效,同时具有良好的可维护性。