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
相关产品推荐
相关产品推荐

