用户认证

Spiral 框架包含了一系列组件,以通过不同来源的临时或永久令牌认证用户身份,并安全地管理用户上下文。

用户认证组件不强制执行任何特定的用户实体接口,也不会将应用程序限制在 HTTP 应用类型(GRPC 应用也可以进行认证)。

工作原理

认证组件会为 Spiral\Auth\AuthContextInterface 创建一个 IoC 作用域,指向当前授权的行为人(用户、API 客户端)。行为人由 Spiral\Auth\TokenInterfaceSpiral\Auth\ActorProviderInterface 进行获取。

Token 由 Spiral\Auth\TokenStorageInterface 管理,并且始终包含有效数据(例如 ['UserId' => $id]、LDAP 证书等)。Token 数据会被 Spiral\Auth\ActorProviderInterface 用于查询当前应用程序用户。

Token 存储既可以把 token 存储于外部源(例如数据库、Redis 或文件),也可以即时解码。为了便于使用,Spiral 框架默认包含了多套 token 实现。

你可以在同一个应用程序中同时使用多套 token 和 actor 提供程序。

安装和配置

要在 Web 应用程序模板创建的应用中安装用户认证组件,可以执行:

$ composer require spiral/auth spiral/auth-http

spiral/auth 包提供了不与任何调度方式(HTTP、GRPC)关联的标准接口,而 spiral/auth-http 则包含了用于 Web 应用的 HTTP 中间件、令牌传输(Cookie、Header)以及防火墙组件。

要激活组件,需要添加 Spiral\Bootloader\Auth\HttpAuthBootloader 引导程序:

[
    // ...
    Framework\Auth\HttpAuthBootloader::class,
    // ...
]

安装完成后,必须要选择用什么方式来存储用户认证令牌。

Session 令牌存储

如果想用 PHP session 来存储令牌,首先确保 spiral/session 组件已安装,并用 Spiral\Bootloader\Auth\TokenStorage\SessionTokensBootloader 来启用 session 存储:

[
    // ...
    Framework\Auth\HttpAuthBootloader::class,
    Framework\Auth\TokenStorage\SessionTokensBootloader::class,
    // ...
]

数据库令牌存储

Spiral 也可以把通过 Cycle ORM 把令牌存储在数据库中,要使用这种方式,需要启用 Spiral\Bootloader\Auth\TokenStorage\CycleTokensBootloader 引导程序:

[
    // ...
    Framework\Auth\HttpAuthBootloader::class,
    Framework\Auth\TokenStorage\CycleTokensBootloader::class,
    // ...
]

此外还必须运行控制台命令 cycle:sync 来生成和执行数据库迁移,以便在数据库中创建所需的表:

$ php app.php migrate:init
$ php app.php cycle:migrate -v -r

行为人提供器和令牌数据

下一步是配置从令牌数据中获取行为人/用户的方式,为了实现这个功能,需要实现和注册 Spiral\Auth\ActorProviderInterface 接口。

interface ActorProviderInterface
{
    public function getActor(TokenInterface $token): ?object;
}

在本文中,我们以 Cycle 实体和数据仓库为例:

namespace App\Database;

use Cycle\Annotated\Annotation as Cycle;

/**
 * @Cycle\Entity(repository="Repository/UserRepository")
 * @Cycle\Table(indexes={
 *     @Cycle\Table\Index(columns={"username"}, unique=true)
 * })
 */
class User
{
    /**
     * @Cycle\Column(type = "primary")
     */
    public $id;

    /** @Cycle\Column(type="string") */
    public $name;

    /** @Cycle\Column(type="string") */
    public $username;

    /** @Cycle\Column(type="string") */
    public $password;
}

UserRepository 上实现 ActorProviderInterface 接口:

namespace App\Database\Repository;

use Cycle\ORM\Select\Repository;
use Spiral\Auth\ActorProviderInterface;
use Spiral\Auth\TokenInterface;

class UserRepository extends Repository implements ActorProviderInterface
{
    public function getActor(TokenInterface $token): ?object
    {
        if (!isset($token->getPayload()['userID'])) {
            return null;
        }

        return $this->findByPK($token->getPayload()['userID']);
    }
}

在准备工作完成后,先来创建一个用户:

public function index(Transaction $t)
{
    $u = new User();
    $u->name = 'Antony';
    $u->username = 'username';
    $u->password = password_hash('password', PASSWORD_DEFAULT);

    $t->persist($u)->run();
}

在你的应用程序中创建和启用一个引导程序,并在引导程序中注册行为人提供器来启用它:

namespace App\Bootloader;

use App\Database\Repository\UserRepository;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Bootloader\Auth\AuthBootloader;

class UserBootloader extends Bootloader
{
    public function boot(AuthBootloader $auth)
    {
        $auth->addActorProvider(UserRepository::class);
    }
}

认证用户

