diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0bf0524..0000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: php - -sudo: required - -matrix: - include: - - php: 7.2 - env: SW_VERSION="4.3.5" - - php: 7.2 - env: SW_VERSION="4.4.0-beta" - - php: 7.3 - env: SW_VERSION="4.3.5" - - php: 7.3 - env: SW_VERSION="4.4.0-beta" - - php: master - env: SW_VERSION="4.3.5" - - php: master - env: SW_VERSION="4.4.0-beta" - - allow_failures: - - php: master - -services: - - mysql - - redis - - docker - -before_install: - - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)" - - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)" - - echo $PHP_MAJOR - - echo $PHP_MINOR - -install: - - cd $TRAVIS_BUILD_DIR - - bash ./tests/swoole.install.sh - - phpenv config-rm xdebug.ini || echo "xdebug not available" - - phpenv config-add ./tests/ci.ini - -before_script: - - cd $TRAVIS_BUILD_DIR - - composer config -g process-timeout 900 && composer update - -script: - - composer analyze - - composer test \ No newline at end of file diff --git a/README.md b/README.md index df6d64c..da35e5b 100644 --- a/README.md +++ b/README.md @@ -2,49 +2,156 @@ ### 采用基于https://github.com/lcobucci/jwt/tree/3.3 进行封装。 ### 黑名单的设置参考了这篇文章https://learnku.com/articles/17883 ### 注意: -由于 `Hyperf` 可以升级 `1.1` 版本,如果你用 `1.1` 版本,请需要修改 `jwt-auth` 的 `composer.json` 文件,把依赖 `Hyperf` 的组件版本全部改为 `~1.1.0` 或者使用 `jwt-auth` 的 `^2.0.1` 版本,这个版本是针对 `Hyperf` 的 `1.1` 版本的 +1、不兼容2.x,如果想要使用3.x,需要重新发布配置,以前的token可能也会失效 +2、按照hyperf原有的组件规范做重写了该包 +3、支持多应用单点登录、多应用多点登录 +4、修改了命名空间名,原来为`JwtAuth`,现在为`JWTAuth` +5、如有建议欢迎给我邮件,562405704@qq.com ### 说明: -> `jwt-auth` 支持单点登录、多点登录、支持注销 token(token会失效)、支持刷新 token +> `jwt-auth` 支持多应用单点登录、多应用多点登录、多应用支持注销 token(token会失效)、支持多应用刷新 token -> 单点登录:只会有一个 token 生效,一旦刷新 token ,前面生成的 token 都会失效,一般以用户 id 来做区分 +> 多应用单点登录:在该应用配置下只会有一个 token 生效,一旦刷新 token ,前面生成的 token 都会失效,一般以用户 id 来做区分 -> 多点登录:token 不做限制,一旦刷新 token ,则当前 token 会失效 +> 多应用多点登录:在该配置应用下token 不做限制,一旦刷新 token ,则当前配置应用的 token 会失效 -> 注意:使用单点登录或者多点登录时,必须要开启黑名单,并且使用 `Hyperf` 的缓存(建议使用 `redis` 缓存)。如果不开启黑名单,无法使 token 失效,生成的 token 会在有效时间内都可以使用(未更换证书或者 secret )。 +> 注意:使用多应用单点登录或者多应用多点登录时,必须要开启黑名单,并且使用 `Hyperf` 的缓存(建议使用 `redis` 缓存)。如果不开启黑名单,无法使 token 失效,生成的 token 会在有效时间内都可以使用(未更换证书或者 secret )。 -> 单点登录原理:`JWT` 有七个默认字段供选择。单点登录主要用到 jti 默认字段,`jti` 字段的值默认为用户 id。当生成 token 时,`getToken` 方法有一个 `$isInsertSsoBlack` 参数来控制是否会把前面生成的 token 都失效,默认是失效的,如果想不失效,设置为 `false` 即可。但是如果是调用 `refreshToken` 来刷新 token 或者调用 `logout` 注销token,默认前面生成的 token 都会失效。 -jwt 的生成的 token 加入黑名单时,会把用户 id 作为缓存的键,当前时间作为值,配置文件中的 `blacklist_cache_ttl` 作为缓存的失效时间。每次生成 token 或者刷新 token 时,会先从 token 中拿到签发时间和 `jti` 值找到对应的缓存拿到时间,拿到时间后跟 token 的签发时间对比,如果签发时间小于等于拿到的时间值,则 token 判断为失效的。(`jti` 在单点登录中,存的值是用户 id) +> 多应用单点登录原理:`JWT` 有七个默认字段供选择。单点登录主要用到 jti 默认字段,`jti` 字段的值默认为缓存到redis中的key(该key的生成为场景值+存储的用户id(`sso_key`)),这个key的值会存一个签发时间,token检测会根据这个时间来跟token原有的签发时间对比,如果token原有时间小于等于redis存的时间,则认为无效 -> 多点登录原理:多点登录跟单点登录差不多,唯一不同的是jti的值不是用户 id,而是一个唯一字符串,每次调用 `refreshToken` 来刷新 `token` 或者调用 `logout` 注销 token 会默认把请求头中的 token 加入到黑名单,而不会影响到别的 token +> 多应用多点登录原理:多点登录跟单点登录差不多,唯一不同的是jti的值不是场景值+用户id(`sso_key`),而是一个唯一字符串,每次调用 `refreshToken` 来刷新 `token` 或者调用 `logout` 注销 token 会默认把请求头中的 token 加入到黑名单,而不会影响到别的 token > token 不做限制原理:token 不做限制,在 token 有效的时间内都能使用,你只要把配置文件中的 `blacklist_enabled` 设置为 `false` 即可,即为关闭黑名单功能 ### 使用: ##### 1、拉取依赖 -> 如果你使用 `Hyperf 1.0.x` 版本,则 +> 使用 `Hyperf 1.1.x` 版本,则 ```shell -composer require phper666/jwt-auth:~1.0.1 +composer require phper666/jwt-auth:~3.0.0 ``` -> 如果你使用 `Hyperf 1.1.x` 版本,则 -```shell -composer require phper666/jwt-auth:~2.0.1 -``` - ##### 2、发布配置 ```shell php bin/hyperf.php jwt:publish --config ``` - +或者 +```shell +php bin/hyperf.php vendor:publish phper666/jwt-auth +``` ##### 3、jwt配置 去配置 `config/autoload/jwt.php` 文件或者在配置文件 `.env` 里配置 -```shell -# 务必改为你自己的字符串 -JWT_SECRET=hyperf -#token过期时间,单位为秒 -JWT_TTL=60 +```php + 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' => [ + 'login_type' => 'sso', // 登录方式,sso为单点登录,mpop为多点登录 + 'sso_key' => 'uid', + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ], + 'application2' => [ + 'login_type' => 'sso', // 登录方式,sso为单点登录,mpop为多点登录 + 'sso_key' => 'uid', + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ], + 'application3' => [ + 'login_type' => 'mppo', // 登录方式,sso为单点登录,mpop为多点登录 + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ] + ], + 'model' => [ // TODO 支持直接获取某模型的数据 + 'class' => '', + 'pk' => 'uid' + ] +]; ``` 更多的配置请到 `config/autoload/jwt.php` 查看 ##### 4、全局路由验证 @@ -53,7 +160,7 @@ JWT_TTL=60 [ - Phper666\JwtAuth\Middleware\JwtAuthMiddleware:class + Phper666\JWTAuth\Middleware\JWTAuthMiddleware:class ], ]; ``` @@ -64,7 +171,7 @@ return [ Router::addGroup('/v1', function () { Router::get('/data', 'App\Controller\IndexController@getData'); -}, ['middleware' => [Phper666\JwtAuth\Middleware\JwtAuthMiddleware::class]]); +}, ['middleware' => [Phper666\JWTAuth\Middleware\JWTAuthMiddleware::class]]); ``` ##### 6、注解的路由验证 请看官方文档:https://doc.hyperf.io/#/zh/middleware/middleware @@ -141,32 +248,65 @@ Authorization Bearer token } ``` ##### 10、例子文件 -```shell +```php 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'); @@ -175,12 +315,13 @@ class IndexController extends AbstractController 'uid' => 1, // 如果使用单点登录,必须存在配置文件中的sso_key的值,一般设置为用户的id 'username' => 'xx', ]; - $token = $this->jwt->getToken($userData); + // 使用默认场景登录 + $token = $this->jwt->setScene('default')->getToken($userData); $data = [ 'code' => 0, 'msg' => 'success', 'data' => [ - 'token' => (string)$token, + 'token' => $token, 'exp' => $this->jwt->getTTL(), ] ]; @@ -189,7 +330,102 @@ class IndexController extends AbstractController return $this->response->json(['code' => 0, 'msg' => '登录失败', 'data' => []]); } - # 刷新token,http头部必须携带token才能访问的路由 + /** + * 模拟登录 + * @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', + ]; + // 使用默认场景登录 + $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', + ]; + // 使用默认场景登录 + $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', + ]; + // 使用默认场景登录 + $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(); @@ -204,42 +440,43 @@ class IndexController extends AbstractController return $this->response->json($data); } - # 注销token,http头部必须携带token才能访问的路由 + /** + * @DeleteMapping(path="logout") + * @Middleware(JWTAuthMiddleware::class) + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ public function logout() { - $this->jwt->logout(); - return true; + return $this->jwt->logout(); } - # http头部必须携带token才能访问的路由 + /** + * http头部必须携带token才能访问的路由 + * @GetMapping(path="list") + * @Middleware(JWTAuthMiddleware::class) + * @return \Psr\Http\Message\ResponseInterface + */ public function getData() { $data = [ 'code' => 0, 'msg' => 'success', - 'data' => [ - 'cache_time' => $this->jwt->getTokenDynamicCacheTime() // 获取token的有效时间,动态的 - ] + 'data' => $this->jwt->getParserData() ]; return $this->response->json($data); } - - public function index() - { - $user = $this->request->input('user', 'Hyperf'); - $method = $this->request->getMethod(); - - return [ - 'method' => $method, - 'message' => "Hello {$user}.", - ]; - } } - ``` ##### 11、获取解析后的 token 数据 提供了一个方法 `getParserData` 来获取解析后的 token 数据。 例如:`$this->jwt->getParserData()` +还提供了一个工具类,\Phper666\JWTAuth\Util\JWTUtil,里面也有getParserData ##### 12、建议 -> 目前 `jwt` 抛出的异常目前有两种类型 `Phper666\JwtAuth\Exception\TokenValidException` 和 `Phper666\JwtAuth\Exception\JWTException,TokenValidException` 异常为 token 验证失败的异常,会抛出 `401` ,`JWTException` 异常会抛出 `400`,最好你们自己在项目异常重新返回错误信息 +> 目前 `jwt` 抛出的异常目前有两种类型 +>`Phper666\JwtAuth\Exception\TokenValidException`、 +>`Phper666\JwtAuth\Exception\JWTException,TokenValidException` +>异常为 `TokenValidException` 验证失败的异常,会抛出 `401` , +>`JWTException` 异常会抛出 `400`, +>最好你们自己在项目异常重新返回错误信息 diff --git a/composer.json b/composer.json index bb8d5fd..c66ce76 100644 --- a/composer.json +++ b/composer.json @@ -17,41 +17,28 @@ "description": "jwt-auth Component", "autoload": { "psr-4": { - "Phper666\\JwtAuth\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "HyperfTest\\": "tests" + "Phper666\\JWTAuth\\": "src/" } }, + "autoload-dev": {}, "require": { "php": ">=7.2", "ext-swoole": ">=4.4", - "hyperf/utils": "~1.1.0", - "hyperf/framework": "~1.1.0", - "hyperf/di": "~1.1.0", - "hyperf/cache": "~1.1.0", "lcobucci/jwt": "^3.2", - "nesbot/carbon": "^1.0 || ^2.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.14", - "hyperf/testing": "~1.1.0", - "phpstan/phpstan": "^0.10.5", - "swoft/swoole-ide-helper": "dev-master" + "nesbot/carbon": "^1.0 || ^2.0", + "hyperf/cache": "~1.1.0", + "hyperf/command": "~1.1.0", + "hyperf/config": "~1.1.0", + "hyperf/di": "~1.1.0" }, + "require-dev": {}, "config": { "sort-packages": true }, - "scripts": { - "test": "co-phpunit -c phpunit.xml --colors=always", - "analyze": "phpstan analyse --memory-limit 300M -l 0 ./src", - "cs-fix": "php-cs-fixer fix $1" - }, + "scripts": {}, "extra": { "hyperf": { - "config": "Phper666\\JwtAuth\\ConfigProvider" + "config": "Phper666\\JWTAuth\\ConfigProvider" } } } diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index d2c615a..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - ./tests/ - - \ No newline at end of file diff --git a/publish/jwt.php b/publish/jwt.php index 3249e0a..71a4722 100644 --- a/publish/jwt.php +++ b/publish/jwt.php @@ -1,42 +1,112 @@ env('JWT_LOGIN_TYPE', 'mpop'), + 'login_type' => env('JWT_LOGIN_TYPE', 'mpop'), // 登录方式,sso为单点登录,mpop为多点登录 - # 单点登录自定义数据中必须存在uid的键值,这个key你可以自行定义,只要自定义数据中存在该键即可 + /** + * 单点登录自定义数据中必须存在uid的键值,这个key你可以自行定义,只要自定义数据中存在该键即可 + */ 'sso_key' => 'uid', - # 非对称加密使用字符串,请使用自己加密的字符串 - 'secret' => env('JWT_SECRET', 'phper666'), + 'secret' => env('JWT_SECRET', 'phper666'), // 非对称加密使用字符串,请使用自己加密的字符串 - /* + /** * JWT 权限keys * 对称算法: HS256, HS384 & HS512 使用 `JWT_SECRET`. * 非对称算法: RS256, RS384 & RS512 / ES256, ES384 & ES512 使用下面的公钥私钥. */ 'keys' => [ - # 公钥,例如:'file://path/to/public/key' - 'public' => env('JWT_PUBLIC_KEY'), + '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加密算法 - # 私钥,例如:'file://path/to/private/key' - 'private' => env('JWT_PRIVATE_KEY'), + /** + * 支持的算法 + */ + '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', ], - # token过期时间,单位为秒 - 'ttl' => env('JWT_TTL', 7200), + /** + * 对称算法名称 + */ + 'symmetry_algs' => [ + 'HS256', + 'HS384', + 'HS512' + ], - # jwt的hearder加密算法 - 'alg' => env('JWT_ALG', 'HS256'), + /** + * 非对称算法名称 + */ + 'asymmetric_algs' => [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + ], - # 是否开启黑名单,单点登录和多点登录的注销、刷新使原token失效,必须要开启黑名单,目前黑名单缓存只支持hyperf缓存驱动 + /** + * 是否开启黑名单,单点登录和多点登录的注销、刷新使原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_BLACKLIST_CACHE_TTL', 86400), + /** + * 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为1天,最好设置跟过期时间一样 + */ + 'blacklist_cache_ttl' => env('JWT_TTL', 86400), + + 'blacklist_prefix' => 'phper666_jwt', // 黑名单缓存的前缀 + + /** + * 区分不同场景的token,比如你一个项目可能会有多种类型的应用接口鉴权,下面自行定义,我只是举例子 + * 下面的配置会自动覆盖根配置,比如application1会里面的数据会覆盖掉根数据 + * 下面的scene会和根数据合并 + * scene必须存在一个default + * 什么叫根数据,这个配置的一维数组,除了scene都叫根配置 + */ + 'scene' => [ + 'default' => [], + 'application1' => [ + 'login_type' => 'sso', // 登录方式,sso为单点登录,mpop为多点登录 + 'sso_key' => 'uid', + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ], + 'application2' => [ + 'login_type' => 'sso', // 登录方式,sso为单点登录,mpop为多点登录 + 'sso_key' => 'uid', + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ], + 'application3' => [ + 'login_type' => 'mppo', // 登录方式,sso为单点登录,mpop为多点登录 + 'ttl' => 100, // token过期时间,单位为秒 + 'blacklist_cache_ttl' => env('JWT_TTL', 100), // 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大一点,默认为100秒,最好设置跟过期时间一样 + ] + ], + 'model' => [ // TODO 支持直接获取某模型的数据 + 'class' => '', + 'pk' => 'uid' + ] ]; diff --git a/src/AbstractJWT.php b/src/AbstractJWT.php new file mode 100644 index 0000000..14768a2 --- /dev/null +++ b/src/AbstractJWT.php @@ -0,0 +1,170 @@ + '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', + ]; + + // 对称算法名称 + private $symmetryAlgs = [ + 'HS256', + 'HS384', + 'HS512' + ]; + + // 非对称算法名称 + private $asymmetricAlgs = [ + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + ]; + + /** + * 当前token生成token的场景值 + * @var string + */ + private $scene = 'default'; + + /** + * @var string + */ + private $scenePrefix = 'scene'; + + /** + * @var ContainerInterface + */ + private $container; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * jwt配置前缀 + * @var string + */ + private $configPrefix = 'jwt'; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + $this->config = $this->container->get(ConfigInterface::class); + + // 合并场景配置,并且兼容2.0.6以下的配置 + $config = $this->config->get($this->configPrefix); + if (empty($config['supported_algs'])) $config['supported_algs'] = $this->supportedAlgs; + if (empty($config['symmetry_algs'])) $config['symmetry_algs'] = $this->symmetryAlgs; + if (empty($config['asymmetric_algs'])) $config['asymmetric_algs'] = $this->asymmetricAlgs; + if (empty($config['blacklist_prefix'])) $config['blacklist_prefix'] = 'phper666_jwt'; + $scenes = $config['scene']; + unset($config['scene']); + foreach ($scenes as $key => $scene) { + $sceneConfig = array_merge($config, $scene); + $this->setSceneConfig($key, $sceneConfig); + } + } + + /** + * @param ContainerInterface $container + * @return $this + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + return $this; + } + + /** + * @return ContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * 设置场景值 + * @param string $scene + */ + public function setScene(string $scene) + { + $this->scene = $scene; + return $this; + } + + /** + * 获取当前场景值 + * @return string + */ + public function getScene() + { + return $this->scene; + } + + /** + * @param string $scene + * @param null $value + * @return $this + */ + public function setSceneConfig(string $scene = 'default', $value = null) + { + $this->config->set("{$this->configPrefix}.{$this->scenePrefix}.{$scene}", $value); + return $this; + } + + /** + * @param string $scene + * @return mixed + */ + public function getSceneConfig(string $scene = 'default') + { + return $this->config->get("{$this->configPrefix}.{$this->scenePrefix}.{$scene}"); + } + + /** + * @param string $token + * @return mixed + */ + public function getSceneConfigByToken(string $token) + { + $scene = JWTUtil::getParserData($token)[$this->tokenScenePrefix]; + return $this->config->get("{$this->configPrefix}.{$this->scenePrefix}.{$scene}"); + } +} diff --git a/src/BlackList.php b/src/BlackList.php new file mode 100644 index 0000000..742638a --- /dev/null +++ b/src/BlackList.php @@ -0,0 +1,146 @@ + + */ +class BlackList extends AbstractJWT +{ + /** + * @var CacheInterface + */ + public $cache; + + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + $this->cache = $this->getContainer()->get(CacheInterface::class); + } + + /** + * 把token加入到黑名单中 + * @param Token $token + * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function addTokenBlack(Token $token, array $config = [], $ssoSelfExp = false) + { + $claims = JWTUtil::claimsToArray($token->getClaims()); + if ($ssoSelfExp) $claims['iat'] += 1; // 如果是当点登录,并且调用了logout方法 + if ($config['blacklist_enabled']) { + $cacheKey = $this->getCacheKey($claims['jti']); + $this->cache->set( + $cacheKey, + ['valid_until' => $this->getGraceTimestamp($claims, $config)], + $this->getSecondsUntilExpired($claims, $config) + ); + } + return $claims; + } + + /** + * Get the number of seconds until the token expiry. + * + * @return int + */ + protected function getSecondsUntilExpired($claims, array $config) + { + $exp = TimeUtil::timestamp($claims['exp']); + $iat = TimeUtil::timestamp($claims['iat']); + + // get the latter of the two expiration dates and find + // the number of minutes until the expiration date, + // plus 1 minute to avoid overlap + return $exp->max($iat->addSeconds($config['blacklist_cache_ttl']))->diffInSeconds(); + } + + /** + * Get the timestamp when the blacklist comes into effect + * This defaults to immediate (0 seconds). + * + * @return int + */ + protected function getGraceTimestamp($claims, array $config) + { + $loginType = $config['login_type']; + $gracePeriod = $config['blacklist_grace_period']; + if ($loginType == 'sso') $gracePeriod = 0; + return TimeUtil::timestamp($claims['iat'])->addSeconds($gracePeriod)->getTimestamp(); + } + + /** + * 判断token是否已经加入黑名单 + * @param $claims + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function hasTokenBlack($claims, array $config = []) + { + $cacheKey = $this->getCacheKey($claims['jti']); + if ($config['blacklist_enabled'] && $config['login_type'] == 'mpop') { + $val = $this->cache->get($cacheKey); + // check whether the expiry + grace has past + return !empty($val) && !TimeUtil::isFuture($val['valid_until']); + } + + if ($config['blacklist_enabled'] && $config['login_type'] == 'sso') { + $val = $this->cache->get($cacheKey); + // 这里为什么要大于等于0,因为在刷新token时,缓存时间跟签发时间可能一致,详细请看刷新token方法 + $isFuture = ($claims['iat'] - $val['valid_until']) >= 0; + // check whether the expiry + grace has past + return !empty($val) && !$isFuture; + } + + return false; + } + + /** + * 黑名单移除token + * @param $key token 中的jit + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function remove($key) + { + return $this->cache->delete($key); + } + + /** + * 移除所有的token缓存 + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function clear() + { + $cachePrefix = $this->getSceneConfig($this->getScene())['blacklist_prefix']; + return $this->cache->delete("{$cachePrefix}.*"); + } + + /** + * @param string $jti + * @return string + */ + private function getCacheKey(string $jti) + { + $config = $this->getSceneConfig($this->getScene()); + return "{$config['blacklist_prefix']}_" . $jti; + } + + /** + * Get the cache time limit. + * + * @return int + */ + public function getCacheTTL() + { + return $this->getSceneConfig($this->getScene())['ttl']; + } +} diff --git a/src/Blacklist.php b/src/Blacklist.php deleted file mode 100644 index 33ffe45..0000000 --- a/src/Blacklist.php +++ /dev/null @@ -1,144 +0,0 @@ - - */ -class Blacklist extends AbstractAnnotation -{ - use CommonTrait; - - /** - * 把token加入到黑名单中 - * @return $claims - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function add(Token $token) - { - $claims = $this->claimsToArray($token->getClaims()); - $jti = $claims['jti']; - if ($this->enalbed) { - $this->storage->set( - $jti, - ['valid_until' => $this->getGraceTimestamp()], - $this->getSecondsUntilExpired($claims) - ); - } - - return $claims; - } - - /** - * Get the number of seconds until the token expiry. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return int - */ - protected function getSecondsUntilExpired($claims) - { - $exp = Utils::timestamp($claims['exp']); - $iat = Utils::timestamp($claims['iat']); - - // get the latter of the two expiration dates and find - // the number of minutes until the expiration date, - // plus 1 minute to avoid overlap - return $exp->max($iat->addSeconds($this->cacheTTL))->addSecond()->diffInSeconds(); - } - - /** - * Get the timestamp when the blacklist comes into effect - * This defaults to immediate (0 seconds). - * - * @return int - */ - protected function getGraceTimestamp() - { - if ($this->loginType == 'sso') $this->gracePeriod = 0; - return Utils::now()->addSeconds($this->gracePeriod)->getTimestamp(); - } - - /** - * 判断token是否已经加入黑名单 - * @param $claims - * @return bool - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function has($claims) - { - if ($this->enalbed && $this->loginType == 'mpop') { - $val = $this->storage->get($claims['jti']); - // check whether the expiry + grace has past - return !empty($val) && !Utils::isFuture($val['valid_until']); - } - - if ($this->enalbed && $this->loginType == 'sso') { - $val = $this->storage->get($claims['jti']); - // 这里为什么要大于等于0,因为在刷新token时,缓存时间跟签发时间可能一致,详细请看刷新token方法 - $isFuture = ($claims['iat'] - $val['valid_until']) >= 0; - // check whether the expiry + grace has past - return !empty($val) && !$isFuture; - } - - return false; - } - - /** - * 黑名单移除token - * @param $jwtJti - * @return bool - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function remove($jwtJti) - { - return $this->storage->delete($jwtJti); - } - - /** - * 移除所有的缓存,注意:这样会把系统所有的缓存都清掉 todo - * @return bool - */ - public function clear() - { - $this->storage->clear(); - - return true; - } - - /** - * Set the cache time limit. - * - * @param int $ttl - * - * @return $this - */ - public function setCacheTTL($ttl) - { - $this->cacheTTL = (int) $ttl; - - return $this; - } - - /** - * Get the cache time limit. - * - * @return int - */ - public function getCacheTTL() - { - return $this->cacheTTL; - } -} diff --git a/src/Command/JwtCommand.php b/src/Command/JWTCommand.php similarity index 94% rename from src/Command/JwtCommand.php rename to src/Command/JWTCommand.php index 7557a89..b7112a5 100644 --- a/src/Command/JwtCommand.php +++ b/src/Command/JWTCommand.php @@ -1,8 +1,6 @@ request = $this->getContainer()->get(RequestInterface::class); + $this->blackList = $blackList; + } + + /** + * 生成token + * @param array $claims + * @param bool $isInsertSsoBlack 是否把单点登录生成的token加入黑名单 + * @param bool $isConversionString 是否把token强制转换成string类型 + * @return Token|string + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getToken(array $claims, $isInsertSsoBlack = true, $isConversionString = true) + { + $config = $this->getSceneConfig($this->getScene()); + $loginType = $config['login_type']; + $ssoKey = $config['sso_key']; + if ($loginType == 'mpop') { // 多点登录,场景值加上一个唯一id + $uniqid = uniqid($this->getScene() . '_', true); + } else { // 单点登录 + if (empty($claims[$ssoKey])) { + throw new JWTException("There is no {$ssoKey} key in the claims", 400); + } + $uniqid = $this->getScene() . "_" . $claims[$ssoKey]; + } + + $signer = new $config['supported_algs'][$config['alg']]; + $time = time(); + $builder = JWTUtil::getBuilder() + ->identifiedBy($uniqid) // 设置jwt的jti + ->issuedAt($time)// (iat claim) 发布时间 + ->canOnlyBeUsedAfter($time)// (nbf claim) 在此之前不可用 + ->expiresAt($time + $config['ttl']);// (exp claim) 到期时间 + + $claims[$this->tokenScenePrefix] = $this->getScene(); // 加入场景值 + foreach ($claims as $k => $v) { + $builder = $builder->withClaim($k, $v); // 自定义数据 + } + + $token = $builder->getToken($signer, $this->getKey($config)); // Retrieves the generated token + + // 单点登录要把所有的以前生成的token都失效 + if ($loginType == 'sso' && $isInsertSsoBlack) $this->blackList->addTokenBlack($token, $config); + + return $isConversionString ? (string)$token : $token; + } + + /** + * 验证token + * @param string|null $token + * @param bool $validate + * @param bool $verify + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Throwable + */ + public function checkToken(string $token = null, $validate = true, $verify = true) + { + try { + if (empty($token)) $token = $this->getHeaderToken(); + $config = $this->getSceneConfigByToken($token); + $token = $this->getTokenObj($token); + } catch (\RuntimeException $e) { + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e->getPrevious()); + } + // 获取token里面的场景值,如果不存在,则使用默认场景值 + $claims = JWTUtil::claimsToArray($token->getClaims()); + // 验证token是否存在黑名单 + if ($config['blacklist_enabled'] && $this->blackList->hasTokenBlack($claims, $config)) throw new TokenValidException('Token authentication does not pass', 401); + + if ($validate && !$this->validateToken($token)) throw new TokenValidException('Token authentication does not pass', 401); + + if ($verify && !$this->verifyToken($token, $config)) throw new TokenValidException('Token authentication does not pass', 401); + + return true; + } + + /** + * 刷新token + * @return Token + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function refreshToken(string $token = null) + { + if (empty($token)) $token = $this->getHeaderToken(); + $config = $this->getSceneConfigByToken($token); + $claims = $this->blackList->addTokenBlack($this->getTokenObj($token), $config); + unset($claims['iat']); + unset($claims['nbf']); + unset($claims['exp']); + unset($claims['jti']); + return $this->getToken($claims); + } + + /** + * 让token失效 + * @param string|null $token + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function logout(string $token = null) + { + if (empty($token)) $token = $this->getHeaderToken(); + $config = $this->getSceneConfigByToken($token); + // 如果是sso,并且使当前token失效 + $ssoSelfExp = false; + if ($config['login_type'] == 'sso') $ssoSelfExp = true; + $this->blackList->addTokenBlack($this->getTokenObj($token), $config, $ssoSelfExp); + return true; + } + + /** + * 获取token动态有效时间 + * @param string|null $token + * @return int|mixed + */ + public function getTokenDynamicCacheTime(string $token = null) + { + $nowTime = time(); + if (empty($token)) $token = $this->getHeaderToken(); + $exp = $this->getTokenObj($token)->getClaim('exp', $nowTime); + $expTime = $exp - $nowTime; + return $expTime; + } + + /** + * 获取jwt token解析的data + * @param string|null $token + * @return array + */ + public function getParserData(string $token = null) + { + $arr = []; + if (empty($token)) $token = $this->getHeaderToken(); + $claims = $this->getTokenObj($token)->getClaims(); + foreach ($claims as $k => $v) { + $arr[$k] = $v->getValue(); + } + return $arr; + } + + /** + * 获取缓存时间 + * @return mixed + */ + public function getTTL(string $token = null) + { + if (!empty($token)) $config = $this->getSceneConfigByToken($token); + if (empty($token)) $config = $this->getSceneConfig($this->getScene()); + return (int)$config['ttl']; + } + + /** + * 获取对应算法需要的key + * @param string $type 配置keys里面的键,获取私钥或者公钥。private-私钥,public-公钥 + * @return Key|null + */ + private function getKey(array $config, string $type = 'private') + { + $key = NULL; + // 对称算法 + if (in_array($config['alg'], $config['symmetry_algs'])) { + $key = new Key($config['secret']); + } + + // 非对称 + if (in_array($config['alg'], $config['asymmetric_algs'])) { + $key = $config['keys'][$type]; + $key = new Key($key); + } + return $key; + } + + /** + * 获取Token对象 + * @param string|null $token + * @return Token + */ + private function getTokenObj(string $token = null) + { + return JWTUtil::getParser()->parse($token); +// if (!empty(str_replace(' ', '', $token))) return JWTUtil::getParser()->parse($token); +// return JWTUtil::getParser()->parse($this->getHeaderToken()); + } + + /** + * 获取http头部token + * @return bool|mixed|string + */ + private function getHeaderToken() + { + $token = $this->request->getHeaderLine('Authorization') ?? ''; + $token = JWTUtil::handleToken($token, $this->tokenPrefix); + if ($token === false) throw new JWTException('A token is required', 400); + return $token; + } + + /** + * 验证jwt token的data部分 + * @param Token $token token object + * @return bool + */ + private function validateToken(Token $token, $currentTime = null) + { + $data = JWTUtil::getValidationData($currentTime); + return $token->validate($data); + } + + /** + * 验证 jwt token + * @param Token $token token object + * @return bool + * @throws \Throwable + */ + private function verifyToken(Token $token, array $config) + { + $alg = $token->getHeader('alg'); + if (empty($config['supported_algs'][$alg])) { + throw new TokenValidException('Algorithm not supported', 401); + } + /** @var Signer $signer */ + $signer = new $config['supported_algs'][$alg]; + return $token->verify($signer, $this->getKey($config, 'public')); + } +} diff --git a/src/JWTInterface.php b/src/JWTInterface.php new file mode 100644 index 0000000..9773efb --- /dev/null +++ b/src/JWTInterface.php @@ -0,0 +1,22 @@ + - */ -class Jwt -{ - use CommonTrait; - - /** - * @Inject - * @var Blacklist - */ - protected $blacklist; - - /** - * 生成token - * @param array $claims - * @param bool $isInsertSsoBlack 是否把单点登录生成的token加入黑名单 - * @return Token - */ - public function getToken(array $claims, $isInsertSsoBlack = true) - { - if ($this->loginType == 'mpop') { // 多点登录 - $uniqid = uniqid(); - } else { // 单点登录 - if (empty($claims[$this->ssoKey])) { - throw new JWTException("There is no {$this->ssoKey} key in the claims", 400); - } - $uniqid = $claims[$this->ssoKey]; - } - - $signer = new $this->supportedAlgs[$this->alg]; - $time = time(); - - $builder = $this->getBuilder() - ->identifiedBy($uniqid) // 设置jwt的jti - ->issuedAt($time)// (iat claim) 发布时间 - ->canOnlyBeUsedAfter($time)// (nbf claim) 在此之前不可用 - ->expiresAt($time + $this->ttl);// (exp claim) 到期时间 - - foreach ($claims as $k => $v) { - $builder = $builder->withClaim($k, $v); // 自定义数据 - } - - $token = $builder->getToken($signer, $this->getKey()); // Retrieves the generated token - - if ($this->loginType == 'sso' && $isInsertSsoBlack) { // 单点登录要把所有的以前生成的token都失效 - $this->blacklist->add($token); - } - - return $token; // 返回的是token对象,使用强转换会自动转换成token字符串。Token对象采用了__toString魔术方法 - } - - /** - * 刷新token - * @return Token - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function refreshToken() - { - if (!$this->getHeaderToken()) { - throw new JWTException('A token is required', 400); - } - $claims = $this->blacklist->add($this->getTokenObj()); - unset($claims['iat']); - unset($claims['nbf']); - unset($claims['exp']); - unset($claims['jti']); - return $this->getToken($claims); - } - - /** - * 让token失效 - * @param string|null $token - * @return bool - * @throws \Psr\SimpleCache\InvalidArgumentException - */ - public function logout(string $token = null) - { - if (!is_null($token) && $token !== '') { - $token = $this->handleHeaderToken($token); - } else { - $token = $this->getHeaderToken(); - } - $this->blacklist->add($this->getTokenObj($token)); - return true; - } - - /** - * 验证token - * @param string $token JWT - * @return true - * @throws \Throwable - */ - public function checkToken(string $token = null, $validate = true, $verify = true) - { - try { - $token = $this->getTokenObj($token); - } catch (\RuntimeException $e) { - throw new \RuntimeException($e->getMessage(), $e->getCode(), $e->getPrevious()); - } - - if ($this->enalbed) { - $claims = $this->claimsToArray($token->getClaims()); - // 验证token是否存在黑名单 - if ($this->blacklist->has($claims)) { - throw new TokenValidException('Token authentication does not pass', 401); - } - } - - if ($validate && !$this->validateToken($token)) { - throw new TokenValidException('Token authentication does not pass', 401); - } - if ($verify && !$this->verifyToken($token)) { - throw new TokenValidException('Token authentication does not pass', 401); - } - - return true; - } - - /** - * 获取Token对象 - * @param string|null $token - * @return Token - */ - public function getTokenObj(string $token = null) - { - if (!is_null($token) && $token !== '') { - return $this->getParser()->parse($token); - } - return $this->getParser()->parse($this->getHeaderToken()); - } - - /** - * 获取token的过期剩余时间,单位为s - * @return int|mixed - */ - public function getTokenDynamicCacheTime(string $token = null) - { - $nowTime = time(); - $exp = $this->getTokenObj($token)->getClaim('exp', $nowTime); - $expTime = $exp - $nowTime; - return $expTime; - } - - /** - * 获取jwt token解析的dataç - * @return array - */ - public function getParserData(string $token = null) - { - $arr = []; - $claims = $this->getTokenObj($token)->getClaims(); - foreach ($claims as $k => $v) { - $arr[$k] = $v->getValue(); - } - return $arr; - } -} diff --git a/src/Middleware/JwtAuthMiddleware.php b/src/Middleware/JWTAuthMiddleware.php similarity index 67% rename from src/Middleware/JwtAuthMiddleware.php rename to src/Middleware/JWTAuthMiddleware.php index b3606f4..a05fa3e 100644 --- a/src/Middleware/JwtAuthMiddleware.php +++ b/src/Middleware/JWTAuthMiddleware.php @@ -5,17 +5,17 @@ * Date: 2019-08-01 * Time: 22:32 */ -namespace Phper666\JwtAuth\Middleware; +namespace Phper666\JWTAuth\Middleware; use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Phper666\JwtAuth\Jwt; -use Phper666\JwtAuth\Exception\TokenValidException; +use Phper666\JWTAuth\JWT; +use Phper666\JWTAuth\Exception\TokenValidException; -class JwtAuthMiddleware implements MiddlewareInterface +class JWTAuthMiddleware implements MiddlewareInterface { /** * @var HttpResponse @@ -26,20 +26,27 @@ class JwtAuthMiddleware implements MiddlewareInterface protected $jwt; - public function __construct(HttpResponse $response, Jwt $jwt) + public function __construct(HttpResponse $response, JWT $jwt) { $this->response = $response; $this->jwt = $jwt; } + /** + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Throwable + */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $isValidToken = false; // 根据具体业务判断逻辑走向,这里假设用户携带的token有效 - $token = $request->getHeader('Authorization')[0] ?? ''; + $token = $request->getHeaderLine('Authorization') ?? ''; if (strlen($token) > 0) { $token = ucfirst($token); - $arr = explode($this->prefix . ' ', $token); + $arr = explode("{$this->prefix }", $token); $token = $arr[1] ?? ''; if (strlen($token) > 0 && $this->jwt->checkToken()) { $isValidToken = true; diff --git a/src/Traits/CommonTrait.php b/src/Traits/CommonTrait.php deleted file mode 100644 index 4ac62c3..0000000 --- a/src/Traits/CommonTrait.php +++ /dev/null @@ -1,258 +0,0 @@ - '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', - ]; - - // 对称算法名称 - public $symmetryAlgs = [ - 'HS256', - 'HS384', - 'HS512' - ]; - - // 非对称算法名称 - public $asymmetricAlgs = [ - 'RS256', - 'RS384', - 'RS512', - 'ES256', - 'ES384', - 'ES512', - ]; - - public $prefix = 'Bearer'; - - /** - * @Inject - * @var RequestInterface - */ - public $request; - - /** - * @Inject - * @var CacheInterface - */ - public $storage; - - - /** - * @Value("jwt.secret") - */ - public $secret; - - /** - * @Value("jwt.keys") - */ - public $keys; - - /** - * @Value("jwt.ttl") - */ - public $ttl; - - /** - * @Value("jwt.alg") - */ - public $alg; - - /** - * @Value("jwt.login_type") - */ - public $loginType = 'mpop'; - - /** - * @Value("jwt.sso_key") - */ - public $ssoKey = 'uid'; - - /** - * @Value("jwt.blacklist_cache_ttl") - */ - public $cacheTTL = 86400; - - /** - * @Value("jwt.blacklist_grace_period") - */ - public $gracePeriod = 0; - - /** - * @Value("jwt.blacklist_enabled") - */ - public $enalbed = true; - - /** - * @see [[Lcobucci\JWT\Builder::__construct()]] - * @return Builder - */ - public function getBuilder(Encoder $encoder = null, ClaimFactory $claimFactory = null) - { - return new Builder($encoder, $claimFactory); - } - - /** - * @see [[Lcobucci\JWT\Parser::__construct()]] - * @return Parser - */ - public function getParser(Decoder $decoder = null, ClaimFactory $claimFactory = null) - { - return new Parser($decoder, $claimFactory); - } - - /** - * @see [[Lcobucci\JWT\ValidationData::__construct()]] - * @return ValidationData - */ - public function getValidationData($currentTime = null) - { - return new ValidationData($currentTime); - } - - - /** - * 验证jwt token的data部分 - * @param Token $token token object - * @return bool - */ - public function validateToken(Token $token, $currentTime = null) - { - $data = $this->getValidationData($currentTime); - return $token->validate($data); - } - - /** - * 验证 jwt token - * @param Token $token token object - * @return bool - * @throws \Throwable - */ - public function verifyToken(Token $token) - { - $alg = $token->getHeader('alg'); - if (empty($this->supportedAlgs[$alg])) { - throw new TokenValidException('Algorithm not supported', 401); - } - /** @var Signer $signer */ - $signer = new $this->supportedAlgs[$alg]; - return $token->verify($signer, $this->getKey('public')); - } - - /** - * 获取对应算法需要的key - * @param string $type 配置keys里面的键,获取私钥或者公钥。private-私钥,public-公钥 - * @return Key|null - */ - public function getKey(string $type = 'private') - { - $key = NULL; - - // 对称算法 - if (in_array($this->alg, $this->symmetryAlgs)) { - $key = new Key($this->secret); - } - - // 非对称 - if (in_array($this->alg, $this->asymmetricAlgs)) { - $key = $this->keys[$type]; - $key = new Key($key); - } - - return $key; - } - - /** - * 获取http头部token - * @param $token - * @param int $dynamicCacheTime - * @return string|null - */ - public function getHeaderToken() - { - $token = $this->request->getHeader('Authorization')[0] ?? ''; - $token = $this->handleHeaderToken($token); - if ($token !== false) { - return $token; - } - - throw new JWTException('A token is required', 400); - } - - /** - * 处理头部token - * @param string $token - * @return bool|string - */ - public function handleHeaderToken(string $token) - { - if (strlen($token) > 0) { - $token = ucfirst($token); - $arr = explode($this->prefix . ' ', $token); - $token = $arr[1] ?? ''; - if (strlen($token) > 0) return $token; - } - return false; - } - - /** - * @param $claims - * @return mixed - */ - public function claimsToArray($claims) - { - foreach($claims as $k => $v) { - $claims[$k] = $v->getValue(); - } - - return $claims; - } - - /** - * 获取缓存时间 - * @return mixed - */ - public function getTTL() - { - return (int)$this->ttl; - } - - public function __get($name) - { - return $this->$name; - } -} diff --git a/src/Util/JWTUtil.php b/src/Util/JWTUtil.php new file mode 100644 index 0000000..640989e --- /dev/null +++ b/src/Util/JWTUtil.php @@ -0,0 +1,93 @@ + $claim) { + $claims[$k] = $claim->getValue(); + } + return $claims; + } + + /** + * 解析token + * @param string $token + * @return array + */ + public static function getParserData(string $token) + { + $arr = []; + $claims = self::getParser()->parse($token)->getClaims(); + foreach ($claims as $k => $v) { + $arr[$k] = $v->getValue(); + } + return $arr; + } + + /** + * 处理token + * @param string $token + * @param string $prefix + * @return bool|mixed|string + */ + public static function handleToken(string $token, string $prefix = 'Bearer') + { + if (strlen($token) > 0) { + $token = ucfirst($token); + $arr = explode("{$prefix} ", $token); + $token = $arr[1] ?? ''; + if (strlen($token) > 0) return $token; + } + return false; + } + + /** + * @see [[Lcobucci\JWT\Builder::__construct()]] + * @return Builder + */ + public static function getBuilder(Encoder $encoder = null, ClaimFactory $claimFactory = null) + { + return new Builder($encoder, $claimFactory); + } + + /** + * @see [[Lcobucci\JWT\Parser::__construct()]] + * @return Parser + */ + public static function getParser(Decoder $decoder = null, ClaimFactory $claimFactory = null) + { + return new Parser($decoder, $claimFactory); + } + + /** + * @see [[Lcobucci\JWT\ValidationData::__construct()]] + * @return ValidationData + */ + public static function getValidationData($currentTime = null) + { + return new ValidationData($currentTime); + } +} diff --git a/src/Helper/Utils.php b/src/Util/TimeUtil.php similarity index 81% rename from src/Helper/Utils.php rename to src/Util/TimeUtil.php index 20b8554..5d252f0 100644 --- a/src/Helper/Utils.php +++ b/src/Util/TimeUtil.php @@ -1,19 +1,16 @@ assertTrue(true); - - $this->assertTrue(extension_loaded('swoole')); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 210da6c..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,13 +0,0 @@ -