簡易 Token 驗證的實現


簡易 Token 驗證的實現

前言

在我們的服務器和客戶端的交互中,由於我們的業務中使用 RESTful API 的形式和客戶端交互,而 API 又是無狀態的,無法幫助我們識別這一次和上一次的請求由誰發出、是否合法,因此我們需要想一個辦法來確認用戶身份,檢查是否請求合法,經調研,較為流行的解決方式是使用 Token 進行驗證。

我將介紹如何設計實現一個簡單的 Token 驗證邏輯,本文的說法僅是基於我自己的一點想法和參考來的知識,如有謬誤麻煩不吝指出。

參考資料

基於 Token 的 WEB 后台認證機制:https://www.cnblogs.com/xiekeli/p/5607107.html

Token 設計

有了密碼認證為什么需要 Token?

Token 就像一把鑰匙,當用戶登錄之后,服務器就把這把鑰匙隨着返回的 json 包發送給用戶,用戶在接下來的請求中,涉及到需要驗證身份和權限的,就按照和服務器的約定,把這把鑰匙放在請求包合適的地方,隨着請求包一起發送,服務器檢查 Token 是否合法有效以確認身份,然后決定要執行請求的操作還是拒絕服務並返回錯誤。這樣就用可過期的 Token 替代了每次需要驗證身份時都需要發送的重要的、不變的密碼。

使用 Token 還有一些其他的優點,例如參考資料中提到的 Token 機制相對於 Cookie 機制有支持跨域訪問、無狀態 (也稱:服務端可擴展行)、更適用 CDN、去耦、更適用於移動應用、CSRF、性能、不需要為登錄頁面做特殊處理、基於標准化的優點。

Token 里有什么?

Token 的目的是用於表明身份,所以它應該包含一些獨特的、只屬於此用戶的、不容易偽造的信息。例如,創建此 Token 的 Unix 時間戳、用戶的唯一 id、唯一賬號、經過特殊算法生成的用戶識別碼等等。但也不能包含一些敏感信息,比如用戶的密碼。如果用戶的明文密碼存在於 Token 中,那么有心人劫取並解析 Token 后,就可以直接登錄了。

安全性

由上所述,安全性是我們需要考慮的很重要的一部分,我們面臨的安全風險主要有跨站腳本攻擊(XSS(Cross Site Scripting)Attacks)、請求篡改、重放攻擊(Replay Attacks)、中間人攻擊(MITM(Man-In-The-Middle)Attacks)。

由於我們使用 RESTful API 的形式,應該不需要考慮 XSS 攻擊的事情,只要注意客戶端傳上來的 json 包內容合法安全即可。如果你有網頁需要顯示,在 PHP 中使用 htmlspecialchars 函數來避免 XSS 攻擊。

請求篡改和中間人攻擊的問題,我們可以通過利用 SSL/TLS 來加密數據包,也就是使用 HTTPS。

參考資料中介紹的重放攻擊概念如下:

所謂重放攻擊就是攻擊者發送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程。比如在瀏覽器端通過用戶名 / 密碼驗證獲得簽名的 Token 被木馬竊取。即使用戶登出了系統,黑客還是可以利用竊取的 Token 模擬正常請求,而服務器端對此完全不知道,因為 JWT 機制是無狀態的。

解決方式有:

  • 時間戳 + 共享秘鑰
  • 時間戳 + 共享秘鑰 + 黑名單

具體可以查看參考資料。

安全只能是相對而言,我們既然是實現簡易的 Token 驗證,那我認為達到防君子不防小人的效果應該算可以接受了,我們應該根據自己的需要來增強自身安全性,盲目追求安全是不可取的。

比較簡單的增強安全性的方式是,給 Token 定義一個過期時間,若 Token 過期將被廢棄,若沒有過期時間,我認為 Token 就是另一種形式的密碼而已。服務器可以參考包含在 Token 中的的過期時間決定是否返回「Token 過期」的錯誤消息,但要注意這時 Token 不能是明文的,且加密 / 混淆算法需要不可 / 難以破解,否則過期時間可能被偽造。因此我建議生成 Token 過期時間后,將它存於數據庫中,驗證時不參考 Token 中包含的過期時間(如果有的話)。

Token 明文當然也可,只要能夠保證 Token 內容有識別意義且難以被偽造,但我們一般將 Token 信息(一般是一個數組)進行 base64 編碼(我們很容易進行解碼),以便於傳輸。

Token 過期后,我們可以要求用戶重新登錄來刷新 Token,或者提供接口讓客戶端自動刷新 Token。自動刷新 Token 需要一個 Refresh Token(刷新 Token),它一般和 Token 生成方式類似,但有效期更長(也可以永久有效)且只能用於刷新 Token,不能用於業務驗證。

本文中,Token 和 Refresh Token 將是一個包含了 Token 過期時間、由 PHP 函數 uniqid 生成的 uniqid、用戶唯一賬號信息,並 base64 編碼后的字符串,Token 過期時間為 7 天,Refresh Token 過期時間為 14 天,這些時間理論上越短越安全。

注意,以上的簡易設計無法解決「重放攻擊」,防范方式參考上文。

接口預設

