跳至内容

JWT 身份验证

注意

Shield 目前仅支持 JWS(已签名 JWT)。不支持 JWE(已加密 JWT)。

什么是 JWT?

JWT 或 JSON Web Token 是一种以 JSON 对象形式在各方之间安全传输信息的紧凑且自包含的方式。它通常用于 Web 应用程序中的身份验证和授权目的。

例如,当用户登录到 Web 应用程序时,服务器会生成一个 JWT 令牌并将其发送给客户端。然后,客户端在对服务器的后续请求的标头中包含此令牌。服务器验证令牌的真实性,并相应地授予对受保护资源的访问权限。

如果您不熟悉 JWT,我们建议您在继续之前查看 JSON Web 令牌简介

设置

要使用 JWT 认证,您需要额外的设置和配置。

手动设置

  1. 通过 Composer 安装 "firebase/php-jwt"。

    composer require firebase/php-jwt:^6.4
  2. AuthJWT.phpvendor/codeigniter4/shield/src/Config/ 复制到项目的 config 文件夹中,并将命名空间更新为 Config。您还需要让这些类扩展原始类。请参见以下示例。

    <?php
    
    // app/Config/AuthJWT.php
    
    declare(strict_types=1);
    
    namespace Config;
    
    use CodeIgniter\Shield\Config\AuthJWT as ShieldAuthJWT;
    
    /**
     * JWT Authenticator Configuration
     */
    class AuthJWT extends ShieldAuthJWT
    {
        // ...
    }
  3. 如果你的 app/Config/Auth.php 没有更新,你也需要更新它。检查 vendor/codeigniter4/shield/src/Config/Auth.php 并应用差异。

    你需要添加以下常量

    public const RECORD_LOGIN_ATTEMPT_NONE    = 0; // Do not record at all
    public const RECORD_LOGIN_ATTEMPT_FAILURE = 1; // Record only failures
    public const RECORD_LOGIN_ATTEMPT_ALL     = 2; // Record all login attempts

    你需要添加 JWT 认证器

    use CodeIgniter\Shield\Authentication\Authenticators\JWT;
    
    // ...
    
    public array $authenticators = [
        'tokens'  => AccessTokens::class,
        'session' => Session::class,
        'jwt'     => JWT::class,
    ];

    如果你想在认证链中使用 JWT 认证器,添加 jwt

    public array $authenticationChain = [
        'session',
        'tokens',
        'jwt'
    ];

配置

根据你的需要配置 app/Config/AuthJWT.php

设置默认声明

注意

有效负载包含实际传输的数据,例如用户 ID、角色或过期时间。有效负载中的项目称为声明

将默认有效负载项目设置为属性 $defaultClaims

例如

public array $defaultClaims = [
    'iss' => 'https://codeigniter.net.cn/',
];

默认声明将包含在 Shield 发出的所有令牌中。

设置密钥

$keys 属性中设置你的密钥,或在 .env 文件中设置它。

例如

authjwt.keys.default.0.secret = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE=

它至少需要 256 位随机字符串。密钥的长度取决于我们使用的算法。默认算法是 HS256,因此为了确保哈希值安全且不易猜测,密钥应至少与哈希函数的输出一样长 - 256 位(32 字节)。你可以使用以下命令获取安全的随机字符串

php -r 'echo base64_encode(random_bytes(32));'

注意

密钥用于签名和验证令牌。

登录尝试日志记录

默认情况下,只有登录失败尝试记录在 auth_token_logins 表中。

public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE;

如果你不希望有任何日志,请将其设置为 Auth::RECORD_LOGIN_ATTEMPT_NONE

如果你想记录所有登录尝试,请将其设置为 Auth::RECORD_LOGIN_ATTEMPT_ALL。这意味着你记录所有请求。

颁发 JWT

要使用 JWT 认证,你需要一个发出 JWT 的控制器。

这是一个示例控制器。当客户端发布有效的凭据(电子邮件/密码)时,它会返回一个新的 JWT。

// app/Config/Routes.php
$routes->post('auth/jwt', '\App\Controllers\Auth\LoginController::jwtLogin');
<?php

// app/Controllers/Auth/LoginController.php

declare(strict_types=1);

namespace App\Controllers\Auth;

use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Authentication\Authenticators\Session;
use CodeIgniter\Shield\Authentication\JWTManager;
use CodeIgniter\Shield\Validation\ValidationRules;

class LoginController extends BaseController
{
    use ResponseTrait;

