支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
参考 如何使用Guard创建一个自定义验证系统 以一个更简单、更灵活的方式来完成类似的自定义身份认证的任务。
如今,使用API key(例如在开发一个web service时)来认证用户是很普遍的事。API key在每次请求中都被提供,作为查询字符串参数(query string parameter )或通过 HTTP 头信息而传入。
基于请求信息来认证用户应通过预认证机制(pre-authentication mechanism)来完成。SimplePreAuthenticatorInterface
接口让你很容易地实现此类需求。
你遇到的真实场景可能有所不同,但在本例中,token的获取来自 apikey
query参数,根据此值来加载相应的用户名,然后User对象得以创建:
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 | // src/AppBundle/Security/ApiKeyAuthenticator.php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
// look for an apikey query parameter
// 寻找一个apikey query 参数
$apiKey = $request->query->get('apikey');
// or if you want to use an "apikey" header, then do something like this:
// 或者,若你想使用一个 “apikey” 头时:
// $apiKey = $request->headers->get('apikey');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// 或者,直接跳过 api key 的认证
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
// 注意:此异常信息将被返回至客户端
// 请不要在这里放置任何敏感信息 / 错误字符串
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
} |
一旦你 配置 好所有内容,就可以把 apikey
参数添加到查询字符串中,比如 http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2
。
认证过程需要若干步骤,你的具体实现可能有所不同:
在请求周期的早期,Symfony调用 createToken()
。此时你要做的就是去创建一个“包含了请求中的所有信息”的token对象,并针对此请求进行用户认证(比如通过 apikey
query parameter) 。如果缺少这些信息,会抛出 BadCredentialsException
异常,从而导致认证失败。你可能想要返回 null
而不是跳过身份认证,这样 Symfony 就可以回退到另一种身份认证方式(authentication method),如果有的话。
若你从 createToken()
方法返回 null
,确保在防火墙中开启了 anonymous
。这样你就可以得到一个 AnonymousToken
。
Symfony 调用 createToken()
之后,将调用你的类中的 supportsToken()
方法(以及任何其它的authentication listeners)来计算出应该由谁来处理(当前的)token。这只是一种 “允许多个身份认证机制用于同一防火墙” 的方式(通过这种方式,你可以先尝试使用certificate或者API key来认证用户,[未通过的话]然后再回滚到表单登录)。
多数情况下,对于由 createToken()
方法所创建的token,只需要去让方法返回true
。你的逻辑应与本例颇为相似。
如果 supportsToken()
返回 true
,Symfony即调用 authenticateToken()
。$userProvider
是一个关键之处,它是个外部的类,帮助你加载用户信息。后面你会了解更多。
在这个特定的例子中,以下事件发生在 authenticateToken()
中:
首先,你使用 $userProvider
来以某种方式查找 $apiKey
相对应的 $username
;
其次,你再次使用 $userProvider
来针对这个 $username
去加载或者创建一个 User
对象;
最后,你要创建一个 authenticated token (即,一个至少拥有一个role的token),它有你赋予的正确的roles,同时User对象也被赋予它。
最终目标是要用 $apiKey
去找出或创建一个 User
对象。具体你要 如何 找出(如,查询数据库),以及你的 User
对象之细节,是多样化的。这些不同,在你的user provider中体现的尤为明显。
$userProvider
可以是任何user provider(参考 如何创建自定义的User Provider )。本例中,$apiKey
用来以某种方式针对用户进行“用户名查找”。这是由 getUsernameForApiKey()
方法完成的,在我们这个使用场景下,它被完全自定义地创建出来(即,它并非Symfony内核中的user provider系统中的某个方法)。
$userProvider
可能如下面所示:
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 | // src/AppBundle/Security/ApiKeyUserProvider.php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
public function getUsernameForApiKey($apiKey)
{
// Look up the username based on the token in the database, via
// an API call, or do something entirely different
// 通过对 API 进行调用,或是某些完全不同的东东,来基于数据库中的token查出用户名
$username = ...;
return $username;
}
public function loadUserByUsername($username)
{
return new User(
$username,
null,
// the roles for the user - you may choose to determine
// these dynamically somehow based on the user
// 用户的roles - 根据用户的不同,你可以选择 “动态地确定” 这些roles
array('ROLE_API')
);
}
public function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
// 此方法用于在session中存储认证信息
// 但在本例中,token在每一次请求中被发送,
// 因此认证信息是stateless(无状态)的。
// 抛出这个异常,对于stateless来说,是最适合的
throw new UnsupportedUserException();
}
public function supportsClass($class)
{
return 'Symfony\Component\Security\Core\User\User' === $class;
}
} |
现在,把user provider注册为服务:
1 2 3 4 | # app/config/services.yml
services:
api_key_user_provider:
class: AppBundle\Security\ApiKeyUserProvider |
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!-- app/config/services.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<service id="api_key_user_provider"
class="AppBundle\Security\ApiKeyUserProvider" />
</services>
</container> |
1 2 3 4 5 | // app/config/services.php
// ...
$container
->register('api_key_user_provider', 'AppBundle\Security\ApiKeyUserProvider'); |
在独立章节 如何创建自定义的User Provider 中进学习。
getUsernameForApiKey()
中的逻辑是由你来决定的。通过在一个(存下了) "token" (的)数据表中获取信息,你可以把API key(如 37b51d
)通过某种方式转换成用户名(如 jondoe
)。
上面的过程同样适用于 loadUserByUsername()
。本例中,创建的只是Symfony的核心 User
类。当你不需要在User对象中存储任何额外的信息时(如 firstName
),它才有意义(译注:内置User类用于简单场合)。否则,你应该创建 你自己的 User类,再通过查询数据库来装载User。如此你就在 User
对象中添加自定义数据了。
最后,只需确保 supportsClass()
方法对User对象返回 true
,这个User类,与你在 loadUserByUsername()
中所返回的相同。
如果你的认证过程如同本例一样是stateless的(即,你希望用户在每次请求中发送API key,因此你毋须把登录信息存到session中了),所以你可以直接在 refreshUser()
中抛出 UnsupportedUserException
异常。
若你真的想在 session 中存储认证数据,以便不必在每次请求中都发送key,参考(在Session中存储Authentication信息)。
当凭据不正确或者身份认证失败时,为了能让你的 ApiKeyAuthenticator
正确的显示 401 http 状态,你应该在authenticator中实现 AuthenticationFailureHandlerInterface
接口。这便提供了一个 onAuthenticationFailure
方法供你创建一个错误信息的Response
。
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/ApiKeyAuthenticator.php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
// ...
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
// 这里包括了关于 *为何* 认证失败的信息,使用此异常,或返回你自己的信息
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
} |
一旦你完成了 ApiKeyAuthenticator
的所有设置,你需要把它注册成服务并在security配置信息中(security.yml
)使用它,首先,注册为服务。
1 2 3 4 5 6 7 | # app/config/config.yml
services:
# ...
apikey_authenticator:
class: AppBundle\Security\ApiKeyAuthenticator
public: false |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<service id="apikey_authenticator"
class="AppBundle\Security\ApiKeyAuthenticator"
public="false" />
</services>
</container> |
1 2 3 4 5 6 7 8 9 | // app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$definition = new Definition('AppBundle\Security\ApiKeyAuthenticator');
$definition->setPublic(false);
$container->setDefinition('apikey_authenticator', $definition); |
现在,在security配置中的 firewall
节点下,分别使用 simple_preauth
和 provider
选项键来激活此服务和你的自定义user provider(参考 如何创建自定义的User Provider):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # app/config/security.yml
security:
# ...
firewalls:
secured_area:
pattern: ^/api
stateless: true
simple_preauth:
authenticator: apikey_authenticator
provider: api_key_user_provider
providers:
api_key_user_provider:
id: api_key_user_provider |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- 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="secured_area"
pattern="^/api"
stateless="true"
provider="api_key_user_provider"
>
<simple-preauth authenticator="apikey_authenticator" />
</firewall>
<provider name="api_key_user_provider" id="api_key_user_provider" />
</config>
</srv:container> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // app/config/security.php
// ..
$container->loadFromExtension('security', array(
'firewalls' => array(
'secured_area' => array(
'pattern' => '^/api',
'stateless' => true,
'simple_preauth' => array(
'authenticator' => 'apikey_authenticator',
),
'provider' => 'api_key_user_provider',
),
),
'providers' => array(
'api_key_user_provider' => array(
'id' => 'api_key_user_provider',
),
),
)); |
如果你也定义了 access_control
, 确保添加一个新的入口:
1 2 3 4 5 6 | # app/config/security.yml
security:
# ...
access_control:
- { path: ^/api, roles: ROLE_API } |
1 2 3 4 5 6 7 8 9 10 | <!-- 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">
<rule path="^/api" role="ROLE_API" />
</srv:container> |
就是这样!现在,在每次请求开始的时候,你的 ApiKeyAuthenticator
会被调用,然后你的身份认证过程将会展开。
stateless
配置选项防止Symfony将用户的认证信息存在session中,因为每次请求都发送了 apikey
,再行存储就没有必要了。如果你真的 需要在session中存储认证信息的话,继续阅读!
目前为止,本节讲解的是,每一次请求都会送出某种authentication token。但在某些情况下(如 OAuth 流程),token仅在一次 请求中被发送。这时,你对用户进行认证后会把authentication信息存在session中,以便用户在后续的每一次请求中自动登录。
要想实现这一功能,首先从防火墙中删除 stateless
选项键,或者把它设为 false
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # app/config/security.yml
security:
# ...
firewalls:
secured_area:
pattern: ^/api
stateless: false
simple_preauth:
authenticator: apikey_authenticator
provider: api_key_user_provider
providers:
api_key_user_provider:
id: api_key_user_provider |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- 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="secured_area"
pattern="^/api"
stateless="false"
provider="api_key_user_provider"
>
<simple-preauth authenticator="apikey_authenticator" />
</firewall>
<provider name="api_key_user_provider" id="api_key_user_provider" />
</config>
</srv:container> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // app/config/security.php
// ..
$container->loadFromExtension('security', array(
'firewalls' => array(
'secured_area' => array(
'pattern' => '^/api',
'stateless' => false,
'simple_preauth' => array(
'authenticator' => 'apikey_authenticator',
),
'provider' => 'api_key_user_provider',
),
),
'providers' => array(
'api_key_user_provider' => array(
'id' => 'api_key_user_provider',
),
),
)); |
即便 token 被存在 session 中,凭据(credentials) - 本例中是API key (即 $token->getCredentials()
) - 出于安全原因,并不会被存在 session 中。要利用 session,更新 ApiKeyAuthenticator
来查看被存储的token是否有一个可以使用的有效User对象:
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/Security/ApiKeyAuthenticator.php
// ...
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
// ...
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
// User is the Entity which represents your user
// User 是能够呈现你的用户的 Entity
$user = $token->getUser();
if ($user instanceof User) {
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
if (!$username) {
// this message will be returned to the client
// 此信息将返回客户端
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
// ...
} |
把认证信息存到session时的工作原理是这样的:
在每一次请求结束时,Symfony会序列化token对象(从 authenticateToken()
返回),同时也会对 User
对象(因为它被设为了token的一个属性)序列化;
在下一次请求中token将被反序列化,同时,被反序列化的 User
对象将被传入user provider的 refreshUser()
函数。
第二步是重要的一步: Symfony 将调用 refreshUser()
方法并把 “序列化到session中的User对象” 传给你。如果你的用户是存储在数据库中,那么你可能希望新查询一个新鲜版本(fresh version)的user以确保它没有过期。但是,不管你的需求是什么,refreshUser()
现在应当返回User对象:
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 | // src/AppBundle/Security/ApiKeyUserProvider.php
// ...
class ApiKeyUserProvider implements UserProviderInterface
{
// ...
public function refreshUser(UserInterface $user)
{
// $user is the User that you set in the token inside authenticateToken()
// after it has been deserialized from the session
// 此处的 $user ,是它从 session 中被反序列化之后,
// 你在 authenticateToken() 中设置在 token 中的 User 对象
// you might use $user to query the database for a fresh user
// 你可能会使用 $user 来查询数据库,以获取用户的最新内容
// $id = $user->getId();
// use $id to make a query / 据此 $id 进行查询
// if you are *not* reading from a database and are just creating
// a User object (like in this example), you can just return it
// 如果你 *不* 从数据库中读取用户,
// 只是创建一个 User 对象(如同本例),则直接返回它
return $user;
}
} |
你可能还需要确保 User
对象被正确地序列化。如果你的 User
对象 包含了private属性,PHP无法对其序列化。这时,你可以取回一个“相关属性都被设为 null
”的User对象。例程参考 如何从数据库中(Entity Provider)加载Security用户。
本节假定你想在 每一次 请求中查找 apikey
认证信息。但在某些情况下(如一个OAuth流程),你只在用户抵达了一个特定的URL(如 OAuth时的重定向URL)的时候才需要查找其的认证信息。
幸运的是,处理这种情况也很容易: 只需要在 createToken()
方法在创建token之前去检查一下当前的URL是什么即可:
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 | // src/AppBundle/Security/ApiKeyAuthenticator.php
// ...
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\Request;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
protected $httpUtils;
public function __construct(HttpUtils $httpUtils)
{
$this->httpUtils = $httpUtils;
}
public function createToken(Request $request, $providerKey)
{
// set the only URL where we should look for auth information
// and only return the token if we're at that URL
// 在此设置我们应当查找auth信息的唯一URL
// 然后仅当我们处于该URL时才返回token
$targetUrl = '/login/check';
if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) {
return;
}
// ...
}
} |
在这里使用了极为好用的 HttpUtils
类来检查当前 URL 是否与你想要获取的 URL 相匹配。此时,URL (/login/check
) 在类中被写死了,但是你也可以把它作为构造函数的第二个参数来进行注入。
接下来,只需更新你的服务配置,注入 security.http_utils
服务即可:
1 2 3 4 5 6 7 8 | # app/config/config.yml
services:
# ...
apikey_authenticator:
class: AppBundle\Security\ApiKeyAuthenticator
arguments: ["@security.http_utils"]
public: false |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!-- app/config/config.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<service id="apikey_authenticator"
class="AppBundle\Security\ApiKeyAuthenticator"
public="false"
>
<argument type="service" id="security.http_utils" />
</service>
</services>
</container> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
// ...
$definition = new Definition(
'AppBundle\Security\ApiKeyAuthenticator',
array(
new Reference('security.http_utils')
)
);
$definition->setPublic(false);
$container->setDefinition('apikey_authenticator', $definition); |
就是这样!Have fun!
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。