注冊接口

客戶端將需要注冊的賬號密碼隨請求包發往服務器,若注冊成功,服務器將為此用戶初始化一組 Token、Refresh Token(刷新 Token)、Expire Time(Token 過期時間),並存於數據庫。

登錄接口

客戶端將需要登錄的賬號密碼隨請求包發往服務器,若登錄成功,服務器將返回用戶此時的 Token、Refresh Token(刷新 Token)、Expire Time(Token 過期時間)。

更新用戶 Token 接口

根據請求包中的用戶刷新 Token 檢查是否匹配和過期,若匹配成功且不過期,刷新用戶的 Token、Refresh Token(刷新 Token)、Expire Time(Token 過期時間)並返回。

其他邏輯

  • 服務器應在每次驗證 Token 時檢查 Token 是否過期
  • 若刷新 Token 有過期時間,在驗證刷新 Token 時也要檢查,若過期應當要求用戶重新登錄且刷新用戶的 Token、Refresh Token(刷新 Token)、Expire Time(Token 過期時間)

具體代碼

在 PHP Laravel 環境下。

/**
 * 生成用戶 Token、刷新 Token、Token 過期時間
 *
 * @param  User  $user
 * @return array $tokenInfo
 */
public function refreshToken(User $user)
{
  // config('app.token_expires_seconds') 是我們自己定義的 Token 過期時間
  $tokenExpireTime = date('Y-m-d H:i:s', time() + config('app.token_expires_seconds'));
  $accessTokenInfo = [
    'uniqid' => uniqid('', true),
    'account' => $user->account,
    'tokenExpireTime' => $tokenExpireTime
  ];
  $refreshTokenInfo = [
    'uniqid' => uniqid('', true),
    'account' => $user->account,
    'tokenExpireTime' => $tokenExpireTime
  ];
  $accessToken = base64_encode(implode(',', $accessTokenInfo));
  $refreshToken = base64_encode(implode(',', $refreshTokenInfo));

  $user->access_token = $accessToken;
  $user->access_refresh_token = $refreshToken;
  $user->access_token_expires_in = $tokenExpireTime;
  $user->save();

  $tokenInfo = [
    'access_token' => $accessToken,
    'refresh_token' => $refreshToken,
    'expire_time' => $tokenExpireTime
  ];
  return $tokenInfo;
}

/**
 * 注冊賬號
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function create(Request $request)
{
  $account = $request->input('account');
  $password = $request->input('password');

  $user = new User;
  $user->account = $account;
  $user->password = Hash::make($password);
  $user->save();

  $this->refreshToken($user);

  return response()->json([
    'error_code' => 200,
    'data' => [
      'user_id' => $user->id,
      'account' => $account
    ]
  ]);
}

/**
 * 登錄賬號
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function login(Request $request)
{
  $account = $request->input('account');
  $password = $request->input('password');

  $user = User::where('account', $account)->first();
  if (!$user) {
    return response()->json([
      'error_code' => 403,
      'error_message' => 'User not exist.'
    ]);
  }

  if (!Hash::check($password, $user->password)) {
    return response()->json([
      'error_code' => 401,
      'error_message' => 'Wrong password.'
    ]);
  }

  return response()->json([
    'error_code' => 200,
    'data' => [
      'user_id' => $user->id,
      'account' => $account,
      'access_token' => $user->access_token,
      'refresh_token' => $user->access_refresh_token,
      'expire_time' => $user->access_token_expires_in
    ]
  ]);
}

/**
 * 更新用戶 Token
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function updateAccessToken(Request $request, User $user)
{
  $refreshToken = $request->header('Authorization');
  // Refresh token 驗證
  if ($refreshToken != $user->access_refresh_token) {
    return response()->json([
      'error_code' => 401,
      'error_message' => 'Wrong access refresh token.'
    ]);
  }

  // 檢查 Refresh token 過期(14 天過期)
  if (strtotime($user->access_token_expires_in)
    + config('app.token_expires_seconds') < time()) {
    $this->refreshToken($user);
    return response()->json([
      'error_code' => 403,
      'error_message' => 'Refresh token expired.'
    ]);
  }

  $tokenInfo = $this->refreshToken($user);

  return response()->json([
    'error_code' => 200,
    'data' => [
      'user_id' => $user->id,
      'access_token' => $tokenInfo['access_token'],
      'refresh_token' => $tokenInfo['refresh_token'],
      'expire_time' => $tokenInfo['expire_time']
    ]
  ]);
}

/**
 * 檢查 Token
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function login(Request $request)
{
  $token = $request->header('Authorization');

  // Token 驗證
  if ($token != $user->access_token) {
    return response()->json([
      'error_code' => 401,
      'error_message' => 'Wrong access token.'
    ]);
  }

  // 檢查 Access token 過期(7 天過期)
  if (strtotime($user->access_token_expires_in) < time()) {
    return response()->json([
      'error_code' => 403,
      'error_message' => 'Access token expired.'
    ]);
  }
}

改進方式

  • 使用各語言 JWT 庫進行 Token 驗證
  • 使用 HTTPS
  • 更好的加密解密算法

本文發布於 ladit.me/simple_token_design


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM