hyperf 使用 jwt-auth3.0.x,支援多應用單點登入、多應用多點登入

php666發表於2020-04-22

我直接照搬了原來文件寫好的redeme…..
專案地址

https://github.com/phper666/jwt-auth

基於Hyperf(https://doc.hyperf.io/#/zh/README) 框架的 jwt 鑑權(json web token)元件。

採用基於https://github.com/lcobucci/jwt/tree/3.3 進行封裝。

黑名單的設定參考了這篇文章部落格:JWT 超詳細分析

注意:

1、不相容2.x,如果想要使用3.x,需要重新發布配置,以前的token可能也會失效
2、按照hyperf原有的元件規範做重寫了該包
3、支援多應用單點登入、多應用多點登入
4、修改了名稱空間名,原來為JwtAuth,現在為JWTAuth
5、修改了檔名稱,原來為Jwt,現在為JWT,原來為Blacklist,現在為BlackList
6、如何支援多應用token獨立,請檢視12、如何支援每個場景生成的 token 不能互相訪問各個應用
7、如有建議歡迎給我郵件,562405704@qq.com

說明:

jwt-auth 支援多應用單點登入、多應用多點登入、多應用支援登出 token(token會失效)、支援多應用重新整理 token

多應用單點登入:在該應用配置下只會有一個 token 生效,一旦重新整理 token ,前面生成的 token 都會失效,一般以使用者 id 來做區分

多應用多點登入:在該配置應用下token 不做限制,一旦重新整理 token ,則當前配置應用的 token 會失效

注意:使用多應用單點登入或者多應用多點登入時,必須要開啟黑名單,並且使用 Hyperf 的快取(建議使用 redis 快取)。如果不開啟黑名單,無法使 token 失效,生成的 token 會在有效時間內都可以使用(未更換證照或者 secret )。

多應用單點登入原理:JWT 有七個預設欄位供選擇。單點登入主要用到 jti 預設欄位,jti 欄位的值預設為快取到redis中的key(該key的生成為場景值+儲存的使用者id(sso_key)),這個key的值會存一個簽發時間,token檢測會根據這個時間來跟token原有的簽發時間對比,如果token原有時間小於等於redis存的時間,則認為無效

多應用多點登入原理:多點登入跟單點登入差不多,唯一不同的是jti的值不是場景值+使用者id(sso_key),而是一個唯一字串,每次呼叫 refreshToken 來重新整理 token 或者呼叫 logout 登出 token 會預設把請求頭中的 token 加入到黑名單,而不會影響到別的 token

token 不做限制原理:token 不做限制,在 token 有效的時間內都能使用,你只要把配置檔案中的 blacklist_enabled 設定為 false 即可,即為關閉黑名單功能

使用:

1、拉取依賴

使用 Hyperf 1.1.x 版本,則

composer require phper666/jwt-auth:~3.0.0
2、釋出配置
php bin/hyperf.php jwt:publish --config

或者

php bin/hyperf.php vendor:publish phper666/jwt-auth
3、jwt配置

去配置 config/autoload/jwt.php 檔案或者在配置檔案 .env 裡配置

<?php
return [
    'login_type' => env('JWT_LOGIN_TYPE', 'mpop'), //  登入方式,sso為單點登入,mpop為多點登入

    /**
     * 單點登入自定義資料中必須存在uid的鍵值,這個key你可以自行定義,只要自定義資料中存在該鍵即可
     */
    'sso_key' => 'uid',

    'secret' => env('JWT_SECRET', 'phper666'), // 非對稱加密使用字串,請使用自己加密的字串

    /**
     * JWT 許可權keys
     * 對稱演算法: HS256, HS384 & HS512 使用 `JWT_SECRET`.
     * 非對稱演算法: RS256, RS384 & RS512 / ES256, ES384 & ES512 使用下面的公鑰私鑰.
     */
    'keys' => [
        'public' => env('JWT_PUBLIC_KEY'), // 公鑰,例如:'file:///path/to/public/key'
        'private' => env('JWT_PRIVATE_KEY'), // 私鑰,例如:'file:///path/to/private/key'
    ],

    'ttl' => env('JWT_TTL', 7200), // token過期時間,單位為秒

    'alg' => env('JWT_ALG', 'HS256'), // jwt的hearder加密演算法

    /**
     * 支援的演算法
     */
    'supported_algs' => [
        'HS256' => 'Lcobucci\JWT\Signer\Hmac\Sha256',
        'HS384' => 'Lcobucci\JWT\Signer\Hmac\Sha384',
        'HS512' => 'Lcobucci\JWT\Signer\Hmac\Sha512',
        'ES256' => 'Lcobucci\JWT\Signer\Ecdsa\Sha256',
        'ES384' => 'Lcobucci\JWT\Signer\Ecdsa\Sha384',
        'ES512' => 'Lcobucci\JWT\Signer\Ecdsa\Sha512',
        'RS256' => 'Lcobucci\JWT\Signer\Rsa\Sha256',
        'RS384' => 'Lcobucci\JWT\Signer\Rsa\Sha384',
        'RS512' => 'Lcobucci\JWT\Signer\Rsa\Sha512',
    ],

    /**
     * 對稱演算法名稱
     */
    'symmetry_algs' => [
        'HS256',
        'HS384',
        'HS512'
    ],

    /**
     * 非對稱演算法名稱
     */
    'asymmetric_algs' => [
        'RS256',
        'RS384',
        'RS512',
        'ES256',
        'ES384',
        'ES512',
    ],

    /**
     * 是否開啟黑名單,單點登入和多點登入的登出、重新整理使原token失效,必須要開啟黑名單,目前黑名單快取只支援hyperf快取驅動
     */
    'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),

    /**
     * 黑名單的寬限時間 單位為:秒,注意:如果使用單點登入,該寬限時間無效
     */
    'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),

    /**
     * 黑名單快取token時間,注意:該時間一定要設定比token過期時間要大一點,預設為1天,最好設定跟過期時間一樣
     */
    'blacklist_cache_ttl' => env('JWT_TTL', 86400),

    'blacklist_prefix' => 'phper666_jwt', // 黑名單快取的字首

    /**
     * 區分不同場景的token,比如你一個專案可能會有多種型別的應用介面鑑權,下面自行定義,我只是舉例子
     * 下面的配置會自動覆蓋根配置,比如application1會里面的資料會覆蓋掉根資料
     * 下面的scene會和根資料合併
     * scene必須存在一個default
     * 什麼叫根資料,這個配置的一維陣列,除了scene都叫根配置
     */
    'scene' => [
        'default' => [],
        'application1' => [
            'secret' => 'application1', // 非對稱加密使用字串,請使用自己加密的字串
            'login_type' => 'sso', //  登入方式,sso為單點登入,mpop為多點登入
            'sso_key' => 'uid',
            'ttl' => 7200, // token過期時間,單位為秒
            'blacklist_cache_ttl' => env('JWT_TTL', 7200), // 黑名單快取token時間,注意:該時間一定要設定比token過期時間要大一點,預設為100秒,最好設定跟過期時間一樣
        ],
        'application2' => [
            'secret' => 'application2', // 非對稱加密使用字串,請使用自己加密的字串
            'login_type' => 'sso', //  登入方式,sso為單點登入,mpop為多點登入
            'sso_key' => 'uid',
            'ttl' => 7200, // token過期時間,單位為秒
            'blacklist_cache_ttl' => env('JWT_TTL', 7200), // 黑名單快取token時間,注意:該時間一定要設定比token過期時間要大一點,預設為100秒,最好設定跟過期時間一樣
        ],
        'application3' => [
            'secret' => 'application3', // 非對稱加密使用字串,請使用自己加密的字串
            'login_type' => 'mppo', //  登入方式,sso為單點登入,mpop為多點登入
            'ttl' => 7200, // token過期時間,單位為秒
            'blacklist_cache_ttl' => env('JWT_TTL', 7200), // 黑名單快取token時間,注意:該時間一定要設定比token過期時間要大一點,預設為100秒,最好設定跟過期時間一樣
        ]
    ],
    'model' => [ // TODO 支援直接獲取某模型的資料
        'class' => '',
        'pk' => 'uid'
    ]
];

更多的配置請到 config/autoload/jwt.php 檢視

4、全域性路由驗證

config/autoload/middlewaress.php 配置檔案中加入 jwt 驗證中介軟體,所有的路由都會進行 token 的驗證,例如:

<?php
return [
    'http' => [
        Phper666\JWTAuth\Middleware\JWTAuthMiddleware:class
    ],
];
5、區域性驗證

config/routes.php 檔案中,想要驗證的路由加入 jwt 驗證中介軟體即可,例如:

<?php

Router::addGroup('/v1', function () {
    Router::get('/data', 'App\Controller\IndexController@getData');
}, ['middleware' => [Phper666\JWTAuth\Middleware\JWTAuthMiddleware::class]]);
6、註解的路由驗證

請看官方文件:https://doc.hyperf.io/#/zh/middleware/midd...
在你想要驗證的地方加入 jwt 驗證中間 件即可。

7、模擬登入獲取token,具體情況下面的例子檔案
<?php

namespace App\Controller;
use \Phper666\JWTAuth\JWT;
class IndexController extends Controller
{
    # 模擬登入,獲取token
    public function login(Jwt $jwt)
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');
        if ($username && $password) {
            $userData = [
                'uid' => 1, // 如果使用單點登入,必須存在配置檔案中的sso_key的值,一般設定為使用者的id
                'username' => 'xx',
            ];
            // 使用預設場景登入
            $token = $this->jwt->setScene('default')->getToken($userData);
            $data = [
                'code' => 0,
                'msg' => 'success',
                'data' => [
                    'token' => $token,
                    'exp' => $this->jwt->getTTL(),
                ]
            ];
            return $this->response->json($data);
        }
        return $this->response->json(['code' => 0, 'msg' => '登入失敗', 'data' => []]);
    }

    # http頭部必須攜帶token才能訪問的路由
    public function getData()
    {
        return $this->response->json(['code' => 0, 'msg' => 'success', 'data' => ['a' => 1]]);
    }
}

注意:暫時不支援傳入使用者物件獲取 token,後期會支援

7、路由
<?php
# 登入
Router::post('/login', 'App\Controller\IndexController@login');

# 獲取資料
Router::addGroup('/v1', function () {
    Router::get('/data', 'App\Controller\IndexController@getData');
}, ['middleware' => [Phper666\JWTAuth\Middleware\JWTAuthMiddleware::class]]);
8、鑑權

在需要鑑權的介面,請求該介面時在 HTTP 頭部加入

Authorization  Bearer token
9、結果
請求:http://{your ip}:9501/login,下面是返回的結果
{
    "code": 0,
    "msg": "獲取token成功",
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NjQ3MzgyNTgsIm5iZiI6MTU2NDczODI1OCwiZXhwIjoxNTY0NzM4MzE4LCJ1aWQiOjEsInVzZXJuYW1lIjoieHgifQ.CJL1rOqRmrKjFpYalY6Wu7JBH6vkbysfvOf-TMQgonQ"
    }
}
請求:http://{your ip}:9501/v1/data
{
    "code": 0,
    "msg": "success",
    "data": {
        "a": 1
    }
}
10、例子檔案
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Annotation\DeleteMapping;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\PutMapping;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Phper666\JWTAuth\JWT;
use Hyperf\HttpServer\Annotation\Middleware;
use Phper666\JWTAuth\Middleware\JWTAuthMiddleware;
use Phper666\JWTAuth\Middleware\JWTAuthSceneDefaultMiddleware;
use Phper666\JWTAuth\Middleware\JWTAuthSceneApplication1Middleware;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerInterface;

