[hyperf]官方元件hyperf-ext/auth和hyperf-ext/jwt完成jwt認證與自動重新整理token

Colorado發表於2020-12-13

Auth元件

  1. 安裝hyperf-ext/auth元件
    composer require hyperf-ext/auth
  2. 釋出配置檔案(檔案位於 config/autoload/auth.php)
    php bin/hyperf.php vendor:publish hyperf-ext/auth
  3. 新增助手方法檔案helper.php
    <?php
    /**
    *  get container
    */
    if (!function_exists('container')) {
     function container()
     {
         return \Hyperf\Utils\ApplicationContext::getContainer();
     }
    }
    /**
    * auth helper
    */
    if (!function_exists('auth')) {
     function auth(string $guard = null)
     {
         if (is_null($guard)) $guard = config('auth.default.guard');
         return container()->get(\HyperfExt\Auth\Contracts\AuthManagerInterface::class)->guard($guard);
     }
    }

Auth依賴元件

  1. 安裝hyperf-ext/hashing
    composer require hyperf-ext/hashing
  2. 釋出配置(配置檔案位於 config/autoload/hashing.php)
    php bin/hyperf.php vendor:publish hyperf-ext/hashing

JWT元件

  1. 安裝hyperf-ext/jwt
    composer require hyperf-ext/jwt
  2. 釋出配置檔案(檔案位於 config/autoload/jwt.php)
    php bin/hyperf.php vendor:publish hyperf-ext/jwt

建立兩個資料庫遷移檔案

php bin/hyperf.php gen:migration create_users_table

php bin/hyperf.php gen:migration create_administrators_table

兩個表內容其實是一樣的,用來模擬多守護認證。如下(根據實際需求調整)

<?php

use Hyperf\Database\Schema\Schema;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->char('username', 20)->default('')->comment('使用者暱稱');
            $table->char('password', 200)->default('')->comment('使用者密碼');
            $table->string('avatar')->default('')->comment('使用者頭像');
            $table->char('email', 50)->default('')->unique('email')->comment('使用者郵箱');
            $table->char('phone', 15)->default('')->unique('phone')->comment('使用者手機號');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
}

