如何通过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)(恰如本文所述的那样)。