如今,使用API去验证一个用户的身份是很平常的事情(例如开发一个web服务的时候)。该 API 密钥可以为每个请求提供服务,并且以查询字符串参数的形式或通过 HTTP 头部信息进行传递。
API密钥身份验证
我们应该通过预身份验证机制请求信息来对用户身份进行验证。SimplePreAuthenticatorInterface 接口能让您很容易的达到这个目的。
您的实际情况可能会有所不同,但在此示例中,一个(token)令牌就是apikey的参数,然后我们就可以正确加载用户名,并创建用户对象。
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 |
// src/AppBundle/Security/ApiKeyAuthenticator.php namespace AppBundle\Security; use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface { public function createToken(Request $request, $providerKey) { // look for an apikey query parameter 获得apikey参数 $apiKey = $request->query->get('apikey'); // or if you want to use an "apikey" header, then do something like this: // $apiKey = $request->headers->get('apikey'); if (!$apiKey) { throw new BadCredentialsException('No API key found'); // or to just skip api key authentication // return null; } return new PreAuthenticatedToken( 'anon.', $apiKey, $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) { throw new AuthenticationException( sprintf('API Key "%s" does not exist.', $apiKey) ); } $user = $userProvider->loadUserByUsername($username); return new PreAuthenticatedToken( $user, $apiKey, $providerKey, $user->getRoles() ); } public function supportsToken(TokenInterface $token, $providerKey) { return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; } } |
一旦你已经配置好了一切,你就可以给apikey添加字符串参数来进行验证了,就像:
http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2
验证需要几个步骤,根据你的情况可能略有不同:
1. createToken方法
在请求周期的早期,symfony会调用createToken().在这里您需要做的就是去创建一个包含所有消息的令牌对象,这些消息是来自于您的某个请求,用户进行身份验证需要您这个请求 (例如 apikey 查询参数) 。如果缺少这些信息,则会抛出 BadCredentialsException 异常从而导致身份验证失败。相反,您可能想要跳过身份验证并且不返回信息,所以 Symfony 可以回退到另一种身份验证方法,如果存在这种方法。
2.supportsToken方法
当 Symfony 调用 createToken() 之后,它将调用您的类中的 supportsToken() 方法(和任何其它的身份验证监听器)来弄清到底应该谁来处理令牌。这只是一种方式,允许多个身份验证机制用于相同的防火墙(用这种方式,你可以先尝试使用证书或者API密钥来进行身份验证,或者滚回到表单登陆)。
大多数情况下,你只需要去让这个方法返回true,也就是令牌是由createToken方法创建的。你的逻辑应该跟这个例子的差不多。
3. authenticateToken
如果supportsToken()返回true,symfony就会立刻调用authenticateToken()。$userProvider是其中的一个关键部分,它来自外部的类,他可以帮助我们加载关于用户的信息。下面你将会了解更多。
在特定的例子中,下面的事情可能会在authenticateToken()中发生:
1.首先,您可以使用$userProvider以某种方式查找$apiKey对应的$username。
2.其次,你使用 $userProvider根据
$username加载和创建一个User
对象。
3.最后,你要创建一个authenticated token(例如,一个token至少有一个角色),你要把适当的角色和用户对象附加给他。
最终就是使用$apiKey去查找并创建用户对象。你如何去做(例如,查询数据库)要看你的用户对象类都有哪些不同。这些差异在user provider最为明显。
User Provider
这个$userProvider能够是任何的用户提供器(请参阅 How to Create a custom User Provider )。在此例子中,以某种方式用$apiKey 为用户查找用户名。这项工作是在 getUsernameForApiKey() 方法中完成的,创建完全自定义的样例(他不是一个symfony核心用户代理系统中的方法)。
$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 |
// 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 $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 array('ROLE_USER') ); } 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 throw new UnsupportedUserException(); } public function supportsClass($class) { return 'Symfony\Component\Security\Core\User\User' === $class; } } |
这时,把您的用户提供程序注册成为一种服务:
1 2 3 4 |
# app/config/services.yml services: api_key_user_provider: class: AppBundle\Security\ApiKeyUserProvider |
请阅读特定的文章来学习 how to create a custom user provider.
getUsernameForApiKey() 方法中的代码逻辑是由你来决定的。你可能使用API KEY(如37b51d)通过某种方式从一个存有token的数据表中获取一些信息包括用户名(如jondoe)。
上面的过程同样适用于 loadUserByUsername()。在这个例子中,symfony核心User类创建很简单。如果你不需要任何额外的信息存储在用户对象中,他就很容易。不过如果您需要去存储更多的信息,那么您可以创建一个您自己的用户类并且通过查询数据库来填充它,这样将允许您在用户(User)对象中添加自定义数据。
最后,就像任何通过loadUserByUsername() 方法返回的用户类一样,我们只需要确保 supportsClass() 方法为用户对象并返回True。如果你的验证是没有状态的就像这个例子(你希望每次请求都发送密钥,你就不需要保存session了),那么你只用在refreshUser()中简单的抛出异常UnsupportedUserException。
如果你想要在 session 中存储身份验证数据,那么并不需要在每个请求中发送秘钥,请参阅下面(在session中存储验证)。
处理失败的验证
当凭据验证失败或者身份验证失败时,为了能让您的 ApiKeyAuthenticator 正确的显示 403 http 状态,您应该在您的身份验证器中实现 AuthenticationFailureHandlerInterface 接口。您可以使用该接口中的onAuthenticationFailure 方法去创建一个错误Response。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Security/ApiKeyAuthenticator.php namespace AppBundle\Security; use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; 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("Authentication Failed.", 403); } } |
配置
当你配置完ApiKeyAuthenticator所有配置后,你需要注册他为一个服务并且使用它在你的安全配置中(security.yml),首先,注册为一个服务。
1 2 3 4 5 6 7 |
# app/config/config.yml services: # ... apikey_authenticator: class: AppBundle\Security\ApiKeyAuthenticator public: false |
现在,你在安全配置中(security.yml)的防火墙中分别使用simple_preauth和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: ^/admin stateless: true simple_preauth: authenticator: apikey_authenticator provider: api_key_user_provider providers: api_key_user_provider: id: api_key_user_provider |
完成了上述步骤!现在,在每个请求开始的时候,您的 ApiKeyAuthenticator 方法都会被调用,然后将进行身份验证过程。
这个stateless参数配置防止symfony将用户验证信息存储在session中,因为每次请求都发送api给你,你就没有存储的必要。如果你需要存储验证到session的话,就继续阅读吧!
在Session中存储验证
到目前为止,每个请求都会传送验证令牌。但某些情况下(例如OAuth流程)这个token请求可能只发送一遍。在这种情况下,你一定会想到对用户进行验证并将验证信息存储在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: ^/admin stateless: false simple_preauth: authenticator: apikey_authenticator provider: api_key_user_provider providers: api_key_user_provider: id: api_key_user_provider |
即使令牌被存储在 session 中,在这种情况下由于某些安全性原因,凭据和 API 密钥 (即 $token->getCredentials()) 不会被存储在 session 中。如果想要利用 session,请更新 ApiKeyAuthenticator 来查看被存储的令牌是否有一个可以使用的有效用户对象:
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 |
// 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 = $token->getUser(); if ($user instanceof User) { return new PreAuthenticatedToken( $user, $apiKey, $providerKey, $user->getRoles() ); } if (!$username) { throw new AuthenticationException( sprintf('API Key "%s" does not exist.', $apiKey) ); } $user = $userProvider->loadUserByUsername($username); return new PreAuthenticatedToken( $user, $apiKey, $providerKey, $user->getRoles() ); } // ... } |
在 session 中存储身份验证信息工作原理是这样的:
1. 在每一个请求结束后,symfony会序列化令牌(token )对象(从authenticateToken()返回),他也序列化User对象(在token中设置了他的属性);
2.在下一个请求中令牌将被反序列化并且被反序列化的用户对象将被传送给用户提供程序中的 refreshUser() 函数。第二步是最重要的: Symfony 将会调用 refreshUser() 方法并把在 session 周期中序列化的用户对象传递给您。如果您的用户信息存储在数据库中,然后你可能想要重新查询一个新版本的用户信息来确保还没过期。但是,如果不理会您的要求,refreshUser() 现在应该返回用户对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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 // you might use $user to query the database for a fresh user // $id = $user->getId(); // use $id to make a query // if you are *not* reading from a database and are just creating // a User object (like in this example), you can just return it return $user; } } |
你还要确保你的User对象被正确序列化。如果你的User对象是私有属性,php则不能序列化。在这种情况下,你可能会得到一个空的对象,他的每个属性都是null.有关示例,请阅读 How to Load Security Users from the Database (the Entity Provider)。
只验证某些URL
该项目假设你想在每一个请求中查找apikey验证。但在某些情况下(如一个OAuth流程)一旦用户已经到达了一个特定的url,他只需要寻找用户的验证信息。(例如 OAuth的重定向)。
幸运的是,处理这种情况很容易: 只要检查一下使用 createToken() 方法创建令牌之前的 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 |
// 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 $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 |
完了!玩的开心!