> Symfony中文手册 > 如何通过Guard创建一个自定义的验证系统

如何通过Guard创建一个自定义的验证系统

不管你是建立一个传统的登陆表单,还是一个基于API token的系统(译注:oauth一类),又或是专有架构的single-sign-on系统,Guard组件都可以把事情变得简单...而有趣!

在本例中,你将构建一个API token认证系统,并学习到如何活用Guard。

创建一个User和一个User Provider ¶

无论你打算如何来认证,都需要建立一个实现了 UserInterface 接口的User类,然后再配置一个 user provider。本例中的user是存于Doctrine驱动的数据库中的,并且每位用户都有一个 apiKey 属性以便能够通过API接口来访问自己的账号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
 
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;
 
    /**
     * @ORM\Column(type="string", unique=true)
     */
    private $username;
 
    /**
     * @ORM\Column(type="string", unique=true)
     */
    private $apiKey;
 
    public function getUsername()
    {
        return $this->username;
    }
 
    public function getRoles()
    {
        return ['ROLE_USER'];
    }
 
    public function getPassword()
    {
    }
    public function getSalt()
    {
    }
    public function eraseCredentials()
    {
    }
 
    // more getters/setters / 更多 getters/setters
}

此处用户并不需要密码,当然你可以添加一个 password 属性,以便同时允许用户通过密码来登陆(借助登陆表单)。

你的 User 类并非一定要存入Doctrine中:请根据需要自行安排。接下来,确保已经配置好一个“user provider”给你的用户:

1
2
3
4
5
6
7
8
9
10
11
# app/config/security.yml
security:
    # ...

    providers:
        your_db_provider:
            entity:
                class: AppBundle:User
                property: apiKey
 
    # ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <config>
        <!-- ... -->
 
        <provider name="your_db_provider">
            <entity class="AppBundle:User" />
        </provider>
 
        <!-- ... -->
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'providers' => array(
        'your_db_provider' => array(
            'entity' => array(
                'class' => 'AppBundle:User',
            ),
        ),
    ),
 
    // ...
));

就是这些!需要进一步了解相关内容,请参考:

  • 如何从数据库中加载Security用户(通过Entity Provider)

  • 如何创建自定义的User Provider

步骤 1)创建Authenticator类 ¶

假设,你有一个API接口,你的客户端在每次请求时都带着它们的API token,并发出一个 X-AUTH-TOKEN 头。你要做的是,读取这个token,然后找到相应的用户(如果有的话)。

为了创建自定义的认证体系,只要创建一个类,令其实现 GuardAuthenticatorInterface接口。或者,去扩展更简单的 AbstractGuardAuthenticator 抽象类——此时需要你实现6个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// src/AppBundle/Security/TokenAuthenticator.php
namespace AppBundle\Security;
 
use Symfony\Component\Httpfoundation\Request;
use Symfony\Component\HttpFoundation\jsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Doctrine\ORM\EntityManager;
 
