Try   HackMD

Laravel 跨網站自動登入實踐:利用 JWT 深入解析

這是一篇關於如何在 Laravel 框架中使用 JSON Web Tokens (JWT) 來實現從一個網站(A 網站)到另一個網站(B 網站)無需重新登入功能的指南。這個功能不僅提高了使用者體驗,同時也確保了跨站點的安全性。下面我們來一步一步地看看如何設置吧!

1. 設置JWT認證

首先,我們得在 A 網站上設置好 JWT。這個過程分幾個簡單的步驟:

1. 安裝 JWT 套件:

打開你的終端機,運行以下命令來安裝 JWT 套件:

composer require tymon/jwt-auth

2. 發布配置文件:

接下來,我們需要生成配置文件和密鑰,以便 JWT 能夠正確地加密和解密令牌:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider" php artisan jwt:secret

3. 配置 config/auth.php:

auth.php 配置文件中,我們要設定使用 jwt 作為驗證驅動:

'guards' => [ 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ],

4. 在登入時生成 JWT:

在你的登入控制器中,添加生成 JWT 的邏輯:

use Tymon\JWTAuth\Facades\JWTAuth; public function login(Request $request) { $credentials = $request->only('email', 'password'); if (!$token = JWTAuth::attempt($credentials)) { return response()->json(['error' => 'invalid_credentials'], 401); } return response()->json(['token' => $token]); }

2. 傳遞 JWT 給 b網站

接著,我們在 A 網站生成一個包含 JWT 的連結,當用戶點擊這個連結時,他們將被導向到 B 網站並自動登入:

1. 生成包含 JWT 的連結:

public function getAuthenticatedLink() { $token = JWTAuth::fromUser(auth()->user()); $link = 'https://b-website.com/login?token=' . $token; return response()->json(['link' => $link]); }

在生成 JWT 時,我們可以加入一些特定的聲明(claims),例如 iat(Issue At,發行時間)、exp(Expiration Time,過期時間)和 nbf(Not Before,不早於時間)。這些聲明幫助我們控制令牌的有效期和使用時機,來避免一些常見的安全問題,比如令牌重放攻擊。

2. 加入自定義聲明和時間處理

假設你希望令牌在生成後的一小時內有效,並且有一些自定義的資料(比如用戶類型等),你可以這樣做:

