Skip to content

2.13 - Ôn tập và đánh giá Module 2 ​

Tóm tắt: Bài cuối module gom lại những điều quan trọng nhất của Module 2 trong một bảng, rồi cho bạn tự đánh giá bằng hai phần: 10 câu trắc nghiệm bám các cái bẫy hay gặp (30 điểm) và một lab sửa lỗi trên API thực hành CrmLab (70 điểm). Mọi đáp án và lời giải đều nằm trong phần thu gọn: làm trước, mở ra sau.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Tóm tắt điều cốt lõi của từng bài trong Module 2 bằng một câu.
  • Trả lời đúng ít nhất 7 trên 10 câu trắc nghiệm về DNS, HTTP, JSON, database, xác thực, hosting và CORS.
  • Tìm và sửa được các lỗi phân quyền, lỗi mã trạng thái trong một API nhỏ, rồi kiểm chứng bằng curl.
  • Tự chấm bài lab theo rubric và biết mình còn yếu ở bài nào.

Cần biết trước: các bài 2.1 tới 2.12 của module, đặc biệt API thực hành CrmLab ở mục 2.3.6.

Nội dung bài học ​

2.13.1 — Module 2 trong một bảng ​

BàiĐiều cốt lõi
2.1 Bản đồ moduleMột request đi qua tám chặng; mã lỗi cho biết chặng nào hỏng
2.2 InternetDNS có TTL nên đổi bản ghi không có hiệu lực ngay; mở kết nối mới tốn DNS + TCP + TLS, nên dùng lại HttpClient và connection pool
2.3 Client - ServerClient luôn hỏi trước; HTTP không nhớ gì nên mỗi request tự mang token; kiểm tra ở client không phải bảo mật
2.4 HTTP/HTTPSGET an toàn, PUT/DELETE idempotent; 401 khác 403; không trả 200 kèm lỗi; giá trị header chỉ được chứa ASCII
2.5 API và JSONĐường dẫn là danh từ, method là động từ; số nguyên lớn hơn 2⁵³ phải gửi dạng chuỗi; lỗi dùng ProblemDetails; vắng mặt khác null
2.6 DatabaseACID; thiếu BEGIN là mất tính trọn vẹn; lost update không phải lỗi của database; connection là tài nguyên khan hiếm
2.7 Xác thực, phân quyềnJWT đọc được nhưng không sửa được, và không thu hồi được; kiểm tra vai trò chưa đủ, phải kiểm tra quyền trên từng bản ghi (IDOR); băm mật khẩu bằng hàm cố tình chậm
2.8 HostingReverse proxy kết thúc TLS và chia tải; mở rộng ngang đòi hỏi ứng dụng không giữ trạng thái cục bộ; base image quyết định ứng dụng có chạy được không; tách liveness và readiness
2.9 .NET runtimeIL và JIT làm lần gọi đầu chậm; Server GC với DATAS từ .NET 9; dùng bản LTS, hiện là .NET 10
2.10 Đào sâuETag tiết kiệm băng thông chứ không tiết kiệm vòng đi về; private khác public; HTTP/2 không tự làm API nhanh hơn
2.11 Ca chẩn đoánKiểm tra từ ngoài vào trong; đọc kỹ thông điệp lỗi; lệch đồng hồ giữa hai máy làm hỏng JWT
2.12 CORSCORS là chính sách của trình duyệt, không bảo vệ API; preflight; UseCors đặt trước xác thực

2.13.2 — Luồng tổng quát của CRM ​

Nối các bài lại, đây là đường đi của một lần mở danh sách khách hàng trong CRM mà khoá học xây dựng:

