[hyperf]hyperf-ext/auth和hyperf-ext/jwt完成jwt认证与自动刷新token

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. 添加助手方法文件app/Functions.php
<?php

if (! function_exists('auth')) {
  /**
 * Auth认证辅助方法
  *
 * @param string|null $guard 守护名称
  *
 * @return \HyperfExt\Auth\Contracts\GuardInterface|\HyperfExt\Auth\Contracts\StatefulGuardInterface|\HyperfExt\Auth\Contracts\StatelessGuardInterface
 */  function auth(string $guard = 'api')
 {  if (is_null($guard)) $guard = config('auth.default.guard');

 return make(\HyperfExt\Auth\Contracts\AuthManagerInterface::class)->guard($guard);
  }
}

加完后记得要用composer自动加载哟

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. .env文件
    JWT_BLACKLIST_GRACE_PERIOD=5  设置宽限期(以秒为单位)以防止并发请求失败。
    JWT_TTL=3600 指定令牌有效的时长(以秒为单位)。默认为 1 小时
  4. 其他配置基本可以不变(config/autoload/jwt.php)

添加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 App\Constants\HttpCode;
use App\Traits\ApiResponseTrait;
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 App\Middleware\Auth\RefreshTokenMiddleware;
use HyperfExt\Jwt\Contracts\JwtFactoryInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * @Controller(prefix="auth")
 * Class AuthController
 * @package App\Controller
 */
class AuthController
{
    use ApiResponseTrait;

    /**
     * @RequestMapping(path="login", methods={"POST"})
     * @param RequestInterface $request
     * @return ResponseInterface
     */
    public function login(RequestInterface $request): ResponseInterface
    {
        $credentials = $request->inputs(['email', 'password']);
        if (!$token = auth('api')->attempt($credentials)) {
            return $this->setHttpCode(HttpCode::UNAUTHORIZED)->fail('Unauthorized');
        }
        return $this->respondWithToken($token);
    }

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

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

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

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

}

自动刷新token中间件

生成中间件RefreshTokenMiddleware

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

内容如下

<?php

declare(strict_types=1);

namespace App\Middleware;

