Series Thực hành SSO | Bài 4: Refresh Token - Bí quyết duy trì phiên đăng nhập không bị "đứt gánh"
Chào anh em! Khi anh em cấu hình luồng Authorization Code Flow ở Bài 2, SSO Server (Laravel Passport) không chỉ trả về cho Client App một cái access_token, mà nó còn đính kèm một thứ gọi là refresh_token.
Tại sao phải đẻ ra tận 2 loại Token làm gì cho phức tạp? Nguyên lý bảo mật cốt lõi của OAuth2 là: Access Token (thẻ ra vào) có quyền lực rất lớn, nên tuổi thọ của nó phải cực kỳ ngắn. Nếu hacker ăn cắp được Access Token, chúng chỉ có thể phá hoại trong vòng 15 phút.
Nhưng để không làm phiền người dùng phải login liên tục, SSO Server cung cấp thêm Refresh Token. Thằng này có tuổi thọ rất dài (30 ngày, 6 tháng...), nhưng nó bị "phế võ công", không thể dùng để gọi API lấy dữ liệu được. Nhiệm vụ duy nhất của nó là: Đem đến cổng bảo vệ của SSO Server để xin cấp một Access Token mới cứng.
Hôm nay, chúng ta sẽ thực hành cấu hình tuổi thọ Token và viết logic tự động làm mới Token tại Client App.
1. Cấu hình tuổi thọ Token tại SSO Server
Mặc định, Laravel Passport để Access Token sống tới 1 năm. Đây là một con số quá nguy hiểm trên môi trường Production. Chúng ta phải bóp nó lại.
Mở project SSO Server (sso-server.test), vào file app/Providers/AuthServiceProvider.php và thêm các cấu hình sau vào hàm boot():
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
use Carbon\Carbon;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
public function boot()
{
$this->registerPolicies();
// 1. Access Token chỉ sống đúng 15 phút
Passport::tokensExpireIn(Carbon::now()->addMinutes(15));
// 2. Refresh Token sống trong 30 ngày
Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
// 3. (Tùy chọn) Mã Authorization Code (dùng lúc login) chỉ sống trong 10 phút
Passport::personalAccessTokensExpireIn(Carbon::now()->addMonths(6));
}
}
Chỉ với 2 dòng code, anh em đã thiết lập xong một ranh giới bảo mật vững chắc ở phía Server.
2. Lưu trữ Token an toàn tại Client App
Khi Client App nhận được Token từ SSO Server ở route /callback (Bài 2), anh em KHÔNG BAO GIỜ được ném cái Refresh Token này xuống Local Storage của trình duyệt (rất dễ bị tấn công XSS).
Nơi an toàn nhất để lưu trữ nó là ở Backend của Client App (trong Session, Redis, hoặc Database gắn với ID của user).
// Trích đoạn logic lưu Token tại route /callback của Client App
$tokenData = $response->json();
// Lưu vào Session có mã hóa của Laravel
session([
'access_token' => $tokenData['access_token'],
'refresh_token' => $tokenData['refresh_token'],
'expires_at' => now()->addSeconds($tokenData['expires_in']),
]);
3. Viết Middleware tự động "Refresh" tại Client App
Mỗi khi user tương tác trên Client App (ví dụ bấm xem danh sách Đơn hàng), Client App sẽ cần dùng Access Token để gọi lên các Microservices khác hoặc gọi lên SSO.
Trước khi gọi, Client App phải kiểm tra xem Access Token đã hết hạn chưa. Nếu hết, nó sẽ âm thầm lấy Refresh Token đi đổi cái mới mà người dùng không hề hay biết.
Hãy tạo một thư viện hoặc Middleware nhỏ tại Client App để xử lý việc này:
use Illuminate\Support\Facades\Http;
function getValidAccessToken()
{
$expiresAt = session('expires_at');
// Nếu token vẫn còn hạn sử dụng, lấy ra dùng luôn
if (now()->lessThan($expiresAt)) {
return session('access_token');
}
// NẾU ĐÃ HẾT HẠN: Mang Refresh Token đi đổi
$refreshToken = session('refresh_token');
$response = Http::asForm()->post(env('SSO_SERVER_URL') . '/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => env('SSO_CLIENT_ID'),
'client_secret' => env('SSO_CLIENT_SECRET'),
'scope' => '',
]);
if ($response->failed()) {
// Refresh Token cũng đã hết hạn hoặc bị thu hồi -> Bắt user Login lại
Auth::logout();
session()->flush();
abort(401, 'Phiên đăng nhập đã hết hạn hoàn toàn, vui lòng đăng nhập lại.');
}
$newTokenData = $response->json();
// Cập nhật lại Token mới vào kho lưu trữ (Session)
session([
'access_token' => $newTokenData['access_token'],
'refresh_token' => $newTokenData['refresh_token'],
'expires_at' => now()->addSeconds($newTokenData['expires_in']),
]);
return $newTokenData['access_token'];
}
Bây giờ, bất cứ chỗ nào trong Client App cần gọi API, anh em chỉ cần xài hàm này:
$validToken = getValidAccessToken();
$apiResponse = Http::withToken($validToken)->get('http://api-service.test/orders');
Tổng kết Bài 4
Với cơ chế Refresh Token, hệ thống SSO của chúng ta đã đạt đến sự cân bằng hoàn hảo giữa Bảo mật khắt khe và Trải nghiệm người dùng xuyên suốt. Access Token có bị lộ thì cũng vô hại sau 15 phút, còn người dùng thì cứ thoải mái làm việc cả tháng trời mà không bị các thông báo bắt đăng nhập lại làm phiền.
Đến đây, anh em đã có trong tay một bộ khung SSO cực kỳ chuẩn chỉnh, sẵn sàng apply vào bất kỳ dự án thực tế nào. Chúc anh em thiết kế hệ thống ngày càng vững tay nghề!
All rights reserved