You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# Web API参数验证防SQL注入:位置选择与最佳实践

关于输入验证的最佳实践与分层建议

1. 参数化查询防注入后,输入验证依然是必须的最佳实践

参数化查询确实能彻底防范SQL注入,但它只负责SQL语法层面的安全,解决不了业务层面的无效输入问题:

  • 比如传入的Year是abc或202这类不符合格式的值,参数化查询不会报错,但会返回空结果,甚至触发数据库类型转换警告;
  • 无效的Code会导致IsoCode.FromName(Code)抛出异常,直接返回500错误,用户体验极差;
  • 放任无效请求流到后续层,会产生无意义的日志、数据库查询,浪费系统资源。

所以输入验证是保障系统健壮性、用户体验和资源效率的关键步骤,和参数化查询是互补而非重复的措施。

2. 输入验证的良好方式

根据输入类型和业务规则,分三类验证:

  • 格式验证:确保输入符合数据格式要求
    • 年份Year:验证是否为4位数字(比如用正则^\d{4}$,或尝试转换为int并判断范围,如2018 <= year <= 2028)
    • Code:验证是否为有效的ISO代码(通过IsoCode.FromName的返回值判断,若不存在则视为无效)
  • 合法性验证:确保输入符合业务规则
    • 验证appId是否是系统中存在的有效ID(比如查询数据库或缓存确认)
    • 验证Year是否在业务允许的查询范围内(比如只允许查近5年到下一年的数据)
  • 空值/边界验证:拦截空字符串、null、超出边界的值
    • 比如Year不能为null或空字符串,appId不能小于1

3. 验证的分层策略

不同分层承担不同的验证职责,避免重复和职责混乱:

  • 控制器层(请求入口):做初步的格式和合法性验证,尽早拦截无效请求
    • 比如你的控制器里,IsoCode.FromName(Code)如果找不到对应Code,应该捕获异常并返回BadRequest("无效的ISO代码"),而非让异常传到服务层;
    • 对Year做格式验证,比如尝试转换为int,失败则直接返回400;
    • 优化后的控制器示例:
      public async Task<ActionResult<List<Level>>> GetLevelsAsync([FromRoute] string Code, string Year)
      {
          // 验证Code有效性
          try
          {
              int appId = IsoCode.FromName(Code).Id;
          }
          catch (ArgumentException)
          {
              return BadRequest("无效的ISO代码");
          }
      
          // 验证Year格式
          if (!int.TryParse(Year, out int yearInt) || yearInt.ToString().Length != 4)
              return BadRequest("年份必须是4位数字");
      
          var result = await _sampleService.GetLevelsAsync(appId, Year);
          return result;
      }
      
  • 服务层(业务核心):做业务规则验证
    • 比如验证Year是否在业务允许的范围内;
    • 验证appId是否有权限查询该年份的数据;
    • 若控制器已做空值/格式验证,这里可简化重复逻辑;
    • 优化后的服务层示例:
      public async Task<List<Level>> GetLevelsAsync(int appId, string year)
      {
          // 业务规则验证:年份范围
          if (!int.TryParse(year, out int yearInt) || yearInt < 2018 || yearInt > DateTime.Now.Year + 1)
              throw new ArgumentOutOfRangeException(nameof(year), "年份不在允许查询范围内");
      
          var result = await _sampleRepository.GetLevelsByYear(appId, year);
          return result;
      }
      
  • 仓储层(数据访问):只做最基础的参数非空验证,避免无效的数据库查询
    • 不需要重复业务规则验证,保持单一职责;
    • 可保留空值验证作为最后一道兜底防线;
    • 优化后的仓储层示例:
      public async Task<List<Level>> GetLevelsByYear(int appId, string year)
      {
          // 兜底验证,避免上层漏验证
          if (string.IsNullOrWhiteSpace(year))
              throw new ArgumentNullException(nameof(year));
      
          var parameters = new { AppId = appId, Year = year };
          string sql = @"SELECT * 
                         FROM [Levels] as l
                         JOIN [LevelYears] as v ON v.LevelId = l.Id 
                         WHERE l.Active = 1 AND l.AppId = @AppId AND v.Year = @Year
                         ORDER BY v.Sort asc";
      
          using (IDbConnection db = new SqlConnection(_settings.SqlServerConnString))
          {
              try
              {
                  return (await db.QueryAsync<Level>(sql, parameters)).ToList();
              }
              catch (Exception e)
              {
                  _logger.LogError(e, "查询年度层级数据失败", new { appId, year });
                  throw;
              }
          }
      }
      

额外优化点

  • 避免重复验证:比如Year的空值验证,控制器层已经拦截无效格式和空值,服务层和仓储层无需重复;
  • 使用System.ComponentModel.DataAnnotations做模型验证:给控制器参数加[RegularExpression(@"^\d{4}$")]、[Required]特性,自动返回验证错误,更简洁;
  • 统一异常处理:用全局异常过滤器捕获业务异常,返回统一格式的错误响应,避免直接抛出500错误。

内容的提问来源于stack exchange,提问作者jen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 05:40:30