Vu la taille des avirons que j'ai du sortir pour réaliser cette ébauche de module de sécurité totalement personnalisé,
j'ai pensé qu'une publication pourrait aider d'autres galériens...
L'objet est de faire un bundle qui présente un écran de login puis utilise un listener personnalisé,
un authenticationProvider, un UserProvider, un User, une Factory et tout le toutim. Avec les fichiers
de configuration qui vont bien.
Normalement, avec une application vierge, si l'on crée un Bundle avec
Et que l'on copie le code ci-dessous, on doit obtenir un squelette de sécurité personnalisé qui fonctionne.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2php app/console generate:bundle --namespace=Acme/SecurityBundle --format=yml
J'ai essayé autant que possible de personnaliser tout ce qui pouvait l'être.
Toute remarque ou suggestion qui permettrait d'améliorer la clarté et/ou la qualité de ce qui est proposé ci dessous est la bienvenue.
Tout d'abord un petit tour du circuit emprunté :
Le premier élément qui va être créé est la Factory. C'est dans cette factory que l'on va préciser le Listener
et le UserProvider que l'on compte utiliser. Pour que cela fonctionne, ils doivent être déclarés comme services
dans security.yml
Ensuite, le Listener et l'authenticationProvider seront initialisés.
À ce moment là, tout est prêt pour lancer l'action. Le loginAction est déclenché et le formulaire affiché.
L'action login_check lance la fonction attemptAuthentication du Listener.
Un jeton (Token) est créé, la fonction d'authentification (authenticate) du
UserProvider est exécutée et, en cas de succès, l'utilisateur correspondant est chargé et
son jeton authentifié (Token) est créé.
Et concrètement, ça donne quoi ?.
L'arborescence du Bundle :
La factory :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 |~src/ | |~Acme/ | | `~SecurityBundle/ | | |~Controller/ | | | |-DefaultController.php | | | `-SecurityController.php | | |~DependencyInjection/ | | | |~Security/ | | | | `~Factory/ | | | | `-UserFactory.php | | | |-AcmeSecurityExtension.php | | | `-Configuration.php | | |~Resources/ | | | |~config/ | | | | |-routing.yml | | | | |-security_factories.yml | | | | `-services.yml | | | `~views/ | | | |~Default/ | | | | `-index.html.twig | | | `~Login/ | | | `-login.html.twig | | |~Security/ | | | |~Authentication/ | | | | |~Firewall/ | | | | | `-AuthListener.php | | | | |~Provider/ | | | | | `-AuthProvider.php | | | | `~Token/ | | | | `-UserToken.php | | | `~User/ | | | |-User.php | | | `-UserProvider.php | | |+Tests/ | | `-AcmeSecurityBundle.php
Le Listener :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // src/Acme/SecurityBundle/DependencyInjection/Security/Factory/UserFactory.php namespace Acme\SecurityBundle\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 UserFactory implements SecurityFactoryInterface { public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.user.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('user.security.authentication.provider')) ->replaceArgument(0, new Reference($userProvider)) ; $listenerId = 'security.authentication.listener.user.'.$id; $listener = $container->setDefinition($listenerId, new DefinitionDecorator('user.security.authentication.listener')); return array($providerId, $listenerId, $defaultEntryPoint); } public function getPosition() { return 'pre_auth'; } public function getKey() { return 'my_user_factory'; // Utilisée dans app/config/security.yml } public function addConfiguration(NodeDefinition $node) {} }
L'authenticationProvider :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // Acme/SecurityBundle/Security/Firewall/AuthListener.php namespace Acme\SecurityBundle\Security\Authentication\Firewall; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Http\Firewall\ListenerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Component\Security\Http\HttpUtils; use Acme\SecurityBundle\Security\Authentication\Token\UserToken; // Lorsque l'on veut un formulaire de login, il faut hériter de AbstractAuthenticationListener // L'implémentation de ListenerInterface ne convient pas dans ce cas. class AuthListener extends AbstractAuthenticationListener { protected $securityContext; protected $authenticationManager; protected $httpUtils; public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $options = array()) { parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, "user", array_merge(array( 'username_parameter' => '_username', 'password_parameter' => '_password', 'intention' => 'authenticate', 'post_only' => true, ), $options)); } /** * Performs authentication. * * @param Request $request A Request instance * * @return TokenInterface The authenticated token, or null if full authentication is not possible * * @throws AuthenticationException if the authentication fails */ protected function attemptAuthentication(Request $request) { $username = trim($request->get($this->options['username_parameter'], null, true)); $password = $request->get($this->options['password_parameter'], null, true); //$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username); return $this->authenticationManager->authenticate(new UserToken($username, $password, $this->providerKey)); } public function getHttpUtils() { return $this->httpUtils; } public function setHttpUtils($httpUtils) { $this->httpUtils = $httpUtils; } }
Le UserToken :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // Acme/SecurityBundle/Security/Authentication/Provider/AuthProvider.php namespace Acme\SecurityBundle\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 Symfony\Component\Security\Core\Exception\BadCredentialsException; use Acme\SecurityBundle\Security\Authentication\Token\UserToken; class AuthProvider 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()); // $userToken = new UserToken(); // $userToken->setUser($user); // echo "it worked"; exit; $newToken = new UserToken($token->getUser(), $token->getCredentials(), "user", array("ROLE_ADMIN")); $username = $newToken->getUser(); if (empty($username)) { throw new BadCredentialsException('Bad credentials :)'); } //return $newToken; if ($user && $this->validate()) { $authenticatedToken = new UserToken($token->getUser(), $token->getCredentials(), "user", $user->getRoles()); $authenticatedToken->setUser($user); return $authenticatedToken; } } public function supports(TokenInterface $token) { return $token instanceof UserToken; } public function validate() { return true; } }
Le User Provider :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // Acme/SecurityBundle/Security/Authenticaion/Token/UserToken.php namespace Acme\SecurityBundle\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; /** * UsernamePasswordToken implements a username and password token. * */ class UserToken extends AbstractToken { private $credentials; private $providerKey; /** * Constructor. * * @param string $user The username (like a nickname, email address, etc.) * @param string $credentials This usually is the password of the user * @param string $providerKey The provider key * @param array $roles An array of roles * * @throws \InvalidArgumentException */ public function __construct($user, $credentials, $providerKey, array $roles = array()) { parent::__construct($roles); if (empty($providerKey)) { throw new \InvalidArgumentException('$providerKey must not be empty.'); } $this->setUser($user); $this->credentials = $credentials; $this->providerKey = $providerKey; parent::setAuthenticated(count($roles) > 0); } /** * {@inheritdoc} */ public function setAuthenticated($isAuthenticated) { if ($isAuthenticated) { throw new \LogicException('Cannot set this token to trusted after instantiation.'); } parent::setAuthenticated(false); } public function getCredentials() { return $this->credentials; } public function getProviderKey() { return $this->providerKey; } /** * {@inheritdoc} */ public function eraseCredentials() { parent::eraseCredentials(); $this->credentials = null; } public function serialize() { return serialize(array($this->credentials, $this->providerKey, parent::serialize())); } public function unserialize($str) { list($this->credentials, $this->providerKey, $parentStr) = unserialize($str); parent::unserialize($parentStr); } }
Le User :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // src/Acme/SecurityBundle/Security/User/UserProvider.php namespace Acme\SecurityBundle\Security\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Acme\SecurityBundle\Security\User\User; class UserProvider implements UserProviderInterface { public function loadUserByUsername($username) { // make a call to your webservice here // $userData = ... // pretend it returns an array on success, false if there is no user $user = new User(); $user->setUsername($username); $user->setPassword("1234"); $user->setRoles(array("ROLE_ADMIN")); return $user; // if ($userData) { // // $password = '...'; // // ... // // return new WebserviceUser($username, $password, $salt, $roles) // } else { // throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); // } } public function refreshUser(UserInterface $user) { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } return $this->loadUserByUsername($user->getUsername()); } public function supportsClass($class) { return $class === 'Acme\SecurityBundle\Security\User'; } }
Le paramétrage
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // src/Acme/SecurityBundle/Security/User/User.php namespace Acme\SecurityBundle\Security\User; use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface { private $username; private $password; private $salt; private $roles; public function getRoles() { return $this->roles; } public function getPassword() { return $this->password; } public function getSalt() { return $this->salt; } public function getUsername() { return $this->username; } public function setRoles($roles) { $this->roles = $roles; } public function setPassword($password) { $this->password = $password; } public function setSalt($salt) { $this->salt = $salt; } public function setUsername($username) { $this->username = $username; } public function eraseCredentials() { } public function equals(UserInterface $user) { if (!$user instanceof User) { return false; } if ($this->password !== $user->getPassword()) { return false; } if ($this->getSalt() !== $user->getSalt()) { return false; } if ($this->username !== $user->getUsername()) { return false; } return true; } }
Le routing du bundle :
La déclaration de la factory
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 # Acme/SecurityBundle/Resources/config/routing.yml AcmeSecurityBundle_homepage: pattern: / defaults: { _controller: AcmeSecurityBundle:Default:index } login: pattern: /login defaults: { _controller: AcmeSecurityBundle:Security:login } login_check: pattern: /login_check
La déclaration des services :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 # Acme/SecurityBundle/Resources/config/security_factories.yml services: security.authentication.factory.user: class: Acme\SecurityBundle\DependencyInjection\Security\Factory\UserFactory tags: - { name: security.listener.factory }
Le paramétrage au niveau de l'application :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # Acme/SecurityBundle/Resources/config/services.yml services: user.security.authentication.provider: class: Acme\SecurityBundle\Security\Authentication\Provider\AuthProvider arguments: ["", %kernel.cache_dir%/security/nonces] user.security.authentication.listener: class: Acme\SecurityBundle\Security\Authentication\Firewall\AuthListener arguments: [@security.context, @security.authentication.manager, @security.authentication.session_strategy, @security.http_utils] tags: - { name: monolog.logger, channel: security } user_provider_service: class: Acme\SecurityBundle\Security\User\UserProvider
Le routing général
Security...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 # /app/config/routing.yml AcmeSecurityBundle: resource: "@AcmeSecurityBundle/Resources/config/routing.yml" prefix: /
Les contrôleurs et les templates
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 # app/config/security.yml security: factories: - "%kernel.root_dir%/../src/Acme/SecurityBundle/Resources/config/security_factories.yml" firewalls: login: pattern: ^/login$ security: false checkpoint: pattern: ^/ my_user_factory: true # Correspond à la valeur renvoyée par la fonction getKey de la factory que je souhaite utiliser form_login: login_path: /login check_path: /login_check logout: path: /logout target: / encoders: Acme\SecurityBundle\Entity\User: sha512 role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] providers: user_provider: id: user_provider_service # défini dans le service.yml du bundle access_control: # - { path: ^/_internal, role: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 } # - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } # - { path: ^/event/new, roles: ROLE_ADMIN } # - { path: ^/hello, roles: ROLE_USER }
Le contrôleur de la page d'accueil...
et le template associé
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <?php // Acme/SecurityBundle/Controller/DefaultController.php namespace Acme\SecurityBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class DefaultController extends Controller { public function indexAction() { // Je profite de l'occasion pour regarder le contexte... (à ne pas faire en prod :-) ) return $this->render('AcmeSecurityBundle:Default:index.html.twig', array('parametres' => print_r($this->container->getParameterBag()->all(), true), 'request' => print_r($this->getRequest(),true))); } }
Le contrôleur de la page de login
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 {# Acme/SecurityBundle/Resources/views/Default/index.html.twig #} Hello ! <pre> Parametres {{ parametres }} Request {{ request }} </pre>
et le template associé
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 <?php // src/Acme/SecurityBundle/Controller/SecurityController.php namespace Acme\SecurityBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\SecurityContext; class SecurityController extends Controller { public function loginAction() { $request = $this->getRequest(); $session = $request->getSession(); // get the login error if there is one $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); $session->remove(SecurityContext::AUTHENTICATION_ERROR); return $this->render('AcmeSecurityBundle:Login:login.html.twig', array( // last username entered by the user 'last_username' => $session->get(SecurityContext::LAST_USERNAME), 'error' => $error, )); } }
Voilà. Je pense avoir fait le tour et j'espère que tout ça est suffisamment clair...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 {# Acme/SecurityBundle/Resources/views/Security/login.html.twig #} {% if error %} <div>{{ error.message }}</div> {% endif %} <form name="loginForm" action="{{ path('login_check') }}" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="_username" /> <label for="password">Password:</label> <input type="password" id="password" name="_password" /> {# If you want to control the URL the user is redirected to on success (more details below) <input type="hidden" name="_target_path" value="/account" /> #} <button type="submit">login</button> </form> </script -->
Amusez-vous bien :-)
Partager