JWT配置

  1. 生成jwt key

    php bin/hyperf.php gen:jwt-secret
  2. 可選(Set the JWT private key and public key used to sign the tokens)

    php bin/hyperf.php gen:jwt-keypair
  3. 其他配置基本可以不變(config/autoload/jwt.php)

    [
     /*
     |--------------------------------------------
     | JWT 金鑰
     |--------------------------------------------
     |
     | 該金鑰用於簽名你的令牌,切記要在 .env 檔案中設定。元件提供了一個輔助命令來完成
     | 這步操作:
     | `php bin/hyperf.php gen:jwt-secret`
     |
     | 注意:該金鑰僅用於對稱演算法(HMAC),RSA 和 ECDSA 使用公私鑰體系(見下方)。
     |
     | 注意:該值必須使用 BASE64 編碼。
     |
     */
    
     'secret' => env('JWT_SECRET'),
    
     /*
     |--------------------------------------------
     | JWT 公私鑰
     |--------------------------------------------
     |
     | 你使用的演算法將決定你的令牌是使用隨機字串(在 `JWT_SECRET` 中定設定)還是
     | 使用以下公鑰和私鑰來簽名。元件提供了一個輔助命令來完成這步操作:
     | `php bin/hyperf.php gen:jwt-keypair`
     |
     | 對稱演算法:
     | HS256、HS384 和 HS512 使用 `JWT_SECRET`。
     |
     | 非對稱演算法:
     | RS256、RS384 和 RS512 / ES256、ES384 和 ES512 使用下面的公私鑰。
     |
     */
    
     'keys' => [
         /*
         |--------------------------------------------
         | 公鑰
         |--------------------------------------------
         |
         | 你的公鑰內容。
         |
         */
    
         'public' => env('JWT_PUBLIC_KEY'),
    
         /*
         |--------------------------------------------
         | 私鑰
         |--------------------------------------------
         |
         | 你的私鑰內容。
         |
         */
    
         'private' => env('JWT_PRIVATE_KEY'),
    
         /*
         |--------------------------------------------
         | 密碼
         |--------------------------------------------
         |
         | 你的私鑰的密碼。不需要密碼可設定為 `null`。
         |
         | 注意:該值必須使用 BASE64 編碼。
         |
         */
    
         'passphrase' => env('JWT_PASSPHRASE'),
     ],
    
     /*
     |--------------------------------------------
     | JWT 生存時間
     |--------------------------------------------
     |
     | 指定令牌有效的時長(以秒為單位)。預設為 1 小時。
     |
     | 你可以將其設定為 `null`,以產生永不過期的令牌。某些場景下有人可能想要這種行為,
     | 例如在用於手機應用的情況下。
     | 不太推薦這樣做,因此請確保你有適當的體系來在必要時可以撤消令牌。
     | 注意:如果將其設定為 `null`,則應從 `required_claims` 列表中刪除 `exp` 元素。
     |
     */
    
     'ttl' => env('JWT_TTL', 3600),
    
     /*
     |--------------------------------------------
     | 重新整理生存時間
     |--------------------------------------------
     |
     | 指定一個時長以在其有效期內可重新整理令牌(以秒為單位)。 例如,使用者可以
     | 在建立原始令牌後的 2 周內重新整理該令牌,直到他們必須重新進行身份驗證為止。
     | 預設為 2 周。
     |
     | 你可以將其設定為 `null`,以提供無限的重新整理時間。某些場景下有人可能想要這種行為,
     | 而不是永不過期的令牌,例如在用於手機應用的情況下。
     | 不太推薦這樣做,因此請確保你有適當的體系來在必要時可以撤消令牌。
     |
     */
    
     'refresh_ttl' => env('JWT_REFRESH_TTL', 3600 * 24 * 14),
    
     /*
     |--------------------------------------------
     | JWT 雜湊演算法
     |--------------------------------------------
     |
     | 用於簽名你的令牌的雜湊演算法。
     |
     | 關於演算法的詳細描述可參閱 https://tools.ietf.org/html/rfc7518。
     |
     | 可能的值:HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
     |
     */
    
     'algo' => env('JWT_ALGO', 'HS512'),
    
     /*
     |--------------------------------------------
     | 必要宣告
     |--------------------------------------------
     |
     | 指定在任一令牌中必須存在的必要宣告。如果在有效載荷中不存在這些宣告中的任意一個,
     | 則將丟擲 `TokenInvalidException` 異常。
     |
     */
    
     'required_claims' => [
         'iss',
         'iat',
         'exp',
         'nbf',
         'sub',
         'jti',
     ],
    
     /*
     |--------------------------------------------
     | 保留宣告
     |--------------------------------------------
     |
     | 指定在重新整理令牌時要保留的宣告的鍵名。
     | 除了這些宣告之外,`sub`、`iat` 和 `prv`(如果有)宣告也將自動保留。
     |
     | 注意:如果有宣告不存在,則會將其忽略。
     |
     */
    
     'persistent_claims' => [
         // 'foo',
         // 'bar',
     ],
    
     /*
     |--------------------------------------------
     | 鎖定主題宣告
     |--------------------------------------------
     |
     | 這將決定是否將一個 `prv` 宣告自動新增到令牌中。
     | 此目的是確保在你擁有多個身份驗證模型時,例如 `App\User` 和 `App\OtherPerson`,
     | 如果兩個令牌在兩個不同的模型中碰巧具有相同的 ID(`sub` 宣告),則我們應當防止
     | 一個身份驗證請求冒充另一個身份驗證請求。
     |
     | 在特定情況下,你可能需要禁用該行為,例如你只有一個身份驗證模型的情況下,
     | 這可以減少一些令牌大小。
     |
     */
    
     'lock_subject' => true,
    
     /*
     |--------------------------------------------
     | 時間容差
     |--------------------------------------------
     |
     | 該屬性為 JWT 的時間戳類宣告提供了一些時間上的容差。
     | 這意味著,如果你的某些伺服器上不可避免地存在輕微的時鐘偏差,
     | 那麼這將可以為此提供一定程度的緩衝。
     |
     | 該設定適用於 `iat`、`nbf` 和 `exp`宣告。
     | 以秒為單位設定該值,僅在你瞭解你真正需要它時才指定。
     |
     */
    
     'leeway' => env('JWT_LEEWAY', 0),
    
     /*
     |--------------------------------------------
     | 啟用黑名單
     |--------------------------------------------
     |
     | 為使令牌無效,你必須啟用黑名單。
     | 如果你不想或不需要此功能,請將其設定為 `false`。
     |
     */
    
     'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
    
     /*
     | -------------------------------------------------------------------------
     | 黑名單寬限期
     | -------------------------------------------------------------------------
     |
     | 當使用同一個 JWT 傳送多個併發請求時,由於每次請求都會重新生成令牌,
     | 因此其中一些可能會失敗。
     |
     | 設定寬限期(以秒為單位)以防止併發請求失敗。
     |
     */
    
     'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
    
     /*
     |--------------------------------------------
     | 黑名單儲存
     |--------------------------------------------
     |
     | 指定用於實現在黑名單中儲存令牌行為的類。
     |
     | 自定義儲存類需要實現 `HyperfExt\Jwt\Contracts\StorageInterface` 介面。
     |
     */
    
     'blacklist_storage' => HyperfExt\Jwt\Storage\HyperfCache::class,
    ];

    新增Auth守護guard

    <?php
    declare(strict_types=1);
    return [
     'default' => [
         'guard' => 'api',    // 預設介面api守護
         'passwords' => 'users',
     ],
     'guards' => [
         'web' => [
             'driver' => \HyperfExt\Auth\Guards\SessionGuard::class,
             'provider' => 'users',
             'options' => [],
         ],
         // 介面api守護
         'api' => [
             'driver' => \HyperfExt\Auth\Guards\JwtGuard::class,
             'provider' => 'api',
             'options' => [],
         ],
         // 管理端admin守護
         'admin' => [
             'driver' => \HyperfExt\Auth\Guards\JwtGuard::class,
             'provider' => 'admin',
             'options' => [],
         ],
     ],
     'providers' => [
         'api' => [
             'driver' => \HyperfExt\Auth\UserProviders\ModelUserProvider::class,
             'options' => [
                 'model' => \App\Model\User::class,    // 使用者模型
                 'hash_driver' => 'bcrypt',
             ],
         ],
    
         'admin' => [
             'driver' => \HyperfExt\Auth\UserProviders\ModelUserProvider::class,
             'options' => [
                 'model' => \App\Model\Admin::class,    // 管理員模型
                 'hash_driver' => 'bcrypt',
             ],
         ]
     ],
     'passwords' => [
         'users' => [
             'driver' => \HyperfExt\Auth\Passwords\DatabaseTokenRepository::class,
             'provider' => 'users',
             'options' => [
                 'connection' => null,
                 'table' => 'password_resets',
                 'expire' => 3600,
                 'throttle' => 60,
                 'hash_driver' => null,
             ],
         ],
     ],
     'password_timeout' => 10800,
     'policies' => [
         //Model::class => Policy::class,
     ],
    ];

