許可權控制

李銘昕發表於2019-10-30

Hyperf交流群 裡每天都會有各種各樣的問題,今天有小夥伴問了一個問題,這裡提供一個思路給大家。

有些 PHPer 會有這樣的疑問

  1. 某些路由必須登入了才能訪問,有些路由卻不需要登入態。
  2. 某些路由在使用者登入的情況下,會在原有資料的基礎上增加一部分特殊資料。

中介軟體配合協程單例

第一個問題其實很好解決,而大多數同學也一直在這麼做,這裡再重新整理一下。

首先我們建立一箇中介軟體 UserMiddleware,並允許它處理所有的路由。然後我們從 Headers 中獲取到 X-Token,當 X-Token不存在時,我們再判斷當前的環境是否是開發環境(這裡方便自己除錯),如果 X-Token 存在,則根據 X-Token 獲取對應的資料。這裡使用 JWT 來做。

<?php

declare(strict_types=1);

namespace App\Middleware;

use App\Constants\Constants;
use App\Service\Instance\JwtInstance;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class UserMiddleware implements MiddlewareInterface
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $token = $request->getHeaderLine(Constants::X_TOKEN);

        if (! empty($token)) {
            JwtInstance::instance()->decode($token);
        } elseif (env('APP_DEBUG', false) === true) {
            JwtInstance::instance()->id = 1;
        }

        return $handler->handle($request);
    }
}

接下來我們實現一個簡單的 JwtInstance,元件使用 "firebase/php-jwt": "^5.0"

<?php

declare(strict_types=1);

namespace App\Service\Instance;

use App\Constants\ErrorCode;
use App\Exception\BusinessException;
use App\Model\User;
use App\Service\Dao\UserDao;
use Firebase\JWT\JWT;
use Hyperf\Utils\Traits\StaticInstance;

class JwtInstance
{
    use StaticInstance;

    const KEY = 'NoteBook';

    /**
     * @var int
     */
    public $id;

    /**
     * @var User
     */
    public $user;

    public function encode(User $user)
    {
        $this->id = $user->id;
        $this->user = $user;

        return JWT::encode(['id' => $user->id], self::KEY);
    }

    public function decode(string $token): self
    {
        try {
            $decoded = (array) JWT::decode($token, self::KEY, ['HS256']);
        } catch (\Throwable $exception) {
            return $this;
        }

        if ($id = $decoded['id'] ?? null) {
            $this->id = $id;
            $this->user = di()->get(UserDao::class)->first($id);
        }

        return $this;
    }

    public function build(): self
    {
        if (empty($this->id)) {
            throw new BusinessException(ErrorCode::TOKEN_INVALID);
        }

        return $this;
    }

    /**
     * @return int
     */
    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @return User
     */
    public function getUser(): ?User
    {
        if ($this->user === null && $this->id) {
            $this->user = di()->get(UserDao::class)->first($this->id);
        }
        return $this->user;
    }
}

常規的 decodeencode就不再贅述了,著重講一下 buildgetId 方法,其實很好了解,當我們有路由必須要登入時,我們就在控制器中透過 build 獲取 JwtInstance

比如我們實現一個 save 方法,每當使用者儲存資訊時,都透過以下程式碼獲取 $userId,然後再進行儲存。這樣就有效的實現了 第一個問題

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Request\NoteSearchRequest;
use App\Request\SaveNoteRequest;
use App\Service\Instance\JwtInstance;
use App\Service\NoteService;
use Hyperf\Di\Annotation\Inject;

class NoteController extends Controller
{
    /**
     * @Inject
     * @var NoteService
     */
    protected $service;

    public function save(SaveNoteRequest $request, int $id)
    {
        $text = $request->input('text');

        $userId = JwtInstance::instance()->build()->getId();

        $result = $this->service->save($id, $userId, $text);

        return $this->response->success($result);
    }
}

接下來我們檢視第二個問題,以下我們實現一個列表方法。然後透過 getId 獲取當前使用者ID,如果使用者有登陸態,則在列表中返回使用者的 user_id

<?php

declare(strict_types=1);

namespace App\Controller;

use App\Request\NoteSearchRequest;
use App\Request\SaveNoteRequest;
use App\Service\Instance\JwtInstance;
use App\Service\NoteService;
use Hyperf\Di\Annotation\Inject;

class NoteController extends Controller
{
    /**
     * @Inject
     * @var NoteService
     */
    protected $service;

    public function index(NoteSearchRequest $request)
    {
        $offset = (int) $request->input('offset');
        $limit = (int) $request->input('limit');

        $result = $this->service->search($userId, $offset, $limit);

        $userId = JwtInstance::instance()->getId();
        if ($userId) {
            $result['user_id'] = $userId;
        }

        return $this->response->success($result);
    }
}

注意事項

現在我發現很多同學都喜歡使用 JWT 來做 Token,但特殊情況下,如果 Token 被別人竊取,使用者也知道了這件事,但就算他選擇登出,也沒有任何效果。因為 Token 還是能被 decode 出有效的資訊。所以,這種情況還是推薦在服務端存一下對應的 Token,當授權判斷時,先驗證一下當前 Token 是否存在。

寫在最後

Hyperf

Hyperf 是基於 Swoole 4.4+ 實現的高效能、高靈活性的 PHP 協程框架,內建協程伺服器及大量常用的元件,效能較傳統基於 PHP-FPM 的框架有質的提升,提供超高效能的同時,也保持著極其靈活的可擴充套件性,標準元件均基於 PSR 標準 實現,基於強大的依賴注入設計,保證了絕大部分元件或類都是 可替換可複用 的。

框架元件庫除了常見的協程版的 MySQL 客戶端Redis 客戶端,還為您準備了協程版的 Eloquent ORMWebSocket 服務端及客戶端JSON RPC 服務端及客戶端GRPC 服務端及客戶端Zipkin/Jaeger (OpenTracing) 客戶端Guzzle HTTP 客戶端Elasticsearch 客戶端Consul 客戶端ETCD 客戶端AMQP 元件Apollo 配置中心阿里雲 ACM 應用配置管理ETCD 配置中心基於令牌桶演算法的限流器通用連線池熔斷器Swagger 文件生成Swoole TrackerBlade 和 Smarty 檢視引擎Snowflake 全域性ID生成器 等元件,省去了自己實現對應協程版本的麻煩。

Hyperf 還提供了 基於 PSR-11 的依賴注入容器註解AOP 面向切面程式設計基於 PSR-15 的中介軟體自定義程式基於 PSR-14 的事件管理器Redis/RabbitMQ 訊息佇列自動模型快取基於 PSR-16 的快取Crontab 秒級定時任務Translation 國際化Validation 驗證器 等非常便捷的功能,滿足豐富的技術場景和業務場景,開箱即用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

相關文章