Cho dù kiến trúc của bạn có sạch sẽ đến đâu, truy vấn có chạy nhanh đến đâu – nếu kẻ tấn công có thể đọc đơn hàng của người dùng khác hoặc giả mạo token, thì tất cả đều không quan trọng.
Hầu hết các vấn đề bảo mật API đều xuất phát từ việc bỏ qua những điều cơ bản:
- Chưa cập nhật gói NuGet có lỗ hổng bảo mật
- Thiếu kiểm tra xác thực
- Thiết lập xác thực yếu
- Chính sách CORS quá linh hoạt
- Secret bị commit vào hệ thống quản lý mã nguồn.
Tin tốt là ASP.NET Core cung cấp cho bạn hầu hết mọi thứ bạn cần tích hợp sẵn.
Trải qua nhiều năm, tôi đã triển khai và đánh giá nhiều API .NET, và cùng một bộ phương pháp giúp chúng an toàn.
Dưới đây là 18 phương pháp, mỗi phương pháp đi kèm với mã nguồn để áp dụng.
Trong bài viết này, chúng ta sẽ khám phá:
- Bắt buộc sử dụng HTTPS ở mọi nơi
- Xác thực bằng token, không phải session
- Xác thực chữ ký, nhà phát hành, đối tượng và thời gian tồn tại của JWT
- Ủy quyền bằng chính sách, không chỉ thuộc tính [Authorize]
- Áp dụng nguyên tắc đặc quyền tối thiểu
- Xác thực và làm sạch tất cả dữ liệu đầu vào
- Bảo vệ chống lại over-posting / mass assignment
- Xác thực nội dung và giới hạn kích thước request
- Sử dụng tham số hóa truy vấn và EF Core
- Triển khai giới hạn tốc độ và throttling
- Cấu hình CORS một cách hạn chế
- Trả về thông tin lỗi tối thiểu
- Đặt các header bảo mật
- Lưu trữ secret an toàn
- Bắt buộc bảo vệ CSRF khi cần thiết
- Đặt phiên bản cho API và ngừng sử dụng các endpoint không an toàn
- Ghi nhật ký và kiểm tra các sự kiện bảo mật
- Giữ cho các phụ thuộc được vá lỗi
Hãy bắt đầu.
Mục lục
1. Bắt buộc sử dụng HTTPS ở mọi nơi
Mọi request đến API của bạn nên được truyền qua kết nối mã hóa.
Không có HTTPS, token, mật khẩu và dữ liệu cá nhân sẽ được gửi dưới dạng văn bản thuần túy, nơi bất kỳ ai trên đường truyền mạng đều có thể đọc được. HTTP thuần túy cũng mở cửa cho các cuộc tấn công downgrade, trong đó kẻ tấn công buộc client sử dụng kết nối không an toàn.
ASP.NET Core cung cấp cho bạn hai công cụ. UseHttpsRedirection chuyển hướng các request HTTP sang HTTPS, và HSTS (HTTP Strict Transport Security) cho trình duyệt biết chỉ nên kết nối qua HTTPS:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true;
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
UseHttpsRedirection nâng cấp bất kỳ request HTTP nào lên HTTPS. UseHsts thêm header Strict-Transport-Security, vì vậy các trình duyệt tương thích sẽ từ chối giao tiếp với API của bạn qua HTTP thuần túy – điều này ngăn chặn các cuộc tấn công downgrade.
HSTS bị bỏ qua trong quá trình phát triển vì bạn thường sử dụng http://localhost.
Để biết cách thiết lập đầy đủ, xem Cấu hình HTTPS Redirection và HSTS trong ASP.NET Core.
2. Xác thực bằng token, không phải session
Ưu tiên xác thực stateless bằng token hơn là session phía server.
Session lưu trữ trạng thái xác thực trong bộ nhớ server hoặc kho lưu trữ chung, điều này gắn mỗi người dùng với một server và khiến việc mở rộng theo chiều ngang khó khăn hơn. Token tự mang theo bằng chứng nhận dạng, vì vậy bất kỳ instance nào của API đều có thể xác thực nó mà không cần tra cứu.
Đối với hầu hết các API, điều đó có nghĩa là JWT bearer token, thường được phát hành bởi nhà cung cấp danh tính thông qua OAuth 2.0 hoặc OpenID Connect:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://your-idp.com";
options.Audience = "shop-api";
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Client gửi token trong header Authorization: Bearer <token> trên mỗi request. API của bạn xác thực nó và đọc danh tính người dùng từ các claim của token – không cần kho lưu trữ session, không cần sticky session, không cần trạng thái phía server để mở rộng.
Điều này giữ cho API của bạn stateless và dễ dàng mở rộng theo chiều ngang.
Nếu bạn cũng cần thu hồi quyền truy cập trước khi token hết hạn, hãy đọc Cách triển khai Refresh Token và Thu hồi Token.
3. Xác thực chữ ký, nhà phát hành, đối tượng và thời gian tồn tại của JWT
Trước khi chấp nhận một token, bạn cần tin tưởng nó.
Một token chỉ an toàn khi bạn xác minh ai đã phát hành nó, nó dành cho ai, nó có hết hạn chưa, và chữ ký của nó có hợp lệ không. Cấu hình các kiểm tra này một cách rõ ràng thông qua TokenValidationParameters thay vì tin vào mặc định của framework:
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://your-idp.com",
ValidateAudience = true,
ValidAudience = "shop-api",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
});
Mỗi cờ đóng một lỗ hổng:
ValidateIssuerSigningKeyxác nhận chữ ký – không bao giờ tắt cờ này, nếu không bất kỳ ai cũng có thể giả mạo token.ValidateIssuervàValidateAudienceđảm bảo token đến từ nhà cung cấp danh tính của bạn và dành cho API của bạn, không phải dịch vụ khác.ValidateLifetimetừ chối các token đã hết hạn.
Chú ý đến ClockSkew.
Nó tồn tại để dung sai sự chênh lệch nhỏ giữa các server, nhưng giá trị mặc định là 5 phút rất hào phóng, vì vậy token hết hạn vẫn có thể được chấp nhận trong tối đa 5 phút nữa. Giảm nó xuống khoảng 30 giây, hoặc thậm chí bằng không, sẽ tăng cường việc thực thi thời hạn token, miễn là đồng hồ của các server được đồng bộ hóa (với NTP).
Xem Các phương pháp hay nhất về Xác thực và Ủy quyền để có bức tranh đầy đủ.
4. Ủy quyền bằng chính sách, không chỉ thuộc tính [Authorize]
Xác thực cho biết người dùng là ai. Ủy quyền quyết định họ được phép làm gì.
Một thuộc tính [Authorize] đơn giản chỉ kiểm tra người dùng đã đăng nhập hay chưa. Các quy tắc thực tế – “chỉ quản lý mới có thể xóa đơn hàng”, “người dùng chỉ có thể xem dữ liệu của riêng họ” – cần ủy quyền dựa trên chính sách và dựa trên tài nguyên.
Định nghĩa một chính sách một lần:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("orders:write", policy =>
policy.RequireRole("Manager")
.RequireClaim("permission", "orders:write"));
});
Áp dụng nó cho một action controller:
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpDelete("{id:int}")]
[Authorize(Policy = "orders:write")]
public IActionResult Delete(int id)
{
_orderService.Delete(id);
return NoContent();
}
}
Hoặc trên một endpoint minimal API:
app.MapDelete("/api/orders/{id:int}", (int id, IOrderService orders) =>
{
orders.Delete(id);
return Results.NoContent();
})
.RequireAuthorization("orders:write");
Khi quy tắc phụ thuộc vào tài nguyên cụ thể – như “chỉ chủ sở hữu mới có thể chỉnh sửa đơn hàng này” – hãy sử dụng ủy quyền dựa trên tài nguyên với IAuthorizationService.AuthorizeAsync(user, order, "OrderOwner"), thực thi chính sách trên thực thể thực tế.
Các chính sách giữ logic ủy quyền ở một nơi, tách biệt khỏi thân endpoint.
5. Áp dụng nguyên tắc đặc quyền tối thiểu
Cung cấp cho mỗi client quyền truy cập tối thiểu cần thiết, và không nhiều hơn.
Một token, một bộ claim, hoặc một API key nên cấp chính xác các quyền cần thiết cho công việc của nó. Một ứng dụng di động chỉ đọc danh mục sản phẩm không nên giữ token có thể xóa đơn hàng.
Phạm vi này ở cấp độ token. Khi nhà cung cấp danh tính phát hành token, nó chỉ bao gồm các scope mà client được cấp, và các chính sách của bạn kiểm tra chúng:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("catalog:read", policy =>
policy.RequireClaim("scope", "catalog:read"));
options.AddPolicy("orders:write", policy =>
policy.RequireClaim("scope", "orders:write"));
});
Nguyên tắc tương tự áp dụng cho API key và tài khoản cơ sở dữ liệu – hãy giới hạn phạm vi của chúng. Nếu thông tin xác thực bị rò rỉ, đặc quyền tối thiểu giới hạn bán kính ảnh hưởng chỉ trong phạm vi mà thông tin đó có thể thực hiện.
6. Xác thực và làm sạch tất cả dữ liệu đầu vào
Xem mọi payload gửi đến là nguy hiểm cho đến khi được chứng minh là hợp lệ.
Dữ liệu đầu vào bị lỗi hoặc độc hại nên bị từ chối ở biên, trước khi đến logic kinh doanh hoặc cơ sở dữ liệu của bạn. ASP.NET Core hỗ trợ ở đây: với thuộc tính [ApiController], request không vượt qua kiểm tra model validation sẽ tự động trả về 400 với body problem-details.
Thêm data annotations hoặc, cho các quy tắc phức tạp hơn, FluentValidation:
public class CreateProductRequest
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
}
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateProductRequest request)
{
// Nếu đến đây, model đã hợp lệ
var product = _productService.Create(request);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}
Minimal API tương đương chạy xác thực thông qua filter hoặc rõ ràng:
app.MapPost("/api/products",
(CreateProductRequest request, IValidator<CreateProductRequest> validator) =>
{
var result = validator.Validate(request);
if (!result.IsValid)
{
return Results.ValidationProblem(result.ToDictionary());
}
var product = productService.Create(request);
return Results.Created($"/api/products/{product.Id}", product);
});
Xác thực sớm biến một lớp tấn công – chuỗi quá dài, số ngoài phạm vi, trường bị thiếu – thành lỗi 400 sạch sẽ thay vì exception sâu trong mã của bạn.
7. Bảo vệ chống lại over-posting / mass assignment
Không bao giờ gắn JSON gửi đến trực tiếp lên thực thể cơ sở dữ liệu của bạn.
Nếu client gửi trực tiếp lên thực thể EF Core, họ có thể đặt các trường họ không bao giờ kiểm soát – IsAdmin, Balance, Status, hoặc Id của người dùng khác. Điều này được gọi là over-posting, hoặc mass assignment.
Gắn vào một request DTO chuyên dụng chỉ hiển thị các trường mà client được phép đặt, sau đó tự ánh xạ nó lên thực thể:
// Request DTO - chỉ những gì client có thể đặt
public record UpdateProductRequest(string Name, decimal Price);
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, UpdateProductRequest request)
{
var product = await _dbContext.Products.FindAsync(id);
if (product is null)
{
return NotFound();
}
// Ánh xạ chỉ các trường được phép
product.Name = request.Name;
product.Price = request.Price;
await _dbContext.SaveChangesAsync();
return NoContent();
}
Thực thể Product cũng có thể có các trường CreatedAt, OwnerId, hoặc IsFeatured, nhưng vì client chỉ có thể gửi Name và Price, những trường này không thể truy cập từ bên ngoài.
Các model request riêng biệt yêu cầu một lượng nhỏ đánh máy và đóng một toàn bộ loại lỗi leo thang đặc quyền.
8. Xác thực nội dung và giới hạn kích thước request
Từ chối những gì bạn không chuẩn bị để xử lý.
Một endpoint chấp nhận JSON nên từ chối các loại media khác, và không endpoint nào nên chấp nhận body request không giới hạn – tải lên vài GB là cách dễ dàng để tiêu hao bộ nhớ server và làm sập API của bạn.
Hạn chế loại nội dung với [Consumes] và giới hạn kích thước body với [RequestSizeLimit]:
[HttpPost]
[Consumes("application/json")]
[RequestSizeLimit(1_000_000)] // 1 MB
public IActionResult Create(CreateProductRequest request)
{
var product = _productService.Create(request);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
Bạn cũng có thể đặt giới hạn toàn cục thông qua Kestrel:
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 1_000_000;
});
Nguyên tắc tương tự áp dụng cho lượng dữ liệu mà client có thể lấy lại.
Luôn bắt buộc phân trang với kích thước trang tối đa trên các endpoint collection, để client không thể yêu cầu tập kết quả không giới hạn:
[HttpGet]
public IActionResult GetProducts(int page = 1, int pageSize = 20)
{
// Giới hạn kích thước trang để không ai có thể yêu cầu tất cả cùng lúc
pageSize = Math.Min(pageSize, 100);
var products = _productService.GetPage(page, pageSize);
return Ok(products);
}
Giới hạn kích thước request, loại nội dung, và kích thước trang đều bảo vệ chống lại cùng một thứ: một request duy nhất cố gắng tiêu thụ nhiều tài nguyên hơn server có thể đáp ứng.
9. Sử dụng tham số hóa truy vấn và EF Core
Không bao giờ xây dựng SQL bằng cách nối chuỗi từ dữ liệu đầu vào.
Nối chuỗi là cách xảy ra SQL injection – một đầu vào được chế tạo như '; DROP TABLE Products; -- trở thành một phần của truy vấn. Các truy vấn tham số hóa giữ dữ liệu đầu vào là dữ liệu, không bao giờ là SQL thực thi được.
EF Core tham số hóa mọi thứ theo mặc định, vì vậy một truy vấn LINQ luôn an toàn:
// 1. An toàn - EF Core tham số hóa 'search'
var products = await _dbContext.Products
.Where(p => p.Name.Contains(search))
.ToListAsync();
Khi bạn cần SQL thô, sử dụng FromSql hoặc FromSqlInterpolated, chuyển các giá trị nội suy thành tham số thay vì văn bảnliterally:
// 2. An toàn - 'category' trở thành tham số SQL, không phải văn bản nối
var products = await _dbContext.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Category = {category}")
.ToListAsync();
// 3. Nguy hiểm - không bao giờ làm điều này
var sql = "SELECT * FROM Products WHERE Category = '" + category + "'";
// 4. Và không bao giờ sử dụng FromSqlRaw với nội suy chuỗi
var products = await _dbContext.Products
.FromSqlRaw($"SELECT * FROM Products WHERE Category = {category}")
.ToListAsync();
Hai cái đầu tiên an toàn; 3 và 4 là cánh cửa cho kẻ tấn công.
10. Triển khai giới hạn tốc độ và throttling
Hạn chế tần suất một client duy nhất có thể gọi API của bạn.
Không có giới hạn, một client – hoặc một kẻ tấn công – có thể tấn công endpoint đăng nhập của bạn bằng các lần thử brute-force, hoặc tràn ngập API của bạn với các request cho đến khi nó sập. ASP.NET Core có bộ giới hạn tốc độ tích hợp mà bạn kết nối trong Program.cs:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 10;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
Sau đó áp dụng chính sách cho một action controller hoặc endpoint minimal API:
[HttpPost("login")]
[EnableRateLimiting("api")]
public IActionResult Login(LoginRequest request) { /* ... */ }
app.MapPost("/login", (LoginRequest request) => { /* ... */ })
.RequireRateLimiting("api");
Khi client vượt quá giới hạn, nó nhận được 429 Too Many Requests thay vì reaching code của bạn. Điều này làm giảm bớt lạm dụng brute-force và denial-of-service.
11. Cấu hình CORS một cách hạn chế
Chỉ cho phép các nguồn tin cậy gọi API của bạn từ trình duyệt.
Cross-Origin Resource Sharing (CORS) kiểm soát哪些 web origin có thể gửi request đến API của bạn.捷径 nguy hiểm là AllowAnyOrigin(), cho phép bất kỳ trang web nào trên internet gọi API của bạn thay mặt cho người dùng đã đăng nhập.
Định nghĩa chính sách có tên liệt kê chính xác các nguồn, phương thức và header bạn cho phép:
builder.Services.AddCors(options =>
{
options.AddPolicy("Shop", policy =>
policy.WithOrigins("https://shop.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type"));
});
var app = builder.Build();
app.UseCors("Shop");
Điều này có nghĩa là: chỉ https://shop.example.com mới có thể gọi chúng tôi, chỉ với các phương thức này, chỉ với các header này. Tất cả khác đều bị từ chối.
Dành AllowAnyOrigin cho các API thực sự công khai, không xác thực – và không bao giờ kết hợp nó với credentials.
Để biết hướng dẫn đầy đủ, xem CORS trong ASP.NET Core.
12. Trả về thông tin lỗi tối thiểu
Phản hồi lỗi nên giúp người gọi, không phải kẻ tấn công.
Trang exception thô tiết lộ stack traces, phiên bản framework, đường dẫn file, và SQL – bản đồ nội bộ của bạn. Trả về lỗi sạch, tiêu chuẩn thay vào đó, sử dụng Problem Details (RFC 9457):
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Production: phản hồi ProblemDetails sạch, không có stack trace
app.UseExceptionHandler();
}
Trong production, exception không được xử lý trả về body Problem Details có cấu trúc với mã trạng thái và thông báo chung – và không có thông tin về nội bộ của bạn.
Giữ thông tin chẩn đoán chi tiết trong nhật ký, nơi chỉ bạn có thể xem, không trong phản hồi HTTP, nơi client có thể thấy.
Bạn có thể đọc thêm về Problem Details trong Các phương pháp hay nhất để xây dựng REST APIs.
13. Đặt các header bảo mật
Một số header phản hồi tăng cường API của bạn chống lại các cuộc tấn công phía trình duyệt phổ biến.
Các header như Content-Security-Policy, X-Content-Type-Options, và Referrer-Policy cho trình duyệt biết cách xử lý phản hồi của bạn – script nào được chạy, có nên đoán loại nội dung không, bao nhiêu thông tin referrer để tiết lộ.
Thêm chúng với một đoạn middleware nhỏ chạy trên mỗi phản hồi:
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
headers["X-Content-Type-Options"] = "nosniff";
headers["Referrer-Policy"] = "no-referrer";
headers["Content-Security-Policy"] = "default-src 'self'";
headers["X-Frame-Options"] = "DENY";
await next();
});
X-Content-Type-Options: nosniffngăn trình duyệt đoán (và chạy sai) loại nội dung.Content-Security-Policyhạn chế nơi script, style và các tài nguyên khác có thể tải từ.Referrer-Policykiểm soát bao nhiêu URL của bạn được gửi đến các trang khác.X-Frame-Options: DENYngăn phản hồi của bạn được nhúng trong frame (clickjacking).
Đối với API JSON thuần, những điều này quan trọng nhất khi phản hồi được hiển thị trong trình duyệt, nhưng chúng là bảo hiểm rẻ tiền cho bất kỳ API nào.
Lưu ý: trong production, hãy cân nhắc sử dụng thư viện được duy trì hoặc reverse proxy để quản lý các header này và Content-Security-Policy chặt chẽ hơn được tùy chỉnh cho ứng dụng của bạn.
14. Lưu trữ secret an toàn
Khóa, connection strings và token không bao giờ được lưu trong hệ thống quản lý mã nguồn.
Secret được commit vào appsettings.json là secret bị rò rỉ – nó tồn tại trong lịch sử Git mãi mãi, hiển thị cho mọi người có quyền truy cập kho lưu trữ. Ngoài ra, khi sử dụng AI Agents, chúng có thể trực tiếp đọc secret từ cấu hình của bạn, trừ khi bạn từ chối quyền truy cập. Hãy lưu ý điều này!
Giữ secret hoàn toàn ngoài codebase.
Trong development, sử dụng .NET Secret Manager:
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Shop" "Server=...;Password=..."
Trong production, sử dụng biến môi trường hoặc kho secret được quản lý như Azure Key Vault:
builder.Configuration.AddAzureKeyVault(
new Uri("https://shop-vault.vault.azure.net/"),
new DefaultAzureCredential());
var connectionString = builder.Configuration.GetConnectionString("Shop");
Mã của bạn sau đó đọc cấu hình theo cùng một cách, bất kể giá trị đến từ đâu.
Ứng dụng không quan tâm đến nguồn – chỉ cần giá trị không nằm trong file đã commit.
15. Bắt buộc bảo vệ CSRF khi cần thiết
Bảo vệ Cross-Site Request Forgery quan trọng khi bạn xác thực bằng cookie.
CSRF lừa trình duyệt của người dùng đã đăng nhập gửi một request không mong muốn, sử dụng cookie mà trình duyệt tự động đính kèm. Nếu API của bạn xác thực bằng cookie, bạn cần anti-forgery token:
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});
var app = builder.Build();
app.UseAntiforgery();
Sau đó yêu cầu token hợp lệ trên các endpoint thay đổi trạng thái:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreateOrderRequest request) { /* ... */ }
Đây là sắc thái quan trọng: các API hoàn toàn dựa trên token phần lớn miễn nhiễm với CSRF.
Nếu client gửi JWT trong header Authorization (không phải cookie), trình duyệt không tự động đính kèm nó, vì vậy request cross-site giả mạo không mang credentials. Bạn chủ yếu cần anti-forgery cho các endpoint xác thực cookie – ứng dụng render server và API sử dụng xác thực cookie.
Phù hợp bảo vệ với mô hình xác thực của bạn: cookie cần bảo vệ CSRF, bearer token thì thường không.
16. Đặt phiên bản cho API và ngừng sử dụng các endpoint không an toàn
Versioning cho phép bạn loại bỏ thiết kế xấu mà không làm hỏng mọi client cùng lúc.
Khi một endpoint hóa ra là không an toàn hoặc có thiết kế kém, bạn cần cách giới thiệu phiên bản đã sửa và loại bỏ phiên bản cũ theo lịch trình. API versioning cung cấp cho bạn con đường đó.
Thêm gói Asp.Versioning.Mvc và đánh dấu phiên bản cũ là deprecated:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.ReportApiVersions = true;
});
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
// v2 sửa hợp đồng không an toàn; v1 được đánh dấu deprecated
}
ReportApiVersions thêm header api-supported-versions và api-deprecated-versions vào phản hồi, để client thấy việc ngừng sử dụng sắp tới và có thể di chuyển trước khi bạn xóa v1.
Điều này biến “chúng tôi không thể sửa vì client phụ thuộc vào nó” thành một quá trình di chuyển được quản lý.
17. Ghi nhật ký và kiểm tra các sự kiện bảo mật
Các sự kiện bảo mật – đăng nhập thất bại, ủy quyền bị từ chối, mẫu request đáng ngờ – cần được ghi nhật ký với đủ bối cảnh để tái tạo những gì đã xảy ra. Chụp chúng dưới dạng nhật ký có cấu trúc:
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var result = await _authService.AuthenticateAsync(request);
if (!result.Succeeded)
{
_logger.LogWarning(
"Failed login for {Email} from {IpAddress}",
request.Email, HttpContext.Connection.RemoteIpAddress);
return Unauthorized();
}
return Ok(result.Token);
}
Ghi nhật ký xác thực thất bại, ủy quyền bị từ chối, và bất kỳ điều gì có vẻ như đang thăm dò.
Ghi lại đủ bối cảnh để hỗ trợ điều tra pháp lý thực tế – có thể bao gồm chi tiết nhạy cảm khi mô hình đe dọa của bạn yêu cầu, chẳng hạn như tài khoản, IP nguồn, và hành động được thử.
Vì những nhật ký này có thể chứa dữ liệu nhạy cảm, hãy xử lý chúng như dữ liệu nhạy cảm: lưu trữ ở nơi chỉ người được ủy quyền mới đọc được, và áp dụng kiểm soát truy cập và lưu giữ.
Để thiết lập, xem Các phương pháp hay nhất về Ghi nhật ký trong ASP.NET Core.
18. Giữ cho các phụ thuộc được vá lỗi
Hầu hết các vi phạm khai thác lỗ hổng đã biết, không phải lỗ hổng mới.
API của bạn phụ thuộc vào hàng tá gói NuGet và .NET runtime, và mỗi gói là một điểm tiềm ẩn khi bản sửa lỗi bảo mật được phát hành và bạn chưa áp dụng. Việc cập nhật là một trong những việc có giá trị cao nhất bạn có thể làm.
.NET có thể liệt kê các gói có lỗ hổng cho bạn:
dotnet list package --vulnerable
dotnet list package --outdated
Chạy chúng thường xuyên, và kết hợp quét vào pipeline CI để phụ thuộc có lỗ hổng đã biết sẽ làm thất bại build:
# Bao gồm cả phụ thuộc transitive - lỗ hổng sâu ba gói vẫn là của bạn
dotnet list package --vulnerable --include-transitive
Cập nhật các gói NuGet và .NET runtime theo lịch trình, không chỉ khi có lỗi. Kết hợp với công cụ như Dependabot hoặc GitHub security alerts, điều này đóng lỗ hổng mà kẻ tấn công dựa vào nhiều nhất.
Tóm tắt
Bảo mật không phải là một tính năng duy nhất bạn thêm vào cuối cùng. Đó là một bộ thói quen được áp dụng trên toàn bộ API.
Hãy tóm tắt các điểm chính:
- Bảo vệ kết nối. Bắt buộc HTTPS với chuyển hướng và HSTS, và thêm header bảo mật để phản hồi khó bị lạm dụng.
- Đúng xác thực và ủy quyền. Sử dụng token stateless, xác thực mọi phần của JWT (bao gồm
ClockSkewchặt chẽ), và ủy quyền bằng chính sách và đặc quyền tối thiểu – không chỉ[Authorize]. - Xem mọi dữ liệu đầu vào là thù địch. Xác thực và làm sạch payload, gắn vào request DTO để ngăn over-posting, giới hạn kích thước request và trang, và để EF Core tham số hóa truy vấn.
- Phòng chống lạm dụng. Giới hạn tốc độ client, hạn chế CORS với các nguồn tin cậy, và áp dụng bảo vệ CSRF ở mọi nơi bạn sử dụng xác thực cookie.
- Không tiết lộ thông tin. Trả về thông tin lỗi tối thiểu với Problem Details, và giữ secret trong vault hoặc môi trường, không bao giờ trong hệ thống quản lý mã nguồn.
- Vận hành an toàn theo thời gian. Đặt phiên bản và ngừng sử dụng các endpoint không an toàn, ghi nhật ký và kiểm tra các sự kiện bảo mật vào kho lưu trữ có kiểm soát truy cập, và giữ cho phụ thuộc được vá lỗi.
Không có phương pháp đơn lẻ nào làm cho API an toàn, và bạn không phải áp dụng tất cả 18 cùng lúc.
Bắt đầu với những phương pháp đóng lỗ hổng lớn nhất của bạn – thường là HTTPS, xác thực token, ủy quyền, và xác thực dữ liệu đầu vào – sau đó làm việc xuống danh sách.