更新模型

<?php

declare (strict_types=1);
namespace App\Model;

use Hyperf\ModelCache\Cacheable;
use HyperfExt\Auth\Authenticatable;
use HyperfExt\Auth\Contracts\AuthenticatableInterface;
use HyperfExt\Jwt\Contracts\JwtSubjectInterface;

/**
 */
class User extends Model implements AuthenticatableInterface ,JwtSubjectInterface
{
    use Authenticatable, Cacheable;
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'users';
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [];
    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [];


    public function getJwtIdentifier()
    {
        return $this->getKey();
    }

    /**
     * JWT自定義載荷
     * @return array
     */
    public function getJwtCustomClaims(): array
    {
        return [
            'guard' => 'api'    // 新增一個自定義載荷儲存守護名稱,方便後續判斷
        ];
    }
}

使用

建立AuthController

php bin/hyperf.php gen:controller AuthController

內容如下

<?php

declare(strict_types=1);

namespace App\Controller;

use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\Middlewares;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use App\Middleware\Auth\RefreshTokenMiddleware;
use HyperfExt\Jwt\Contracts\JwtFactoryInterface;

/**
 * @Controller(prefix="auth")
 * Class AuthController
 * @package App\Controller
 */
class AuthController
{
    /**
     * @Inject
     * @var ResponseInterface
     */
    private $response;

