Mọi API thật đều cần biết ai đang gọi. Với REST, bạn gắn token vào HTTP header Authorization. gRPC không có "header" theo nghĩa HTTP thuần, nhưng có thứ tương đương: metadata — những cặp key-value gửi kèm mỗi request, hệt như header. Kết hợp metadata với interceptor (bài grpc-06), bạn kiểm xác thực tập trung một chỗ cho mọi RPC, thay vì rải code kiểm token trong từng handler. Bài này (phần 8/12) đo thật luồng auth hoàn chỉnh trong go-lab: client gửi token, server kiểm trong interceptor, và trích danh tính người dùng ra để dùng.

Metadata: header của gRPC

Metadata là cặp key-value đi kèm request (và cả response). Client gắn vào context gửi đi; server đọc từ context nhận được:

// CLIENT: gắn token vào metadata
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer tok-alice")
resp, err := c.Me(ctx, req)   // token đi KÈM sang server

// SERVER (trong interceptor): đọc metadata
md, _ := metadata.FromIncomingContext(ctx)
tok := strings.TrimPrefix(md.Get("authorization")[0], "Bearer ")

Kiểm token trong một interceptor (thay vì từng handler) là cách làm chuẩn — áp một lần cho mọi RPC, và sau khi xác thực, gắn danh tính người dùng vào context cho handler dùng:

Ảnh chụp đoạn mã nền tối minh hoạ metadata và xác thực token đi kèm request như header HTTP, metadata là cặp key-value gửi kèm mỗi RPC giống HTTP header interceptor kiểm token tập trung cho mọi call. Client gắn token vào metadata gửi đi AppendToOutgoingContext thêm cặp key-value vào metadata của request ctx bằng metadata.AppendToOutgoingContext ctx authorization Bearer tok-alice resp err bằng c.Me ctx req token đi kèm sang server per-RPC credentials tự gắn token cho mọi call khỏi lặp tay. Server đọc metadata cộng kiểm trong interceptor func authInterceptor ctx req info handler md bằng metadata.FromIncomingContext ctx tok bằng strings.TrimPrefix md.Get authorization 0 Bearer if thiếu token return nil status.Error codes.Unauthenticated if token sai return nil status.Error codes.PermissionDenied ctx bằng context.WithValue ctx userKey user gắn user cho handler return handler ctx req. Vài điều cần nhớ về metadata metadata giống HTTP header key thường text hoặc key -bin binary kiểm auth một chỗ interceptor thay vì lặp trong từng handler metadata không tự mã hoá bắt buộc chạy trên TLS bài bảo mật

Hình 1: Client gắn token bằng metadata.AppendToOutgoingContext; server đọc bằng metadata.FromIncomingContext trong một auth interceptor, kiểm token rồi gắn user vào context cho handler. Metadata như HTTP header — và không tự mã hoá nên bắt buộc chạy trên TLS.

Đo thật: ba loại token

Mình dựng service Account với RPC Me() được bảo vệ bởi một auth interceptor, rồi gọi với ba loại token:

Ảnh chụp bảng kết quả đo thật xác thực qua metadata trong interceptor output thật go-lab grpc-go v1.67.1 UnaryInterceptor auth. Gọi Me với ba loại token khác nhau, một không token Unauthenticated thieu token metadata authorization, hai token sai sai-be-bet PermissionDenied token khong hop le, ba token đúng tok-alice OK user alice role member, log server auth token OK user alice cho qua svc.Account Me, interceptor auth chặn ở cửa thiếu token trả Unauthenticated 401 token sai trả PermissionDenied 403 token đúng mới cho handler chạy và server lấy được user alice từ token để dùng trong logic kiểm quyền sở hữu ghi log kiểm một chỗ áp mọi RPC

Hình 2: Đo thật. (1) Không token → Unauthenticated. (2) Token sai → PermissionDenied. (3) Token đúng (tok-alice) → auth interceptor cho qua, server đọc user=alice từ token → OK, role=member.

Kết quả thật:

  • ① Không token → Unauthenticated ("thieu token (metadata authorization)"). Interceptor thấy không có key authorization trong metadata, chặn ngay với code 401-tương-đương.
  • ② Token sai ("sai-be-bet") → PermissionDenied ("token khong hop le"). Có token nhưng không hợp lệ — code 403-tương-đương. Lưu ý sự phân biệt: thiếu token (chưa xác thực) khác sai token (xác thực thất bại) — hai code khác nhau để client xử lý đúng.
  • ③ Token đúng ("tok-alice") → interceptor log token OK -> user=alice, cho qua, handler chạy, trả user=alice, role=member. Quan trọng: server trích được danh tính (alice) từ token và gắn vào context — handler dùng nó cho logic nghiệp vụ (kiểm quyền sở hữu ở bài IDOR của sê-ri bảo mật, ghi log có user, v.v.).

Toàn bộ logic auth nằm một chỗ (interceptor), áp cho mọi RPC của service. Thêm một RPC mới? Nó tự động được bảo vệ, không cần nhớ chép code kiểm token.

Đánh đổi cần cân nhắc

Metadata KHÔNG tự mã hoá — bắt buộc chạy trên TLS. Giống HTTP header gửi qua HTTP thuần, metadata (gồm token) đi trần trên dây nếu không có TLS. Bất kỳ ai nghe được đường truyền đọc được token. Đây là lý do production luôn chạy gRPC trên TLS (credentials.NewTLS thay vì insecure) — demo này dùng insecure chỉ vì chạy localhost trong lab. Nối thẳng với bài TLS của sê-ri bảo mật: token trên kênh không mã hoá là token bị lộ.

Per-RPC credentials tiện hơn gắn tay mỗi call. Demo gắn token thủ công bằng AppendToOutgoingContext mỗi lần — dễ quên. gRPC có PerRPCCredentials (một interface tự cung cấp metadata cho mọi call của connection), hoặc dùng interceptor phía client. Với token OAuth tự refresh, per-RPC credentials là cách sạch hơn: cấu hình một lần, mọi call tự mang token mới nhất.

Xác thực khác uỷ quyền — interceptor chỉ lo phần đầu. Auth interceptor trả lời "bạn là ai" (xác thực). Nhưng "bạn được làm gì với đối tượng này" (uỷ quyền mức đối tượng) thường phải kiểm trong handler, nơi biết đối tượng cụ thể — đúng như bài Broken Access Control/IDOR. Đừng tưởng qua được auth interceptor là được làm mọi thứ; interceptor chặn kẻ lạ, handler vẫn phải kiểm quyền trên từng tài nguyên.

Ba ý mang về

  1. Metadata là header của gRPC — cặp key-value kèm mỗi request. Client gắn bằng metadata.AppendToOutgoingContext, server đọc bằng metadata.FromIncomingContext. Dùng cho token auth, request-id, trace context... như HTTP header.
  2. Kiểm auth trong interceptor: một chỗ, mọi RPC. Đo thật: không token → Unauthenticated, token sai → PermissionDenied, token đúng → cho qua và server trích user=alice từ token để dùng. Thêm RPC mới tự động được bảo vệ.
  3. Metadata không tự mã hoá và auth ≠ authz. Bắt buộc chạy trên TLS (token trần = token lộ); dùng per-RPC credentials cho token tự refresh; và nhớ interceptor chỉ lo xác thực — uỷ quyền trên từng đối tượng vẫn phải kiểm trong handler.

Nguồn

Phần sau ta đo hiệu năng streaming: so sánh gửi nhiều message qua một stream với gọi nhiều unary call riêng lẻ, và flow control của HTTP/2 kiểm soát backpressure thế nào.