安全编码实践
大约 10 分钟约 2885 字
安全编码实践
简介
安全编码是预防软件漏洞的编程实践。本篇介绍 .NET 开发中常见的安全风险及防范措施,涵盖 SQL 注入、XSS、CSRF、认证授权、数据保护和日志安全等方面。
特点
输入验证
模型验证
public class CreateUserRequest
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "用户名长度 2-50")]
[RegularExpression(@"^[a-zA-Z0-9_\u4e00-\u9fa5]+$", ErrorMessage = "用户名包含非法字符")]
public string Username { get; set; } = "";
[Required]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; } = "";
[Required]
[StringLength(100, MinimumLength = 8)]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
ErrorMessage = "密码必须包含大小写字母和数字")]
public string Password { get; set; } = "";
[Range(0, 150)]
public int Age { get; set; }
[Url]
public string? Website { get; set; }
[EnumDataType(typeof(UserRole))]
public UserRole Role { get; set; }
}
// 控制器验证
[ApiController]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] CreateUserRequest request)
{
if (!ModelState.IsValid)
{
var errors = ModelState
.Where(kvp => kvp.Value.Errors.Count > 0)
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
);
return BadRequest(new { Errors = errors });
}
return Ok();
}
}SQL 注入防护
参数化查询
// 错误:字符串拼接(SQL 注入风险)
public async Task<User?> GetUserUnsafe(string username)
{
var sql = $"SELECT * FROM Users WHERE Username = '{username}'";
// 输入: ' OR '1'='1' -- 可以绕过认证
return await _context.Users.FromSqlRaw(sql).FirstOrDefaultAsync(); // ❌ 危险
}
// 正确:参数化查询
public async Task<User?> GetUserSafe(string username)
{
return await _context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}")
.FirstOrDefaultAsync(); // ✅ 安全
}
// Dapper 参数化
public async Task<User?> GetUserDapper(string username)
{
var sql = "SELECT * FROM Users WHERE Username = @Username";
return await _connection.QueryFirstOrDefaultAsync<User>(sql,
new { Username = username }); // ✅ 安全
}
// LIKE 查询
public async Task<List<User>> SearchUsers(string keyword)
{
var pattern = $"%{keyword}%";
return await _context.Users
.Where(u => EF.Functions.Like(u.Username, pattern))
.ToListAsync(); // ✅ EF Core 自动参数化
}XSS 防护
输出编码
// NuGet: Microsoft.AspNetCore.Html.Abstractions
using Microsoft.AspNetCore.Html;
// HTML 编码
public static class SafeHtml
{
// 编码用户输入
public static string Encode(string input)
{
return System.Web.HttpUtility.HtmlEncode(input);
}
// 安全的 HTML 构建
public static HtmlString SafeLink(string url, string text)
{
// 验证 URL 协议
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|| (uri.Scheme != "https" && uri.Scheme != "http"))
{
return new HtmlString("");
}
return new HtmlString(
$"<a href=\"{Encode(uri.ToString())}\">{Encode(text)}</a>");
}
}
// CSP 头
app.Use(async (context, next) =>
{
context.Response.Headers.ContentSecurityPolicy =
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self'";
await next(context);
});
// X-Content-Type-Options
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers.XFrameOptions = "DENY";
context.Response.Headers.XXSSProtection = "1; mode=block";
context.Response.Headers.ReferrerPolicy = "strict-origin-when-cross-origin";
await next(context);
});数据保护
敏感数据加密
// ASP.NET Core Data Protection API
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/app/keys"))
.SetApplicationName("MyApp");
public class SecureDataService
{
private readonly IDataProtector _protector;
public SecureDataService(IDataProtectionProvider provider)
{
_protector = provider.CreateProtector("SensitiveData");
}
// 加密
public string Encrypt(string plainText)
{
return _protector.Protect(plainText);
}
// 解密
public string Decrypt(string cipherText)
{
return _protector.Unprotect(cipherText);
}
// 带过期时间的加密
public string EncryptWithExpiry(string plainText, TimeSpan lifetime)
{
var timeLimitedProtector = _protector.ToTimeLimitedDataProtector();
return timeLimitedProtector.Protect(plainText, lifetime);
}
}
// 密码哈希
public class PasswordHasher
{
public string HashPassword(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
}
public bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}
// 连接字符串中的敏感信息
// appsettings.json 不要存储明文密钥
// 使用 User Secrets(开发)/ Azure Key Vault(生产)
builder.Configuration.AddAzureKeyVault(
new Uri("https://myvault.vault.azure.net/"),
new DefaultAzureCredential());日志安全
安全日志实践
public class SecureLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SecureLoggingMiddleware> _logger;
public SecureLoggingMiddleware(RequestDelegate next,
ILogger<SecureLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// ✅ 记录请求信息(不含敏感数据)
_logger.LogInformation("请求: {Method} {Path} from {IP}",
context.Request.Method,
context.Request.Path,
context.Connection.RemoteIpAddress);
// ❌ 不要记录密码、Token 等
// _logger.LogInformation("Body: {Body}", body); // 危险
await _next(context);
}
}
// 敏感数据脱敏
public static class SensitiveDataMasker
{
public static string MaskEmail(string email)
{
if (string.IsNullOrEmpty(email) || !email.Contains('@'))
return "***";
var parts = email.Split('@');
return $"{parts[0][..2]}***@{parts[1]}";
}
public static string MaskPhone(string phone)
{
if (string.IsNullOrEmpty(phone) || phone.Length < 7)
return "***";
return $"{phone[..3]}****{phone[^4..]}";
}
public static string MaskIdCard(string idCard)
{
if (string.IsNullOrEmpty(idCard) || idCard.Length < 8)
return "***";
return $"{idCard[..4]}********{idCard[^4..]}";
}
}安全检查清单
| 检查项 | 说明 |
|---|---|
| 输入验证 | 所有外部输入验证类型/长度/格式 |
| SQL 参数化 | 禁止字符串拼接 SQL |
| XSS 防护 | 用户输入输出前编码 |
| HTTPS | 全站强制 HTTPS |
| 认证授权 | 最小权限原则 |
| 密码存储 | BCrypt/Argon2 哈希 |
| 敏感数据 | 加密存储,日志脱敏 |
| 错误信息 | 不暴露内部实现细节 |
| CORS | 限制允许的 Origin |
| 依赖更新 | 定期更新 NuGet 包 |
CSRF 防护
跨站请求伪造防护
// ASP.NET Core 内置 CSRF 防护(Antiforgery)
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.Name = "XSRF-TOKEN";
options.Cookie.HttpOnly = false; // 前端需要读取
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
// 在 Controller 中使用
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateOrder([FromBody] CreateOrderRequest request)
{
return Ok();
}
// API 场景:手动验证 Antiforgery Token
public class CsrfMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public CsrfMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task InvokeAsync(HttpContext context)
{
if (HttpMethods.IsPost(context.Request.Method) ||
HttpMethods.IsPut(context.Request.Method) ||
HttpMethods.IsDelete(context.Request.Method))
{
try
{
await _antiforgery.ValidateRequestAsync(context);
}
catch (AntiforgeryValidationException)
{
context.Response.StatusCode = 403;
return;
}
}
await _next(context);
}
}
// 前端获取 Token
app.MapGet("/api/csrf-token", (IAntiforgery antiforgery, HttpContext context) =>
{
var token = antiforgery.GetAndStoreTokens(context);
return Results.Ok(new { token = token.RequestToken });
});CORS 配置安全
跨域资源共享策略
// 安全的 CORS 配置
builder.Services.AddCors(options =>
{
options.AddPolicy("Production", policy =>
{
// 不要使用 AllowAnyOrigin
policy.WithOrigins(
"https://www.example.com",
"https://admin.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials() // 需要指定具体 Origin
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
options.AddPolicy("Development", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// 按环境选择策略
app.UseCors(builder.Environment.IsDevelopment() ? "Development" : "Production");
// 常见错误:AllowAnyOrigin + AllowCredentials(不允许同时使用)
// 正确做法:指定具体 Origin + AllowCredentials文件上传安全
安全的文件上传
public class FileUploadService
{
private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".jpg", ".jpeg", ".png", ".gif", ".pdf", ".docx", ".xlsx"
};
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
public async Task<string> UploadAsync(IFormFile file)
{
// 1. 验证文件大小
if (file.Length > MaxFileSize)
throw new BizException("文件大小超过限制");
// 2. 验证文件扩展名
var extension = Path.GetExtension(file.FileName);
if (!AllowedExtensions.Contains(extension))
throw new BizException("不支持的文件类型");
// 3. 验证 Content-Type(不能仅依赖客户端声明的类型)
var allowedContentTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" },
{ ".png", "image/png" }, { ".gif", "image/gif" },
{ ".pdf", "application/pdf" }
};
if (allowedContentTypes.TryGetValue(extension, out var expectedType)
&& !string.Equals(file.ContentType, expectedType, StringComparison.OrdinalIgnoreCase))
{
throw new BizException("文件类型与扩展名不匹配");
}
// 4. 生成安全文件名(防止路径遍历攻击)
var safeFileName = $"{Guid.NewGuid():N}{extension}";
var uploadPath = Path.Combine("uploads", safeFileName);
// 5. 确保路径不越界
var fullPath = Path.GetFullPath(uploadPath);
var basePath = Path.GetFullPath("uploads");
if (!fullPath.StartsWith(basePath))
throw new UnauthorizedAccessException("非法文件路径");
await using var stream = new FileStream(fullPath, FileMode.Create);
await file.CopyToAsync(stream);
return safeFileName;
}
}
// Kestrel 全局请求大小限制
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
});API 安全最佳实践
接口安全加固
// 1. Rate Limiting — 防暴力破解
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("login", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 5;
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 0;
});
});
// 2. 安全响应头中间件
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers.XFrameOptions = "DENY";
context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
context.Response.Headers.ReferrerPolicy = "strict-origin-when-cross-origin";
// HSTS(HTTP 严格传输安全)
context.Response.Headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
// Permissions-Policy(限制浏览器功能)
context.Response.Headers["Permissions-Policy"] =
"camera=(), microphone=(), geolocation=(), payment=()";
await next();
});
// 3. 错误信息不泄露内部细节
app.UseExceptionHandler("/Error");
// 在开发环境中使用详细错误页面
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// 4. API 版本安全 — 旧版本 API 逐步废弃
app.MapGet("/api/v1/users", () => "Old API")
.WithMetadata(new ApiVersionMetadata(1, 0));
// 5. 依赖安全扫描
// dotnet list package --vulnerable
// 定期更新:dotnet add package [包名] --version [安全版本]依赖安全审计
NuGet 安全管理
# 扫描已知漏洞
dotnet list package --vulnerable --include-transitive
# 输出示例:
# 已发现以下漏洞:
# Package "Newtonsoft.Json" 13.0.1 → CVE-2024-XXXX (高危)
# 建议升级到 13.0.3+
# 使用 SourceLink 启用源码调试
dotnet add package Microsoft.SourceLink.GitHub
# 强制安全版本(Central Package Management)
# Directory.Packages.props:
# <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />密钥管理
安全配置存储
// 1. 开发环境 — User Secrets(不提交到代码库)
// dotnet user-secrets set "Jwt:SecretKey" "your-secret-key"
// 存储位置: %APPDATA%\Microsoft\UserSecrets\<user-secrets-id>\secrets.json
// 2. 生产环境 — 环境变量
// Linux: export Jwt__SecretKey="your-secret-key"
// Docker: -e Jwt__SecretKey="your-secret-key"
// Kubernetes: ConfigMap / Secret
// 3. 生产环境 — Azure Key Vault
// dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
builder.Configuration.AddAzureKeyVault(
new Uri("https://myvault.vault.azure.net/"),
new DefaultAzureCredential());
// 4. 生产环境 — HashiCorp Vault
// dotnet add package VaultSharp
builder.Configuration.AddVaultConfiguration(options =>
{
options.Address = "https://vault.example.com";
options.Token = Environment.GetEnvironmentVariable("VAULT_TOKEN");
options.SecretPaths.Add("secret/myapp/");
});
// 5. 密钥轮换策略
public class KeyRotationService
{
// 同时支持新旧密钥解密,但只使用新密钥加密
private readonly List<string> _activeKeys = new();
public string Encrypt(string data)
{
var latestKey = _activeKeys[0];
return EncryptWithKey(data, latestKey);
}
public string Decrypt(string cipherText, int keyVersion)
{
var key = _activeKeys.FirstOrDefault(k => GetVersion(k) == keyVersion)
?? throw new InvalidOperationException("密钥版本不存在");
return DecryptWithKey(cipherText, key);
}
}优点
缺点
总结
安全编码核心原则:所有外部输入不可信、最小权限原则、纵深防御。SQL 注入用参数化查询防范。XSS 用输出编码和 CSP 头防护。敏感数据用 Data Protection API 加密。密码用 BCrypt 哈希。日志中脱敏敏感数据。HTTPS 全站强制。定期更新依赖包修复已知漏洞。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《安全编码实践》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《安全编码实践》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《安全编码实践》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《安全编码实践》最大的收益和代价分别是什么?
