Giới thiệu
Microsoft Graph là cánh cửa để truy cập các dịch vụ của Microsoft 365. Nó cho phép bạn truy cập thông tin như:
- Dữ liệu của người dùng và nhóm.
- Email và lịch trên Outlook.
- Tệp trên OneDrive.
- Tin nhắn trên Teams.
Để gọi Microsoft Graph, bạn không chỉ cần thực hiện một yêu cầu GET với http.Client. Bạn cần xác thực qua Microsoft Entra ID (trước đây là Azure Active Directory), lấy Access Token OAuth2, và sau đó sử dụng nó trong mỗi yêu cầu.
👉 Điều này có thể làm phức tạp ứng dụng chính, vì nó cần biết chi tiết về xác thực. Một giải pháp thanh lịch là sử dụng mô hình Sidecar: một dịch vụ phụ trợ, chạy bên cạnh ứng dụng, xử lý sự phức tạp này.
1. Sidecar là gì?
Giả sử bạn có một ứng dụng (trong Go, .NET, Python, v.v.) cần truy cập Graph.
Bạn có thể nhúng logic xác thực trong chính ứng dụng của mình. Nhưng nếu nhiều ứng dụng cần làm điều đó, bạn sẽ có mã trùng lặp ở mọi nơi.
Sidecar giải quyết vấn đề này:
- Là một dịch vụ nhỏ tách biệt (trong trường hợp này là Go) chạy bên cạnh ứng dụng.
- Nó giao tiếp với Microsoft Entra ID để lấy token.
- Nó gọi đến Microsoft Graph.
- Nó cung cấp một API đơn giản (qua gRPC) để ứng dụng chính chỉ cần yêu cầu:
"Cho tôi dữ liệu của người dùng X"
Và sidecar sẽ xử lý phần còn lại.
📌 Trong kiến trúc Kubernetes, sidecar chạy trong cùng một pod với ứng dụng chính.
Điều này có nghĩa là ứng dụng truy cập sidecar qua localhost, mà không phụ thuộc vào mạng bên ngoài.
2. Kiến trúc
📌 Các bước diễn ra như sau:
-
Ứng dụng gọi
GetUsertrên sidecar. -
Sidecar kiểm tra xem đã có Access Token hợp lệ trong bộ nhớ cache chưa.
- Nếu không có, nó sẽ yêu cầu một cái mới từ Microsoft Entra ID.
- Sidecar gọi Microsoft Graph với token trong header.
- Graph trả về dữ liệu người dùng.
- Sidecar chuyển đổi thành
UserResponsevà trả về cho ứng dụng qua gRPC.
3. Định nghĩa hợp đồng gRPC
Trước khi viết mã Go, chúng ta cần định nghĩa hợp đồng giao tiếp.
Tạo tệp graph.proto:
proto
syntax = "proto3";
package graph;
// Dịch vụ mà sidecar sẽ cung cấp
service GraphService {
rpc GetUser (UserRequest) returns (UserResponse);
}
// Yêu cầu: khách hàng có thể cung cấp user_id
// Nếu trống, sidecar truy vấn "me" (người dùng hiện tại)
message UserRequest {
string user_id = 1;
}
// Phản hồi: dữ liệu cơ bản của người dùng trong Graph
message UserResponse {
string id = 1;
string display_name = 2;
string given_name = 3;
string surname = 4;
string user_principal_name = 5;
}
Giải thích
- Dịch vụ
GraphServicecung cấp phương thứcGetUser. UserRequestcho phép truy vấn một người dùng cụ thể (users/{id}) hoặc người dùng hiện tại (me).UserResponsetrả về một số trường thông thường từ Graph.
Tạo mã Go
Với protoc đã cài đặt, chạy:
bash
protoc --go_out=. --go-grpc_out=. graph.proto
Điều này sẽ tạo ra các tệp .pb.go, chứa các giao diện gRPC mà chúng ta sẽ triển khai.
4. Triển khai sidecar trong Go
4.1 Cấu trúc cơ bản
Tạo tệp sidecar.go:
go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"sync"
"time"
pb "example.com/graph/proto" // điều chỉnh cho đúng đường dẫn
"google.golang.org/grpc"
)
const (
tokenURL = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
graphURL = "https://graph.microsoft.com/v1.0/%s"
)
// Máy chủ gRPC sẽ chạy như một sidecar
type server struct {
pb.UnimplementedGraphServiceServer
mu sync.Mutex
accessToken string
expiration time.Time
}
📌 Ở đây, chúng ta đã định nghĩa:
- Các hằng số với các endpoint của xác thực (Entra ID) và Graph.
- Một struct
servermà:- Triển khai dịch vụ gRPC của chúng ta.
- Lưu trữ trong bộ nhớ (
accessToken,expiration) token cuối cùng đã lấy.
4.2 Quản lý Token
go
func (s *server) getToken(ctx context.Context) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Nếu đã có token hợp lệ, tái sử dụng
if time.Now().Before(s.expiration) && s.accessToken != "" {
return s.accessToken, nil
}
// Nếu không, yêu cầu token mới từ Entra ID
tenantID := os.Getenv("ENTRA_TENANT_ID")
clientID := os.Getenv("ENTRA_CLIENT_ID")
clientSecret := os.Getenv("ENTRA_CLIENT_SECRET")
url := fmt.Sprintf(tokenURL, tenantID)
data := []byte(fmt.Sprintf(
"client_id=%s&scope=https%%3A%%2F%%2Fgraph.microsoft.com%%2F.default&client_secret=%s&grant_type=client_credentials",
clientID, clientSecret,
))
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("lỗi khi lấy token: %s", string(body))
}
var token struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
if err := json.Unmarshal(body, &token); err != nil {
return "", err
}
// Cập nhật cache (tái tạo 1 phút trước khi hết hạn)
s.accessToken = token.AccessToken
s.expiration = time.Now().Add(time.Duration(token.ExpiresIn-60) * time.Second)
return s.accessToken, nil
}
Giải thích
- Bộ nhớ cache token: sidecar không yêu cầu token mỗi lần → tiết kiệm yêu cầu.
- Mutex (
mu): đảm bảo rằng nhiều cuộc gọi đồng thời không thực hiệnPOSTcùng một lúc. - Gia hạn trước: 1 phút trước khi hết hạn, sidecar yêu cầu mới.
4.3 Triển khai GetUser
go
func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
// 1. Đảm bảo token hợp lệ
token, err := s.getToken(ctx)
if err != nil {
return nil, err
}
// 2. Tạo endpoint
userEndpoint := "me"
if req.UserId != "" {
userEndpoint = "users/" + req.UserId
}
url := fmt.Sprintf(graphURL, userEndpoint)
httpReq, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
httpReq.Header.Set("Authorization", "Bearer "+token)
// 3. Thực hiện yêu cầu tới Microsoft Graph
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("lỗi khi tìm người dùng: %s", string(body))
}
// 4. Ánh xạ phản hồi sang UserResponse
var user map[string]interface{}
if err := json.Unmarshal(body, &user); err != nil {
return nil, err
}
return &pb.UserResponse{
Id: user["id"].(string),
DisplayName: user["displayName"].(string),
GivenName: user["givenName"].(string),
Surname: user["surname"].(string),
UserPrincipalName: user["userPrincipalName"].(string),
}, nil
}
Giải thích
- Lấy token từ bộ nhớ cache (hoặc làm mới).
- Xác định endpoint (
/mehoặc/users/{id}). - Thực hiện
GETtrên Microsoft Graph. - Chuyển đổi phản hồi JSON sang struct
UserResponse.
4.4 Khởi động máy chủ gRPC
go
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
panic(err)
}
grpcServer := grpc.NewServer()
pb.RegisterGraphServiceServer(grpcServer, &server{})
fmt.Println("Sidecar Microsoft Graph đang chạy trên :50051")
if err := grpcServer.Serve(lis); err != nil {
panic(err)
}
}
Giải thích
- Sidecar lắng nghe trên cổng
50051. - Nó đăng ký dịch vụ
GraphService. - Nó vẫn chạy, sẵn sàng đáp ứng các cuộc gọi gRPC từ ứng dụng chính.
5. Thực hiện
- Cấu hình biến môi trường:
bash
export ENTRA_CLIENT_ID="client-id-của-bạn"
export ENTRA_CLIENT_SECRET="secret-của-bạn"
export ENTRA_TENANT_ID="tenant-id-của-bạn"
- Chạy sidecar:
bash
go run sidecar.go
- Kiểm tra với
grpcurl:
bash
grpcurl -plaintext -d '{}' localhost:50051 graph.GraphService/GetUser
6. Ví dụ phản hồi
json
{
"id": "1234abcd-...",
"display_name": "Nguyễn Văn A",
"given_name": "Nguyễn",
"surname": "Văn A",
"user_principal_name": "nguyenvana@doanhnghiep.com"
}
7. Tại sao nên sử dụng Sidecar?
- Đơn giản: ứng dụng chỉ cần gọi gRPC, không cần lo lắng về OAuth2.
- Tái sử dụng: nhiều dịch vụ có thể chia sẻ cùng một sidecar.
- Bảo mật: thông tin xác thực chỉ nằm trong sidecar.
- Mở rộng: sidecar có thể được sao chép trong các pod khác nhau.
- Theo dõi: số liệu và nhật ký có thể được tập trung trong sidecar.
Kết luận
Chúng ta đã tạo ra một sidecar bằng Go mà:
- Xác thực trên Microsoft Entra ID qua Client Credentials Flow.
- Sử dụng Microsoft Graph để truy vấn người dùng.
- Cung cấp một dịch vụ gRPC đơn giản cho ứng dụng chính.
Với điều này, chúng ta đã đạt được một thiết kế sạch hơn, an toàn hơn và dễ mở rộng, tách biệt trách nhiệm và tận dụng lợi ích của mô hình Sidecar.
💡 Bạn thấy hay?
Nếu muốn trao đổi về AI, cloud và kiến trúc, hãy theo dõi tôi trên mạng xã hội:
- Các Mạng Xã Hội
Tôi thường xuyên đăng các nội dung kỹ thuật từ thực tiễn. Và khi khám phá một công cụ giúp tiết kiệm thời gian và mang lại hiệu quả, bạn sẽ được biết ngay lập tức.