public function getAuthenticatedLink() { $user = auth()->user(); $currentTime = new \DateTimeImmutable(); $customClaims = [ 'customize' => 'customize', // 自定義 'exp' => $currentTime->modify('+1 hour'), // 過期時間 ]; $token = JWTAuth::claims($customClaims)->fromUser($user); $link = 'https://b-website.com/login?token=' . $token; return redirect($link); }

3. 在 b網站 驗證 JWT 並登入用戶

現在,在 B 網站,我們需要設置好 JWT 並確保能夠接收來自 A 網站的 JWT,進行驗證和登入用戶:

1. 安裝 JWT 套件(同 a網站)。

2. 設置 JWT 認證配置(同 a網站)。

透過php artisan jwt:secret生成的 JWT_SECRET 需也放到b網站

3. 在登入控制器中添加 JWT 驗證邏輯:

use Tymon\JWTAuth\Facades\JWTAuth; public function loginWithToken(Request $request) { $token = $request->get('token'); if (!$token) { \Log::error('Token not provided'); return response()->json(['error' => 'token_not_provided'], 400); } try { $payload = JWTAuth::parseToken($token)->getPayload(); $user = JWTAuth::parseToken($token)->authenticate(); if (!$user) { return response()->json(['error' => 'user_not_found'], 404); } } catch (Tymon\JWTAuth\Exceptions\TokenExpiredException $e) { \Log::error('Token expired', ['token' => $token, 'message' => $e->getMessage()]); return response()->json(['error' => 'token_expired'], 401); } catch (Tymon\JWTAuth\Exceptions\TokenInvalidException $e) { \Log::error('Token invalid', ['token' => $token, 'message' => $e->getMessage()]); return response()->json(['error' => 'token_invalid'], 401); } catch (Tymon\JWTAuth\Exceptions\JWTException $e) { \Log::error('Token absent', ['token' => $token, 'message' => $e->getMessage()]); return response()->json(['error' => 'token_absent'], 401); } // 登入用戶 Auth::login($user); // 取得自定義的 customize $customize = $payload->get('customize'); return redirect('/home'); // 重定向到登入後的頁面 }

生成不含用戶資訊的 JWT

當你需要產生一個不包括使用者身分(如使用者 ID 或使用者名稱)的 JWT 時,可以使用 JWTFactory 類別來建立一個包含自訂聲明的 token。這種方式不依賴特定的使用者模型。

以下是一個簡單的例子,展示如何產生這種 JWT

use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Facades\JWTFactory; public function index() { $customClaims = [ 'iss' => 'your-issuer-identifier', // Issuer 'sub' => 'subject-or-user-id', // Subject, generally user identifier 'aud' => 'your-audience', // Audience 'customize' => 'customize', 'action' => 'menu', ]; $payload = JWTFactory::customClaims($customClaims)->make(); $token = JWTAuth::encode($payload)->get(); $link = 'https://b-website.com/login-withtoken/?token=' . $token; return redirect($link); }

說明

在上述代碼中,我們首先定義了一組自定義聲明,這些聲明描述了 JWT 的一些基本特性,如發行者、主題和觀眾。然後,我們通過 JWTFactory 創建了一個 payload,這個 payload 包含了我們的自定義聲明。接著,使用 JWTAuth::encode() 方法將這個 payload 編碼成一個 JWT。最後,我們將這個 JWT 添加到一個 URL 中,並將用戶重定向到該 URL

此方法允許你靈活地生成 JWT,適用於不需要綁定特定用戶身份的場景,從而可以在多種應用環境中靈活使用。

注意事項

  • 安全性:不包含用戶標識的 JWT 只包含自定義的宣告,所以在驗證用戶身份時可能需要額外的安全措施。
  • 靈活性:根據你的具體需求來決定是否需要包含用戶標識。包含用戶標識的 JWT 可以更方便地關聯用戶數據。

實現多組密鑰

在使用 Laraveltymon/jwt-auth 套件時,通常我們只需要一組密鑰(JWT_SECRET)來簽名和驗證令牌,這對大多數應用來說已經夠用了。但是,如果你的應用情境比較特殊,比如說在一個多租戶系統中,或者需要和多個不同的服務進行接口對接,你可能會需要用到多組密鑰。雖然 tymon/jwt-auth 本身不直接支持多組密鑰,但我們可以自己動手搞定。

步驟一:定義多組密鑰

首先,在你的 .env 檔案裡面加入多組密鑰。就像這樣設定:

JWT_SECRET_DEFAULT=your_default_secret_key JWT_SECRET_ALTERNATE=your_alternate_secret_key

步驟二:自訂密鑰切換邏輯

接著,我們要創建一個中間件,這個中間件會根據請求的某些特定條件(比如說一個特定的標頭或者是URL路徑),來選擇使用哪一組密鑰。這裡有個例子展示如何在中間件中根據請求標頭來切換密鑰:

use Tymon\JWTAuth\JWTAuth; use Closure; class CustomJWTMiddleware { protected $jwtAuth; public function __construct(JWTAuth $jwtAuth) { $this->jwtAuth = $jwtAuth; } public function handle($request, Closure $next, $guard = null) { // 讀取請求頭中的 'X-Key-Type' $keyType = $request->header('X-Key-Type', 'default'); // 根據 'X-Key-Type' 決定使用哪個密鑰 $secret = config('jwt.secrets.' . $keyType, config('jwt.secret')); // 更新配置和 JWTAuth 實例中的密鑰 config(['jwt.secret' => $secret]); $this->jwtAuth->setSecret($secret); return $next($request); } }

步驟三:註冊並使用中間件

最後,把這個中間件註冊到你的路由或者是全局中間件中。這樣,每次的請求都會先經過這個中間件,確保使用正確的密鑰。

這樣一來,你就能根據不同的需要,動態切換使用哪一組 JWT 密鑰了。

踩坑紀錄

當涉及到使用 JWT 進行跨網站認證時,伺服器之間可能存在的時間差異是一個需要特別注意的問題。這種時間差異可能導致令牌驗證失敗,比如在一個伺服器上剛剛生成的令牌,在另一個時間設定不同的伺服器上可能被認為還沒到有效期或已經過期。以下是一些解決這個問題的策略:

1. 時間同步

最直接的解決方法是確保所有伺服器的系統時間都同步。這通常可以通過使用 NTP(Network Time Protocol)服務來實現,它會確保所有伺服器的時間與全球標準時間保持一致。

  • 檢查伺服器時間:
date
  • 同步伺服器時間(以 Ubuntu 為例):
sudo apt-get install ntp sudo service ntp restart

2. 增加時間容錯

在生成和驗證 JWT 時,可以人為地設定一個時間容錯範圍。例如,當設定令牌的 nbf(Not Before)exp(Expiration Time)時,可以考慮在這些時間前後加上一定的緩衝期。這樣即使伺服器之間存在小幅度的時間偏差,也不會影響令牌的有效性。

以下是具體如何在 Laravel 中應用這一策略的示例:

public function getAuthenticatedLink() { $user = auth()->user(); $currentTime = new \DateTimeImmutable(); // 自定義聲明,增加時間容錯 $customClaims = [ 'custmer' => 'custmer', // 自定義資料 'iat' => $currentTime->modify('-1 minute'), // 發證時間提前1分鐘,增加容錯 'exp' => $currentTime->modify('+1 hour 5 minutes'), // 有效期稍長,增加容錯 'nbf' => $currentTime->modify('-1 minute') // 不早於時間提前1分鐘,增加容錯 ]; $token = JWTAuth::claims($customClaims)->fromUser($user); $link = 'https://b-website.com/login?token=' . $token; return response()->json(['link' => $link]); }

在這個例子中,我們將 iatnbf 設置為當前時間前1分鐘,而將 exp 擴展到1小時5分鐘後。這樣做可以提供足夠的時間容錯,以適應可能存在的時間差異。

3. 使用中間件統一時間校驗

如果在應用層面進行時間校驗還不足以解決問題,你可以考慮實施中間件來處理時間相關的驗證邏輯。這個中間件可以在每次令牌解析前,先校正或確認時間的一致性,再進行常規的 JWT 驗證。

綜合這些策略,可以有效減少由於伺服器時間不一致導致的認證問題,從而提高系統的穩定性和用戶體驗。希望這些建議能幫助你更好地管理和解決跨服務的時間同步問題!

小結

希望這能幫助你更好地理解和實現 JWT 在實際開發中的應用。記得,保持 JWT 的安全是很重要的,包括適當的令牌時效和安全的存儲方式。祝你開發愉快!