class TokenAuthenticator extends AbstractGuardAuthenticator
{
    private $em;
 
    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }
 
    /**
     * Called on every request. Return whatever credentials you wAnt,
     * or null to stop authentication.
     */
    public function getCredentials(Request $request)
    {
        if (!$token = $request->headers->get('X-AUTH-TOKEN')) {
            // no token? Return null and no other methods will be called
            // 没有token就返回null,不调用其他方法
            return;
        }
 
        // What you return here will be passed to getUser() as $credentials
        // 这里你返回的值,将被作为$credentials传入getUser()
        return array(
            'token' => $token,
        );
    }
 
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $apiKey = $credentials['token'];
 
        // if null, authentication will fail
        // 如果是空,认证失败
        // if a User object, checkCredentials() is called
        // 如果是个User对象,checkCredentials()将被调用
        return $this->em->getRepository('AppBundle:User')
            ->findOneBy(array('apiKey' => $apiKey));
    }
 
    public function checkCredentials($credentials, UserInterface $user)
    {
        // check credentials - e.g. make sure the password is valid
        // 检查credentials - 比如,确保密码是有效的
        // no credential check is needed in this case
        // 但在本例中并不需要对credential检查
        // return true to cause authentication success
        // 返回true即是认证成功
        return true;
    }
 
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // on success, let the request continue
        // 成功之后,让请求继续
        return null;
    }
 
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $data = array(
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
 
            // or to translate this message
            // 或者翻译信息如下
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        );
 
        return new JsonResponse($data, 403);
    }
 
    /**
     * Called when authentication is needed, but it's not sent
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $data = array(
            // you might translate this message
            // 你也可以翻译这条信息
            'message' => 'Authentication Required'
        );
 
        return new JsonResponse($data, 401);
    }
 
    public function supportsRememberMe()
    {
        return false;
    }
}

干得漂亮!对每一个方法的解释,请参考:Guard Authenticator方法

步骤 2)配置Authenticator类 ¶

要做这一步,先将类定义为服务:

1
2
3
4
# app/config/services.yml
services:
    app.token_authenticator:
        class: AppBundle\Security\TokenAuthenticator
1
2
3
4
<!-- app/config/services.xml -->
<services>
    <service id="app.token_authenticator" class="AppBundle\Security\TokenAuthenticator" />
</services>
1
2
3
4
5
6
// app/config/services.php
use AppBundle\Security\TokenAuthenticator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->register('app.token_authenticator', TokenAuthenticator::class);

最后,配置 security.yml 中的 firewall 节点下的选项,即可使用此authenticator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# app/config/security.yml
security:
    # ...

    firewalls:
        # ...

        main:
            anonymous: ~
            logout: ~

            guard:
                authenticators:
                    - app.token_authenticator
 
            # if you want, disable storing the user in the session
            # 如果你想,可以关闭在session中存储用户
            # stateless: true
 
            # maybe other things, like form_login, remember_me, etc
            # ... 其他一些东东,像是form_login, remember_me, 等等 ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
    <config>
        <!-- ... -->
 
        <firewall name="main"
            pattern="^/"
            anonymous="true"
        >
            <logout />
 
            <guard>
                <authenticator>app.token_authenticator</authenticator>
            </guard>
 
            <!-- ... -->
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/config/security.php
 
// ..
 
$container->loadFromExtension('security', array(
    'firewalls' => array(
        'main'       => array(
            'pattern'        => '^/',
            'anonymous'      => true,
            'logout'         => true,
            'guard'          => array(
                'authenticators'  => array(
                    'app.token_authenticator'
                ),
            ),
            // ...
        ),
    ),
));

你做到了!现在你已经有了一个“完全作动正常”的API token认证系统。如果你的homepage页面需要 ROLE_USER ,那么你可以在以下条件下进行测试:

1
2
3
4
5
6
7
8
9
10
11
# test with no token / 不用token来测试
curl http://localhost:8000/
# {"message":"Authentication Required"}
 
# test with a bad token / 用非法token测试
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
# {"message":"Username could not be found."}
 
# test with a working token / 用一个正常的token来测试
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
# the homepage controller is executed: the page loads normally

现在,准备深入学习每一个方法。

Guard Authenticator方法 ¶

每一个authenticator都需要以下方法:

getCredentials(Request $request)
该方法将在 每次 请求时被调用,你的任务就是从请求中读取token(或任何需要“认证”的信息)然后返回它。如果你返回 null ,剩下的认证进程将被忽略。否则,getUser() 将被调用,那个返回值将作为第一个参数传入。
getUser($credentials, UserProviderInterface $userProvider)
如果 getCredentials() 返回的是非空值,那么本方法将被调用,返回值作为 $credentials 参数。你要做的是,返回一个实现了 UserInterface 的对象。如果返回正确,checkCredentials() 方法将被调用。如果你返回的是 null (或抛出了 AuthenticationException) 那么就认证失败。
checkCredentials($credentials, UserInterface $user)
如果 getUser() 返回的是一个User对象,本方法将被调用。你要做的是,认证credentials的正确性。对于表单登陆来说,这就是你检查用户密码是否正确的地方。为了通过认证,必须返回 true任何 其他返回值 (或抛出了 AuthenticationException),即为认证失败。
onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
认证成功时调用。你要做的是,返回一个发送到客户端的 Response 对象,或者返回 null 以将当前请求继续(也就是允许路由/控制器按常规方式运作)。由于本例是一个API,每次请求都要认证请求自身,因此返回 null 即可。
onAuthenticationFailure(Request $request, AuthenticationException $exception)
认证失败时调用。你要做的是返回 Response 对象到客户端。 $exception 会告诉你认证过程哪里出了错。
start(Request $request, AuthenticationException $authException = null)
如果客户端在访问URL/resource时需要认证,但却没有认证细节发出(比如你在 getCredentials() 中返回了null)时,本方法将被调用。你要做的是,返回一个 Response 来帮助用户进行认证(如,打出401响应头,告之“找不到token!”)
supportsRememberMe()
如果你需要“remember me”功能,利用本方法返回true。你不得不在firewall中激活 remember_me 以便能够正常工作。由于本例是一个stateless API,你并不需要支持“remember me”功能。
createAuthenticatedToken(UserInterface $user, string $providerKey)
如果你实现的是 GuardAuthenticatorInterface 接口而不是继承 AbstractGuardAuthenticator 抽象类,你就得使用本方法。它将在认证成功之后用,为用户创建并返回token,此用户即是提供的第一个参数。

自定义错误信息 ¶

onAuthenticationFailure() 被调用时,会有一个 AuthenticationException 传入,它通过 $e->getMessageKey() 来描述为何认证失败(另有 $e->getMessageData() )。这些错误信息,基于“认证失败”发生阶段的不同(如 getUser() 之于 checkCredentials() ),而有所区别。

但是,你可以轻松返回自定义的错误信息,通过 CustomUserMessageAuthenticationException 来实现。你可以在 getCredentials()getUser()checkCredentials() 等地方来引发认证失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/AppBundle/Security/TokenAuthenticator.php
// ...
 
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
 
class TokenAuthenticator extends AbstractGuardAuthenticator
{
    // ...
 
    public function getCredentials(Request $request)
    {
        // ...
 
        if ($token == 'ILuvAPIs') {
            throw new CustomUserMessageAuthenticationException(
                'ILuvAPIs is not a real API key: it\'s just a silly phrase'
            );
        }
 
        // ...
    }
 
    // ...
}

下面的例程中,因为“ILuvAPIs”是一个滑稽的API key,你大可抛出“彩蛋”来返回定制的错误信息:

1
2
curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
# {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"}

常见问题 ¶

可以有多个Authenticator吗?

可以!只是你这样做的时候,必须指定 一个 authenticator为“entry_point”(译注:入口级认证器)。这意味着你将要选择,究竟 哪个 authenticator的 start()方法应该被调用——当一个用户要访问受保护的内容时。例如,假设你有一个 app.form_login_authenticator 来处理传统的表单登录。当一个用户访问受保护页面时,你要执行authenticator中的 start() 方法,然后将他们重定向到登录页面(而不再返回一个JSON响应):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/config/security.yml
security:
    # ...

    firewalls:
        # ...

        main:
            anonymous: ~
            logout: ~

            guard:
                authenticators:
                    - app.token_authenticator
 
            # if you want, disable storing the user in the session
            # 如果需要,你可以关闭将用户存到session中
            # stateless: true
 
            # maybe other things, like form_login, remember_me, etc
            # 下面可能是其他内容,诸如form_login, remember_me等配置
            # ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
    <config>
        <!-- ... -->
 
        <firewall name="main"
            pattern="^/"
            anonymous="true"
        >
            <logout />
 
            <guard>
                <authenticator>app.token_authenticator</authenticator>
            </guard>
 
            <!-- ... -->
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/config/security.php
 
// ..
 
$container->loadFromExtension('security', array(
    'firewalls' => array(
        'main'       => array(
            'pattern'        => '^/',
            'anonymous'      => true,
            'logout'         => true,
            'guard'          => array(
                'authenticators'  => array(
                    'app.token_authenticator'
                ),
            ),
            // ...
        ),
    ),
));
可以和form_login一起使用吗?
可以!form_login 是认证用户的 一种,因此你可以使用它,然后 添加一或多个authenticator。使用Guard Authenticator并不与其他认证方式冲突。
可以和FOSUserBundle一起使用吗?
可以!实际上,FOSUserBundle并不操作Security:它只提供给你一个 User 对象,和一些路由、控制器来方便登陆、注册、忘记密码等操作。当你使用FOSUserBundle时,一般都会选择 form_login 来完成用户的认证。但你还可以做更多(参考前面两个问题),或者使用FOSUserBundle中的 User 对象来创建你自己的authenticator(s)(恰如本文所述的那样)。