/**
 * @\Hyperf\HttpServer\Annotation\Controller(prefix="api")
 * Class IndexController
 * @package App\Controller
 */
class IndexController
{
    /**
     *
     * @Inject
     * @var JWT
     */
    protected $jwt;

    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var RequestInterface
     */
    protected $request;

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

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        $this->request = $container->get(RequestInterface::class);
        $this->response = $container->get(ResponseInterface::class);
    }

    /**
     * 模擬登入
     * @PostMapping(path="login")
     * @return \Psr\Http\Message\ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function loginDefault()
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');
        if ($username && $password) {
            $userData = [
                'uid' => 1, // 如果使用單點登入,必須存在配置檔案中的sso_key的值,一般設定為使用者的id
                'username' => 'xx',
            ];
            // 使用預設場景登入
            $token = $this->jwt->setScene('default')->getToken($userData);
            $data = [
                'code' => 0,
                'msg' => 'success',
                'data' => [
                    'token' => $token,
                    'exp' => $this->jwt->getTTL(),
                ]
            ];
            return $this->response->json($data);
        }
        return $this->response->json(['code' => 0, 'msg' => '登入失敗', 'data' => []]);
    }

    /**
     * 模擬登入
     * @PostMapping(path="login1")
     * @return \Psr\Http\Message\ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function loginApplication1()
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');
        if ($username && $password) {
            $userData = [
                'uid' => 1, // 如果使用單點登入,必須存在配置檔案中的sso_key的值,一般設定為使用者的id
                'username' => 'xx',
            ];
            // 使用application1場景登入
            $token = $this->jwt->setScene('application1')->getToken($userData);
            $data = [
                'code' => 0,
                'msg' => 'success',
                'data' => [
                    'token' => $token,
                    'exp' => $this->jwt->getTTL(),
                ]
            ];
            return $this->response->json($data);
        }
        return $this->response->json(['code' => 0, 'msg' => '登入失敗', 'data' => []]);
    }

    /**
     * 模擬登入
     * @PostMapping(path="login2")
     * @return \Psr\Http\Message\ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function loginApplication2()
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');
        if ($username && $password) {
            $userData = [
                'uid' => 1, // 如果使用單點登入,必須存在配置檔案中的sso_key的值,一般設定為使用者的id
                'username' => 'xx',
            ];
            // 使用application2場景登入
            $token = $this->jwt->setScene('application2')->getToken($userData);
            $data = [
                'code' => 0,
                'msg' => 'success',
                'data' => [
                    'token' => $token,
                    'exp' => $this->jwt->getTTL(),
                ]
            ];
            return $this->response->json($data);
        }
        return $this->response->json(['code' => 0, 'msg' => '登入失敗', 'data' => []]);
    }

    /**
     * 模擬登入
     * @PostMapping(path="login3")
     * @return \Psr\Http\Message\ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function loginApplication3()
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');
        if ($username && $password) {
            $userData = [
                'uid' => 1, // 如果使用單點登入,必須存在配置檔案中的sso_key的值,一般設定為使用者的id
                'username' => 'xx',
            ];
            // 使用application3場景登入
            $token = $this->jwt->setScene('application3')->getToken($userData);
            $data = [
                'code' => 0,
                'msg' => 'success',
                'data' => [
                    'token' => $token,
                    'exp' => $this->jwt->getTTL(),
                ]
            ];
            return $this->response->json($data);
        }
        return $this->response->json(['code' => 0, 'msg' => '登入失敗', 'data' => []]);
    }

    /**
     * @PutMapping(path="refresh")
     * @Middleware(JWTAuthMiddleware::class)
     * @return \Psr\Http\Message\ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function refreshToken()
    {
        $token = $this->jwt->refreshToken();
        $data = [
            'code' => 0,
            'msg' => 'success',
            'data' => [
                'token' => (string)$token,
                'exp' => $this->jwt->getTTL(),
            ]
        ];
        return $this->response->json($data);
    }

    /**
     * @DeleteMapping(path="logout")
     * @Middleware(JWTAuthMiddleware::class)
     * @return bool
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function logout()
    {
        return $this->jwt->logout();
    }

    /**
     * 只能使用default場景值生成的token訪問
     * @GetMapping(path="list")
     * @Middleware(JWTAuthSceneDefaultMiddleware::class)
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function getDefaultData()
    {
        $data = [
            'code' => 0,
            'msg' => 'success',
            'data' => $this->jwt->getParserData()
        ];
        return $this->response->json($data);
    }

    /**
     * 只能使用application1場景值生成的token訪問
     * @GetMapping(path="list1")
     * @Middleware(JWTAuthSceneApplication1Middleware::class)
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function getApplication1Data()
    {
        $data = [
            'code' => 0,
            'msg' => 'success',
            'data' => $this->jwt->getParserData()
        ];
        return $this->response->json($data);
    }
}
11、獲取解析後的 token 資料

提供了一個方法 getParserData 來獲取解析後的 token 資料。
例如:$this->jwt->getParserData()
還提供了一個工具類,\Phper666\JWTAuth\Util\JWTUtil,裡面也有getParserData

12、如何支援每個場景生成的token不能互相訪問各個應用

具體你可以檢視Phper666\JWTAuth\Middleware\JWTAuthSceneDefaultMiddleware和Phper666\JWTAuth\Middleware\JWTAuthSceneApplication1Middleware這兩個中介軟體,根據這兩個中介軟體你可以編寫自己的中介軟體來支援每個場景生成的token不能互相訪問各個應用

13、建議

目前 jwt 丟擲的異常目前有兩種型別
Phper666\JwtAuth\Exception\TokenValidException
Phper666\JwtAuth\Exception\JWTException,TokenValidException
異常為 TokenValidException 驗證失敗的異常,會丟擲 401 ,
JWTException 異常會丟擲 400
最好你們自己在專案異常重新返回錯誤資訊

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

相關文章