支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
不管你是建立一个传统的登陆表单,还是一个基于API token的系统(译注:oauth一类),又或是专有架构的single-sign-on系统,Guard组件都可以把事情变得简单...而有趣!
在本例中,你将构建一个API token验证系统,并学习到如何活用Guard。
无论你打算如何来验证,都需要建立一个实现了 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 | # app/config/security.yml
security:
# ...
providers:
your_db_provider:
entity:
class: AppBundle:User
# ... |
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> |
就是这些!需要进一步了解相关内容,请参考:
假设,你有一个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方法
要做这一步,先将类定义为服务:
1 2 3 4 5 | # app/config/services.yml
services:
app.token_authenticator:
class: AppBundle\Security\TokenAuthenticator
arguments: ['@doctrine.orm.entity_manager'] |
1 2 3 4 5 6 | <!-- app/config/services.xml -->
<services>
<service id="app.token_authenticator" class="AppBundle\Security\TokenAuthenticator">
<argument type="service" id="doctrine.orm.entity_manager"/>
</service>
</services> |
1 2 3 4 5 6 7 8 | // app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
$container->setDefinition('app.token_authenticator', new Definition(
'AppBundle\Security\TokenAuthenticator',
array(new Reference('doctrine.orm.entity_manager'))
)); |
最后,配置 security.yml
中的 firewall
节点下的选项,即可使用此authenticator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # app/config/security.yml
security:
# ...
firewalls:
# ...
main:
anonymous: ~
logout: ~
guard:
authenticators:
- app.token_authenticator
# if you want, disable storing the user in the session
# stateless: true
# maybe other things, like form_login, remember_me, etc
# ... |
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 |
你做到了!现在你已经有了一个“完全作动正常”的API token验证系统。如果你的homepage页面需要 ROLE_USER
,那么你可以在以下条件下进行测试:
1 2 3 4 5 6 7 8 9 10 11 | # test with no token
curl http://localhost:8000/
# {"message":"Authentication Required"}
# test with a bad token
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
# {"message":"Username could not be found."}
# test with a working token
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
# the homepage controller is executed: the page loads normally |
现在,准备深入学习每一个方法。
每一个authenticator都需要以下方法:
null
,剩下的验证进程将被忽略。否则,getUser()
将被调用,那个返回值将作为第一个参数传入。getCredentials()
返回的是非空值,那么本方法将被调用,返回值作为 $credentials
参数。你要做的是,返回一个实现了 UserInterface
的对象。如果返回正确,checkCredentials()
方法将被调用。如果你返回的是 null
(或抛出了 AuthenticationException) 那么就验证失败。getUser()
返回的是一个User对象,本方法将被调用。你要做的是,验证credentials的正确性。对于表单登陆来说,这就是你检查用户密码是否正确的地方。为了通过验证,必须返回 true
。任何 其他返回值 (或抛出了 AuthenticationException),即为验证失败。Response
对象,或者返回 null
以将当前请求继续(也就是允许路由/控制器按常规方式运作)。由于本例是一个API,每次请求都要验证请求自身,因此返回 null
即可。Response
对象到客户端。 $exception
会告诉你验证过程哪里出了错。getCredentials()
中返回了null
)时,本方法将被调用。你要做的是,返回一个 Response
来帮助用户进行验证(如,打出401响应头,告之“找不到token!”)remember_me
以便能够正常工作。由于本例是一个stateless API,你并不需要支持“remember me”功能。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为“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 |
form_login
是验证用户的 一种,因此你可以使用它,然后 添加一或多个authenticator。使用Guard Authenticator并不与其他验证方式冲突。User
对象,和一些路由、控制器来方便登陆、注册、忘记密码等操作。当你使用FOSUserBundle时,一般都会选择 form_login
来完成用户的验证。但你还可以做更多(参考前面两个问题),或者使用FOSUserBundle中的 User
对象来创建你自己的authenticator(s)(恰如本文所述的那样)。本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。