text
Người dùng mở trình duyệt
    -> DNS: crm.company.com -> IP của reverse proxy            (2.2)
    -> TCP + TLS tới cổng 443, proxy giữ chứng chỉ               (2.2, 2.8)
    -> GET /api/customers kèm "Authorization: Bearer <JWT>"     (2.3, 2.4)
    -> proxy chuyển tiếp HTTP tới một trong các container API   (2.8)
    -> ASP.NET Core: CORS -> xác thực JWT -> kiểm tra quyền     (2.7, 2.12)
    -> EF Core truy vấn PostgreSQL qua connection pool           (2.6)
    -> trả JSON, id kiểu long dạng chuỗi, lỗi dạng ProblemDetails (2.5)
    -> frontend hiển thị danh sách

Khoá học dùng PostgreSQL làm database mặc định, bản SQL Server có ở các bài phụ thuộc database. Hosting mặc định là container sau reverse proxy; cách triển khai lên cloud nằm ở Module 15.

2.13.3 — Cách làm phần đánh giá ​

PhầnNội dungĐiểm
A — Trắc nghiệm10 câu ở mục Tự kiểm tra, mỗi câu 3 điểm30
B — LabBài 1 và Bài 2 dưới đây, chấm theo rubric ở cuối Bài 270

Làm trắc nghiệm trước, ghi đáp án và một dòng lý do cho từng câu, rồi mới mở đáp án. Câu nào sai, đọc lại bài được ghi trong đáp án.

MứcTổng điểmÝ nghĩa
Đạttừ 70Đủ nền để học tiếp Module 3
Khátừ 85Nắm chắc, chỉ còn vài chi tiết cần xem lại
Giỏitừ 95Làm được cả phần mở rộng của lab và giải thích được vì sao

Bài tập áp dụng ​

Bài 1 — Đọc JSON trong ứng dụng console (10 điểm) ​

Tạo project console, dán đoạn code dưới đây và chạy. Kết quả không như mong đợi. Tìm nguyên nhân và sửa.

bash
dotnet --version          # phải bắt đầu bằng 10.0
dotnet new console -n HelloCrm
cd HelloCrm
csharp
// Mô phỏng đơn giản: đọc JSON khách hàng thành đối tượng C#
using System.Text.Json;

string json = """
{
    "id": 1,
    "name": "Nguyen Van A",
    "email": "[email protected]",
    "isActive": true
}
""";

var customer = JsonSerializer.Deserialize<Customer>(json);
Console.WriteLine($"Chào mừng khách hàng: {customer?.Name} ({customer?.Email})");

record Customer(int Id, string Name, string Email, bool IsActive);

Tiêu chí hoàn thành: chương trình in ra Chào mừng khách hàng: Nguyen Van A ([email protected]), và bạn giải thích được vì sao cùng JSON đó thì một API ASP.NET Core đọc đúng mà không cần sửa gì.

Gợi ý và lời giải — Bài 1

Gợi ý. So tên trường trong JSON với tên thuộc tính của Customer, để ý chữ hoa chữ thường. Đọc lại mục 2.5.6.

Lời giải. Chạy dotnet run, kết quả thật trên .NET 10:

text
Chào mừng khách hàng:  ()

Không có lỗi nào, chỉ có hai giá trị rỗng. System.Text.Json mặc định phân biệt hoa thường: trường "name" trong JSON không khớp thuộc tính Name, nên Name và Email không được gán. Sửa bằng bộ tuỳ chọn dành cho web:

csharp
var customer = JsonSerializer.Deserialize<Customer>(json, JsonSerializerOptions.Web);
text
Chào mừng khách hàng: Nguyen Van A ([email protected])

JsonSerializerOptions.Web không phân biệt hoa thường và dùng tên kiểu camelCase. ASP.NET Core dùng sẵn bộ tuỳ chọn này cho mọi API, nên trong API bạn không gặp lỗi trên; chỉ khi tự gọi JsonSerializer ở chỗ khác (ứng dụng console, job nền, test) thì phải tự truyền vào.

Thử thêm: Web API mặc định.

bash
dotnet new webapi -n MyCrmApi
cd MyCrmApi
dotnet run