    /**
     * Authenticate Existing User and Issue JWT.
     */
    public function jwtLogin(): ResponseInterface
    {
        // Get the validation rules
        $rules = $this->getValidationRules();

        // Validate credentials
        if (! $this->validateData($this->request->getJSON(true), $rules, [], config('Auth')->DBGroup)) {
            return $this->fail(
                ['errors' => $this->validator->getErrors()],
                $this->codes['unauthorized']
            );
        }

        // Get the credentials for login
        $credentials             = $this->request->getJsonVar(setting('Auth.validFields'));
        $credentials             = array_filter($credentials);
        $credentials['password'] = $this->request->getJsonVar('password');

        /** @var Session $authenticator */
        $authenticator = auth('session')->getAuthenticator();

        // Check the credentials
        $result = $authenticator->check($credentials);

        // Credentials mismatch.
        if (! $result->isOK()) {
            // @TODO Record a failed login attempt

            return $this->failUnauthorized($result->reason());
        }

        // Credentials match.
        // @TODO Record a successful login attempt

        $user = $result->extraInfo();

        /** @var JWTManager $manager */
        $manager = service('jwtmanager');

        // Generate JWT and return to client
        $jwt = $manager->generateToken($user);

        return $this->respond([
            'access_token' => $jwt,
        ]);
    }

    /**
     * Returns the rules that should be used for validation.
     *
     * @return array<string, array<string, array<string>|string>>
     * @phpstan-return array<string, array<string, string|list<string>>>
     */
    protected function getValidationRules(): array
    {
        $rules = new ValidationRules();

        return $rules->getLoginRules();
    }
}

你可以通过 curl 发送带有现有用户凭据的请求,如下所示

curl --location 'http://localhost:8080/auth/jwt' \
--header 'Content-Type: application/json' \
--data-raw '{"email" : "[email protected]" , "password" : "passw0rd!"}'

在向 API 发出所有未来请求时,客户端应在 Authorization 标头中以 Bearer 令牌的形式发送 JWT。

你可以通过 curl 发送带有 Authorization 标头的请求,如下所示

curl --location --request GET 'http://localhost:8080/api/users' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGllbGQgVGVzdCBBcHAiLCJzdWIiOiIxIiwiaWF0IjoxNjgxODA1OTMwLCJleHAiOjE2ODE4MDk1MzB9.DGpOmRPOBe45whVtEOSt53qJTw_CpH0V8oMoI_gm2XI'

保护路由

指定哪些路由受保护的第一种方法是使用 jwt 控制器过滤器。

例如,为了确保它保护 /api 路由组下的所有路由,你可以在 app/Config/Filters.php 上使用 $filters 设置。

public $filters = [
    'jwt' => ['before' => ['api', 'api/*']],
];

您还可以指定过滤器在路由文件本身中的一条或多条路由上运行

$routes->group('api', ['filter' => 'jwt'], static function ($routes) {
    // ...
});

$routes->get('users', 'UserController::list', ['filter' => 'jwt']);

当过滤器运行时,它检查 Authorization 头部以查找具有 JWT 的 Bearer 值。然后它验证令牌。如果令牌有效,它可以确定正确的用户,然后可以通过 auth()->user() 调用获得该用户。

方法引用

生成已签名的 JWT

针对特定用户的 JWT

JWT 通过 JWTManager::generateToken() 方法创建。这需要一个用户对象作为第一个参数提供给令牌。它还可以采用可选的附加声明数组、以秒为单位的生存时间、Config\AuthJWT::$keys 中的一个键组(数组键)和附加头数组

public function generateToken(
    User $user,
    array $claims = [],
    ?int $ttl = null,
    $keyset = 'default',
    ?array $headers = null
): string

以下代码为用户生成 JWT。

use CodeIgniter\Shield\Authentication\JWTManager;

/** @var JWTManager $manager */
$manager = service('jwtmanager');

$user   = auth()->user();
$claims = [
    'email' => $user->email,
];
$jwt = $manager->generateToken($user, $claims);

它将 Config\AuthJWT::$defaultClaims 设置为令牌,并在 "sub"(主题)声明中添加 'email' 声明和用户 ID。如果您未指定,它还会自动设置 "iat"(签发时间)和 "exp"(到期时间)声明。

任意 JWT

您可以使用 JWTManager::issue() 方法生成任意 JWT。

它采用 JWT 声明数组,并且可以采用以秒为单位的生存时间、Config\AuthJWT::$keys 中的一个键组(数组键)和附加头数组

public function issue(
    array $claims,
    ?int $ttl = null,
    $keyset = 'default',
    ?array $headers = null
): string

以下代码生成 JWT。

use CodeIgniter\Shield\Authentication\JWTManager;

/** @var JWTManager $manager */
$manager = service('jwtmanager');

$payload = [
    'user_id' => '1',
    'email'   => '[email protected]',
];
$jwt = $manager->issue($payload, DAY);

它使用 Config\AuthJWT::$keys['default'] 中的 secretalg

它将 Config\AuthJWT::$defaultClaims 设置为令牌,并自动设置 "iat"(签发时间)和 "exp"(到期时间)声明,即使您未传递它们。

日志记录

根据上述配置,登录尝试记录在 auth_token_logins 表中。

当记录失败的登录尝试时,发送的原始令牌值保存在 identifier 列中。

当记录成功的登录尝试时,发送的令牌的 SHA256 哈希值保存在 identifier 列中。