use App\Constants\HttpCode;
use App\Utils\ApiResponseTrait;
use Exception;
use Hyperf\Di\Annotation\Inject;
use HyperfExt\Jwt\Contracts\ManagerInterface;
use HyperfExt\Jwt\Exceptions\TokenExpiredException;
use HyperfExt\Jwt\JwtFactory;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class RefreshTokenMiddleware implements MiddlewareInterface
{
    use ApiResponseTrait;

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

    /**
     * @Inject
     * @var ManagerInterface
     */
    private $manager;

    /**
     * @Inject
     * @var JwtFactory
     */
    private $jwtFactory;

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

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $jwt = $this->jwtFactory->make();

        try {
            $jwt->checkOrFail();
        } catch (Exception $exception) {
            if (! $exception instanceof TokenExpiredException) {
                return $this->setHttpCode(HttpCode::UNPROCESSABLE_ENTITY)->fail($exception->getMessage());
            }
            try {
                $token = $jwt->getToken();

                // 刷新token
                $new_token = $jwt->getManager()->refresh($token);

                // 解析token载荷信息
                $payload = $jwt->getManager()->decode($token, false, true);

                // 旧token加入黑名单
                $jwt->getManager()->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(HttpCode::UNPROCESSABLE_ENTITY)->fail($exception->getMessage());
            }
        }

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

补充 ApiResponseTrait

<?php

namespace App\Traits;


use App\Constants\HttpCode;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\Codec\Json;
use Hyperf\Utils\Context;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use Psr\Http\Message\ResponseInterface;

trait ApiResponseTrait
{
    private $httpCode = HttpCode::OK;
    private $headers = [
        'Author' => 'Colorado'
    ];

    private $errorCode = 100000;
    private $errorMsg = '';

    protected $response;

    /**
     * 成功响应
     * @param mixed $data
     * @return ResponseInterface
     */
    public function success($data): ResponseInterface
    {
        return $this->respond($data);
    }

    /**
     * 错误返回
     * @param string $err_msg     错误信息
     * @param int    $err_code    错误业务码
     * @param array  $data        额外返回的数据
     * @return ResponseInterface
     */
    public function fail(string $err_msg = '', int $err_code = 200000, array $data = []): ResponseInterface
    {
        return $this->setHttpCode($this->httpCode == HttpCode::OK ? HttpCode::BAD_REQUEST : $this->httpCode)
            ->respond([
                'err_code' => $err_code ?? $this->errorCode,
                'err_msg' => $err_msg ?? $this->errorMsg,
                'data' => $data
            ]);
    }

    /**
     * 设置http返回码
     * @param int $code    http返回码
     * @return $this
     */
    final public function setHttpCode(int $code = HttpCode::OK): self
    {
        $this->httpCode = $code;
        return $this;
    }

    /**
     * 设置返回头部header值
     * @param string $key
     * @param        $value
     * @return $this
     */
    public function addHttpHeader(string $key, $value): self
    {
        $this->headers += [$key => $value];
        return $this;
    }

    /**
     * 批量设置头部返回
     * @param array $headers    header数组:[key1 => value1, key2 => value2]
     * @return $this
     */
    public function addHttpHeaders(array $headers = []): self
    {
        $this->headers += $headers;
        return $this;
    }

    /**
     * @param null|array|Arrayable|Jsonable|string $response
     * @return ResponseInterface
     */
    private function respond($response): ResponseInterface
    {
        if (is_string($response)) {
            return $this->response()->withAddedHeader('content-type', 'text/plain')->withBody(new SwooleStream($response));
        }

        if (is_array($response) || $response instanceof Arrayable) {
            return $this->response()
                ->withAddedHeader('content-type', 'application/json')
                ->withBody(new SwooleStream(Json::encode($response)));
        }

        if ($response instanceof Jsonable) {
            return $this->response()
                ->withAddedHeader('content-type', 'application/json')
                ->withBody(new SwooleStream((string)$response));
        }

        return $this->response()->withAddedHeader('content-type', 'text/plain')->withBody(new SwooleStream((string)$response));
    }

    /**
     * @return mixed|ResponseInterface|null
     */
    protected function response(): ResponseInterface
    {
        $response = Context::get(ResponseInterface::class);
        foreach ($this->headers as $key => $value) {
            $response = $response->withHeader($key, $value);
        }
        return $response;
    }
}

hyperf新人,老手勿喷,共同研究,一起进步

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
讨论数量: 20
CodingHePing

可以,支持。后面的jwt考虑使用这个

3年前 评论
Colorado (楼主) 3年前
wenber

ApiResponseTrait 没有考虑 resource 部分么, 正在使用, 感谢付出

3年前 评论
Colorado (楼主) 3年前

你是hyperf-ext/jwt的作者吗,这个算是官方教程吗

3年前 评论
Colorado (楼主) 3年前
gently (作者) 3年前
Colorado (楼主) 3年前

php bin/hyperf.php gen:jwt-keypair 执行的时候,选择算法后卡住了,是不是我的操作有什么问题。

3年前 评论

能支持使用redis存储token吗

2年前 评论
7NIAO

App\Constants\HttpCode 这个demo呢?

2年前 评论

'Author' => 'Colorado' 这个是什么意思?

2年前 评论
wenber
2年前 评论
Colorado (楼主) 2年前

学习了,感谢分享

2年前 评论

大佬,我想用jwt里面的setSecret去自定义不同的member密钥,admin密钥。但是我用$this->jwt->setSecret 这样去定义密钥的话 是不行的,运行的时候提示这个方法不存在。还希望能指点一下

2年前 评论

本文刷新token时,guard会丢失,需要先把payload解开,然后再加入到新token中

$new_token = $jwt->getManager()->refresh($token, false, ['guard' => $guard]);
2年前 评论

求助,按照配置,登录成功之后,获得信息:/auth/user,之后返回Connection refused,这个是什么意思,怎么解决?

1年前 评论

照上面教程做登录成功,获取用户信息返回null这是为啥

10个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!