Template chọn cổng ngẫu nhiên cho mỗi project, ghi trong Properties/launchSettings.json. Đọc địa chỉ trong dòng Now listening on rồi mở <địa chỉ>/weatherforecast. Kết quả chạy thật (cổng của bạn sẽ khác):

text
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5159
text
[{"date":"2026-10-18","temperatureC":49,"summary":"Mild","temperatureF":120},...]

Bài 2 — Lab sửa lỗi CrmLab (60 điểm) ​

Dùng CrmLab ở mục 2.3.6. Qua các bài 2.3, 2.4 và 2.7 bạn đã tìm thấy vài lỗi của nó. Lab này yêu cầu sửa chúng và chứng minh đã sửa bằng curl.

  1. Phân quyền chức năng. DELETE /api/users/{id} chỉ Admin được gọi. Tài khoản Sales gọi phải nhận 403.
  2. IDOR. GET /api/customers/{id} chỉ trả khách hàng của chính người gọi (Admin xem được tất cả). Gọi id của người khác phải nhận 404.
  3. Tìm kiếm. GET /api/customers/search không còn trả 200 kèm success: false. Không có kết quả thì trả 200 với mảng rỗng [].
  4. Đo thời gian. Mọi phản hồi có header Server-Timing: app;dur=<số mili giây>, số viết bằng dấu chấm thập phân.
  5. Viết lại luồng. Một đoạn 5 tới 10 câu, bằng lời của bạn, mô tả đường đi của request GET /api/customers từ trình duyệt tới database và ngược lại, nêu tên ít nhất năm khái niệm của module.

Mở rộng (để đạt mức Giỏi). Endpoint tìm kiếm còn một lỗ hổng nữa, cùng loại với yêu cầu 2. Tìm nó bằng curl trước khi đọc lời giải, rồi sửa.

Tiêu chí hoàn thành: chạy được bộ lệnh kiểm chứng trong lời giải và nhận đúng các mã trạng thái ghi ở đó; đoạn văn ở yêu cầu 5 đúng thứ tự các chặng.

Gợi ý và lời giải — Bài 2

Gợi ý. Yêu cầu 1 chỉ cần đổi một dòng. Yêu cầu 2 xem lại Bài 2 của bài 2.7. Yêu cầu 4 xem lại Bài 1 của bài 2.1, và nhớ thêm hai dòng using.

Lời giải — các chỗ sửa trong Program.cs:

csharp
// Thêm ở đầu file
using System.Diagnostics;
using System.Globalization;
csharp
// Yêu cầu 4: ngay sau var app = builder.Build();
app.Use(async (ctx, next) =>
{
    var sw = Stopwatch.StartNew();
    ctx.Response.OnStarting(() =>
    {
        ctx.Response.Headers["Server-Timing"] = string.Create(
            CultureInfo.InvariantCulture, $"app;dur={sw.Elapsed.TotalMilliseconds:F1}");
        return Task.CompletedTask;
    });
    await next();
});
csharp
// Yêu cầu 2: lọc theo chủ sở hữu ngay trong điều kiện tìm
app.MapGet("/api/customers/{id:long}", (long id, ClaimsPrincipal user) =>
{
    var userId = int.Parse(user.FindFirstValue("sub")!);   // lấy từ token, KHÔNG từ request
    var customer = customers.FirstOrDefault(c =>
        c.Id == id && (c.OwnerId == userId || user.IsInRole("Admin")));
    return customer is null ? Results.NotFound() : Results.Ok(customer);
}).RequireAuthorization();
csharp
// Yêu cầu 3 và phần mở rộng: trả thẳng danh sách, và chỉ tìm trong khách hàng của mình
app.MapGet("/api/customers/search", (string name, ClaimsPrincipal user) =>
{
    var userId = int.Parse(user.FindFirstValue("sub")!);
    var found = customers
        .Where(c => c.OwnerId == userId || user.IsInRole("Admin"))
        .Where(c => c.Name.Contains(name, StringComparison.OrdinalIgnoreCase))
        .ToList();
    return Results.Ok(found);   // không có kết quả thì trả mảng rỗng [], vẫn là 200
}).RequireAuthorization();
csharp
// Yêu cầu 1: chỉ Admin
app.MapDelete("/api/users/{id:int}", (int id) =>
    users.RemoveAll(u => u.Id == id) > 0 ? Results.NoContent() : Results.NotFound())
   .RequireAuthorization("AdminOnly");