    /**
     * @RequestMapping(path="login", methods={"POST"})
     * @param RequestInterface $request
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function login(RequestInterface $request)
    {
        $credentials = $request->inputs(['email', 'password']);
        if (!$token = auth('api')->attempt($credentials)){
            return $this->response->json(['error' => 'Unauthorized'])->withStatus(401);
        }
        return $this->respondWithToken($token);
    }

    /**
     * @RequestMapping(path="user")
     * @Middlewares({@Middleware(RefreshTokenMiddleware::class)})
     */
    public function me()
    {
        return $this->response->json(auth('api')->user());
    }

    /**
     * @RequestMapping(path="refresh", methods={"GET"})
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * @RequestMapping(path="logout", methods={"DELETE"})
     */
    public function logout()
    {
        auth('api')->logout();
        return $this->response->json(['message' => 'Successfully logged out']);
    }

    /**
     * @param $token
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function respondWithToken($token)
    {
        return $this->response->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expire_in' => make(JwtFactoryInterface::class)->getPayloadFactory()->getTtl()
        ]);
    }

}

自動重新整理token中介軟體

生成中介軟體RefreshTokenMiddleware

php bin/hyperf.php gen:middleware Auth\\RefreshTokenMiddleware

內容如下

<?php

declare(strict_types=1);

namespace App\Middleware\Auth;

use App\Constants\HttpStatus;
use App\Traits\ApiResponseTrait;
use HyperfExt\Jwt\Contracts\JwtFactoryInterface;
use HyperfExt\Jwt\Exceptions\TokenBlacklistedException;
use HyperfExt\Jwt\Exceptions\TokenExpiredException;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;

class RefreshTokenMiddleware implements MiddlewareInterface
{
    use ApiResponseTrait;
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var HttpResponse
     */
    protected $response;

    /**
     * @var \HyperfExt\Jwt\Jwt
     */
    protected $jwt;

    public function __construct(
        ContainerInterface $container,
        HttpResponse $response,
        JwtFactoryInterface $jwtFactory
    )
    {
        $this->container = $container;
        $this->response = $response;
        $this->jwt = $jwtFactory->make();
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            $this->jwt->parseToken()->checkOrFail();
        } catch (\Exception $exception) {
            if ($exception instanceof TokenExpiredException) {
                try {
                    $token = $this->jwt->getToken();
                    $payload = $this->jwt->getManager()->decode($token, false, true);
                    if ($this->jwt->getManager()->getBlacklist()->has($payload)) {
                        throw new TokenBlacklistedException('The token has been blacklisted');
                    }
                    $new_token = $this->jwt->getManager()->setBlacklistEnabled(false)->refresh($token);
                    $this->jwt->getManager()->setBlacklistEnabled(true)->getBlacklist()->add($payload);

                    auth($payload->get('guard') ?? config('auth.default.guard'))->onceUsingId($payload->get('sub'));

                    return $handler->handle($request)->withHeader('Authorization', 'Bearer ' . $new_token);
                } catch (\Exception $exception) {
                    return $this->setHttpCode(HttpStatus::UNAUTHORIZED)->fail($exception->getMessage());
                }
            }
            return $this->setHttpCode(HttpStatus::UNAUTHORIZED)->fail($exception->getMessage());
        }
        return $handler->handle($request);
    }
}

hyperf新人,老手勿噴,共同研究,一起進步

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章