用户认证过程通过 Spiral\Auth\AuthContextInterface 进行。你可以通过方法注入获得实现该接口的当前认证上下文对象实例。

public function index(AuthContextInterface $auth)
{
    // 使用认证上下文
}

注意:不要在单例服务中存储 AuthContextInterface,你可以参考上面我们是如何传递这个对象的。

除此之外,你也可以使用 Spiral\Auth\AuthScope, 这是可以存储在单例服务中的,或者使用原型开发辅助里的 auth 属性。

namespace App\Controller;

use Spiral\Prototype\Traits\PrototypeTrait;

class HomeController
{
    use PrototypeTrait;

    public function index()
    {
        dump($this->auth);
    }
}

用户登录

实现用户登录需要我们创建一个登录表单以及相应的请求过滤器。

namespace App\Request;

use Spiral\Filters\Filter;

class LoginRequest extends Filter
{
    public const SCHEMA = [
        'username' => 'data:username',
        'password' => 'data:password'
    ];

    public const VALIDATES = [
        'username' => ['notEmpty'],
        'password' => ['notEmpty']
    ];
}

Create login method in the controller dedicated to authentication: 在控制器中创建一个专门用于用户认证的登录方法,

public function login(LoginRequest $login)
{
    if (!$login->isValid()) {
        return [
            'status' => 400,
            'errors' => $login->getErrors()
        ];
    }

    // application specific login logic
    $user = $this->users->findOne(['username' => $login->getField('username')]);
    if (
        $user === null
        || !password_verify($login->getField('password'), $user->password)
    ) {
        return [
            'status' => 404,
            'error'  => '用户不存在'
        ];
    }

    // 创建令牌
}

要为上面的请求认证用户,必须创建和你选择的 ActorProviderInterface 兼容的数据对象(['UserId' => $id]),要实现这个,我们会用到 AuthContextInterface 接口的实例以及 TokenStorageInterface 的实例。这两个对象,分别可以通过原型开发辅助的 authauthTokens 属性来访问到:

public function login(LoginRequest $login)
{
    // ... 代码略,见上文

    // 创建令牌
    $this->auth->start(
        $this->authTokens->create(['userID' => $user->id])
    );

    return [
        'status'  => 200,
        'message' => '认证成功!'
    ];
}

这样,就完成了用户登录认证。

检查登录

要检查当前用户的登录状态,只要检查认证上下文(auth context)拥有一个不为空的行为人(actor)即可:

public function index()
{
    if ($this->auth->getActor() === null) {
        throw new ForbiddenException();
    }
    
    dump($this->auth->getActor());
}

你可以使用 RBAC 安全组件来同时进行用户认证和用户鉴权。

用户注销

要注销当前用户的登录,可以调用认证上下文或者认证作用域(AuthScope)的 close 方法:

public function logout()
{
    $this->auth->close();
}

RBAC 安全组件

你可以使用已经认证的用户作为 RBAC 安全组件的行为人,如果需要这样使用,需确保你的用户实体(例如 App\Database\User)实现 Spiral\Security\ActorInterface 接口:

namespace App\Database;

use Cycle\Annotated\Annotation as Cycle;
use Spiral\Security\ActorInterface;

/**
 * @Cycle\Entity(repository="Repository/UserRepository")
 * @Cycle\Table(indexes={
 *     @Cycle\Table\Index(columns={"username"}, unique=true)
 * })
 */
class User implements ActorInterface
{
    /**
     * @Cycle\Column(type = "primary")
     */
    public $id;

    /** @Cycle\Column(type="string") */
    public $name;

    /** @Cycle\Column(type="string") */
    public $username;

    /** @Cycle\Column(type="string") */
    public $password;

    public function getRoles(): array
    {
        return ['user'];
    }
}

并且启用 Spiral\Bootloader\Auth\SecurityActorBootloader 引导程序把两个组件(用户认证组件、RBAC 安全组件)连接起来:

[
    // ...
    Framework\Auth\SecurityActorBootloader::class,
    // ...
]

防火墙中间件

你可以把防火墙中间件绑定到特定路由目标,以避免未授权的访问。

默认情况下,Spiral 只提供了一个防火墙,它会将未认证的用户重定向到其它 url:

use Spiral\Auth\Middleware\Firewall\OverwriteFirewall;

// ...

(new Route('/account/<controller>/<action>', $accountTarget))
        ->withMiddleware(new OverwriteFirewall(new Uri('/account/login')));

自定义防火墙

要实现自己的防火墙中间件,只要扩展 Spiral\Auth\Middleware\Firewall\AbstractFirewall 即可:

final class OverwriteFirewall extends AbstractFirewall
{
    protected function denyAccess(Request $request, RequestHandlerInterface $handler): Response
    {
        // 用户未认证
        return $handler->handle($request);
    }
}
上一页
下一页