Lỗ hổng ở phần mở rộng. Bản gốc của endpoint tìm kiếm không lọc theo chủ sở hữu, nên An tìm hoa là thấy khách hàng 103 của Bình. Kết quả chạy thật trước khi sửa phần lọc:

text
[{"id":103,"name":"Nha sach Hoa Mai","ownerId":2}] <- 200

Bộ lệnh kiểm chứng. Chạy lại CrmLab đã sửa, lấy TOKEN của [email protected], rồi:

bash
B=http://localhost:5000
curl -s -i -X DELETE $B/api/users/2 -H "Authorization: Bearer $TOKEN" | head -1
for id in 101 103; do
  curl -s -o /dev/null -w "$id -> %{http_code}\n" $B/api/customers/$id -H "Authorization: Bearer $TOKEN"
done
curl -s -w " <- %{http_code}\n" "$B/api/customers/search?name=xyz" -H "Authorization: Bearer $TOKEN"
curl -s -w " <- %{http_code}\n" "$B/api/customers/search?name=hoa" -H "Authorization: Bearer $TOKEN"
curl -s -w " <- %{http_code}\n" "$B/api/customers/search?name=anh" -H "Authorization: Bearer $TOKEN"
curl -s -i $B/api/customers -H "Authorization: Bearer $TOKEN" | grep -iE "HTTP/|server-timing"

Kết quả chạy thật với bản đã sửa:

text
HTTP/1.1 403 Forbidden
101 -> 200
103 -> 404
[] <- 200
[] <- 200
[{"id":101,"name":"Cong ty Anh Duong","ownerId":1}] <- 200
HTTP/1.1 200 OK
Server-Timing: app;dur=1.4

Số mili giây trong Server-Timing sẽ khác ở máy bạn; điều cần kiểm tra là có header và số dùng dấu chấm.

Yêu cầu 5 — một đoạn mẫu: "Trình duyệt hỏi DNS để đổi crm.company.com thành địa chỉ IP, rồi mở kết nối TCP và bắt tay TLS tới reverse proxy. Request GET /api/customers mang theo JWT trong header Authorization, vì HTTP không nhớ ai đã đăng nhập. Proxy chuyển request tới một thể hiện API; ở đó middleware CORS chạy trước, rồi tới xác thực token (sai thì 401) và kiểm tra quyền (thiếu quyền thì 403). Endpoint lấy userId từ token, mượn một kết nối từ connection pool và truy vấn PostgreSQL, chỉ lấy khách hàng của người đó. Kết quả trả về dạng JSON với mã 200, kèm header Server-Timing để biết máy chủ mất bao lâu."

Rubric chấm Bài 1 và Bài 2 (70 điểm):

Tiêu chíĐiểmChưa đạtĐạtTốt
Bài 1: đọc JSON10Chưa in đúng tên và emailIn đúng nhờ JsonSerializerOptions.WebGiải thích được vì sao API không gặp lỗi này (0 / 7 / 10 điểm)
Yêu cầu 1: phân quyền chức năng10Sales vẫn xoá được người dùngSales nhận 403Admin vẫn xoá được, đã thử bằng token Admin (0 / 7 / 10)
Yêu cầu 2: IDOR15Vẫn đọc được khách của người khácNhận 404 cho khách của người khácĐiều kiện quyền nằm trong điều kiện tìm, không phải if sau khi đã lấy; Admin vẫn xem được (0 / 10 / 15)
Yêu cầu 3: mã trạng thái tìm kiếm10Còn success: false200 với [] khi không có kết quảGiải thích được khi nào 404 đúng và khi nào [] đúng (0 / 7 / 10)
Yêu cầu 4: Server-Timing10Không có header, hoặc ứng dụng ghi lỗi Headers are read-onlyCó headerSố dùng dấu chấm thập phân, header gắn trong OnStarting (0 / 7 / 10)
Yêu cầu 5: đoạn văn15Thiếu chặng hoặc sai thứ tựĐủ chặng, đúng thứ tựNêu được ít nhất năm khái niệm và vai trò của từng cái (0 / 10 / 15)

