发布于 2015-08-27 16:52:56 | 222 次阅读 | 评论: 0 | 来源: 网络整理
小技巧
Creating a custom authentication system is hard, and this entry will walk you through that process. But depending on your needs, you may be able to solve your problem in a simpler way using these documents:
If you have read the chapter on 安全性, you understand the distinction Symfony makes between authentication and authorization in the implementation of security. This chapter discusses the core classes involved in the authentication process, and how to implement a custom authentication provider. Because authentication and authorization are separate concepts, this extension will be user-provider agnostic, and will function with your application’s user providers, may they be based in memory, a database, or wherever else you choose to store them.
The following chapter demonstrates how to create a custom authentication provider for WSSE authentication. The security protocol for WSSE provides several security benefits:
WSSE is very useful for the securing of web services, may they be SOAP or REST.
There is plenty of great documentation on WSSE, but this article will focus not on the security protocol, but rather the manner in which a custom protocol can be added to your Symfony application. The basis of WSSE is that a request header is checked for encrypted credentials, verified using a timestamp and nonce, and authenticated for the requested user using a password digest.
注解
WSSE also supports application key validation, which is useful for web services, but is outside the scope of this chapter.
The role of the token in the Symfony security context is an important one. A token represents the user authentication data present in the request. Once a request is authenticated, the token retains the user’s data, and delivers this data across the security context. First, you’ll create your token class. This will allow the passing of all relevant information to your authentication provider.
// src/AppBundle/Security/Authentication/Token/WsseUserToken.php
namespace AppBundleSecurityAuthenticationToken;
use SymfonyComponentSecurityCoreAuthenticationTokenAbstractToken;
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 '';
}
}
注解
The WsseUserToken
class extends the Security component’s
AbstractToken
class, which provides basic token functionality. Implement the
TokenInterface
on any class to use as a token.
Next, you need a listener to listen on the firewall. The listener
is responsible for fielding requests to the firewall and calling the authentication
provider. A listener must be an instance of
ListenerInterface
.
A security listener should handle the
GetResponseEvent
event, and
set an authenticated token in the token storage if successful.
// src/AppBundle/Security/Firewall/WsseListener.php
namespace AppBundleSecurityFirewall;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelEventGetResponseEvent;
use SymfonyComponentSecurityCoreAuthenticationAuthenticationManagerInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityHttpFirewallListenerInterface;
use AppBundleSecurityAuthenticationTokenWsseUserToken;
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);
}
}
This listener checks the request for the expected X-WSSE
header, matches
the value returned for the expected WSSE information, creates a token using
that information, and passes the token on to the authentication manager. If
the proper information is not provided, or the authentication manager throws
an AuthenticationException
,
a 403 Response is returned.
注解
A class not used above, the
AbstractAuthenticationListener
class, is a very useful base class which provides commonly needed functionality
for security extensions. This includes maintaining the token in the session,
providing success / failure handlers, login form URLs, and more. As WSSE
does not require maintaining authentication sessions or login forms, it
won’t be used for this example.
注解
Returning prematurely from the listener is relevant only if you want to chain authentication providers (for example to allow anonymous users). If you want to forbid access to anonymous users and have a nice 403 error, you should set the status code of the response before returning.
The authentication provider will do the verification of the WsseUserToken
.
Namely, the provider will verify the Created
header value is valid within
five minutes, the Nonce
header value is unique within five minutes, and
the PasswordDigest
header value matches with the user’s password.
// src/AppBundle/Security/Authentication/Provider/WsseProvider.php
namespace AppBundleSecurityAuthenticationProvider;
use SymfonyComponentSecurityCoreAuthenticationProviderAuthenticationProviderInterface;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityCoreExceptionNonceExpiredException;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use AppBundleSecurityAuthenticationTokenWsseUserToken;
use SymfonyComponentSecurityCoreUtilStringUtils;
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;
}
}
注解
The AuthenticationProviderInterface
requires an authenticate
method on the user token, and a supports
method, which tells the authentication manager whether or not to use this
provider for the given token. In the case of multiple providers, the
authentication manager will then move to the next provider in the list.
注解
The comparsion of the expected and the provided digests uses a constant
time comparison provided by the
equals()
method of the StringUtils
class. It is used to mitigate possible
timing attacks.
You have created a custom token, custom listener, and custom provider. Now
you need to tie them all together. How do you make a unique provider available
for every firewall? The answer is by using a factory. A factory
is where you hook into the Security component, telling it the name of your
provider and any configuration options available for it. First, you must
create a class which implements
SecurityFactoryInterface
.
// src/AppBundle/DependencyInjection/Security/Factory/WsseFactory.php
namespace AppBundleDependencyInjectionSecurityFactory;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionReference;
use SymfonyComponentDependencyInjectionDefinitionDecorator;
use SymfonyComponentConfigDefinitionBuilderNodeDefinition;
use SymfonyBundleSecurityBundleDependencyInjectionSecurityFactorySecurityFactoryInterface;
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)
{
}
}
The SecurityFactoryInterface
requires the following methods:
create
getPosition
pre_auth
, form
, http
,
and remember_me
and defines the position at which the provider is called.getKey
addConfiguration
注解
A class not used in this example,
AbstractFactory
,
is a very useful base class which provides commonly needed functionality
for security factories. It may be useful when defining an authentication
provider of a different type.
Now that you have created a factory class, the wsse
key can be used as
a firewall in your security configuration.
注解
You may be wondering “why do you need a special factory class to add listeners and providers to the dependency injection container?”. This is a very good question. The reason is you can use your firewall multiple times, to secure multiple parts of your application. Because of this, each time your firewall is used, a new service is created in the DI container. The factory is what creates these new services.
It’s time to see your authentication provider in action. You will need to
do a few things in order to make this work. The first thing is to add the
services above to the DI container. Your factory class above makes reference
to service ids that do not exist yet: wsse.security.authentication.provider
and
wsse.security.authentication.listener
. It’s time to define those services.
# src/AppBundle/Resources/config/services.yml
services:
wsse.security.authentication.provider:
class: AppBundleSecurityAuthenticationProviderWsseProvider
arguments: ["", "%kernel.cache_dir%/security/nonces"]
wsse.security.authentication.listener:
class: AppBundleSecurityFirewallWsseListener
arguments: ["@security.token_storage", "@security.authentication.manager"]
<!-- src/AppBundle/Resources/config/services.xml -->
<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="wsse.security.authentication.provider"
class="AppBundleSecurityAuthenticationProviderWsseProvider" public="false">
<argument /> <!-- User Provider -->
<argument>%kernel.cache_dir%/security/nonces</argument>
</service>
<service id="wsse.security.authentication.listener"
class="AppBundleSecurityFirewallWsseListener" public="false">
<argument type="service" id="security.token_storage"/>
<argument type="service" id="security.authentication.manager" />
</service>
</services>
</container>
// src/AppBundle/Resources/config/services.php
use SymfonyComponentDependencyInjectionDefinition;
use SymfonyComponentDependencyInjectionReference;
$container->setDefinition('wsse.security.authentication.provider',
new Definition(
'AppBundleSecurityAuthenticationProviderWsseProvider', array(
'',
'%kernel.cache_dir%/security/nonces',
)
)
);
$container->setDefinition('wsse.security.authentication.listener',
new Definition(
'AppBundleSecurityFirewallWsseListener', array(
new Reference('security.token_storage'),
new Reference('security.authentication.manager'),
)
)
);
Now that your services are defined, tell your security context about your factory in your bundle class:
// src/AppBundle/AppBundle.php
namespace AppBundle;
use AppBundleDependencyInjectionSecurityFactoryWsseFactory;
use SymfonyComponentHttpKernelBundleBundle;
use SymfonyComponentDependencyInjectionContainerBuilder;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new WsseFactory());
}
}
You are finished! You can now define parts of your app as under WSSE protection.
security:
firewalls:
wsse_secured:
pattern: /api/.*
stateless: true
wsse: true
<config>
<firewall name="wsse_secured" pattern="/api/.*">
<stateless />
<wsse />
</firewall>
</config>
$container->loadFromExtension('security', array(
'firewalls' => array(
'wsse_secured' => array(
'pattern' => '/api/.*',
'stateless' => true,
'wsse' => true,
),
),
));
Congratulations! You have written your very own custom security authentication provider!
How about making your WSSE authentication provider a bit more exciting? The possibilities are endless. Why don’t you start by adding some sparkle to that shine?
You can add custom options under the wsse
key in your security configuration.
For instance, the time allowed before expiring the Created
header item,
by default, is 5 minutes. Make this configurable, so different firewalls
can have different timeout lengths.
You will first need to edit WsseFactory
and define the new option in
the addConfiguration
method.
class WsseFactory implements SecurityFactoryInterface
{
// ...
public function addConfiguration(NodeDefinition $node)
{
$node
->children()
->scalarNode('lifetime')->defaultValue(300)
->end();
}
}
Now, in the create
method of the factory, the $config
argument will
contain a lifetime
key, set to 5 minutes (300 seconds) unless otherwise
set in the configuration. Pass this argument to your authentication provider
in order to put it to use.
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']);
// ...
}
// ...
}
注解
You’ll also need to add a third argument to the wsse.security.authentication.provider
service configuration, which can be blank, but will be filled in with
the lifetime in the factory. The WsseProvider
class will also now
need to accept a third constructor argument - the lifetime - which it
should use instead of the hard-coded 300 seconds. These two steps are
not shown here.
The lifetime of each WSSE request is now configurable, and can be set to any desirable value per firewall.
security:
firewalls:
wsse_secured:
pattern: /api/.*
stateless: true
wsse: { lifetime: 30 }
<config>
<firewall name="wsse_secured"
pattern="/api/.*"
>
<stateless />
<wsse lifetime="30" />
</firewall>
</config>
$container->loadFromExtension('security', array(
'firewalls' => array(
'wsse_secured' => array(
'pattern' => '/api/.*',
'stateless' => true,
'wsse' => array(
'lifetime' => 30,
),
),
),
));
The rest is up to you! Any relevant configuration items can be defined in the factory and consumed or passed to the other classes in the container.