[hyperf]hyperf-ext/auth和hyperf-ext/jwt完成jwt认证与自动刷新token
Auth组件
- 安装hyperf-ext/auth组件
composer require hyperf-ext/auth
- 发布配置文件(文件位于 config/autoload/auth.php)
php bin/hyperf.php vendor:publish hyperf-ext/auth
- 添加助手方法文件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依赖组件
- 安装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
- .env文件
JWT_BLACKLIST_GRACE_PERIOD=5 设置宽限期(以秒为单位)以防止并发请求失败。 JWT_TTL=3600 指定令牌有效的时长(以秒为单位)。默认为 1 小时
- 其他配置基本可以不变(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 协议》,转载必须注明作者和本文链接
本帖由系统于 3年前 自动加精
可以,支持。后面的jwt考虑使用这个
ApiResponseTrait
没有考虑 resource 部分么, 正在使用, 感谢付出你是hyperf-ext/jwt的作者吗,这个算是官方教程吗
php bin/hyperf.php gen:jwt-keypair 执行的时候,选择算法后卡住了,是不是我的操作有什么问题。
能支持使用redis存储token吗
App\Constants\HttpCode
这个demo呢?'Author' => 'Colorado' 这个是什么意思?
@gently
学习了,感谢分享
大佬,我想用jwt里面的setSecret去自定义不同的member密钥,admin密钥。但是我用$this->jwt->setSecret 这样去定义密钥的话 是不行的,运行的时候提示这个方法不存在。还希望能指点一下
本文刷新token时,guard会丢失,需要先把payload解开,然后再加入到新token中
求助,按照配置,登录成功之后,获得信息:/auth/user,之后返回Connection refused,这个是什么意思,怎么解决?
照上面教程做登录成功,获取用户信息返回null这是为啥