Phần mở rộng không cộng điểm, nhưng là điều kiện để xếp mức Giỏi ở mục 2.13.3.

Tự kiểm tra ​

Mười câu trắc nghiệm, mỗi câu 3 điểm. Chọn một đáp án.

Câu 1. Bạn đổi bản ghi A của crm.company.com sang máy chủ mới lúc 9:00. Bản ghi có TTL 3600. Đến 9:30 vẫn có người dùng vào máy chủ cũ. Vì sao? A. DNS đổi bản ghi mất 24 giờ mới lan ra. B. Các resolver được phép giữ kết quả cũ tới hết TTL, tối đa tới khoảng 10:00. C. Trình duyệt luôn lưu IP vĩnh viễn. D. Bản ghi A không hỗ trợ đổi IP.

B. Mỗi resolver và bộ đệm trên đường được phép giữ kết quả cũ trong tối đa một TTL (3600 giây). Muốn chuyển nhanh thì hạ TTL trước rồi mới đổi bản ghi. Xem bài 2.2.

Câu 2. Một nhân viên Sales đã đăng nhập gọi endpoint chỉ dành cho Admin. Mã đúng là gì? A. 400. B. 401. C. 403. D. 404.

C. Server đã biết người gọi là ai (đã xác thực) nhưng người đó không đủ quyền. 401 dành cho trường hợp chưa biết bạn là ai, và trả nhầm 401 khiến client cố làm mới token vô ích. Xem bài 2.4 và bài 2.7.

Câu 3. Method nào được thiết kế để thay thế toàn bộ tài nguyên và là idempotent? A. POST. B. PUT. C. PATCH. D. GET.

B. PUT thay thế toàn bộ và idempotent: gửi lại nhiều lần cho cùng kết quả. PATCH sửa một phần và không bảo đảm idempotent; POST tạo mới và không idempotent; GET chỉ đọc. Xem bài 2.4.

Câu 4. API trả {"id": 9007199254740993}. Frontend JavaScript đọc được giá trị nào? A. 9007199254740993. B. 9007199254740992, không báo lỗi. C. Ném lỗi tràn số. D. null.

B. JavaScript lưu mọi số bằng double, chỉ giữ chính xác số nguyên tới 2⁵³ − 1 = 9007199254740991. Số lớn hơn bị làm tròn im lặng. Cách sửa là gửi id dạng chuỗi bằng [JsonNumberHandling(...)]. Xem bài 2.5.

Câu 5. Người dùng A gọi GET /api/customers/103, khách hàng thuộc người dùng B, và nhận dữ liệu. Endpoint đã có RequireAuthorization(). Đây là lỗi gì, và sau khi sửa nên trả mã nào? A. Lỗi CORS, trả 403. B. IDOR, trả 403. C. IDOR, trả 404. D. Lỗi xác thực, trả 401.

C. Đây là IDOR: endpoint chỉ kiểm tra đã đăng nhập, không kiểm tra quyền trên bản ghi. Trả 404 để không xác nhận bản ghi tồn tại; trả 403 giúp kẻ tấn công dò ra id hợp lệ. Xem bài 2.7.

Câu 6. Hai phiên cùng đọc số dư 1000, phiên A ghi 1500, phiên B ghi 1300, cả hai commit ở mức READ COMMITTED. Kết quả và cách phòng tốt nhất? A. Database báo lỗi, không cần phòng. B. Còn 1300; phòng bằng UPDATE ... SET balance = balance + 300. C. Còn 1800 vì database tự cộng. D. Còn 1500 vì phiên A commit trước.

