Auth元件
- 安裝hyperf-ext/auth元件
composer require hyperf-ext/auth
- 釋出配置檔案(檔案位於 config/autoload/auth.php)
php bin/hyperf.php vendor:publish hyperf-ext/auth
- 新增助手方法檔案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依賴元件
- 安裝hyperf-ext/hashing
composer require hyperf-ext/hashing
- 釋出配置(配置檔案位於 config/autoload/hashing.php)
php bin/hyperf.php vendor:publish hyperf-ext/hashing
JWT元件
- 安裝hyperf-ext/jwt
composer require hyperf-ext/jwt
- 釋出配置檔案(檔案位於 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配置
生成jwt key
php bin/hyperf.php gen:jwt-secret
可選(Set the JWT private key and public key used to sign the tokens)
php bin/hyperf.php gen:jwt-keypair
其他配置基本可以不變(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 協議》,轉載必須註明作者和本文連結