从0开始构建一个Oauth2Server服务 <19> Token 编解码

2023-10-16 09:29:33 浏览数 (1)

Token 编解码

令牌提供了一种通过在令牌字符串本身中编码所有必要信息来避免将令牌存储在数据库中的方法。这样做的主要好处是 API 服务器能够验证访问令牌,而无需对每个 API 请求进行数据库查找,从而使 API 更容易扩展。

OAuth 2.0 Bearer Tokens 的好处是应用程序不需要知道您决定如何在您的服务中实现访问令牌。这意味着以后可以在不影响客户端的情况下更改您的实现。

如果您已经拥有一个可水平扩展的分布式数据库系统,那么您可能无法通过使用自编码令牌获得任何好处。事实上,如果您已经解决了分布式数据库问题,则使用自编码令牌只会引入新问题,因为使自编码令牌无效成为一个额外的障碍。

有很多方法可以对令牌进行自编码。您选择的实际方法只对您的实施很重要,因为令牌信息不会暴露给外部开发人员。

实现自编码令牌的最常见方法是使用 JWS 规范,创建要包含在令牌中的所有数据的 JSON 序列化表示,并使用只有授权服务器知道的私钥对生成的字符串进行签名.

JWT 访问令牌编码

下面的代码是用 PHP 编写的,并使用Firebase PHP-JWT库来编码和验证令牌。您需要包含该库才能运行示例代码实际上,授权服务器将有一个用于签署令牌的私钥,资源服务器将从授权服务器元数据中获取公钥以用于验证令牌。在这个例子中,我们每次都生成一个新的私钥,并在同一个脚本中验证令牌。实际上,您需要将私钥存储在某处以使用相同的密钥一致地签署令牌。

代码语言:javascript复制
<?php
use FirebaseJWTJWT;
 
# Generate a private key to sign the token.
# The public key would need to be published at the authorization
# server if a separate resource server needs to validate the JWT
 
$private_key = openssl_pkey_new([
  'digest_alg' => 'sha256',
  'private_key_bits' => 1024,
  'private_key_type' => OPENSSL_KEYTYPE_RSA
]);
 
# Set the user ID of the user this token is for
$user_id = "1000";
 
# Set the client ID of the app that is generating this token
$client_id = 'https://example-app.com';
 
# Provide the list of scopes this token is valid for
$scope = 'read write';
 
$token_data = array(
 
  # Issuer (the authorization server identifier)
  'iss' => 'https://' . $_SERVER['PHP_SELF'],
 
  # Expires At
  'exp' => time() 7200, // Valid for 2 hours
 
  # Audience (The identifier of the resource server)
  'aud' => 'api://default',
 
  # Subject (The user ID)
  'sub' => $user_id,
 
  # Client ID
  'client_id' => $client_id,
 
  # Issued At
  'iat' => time(),
 
  # Identifier of this token
  'jti' => microtime(true).'.'.bin2hex(random_bytes(10)),
 
  # The list of OAuth scopes this token includes
  'scope' => $scope
);
$token_string = JWT::encode($token_data, $private_key, 'RS256');

这将产生一个字符串,例如:

代码语言:javascript复制
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodH
RwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmNvbS8iLCJleHAiO
jE2MzczNDQ1NzIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJzdWIi
OiIxMDAwIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly9leGFtcGxlLWF
wcC5jb20iLCJpYXQiOjE2MzczMzczNzIsImp0aSI6IjE2MzczMz
czNzIuMjA1MS42MjBmNWEzZGMwZWJhYTA5NzMxMiIsInNjb3BlI
joicmVhZCB3cml0ZSJ9.SKDO_Gu96WeHkR_Tv0d8gFQN1SEdpN8
S_h0IJQyl_5syvpIRA5wno0VDFi34k5jbnaY5WHn6Y912IOmg6t
MO91KlYOU1MNdVhHUoPoNUzYtl_nNab7Ywe29kxgrekm-67ZInD
I8RHbSkL7Z_N9eZz_J8c3EolcsoIf-Dd5n9y_Y

该令牌由三个部分组成,以句点分隔。第一部分描述了使用的签名方法。第二部分包含令牌数据。第三部分是签名。

例如,此令牌的第一个组件是此 JSON 对象:

代码语言:javascript复制
{
   "typ":"JWT",
   "alg":"RS256"
 }

第二个组件包含 API 端点处理请求所需的实际数据,例如用户标识和范围访问。

代码语言:javascript复制
{
  "iss": "https://authorization-server.com/",
  "exp": 1637344572,
  "aud": "api://default",
  "sub": "1000",
  "client_id": "https://example-app.com",
  "iat": 1637337372,
  "jti": "1637337372.2051.620f5a3dc0ebaa097312",
  "scope": "read write"
}

然后对这两个部分进行 base64 编码,JWT 库计算这两个字符串的 RS256 签名,然后用句点连接所有三个部分。

解码

可以使用相同的 JWT 库验证访问令牌。该库将同时对签名进行解码和验证,如果签名无效或令牌的到期日期已过,则抛出异常。

您需要与签署令牌的私钥相对应的公钥。通常,您可以从授权服务器的元数据文档中获取它,但在本例中,我们将从之前生成的私钥中派生出公钥。

注意:任何人都可以通过对令牌字符串的中间部分进行base64解码来读取令牌信息。因此,不要在令牌中存储私人信息或您不希望用户或开发人员看到的信息,这一点很重要。如果想隐藏token信息,可以使用JSON Web Encryption spec对token中的数据进行加密。

代码语言:javascript复制
<?php
$public_key = openssl_pkey_get_details($private_key)['key'];
 
try {
  # Note: You must provide the list of supported algorithms in order to prevent 
  # an attacker from bypassing the signature verification. See:
  # https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
  $token = JWT::decode($token_string, $jwt_key, ['RS256']);
  $error = false;
} catch(FirebaseJWTExpiredException $e) {
  $token = false;
  $error = 'expired';
  $error_description = 'The token has expired';
} catch(FirebaseJWTSignatureInvalidException $e) {
  $token = false;
  $error = 'invalid';
  $error_description = 'The token provided was malformed';
} catch(Exception $e) {
  $token = false;
  $error = 'unauthorized';
  $error_description = $e->getMessage();
}
 
if($error) {
  header('HTTP/1.1 401 Unauthorized');
  echo json_encode(array(
    'error'=>$error, 
    'error_description'=>$error_description
  ));
  die();
} else {
  // Now $token has all the data that we encoded in it originally
  print_r($token);
}
Invalidating

因为令牌可以在不进行数据库查找的情况下进行验证,所以在令牌过期之前无法使其失效。您需要采取额外的步骤来使自编码的令牌无效,例如临时存储已撤销令牌的列表,这是令jti牌中声明的一种用途。有关详细信息,请参阅刷新访问令牌。

0 人点赞