B. Đây là lost update: cả hai giao dịch đều hợp lệ nên database không báo lỗi, và thay đổi của A biến mất. Cách rẻ nhất là cập nhật nguyên tử, tính ngay trong câu UPDATE; ngoài ra có SELECT ... FOR UPDATE hoặc khoá lạc quan bằng cột phiên bản. Xem bài 2.6.

Câu 7. Database chậm 30 giây. Endpoint nào nên chuyển sang không khoẻ? A. Chỉ liveness. B. Chỉ readiness. C. Cả hai. D. Không cái nào.

B. Readiness báo "đừng gửi request tới đây", còn liveness báo "khởi động lại tôi đi". Database chậm không phải lý do để khởi động lại tiến trình; nếu liveness cũng kiểm tra database, mọi thể hiện bị khởi động lại cùng lúc và dồn thêm tải lên database. Xem bài 2.8.

Câu 8. Frontend gọi GET đơn giản thì chạy, nhưng POST với Content-Type: application/json báo lỗi CORS. Nguyên nhân thường gặp nhất? A. Server không hỗ trợ POST. B. Request JSON kích hoạt preflight OPTIONS, và preflight bị từ chối hoặc thiếu header CORS. C. JSON không hợp lệ. D. Trình duyệt chặn mọi POST sang origin khác.

B. application/json khiến request không còn là "request đơn giản", nên trình duyệt gửi preflight trước. Nếu preflight trả lỗi hoặc thiếu Access-Control-Allow-* (thường do UseCors đặt sau middleware kiểm tra quyền), request thật không được gửi. Xem bài 2.12.

Câu 9. Một service tạo new HttpClient() cho mỗi lời gọi ra ngoài. Sau vài giờ chạy tải cao, nó bắt đầu ném SocketException. Vì sao? A. Do DNS hết hạn. B. Mỗi kết nối đã đóng còn nằm ở trạng thái TIME_WAIT một lúc, nên cổng mạng cạn dần. C. Do JSON quá lớn. D. Do GC thu gom HttpClient quá sớm.

B. Mỗi HttpClient mới mở kết nối mới; kết nối đóng xong chưa giải phóng cổng ngay. Tải cao thì dải cổng khả dụng cạn. Cách đúng là dùng lại HttpClient, trong ASP.NET Core lấy qua IHttpClientFactory. Xem bài 2.2.

Câu 10. Hai máy API sau một load balancer. Người dùng đăng nhập xong thì một nửa số request nhận 401 với "The token expired at ...", dù token vừa cấp. Nghi ngờ đầu tiên? A. Khoá ký JWT bị lộ. B. Đồng hồ một máy chạy nhanh hơn máy phát token quá 5 phút. C. CORS sai. D. Database chậm.

B. exp là mốc tuyệt đối do máy phát tính; máy kiểm tra so với đồng hồ của mình. Máy kiểm tra nhanh hơn quá ClockSkew (mặc định 5 phút) thì token vừa cấp đã "hết hạn". Lỗi chỉ xảy ra một nửa số lần vì chỉ một máy lệch. Xem bài 2.11.

Kết luận ​

Ba điều đáng nhớ nhất của Module 2:

  1. Mỗi tầng có quy tắc riêng, và lỗi thường nằm ở chỗ giao nhau: header không ASCII, số lớn qua JSON, đồng hồ giữa hai máy, thứ tự middleware.
  2. Bảo mật nằm ở server. Giao diện ẩn nút, CORS, hay việc đã đăng nhập đều không thay được kiểm tra quyền trên từng bản ghi.
  3. Chẩn đoán bằng đo đạc, không bằng đoán. curl -i, DevTools, Server-Timing và thông điệp lỗi trong header nói cho bạn biết chặng nào hỏng.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.