创建一个自定义的验证系统很难,本章将引导您来完成这一过程。但根据您的需求,也许能让你的问题解决起来简单一些,或者使用一些社区的bundle:
- How to Create a Custom Form Password Authenticator
- How to Authenticate Users with API Keys
- 如果使用第三方服务OAuth 如:Google, Facebook or Twitter, 你可以尝试使用 HWIOAuthBundle .
如果您已经也读了book的安全一章,您就已经理解了在symfony中实现安全的验证和授权的不同。本章将讨论在身份验证过程中所涉及的核心类以及如何实现一个自定义的身份验证提供者Authentication Provider。由于身份验证和授权是不同的概念,此扩展为未知的user-provider,并且会跟您应用程序的user providers一同运行,并且他们可能基于内存,数据库,或者您希望保存的其他地方。
符合 WSSE
下面的章节将演示如何创建一个自定义的authentication provider来完成WSSE验证。这个WSSE安全协议提供了几个好处:
1. 用户名/密码 加密
2. 安全的防范再次攻击
3. 不需要服务器配置
WSSE来保护像SOAP 和REST这样的web服务是非常有用的。
目前有很多关于WSSE的文档,但在这章中我们的焦点不在安全协议上,而是把一个自定义的协议添加到我们自己的symfony应用程序中。WSSE基础:在一个请求头(request header)中检查加密凭证,使用timestamp和 nonce来核实,并且使用密码报文来为发出请求的用户进行身份验证。
WSSE 还支持应用程序密钥验证,这对 web 服务非常有用,但是该内容超出了本章的介绍范围.
Token
这个token在symfony的security context中起到重要角色。token代表当前请求用户验证数据。一旦请求被认证,token会保留用户的数据,并传递这些数据到security context。首先,你需要创建你的token类。这将允许传递所有的相关信息到你的验证提供者(authentication provider)。
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/Authentication/Token/WsseUserToken.php namespace AppBundle\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; class WsseUserToken extends AbstractToken { public $created; public $digest; public $nonce; public function __construct(array $roles = array()) { parent::__construct($roles); // If the user has roles, consider it authenticated $this->setAuthenticated(count($roles) > 0); } public function getCredentials() { return ''; } } |
这个WsseUserToken类继承自安全组件AbstractToken类,来提供基础的token功能。实现TokenInterface上的任何类作为一个token。
监听器
接下来,您需要一个监听器来监听防火墙。这个监听器负责监听向防火墙发来的请求,并调用authentication provider。一个监听器必须是一个 ListenerInterface的实例。一个安全监听应该处理 GetResponseEvent事件,如果验证成功,设置一个验证token保存在token storage中。
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 |
// src/AppBundle/Security/Firewall/WsseListener.php namespace AppBundle\Security\Firewall; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Firewall\ListenerInterface; use AppBundle\Security\Authentication\Token\WsseUserToken; class WsseListener implements ListenerInterface { protected $tokenStorage; protected $authenticationManager; public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager) { $this->tokenStorage = $tokenStorage; $this->authenticationManager = $authenticationManager; } public function handle(GetResponseEvent $event) { $request = $event->getRequest(); $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/'; if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { return; } $token = new WsseUserToken(); $token->setUser($matches[1]); $token->digest = $matches[2]; $token->nonce = $matches[3]; $token->created = $matches[4]; try { $authToken = $this->authenticationManager->authenticate($token); $this->tokenStorage->setToken($authToken); return; } catch (AuthenticationException $failed) { // ... you might log something here // To deny the authentication clear the token. This will redirect to the login page. // Make sure to only clear your token, not those of other authentication listeners. // $token = $this->tokenStorage->getToken(); // if ($token instanceof WsseUserToken && $this->providerKey === $token->getProviderKey()) { // $this->tokenStorage->setToken(null); // } // return; } // By default deny authorization $response = new Response(); $response->setStatusCode(Response::HTTP_FORBIDDEN); $event->setResponse($response); } } |
这个监听器检查这个请求的预期头X-WSSE,比对这个值并返回WSSE信息,使用这个信息创建一个token,并且传递这个token到验证管理。如果没有提供正确的信息,或身份验证管理抛出一个 AuthenticationException,将会返回一个403页面。
在上面的过程中没有使用到 AbstractAuthenticationListener 类,它是一个非常有用并且为安全性扩展插件提供了常用功能的基类。其中包括在 session 中维持令牌功能,提供成功 / 失败的处理程序、 登录表单的 URL,以及更多的功能。因为 WSSE 不需要在 session 中 保持身份验证或登录表单,所以在本实例中没有用到它。
你如果你想链接验证提供者你可以让监听早一点返回(例如允许匿名用户)。如果您想要禁止匿名用户访问,并且能够较好地展示 403 错误,则应在返回结果之前设置响应的状态码。
Authentication Provider
这个authentication provider需要做的就是核实WsseUserToken。 也就是说,这个provider (验证器)要在5分钟之内验证Created头部的值是否有效,在五分钟里Nonce头的值是唯一的,并且PasswordDigest头的值要匹配用户密码。
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 |
// src/AppBundle/Security/Authentication/Provider/WsseProvider.php namespace AppBundle\Security\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\NonceExpiredException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use AppBundle\Security\Authentication\Token\WsseUserToken; use Symfony\Component\Security\Core\Util\StringUtils; class WsseProvider implements AuthenticationProviderInterface { private $userProvider; private $cacheDir; public function __construct(UserProviderInterface $userProvider, $cacheDir) { $this->userProvider = $userProvider; $this->cacheDir = $cacheDir; } public function authenticate(TokenInterface $token) { $user = $this->userProvider->loadUserByUsername($token->getUsername()); if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { $authenticatedToken = new WsseUserToken($user->getRoles()); $authenticatedToken->setUser($user); return $authenticatedToken; } throw new AuthenticationException('The WSSE authentication failed.'); } /** * This function is specific to Wsse authentication and is only used to help this example * * For more information specific to the logic here, see * https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129 */ protected function validateDigest($digest, $nonce, $created, $secret) { // Check created time is not in the future if (strtotime($created) > time()) { return false; } // Expire timestamp after 5 minutes if (time() - strtotime($created) > 300) { return false; } // Validate that the nonce is *not* used in the last 5 minutes // if it has, this could be a replay attack if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) { throw new NonceExpiredException('Previously used nonce detected'); } // If cache directory does not exist we create it if (!is_dir($this->cacheDir)) { mkdir($this->cacheDir, 0777, true); } file_put_contents($this->cacheDir.'/'.$nonce, time()); // Validate Secret $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); return StringUtils::equals($expected, $digest); } public function supports(TokenInterface $token) { return $token instanceof WsseUserToken; } } |
AuthenticationProviderInterface需要一个authenticate方法验证用户token,和一种能够告诉authentication manager是否为给定的token使用provider的supports方法。在众多提供程序中,身份验证管理器会根据列表依次移动到每个提供程序。
预期的比较和提供的报文会使用
StringUtils类
equals()方法提供的恒定的时间比较。它的作用是用来减少可能的 timing attacks。
工厂模式
你已经创建一个自定义的token,定义了监听,并且定义了provider(提供者)。现在你需要把他们放到一起。如何做才能为每个防火墙提供一个独特的provider程序呢?答案是通过使用工厂。工厂是你在安全组件之前处理(hook),告诉他你的提供者名称和它的所有配置选项。首先,你一定要创建一个类并实现SecurityFactoryInterface
.
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 |
// src/AppBundle/DependencyInjection/Security/Factory/WsseFactory.php namespace AppBundle\DependencyInjection\Security\Factory; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; class WsseFactory implements SecurityFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.wsse.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) ->replaceArgument(0, new Reference($userProvider)) ; $listenerId = 'security.authentication.listener.wsse.'.$id; $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener')); return array($providerId, $listenerId, $defaultEntryPoint); } public function getPosition() { return 'pre_auth'; } public function getKey() { return 'wsse'; } public function addConfiguration(NodeDefinition $node) { } } |
SecurityFactoryInterface 需要下列方法:
create 方法
这个方法把这个监听和authentication provider添加到security context适合的依赖容器。
getPosition 方法
当provider被调用时返回。他能够返回一个pre_auth
,form
, http
或remember_me
.
getKey 方法
该方法的定义是用来引用防火墙配置中的提供程序的配置键。
addConfiguration 方法
该方法用于定义您安全配置中的配置键下的配置选项。在后面将要介绍设置配置选项。
在本例中,一直没有使用
AbstractFactory类,他是一个非常有用的基类,并且他为安全工厂模式提供了常用的功能。当定义不同类型的验证提供者(authentication provider)时,它非常有用。
现在,您已经创建了一个工厂类,在您的安全配置中wsse键可以作为一个防火墙。
你可能会问,“您为什么需要一个特殊的工厂类,将监听和提供者添加到依赖注入容器呢?”这是一个非常好的问题。原因是,你可以多次使用你的防火墙,来保护您应用程序的各个部分。正因为如此,每次使用防火墙时,再依赖容器中便会创建一个新服务。工厂的作用就是创建这些新服务。
配置
是时候该看看你的authentication provider在action中的程序。为了让他工作,你需要做几件事。你要做的第一件事就是将上面的服务添加到依赖容器。这个工厂类中的服务id还不存在: wsse.security.authentication.provider和
dwsse.security.authentication.listener。现在是时候来定义这些服务了。
1 2 3 4 5 6 7 8 9 |
# src/AppBundle/Resources/config/services.yml ----- services: wsse.security.authentication.provider: class: AppBundle\Security\Authentication\Provider\WsseProvider arguments: ["", "%kernel.cache_dir%/security/nonces"] wsse.security.authentication.listener: class: AppBundle\Security\Firewall\WsseListener arguments: ["@security.token_storage", "@security.authentication.manager"] |
到现在,您的服务已经定义好了,现在可以把您的bundle类中的工厂告诉您的安全环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// src/AppBundle/AppBundle.php namespace AppBundle; use AppBundle\DependencyInjection\Security\Factory\WsseFactory; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; class AppBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $extension = $container->getExtension('security'); $extension->addSecurityListenerFactory(new WsseFactory()); } } |
这样我们就完成了配置!您现在可以在 WSSE 的保护下以定义您的应用程序了。
1 2 3 4 5 6 7 |
# ------ security: firewalls: wsse_secured: pattern: /api/.* stateless: true wsse: true |
祝贺您!您已经完成了您的定义安全身份验证提供程序的编写!
补充
为什么不让你的WSSE authentication provider 更加精彩呢?可能性是很多的。你为什么不加入一些闪烁的光芒呢?
配置
您可以在您的安全配置中的 wsse 键下添加自定义选项。举个例子,允许Created头部项的默认时间是5分钟。使用配置,可以让不同的防火墙有不同的超时时间。
首先,你需要编辑WsseFactory并且在addConfiguration方法中添加新选项;
1 2 3 4 5 6 7 8 9 10 11 12 |
class WsseFactory implements SecurityFactoryInterface { // ... public function addConfiguration(NodeDefinition $node) { $node ->children() ->scalarNode('lifetime')->defaultValue(300) ->end(); } } |
现在,在工厂的create方法中,这个$config参数将包含一个lifetime键,设置为5分钟(300秒)除非在配置中设置了其他时间。然后向您的身份验证提供程序(authentication provider)传递此参数来使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class WsseFactory implements SecurityFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.wsse.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) ->replaceArgument(0, new Reference($userProvider)) ->replaceArgument(2, $config['lifetime']); // ... } // ... } |
您还需要为 wsse.security.authentication.provider 服务配置添加第三个参数,它可以是空白的,但必须在工厂的生存期内填充。WsseProvider类的构造函数还需要第三个参数-lifetime – 它用来替代硬编码的300秒。这两个步骤都没有在这里显示。
每个WSSE请求的生命周期都是可配置的,并且可以被设置为每个防火墙都期望的值。
1 2 3 4 5 6 |
security: firewalls: wsse_secured: pattern: /api/.* stateless: true wsse: { lifetime: 30 } |
剩下的就靠你了!在工厂中你可以定义任何相关的配置,在容器中消耗或传递到其他类。