symfony2安全系统是框架比较复杂的地方,很多人很难明白并运用到工作中。他非常的强大和灵活,但老实说他并不简单。对于自定义身份验证,symfony之前版本有一些文章。
现在symfony已经更新到了3.0,一个新的组件也被包含到了symfony框架体系:Guard。这个组件的目的是与安全系统集成,并提供一个更简单的方法。他公开了一个单个接口,把你从身份验证开始到结束都链起来:逻辑和所有组合都在一起。
在这篇文章里,我们来创建一个简单的表单验证,需要一个用户登录并给他ROLE_ADMIN角色到每一个页面。原始的方式是创建一个form authentication,他现在仍然可以使用,但我们将使用这样一个简单的步骤来举例说明Guard。你就能够了解这个概念并复用到其他的验证(token,社交媒体等)。
安全配置
这个安全配置将需要一个用户类(来表示用户数据)还需要UserProvider(获取用户数据)。为了让事情简单,我们直接使用InMemory用户提供器(user provider)自然而然的我们使用默认的symfony的User类。我们的security.yml就是这个样子:
1 2 3 4 5 6 7 8 |
security: providers: in_memory: memory: users: admin: password: admin roles: 'ROLE_ADMIN' |
更多关于symfony安全系统的文章请阅读book。
我们的InMemory提供器现在有一个写死的test用户并付给他ROLE_ADMIN.
下面是我们定义的防火墙:
1 2 3 4 5 6 7 8 |
secured_area anonymous: ~ logout: path: /logout target: / guard: authenticators: - form_authenticator |
上面基本上是说,匿名用户可以访问防火墙路径,还记录了用户退出路径。这个新的guard键是表明防火墙使用Guard配置的验证器:form_authenticator。他传入的是服务名称并且我们将在下一分钟内看到他的定义。
最后在安全配置中,我们可以指定一些访问规则:
1 2 3 |
access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: ROLE_ADMIN } |
在这个示例中,我们指定没有登录的用户只能访问/login路径。对于其他用户要有ROLE_ADMIN角色。
登录控制器(Login Controller)
在真正做身份验证之前,让我们看看实际的登录表单和控制器。DefaultController的action里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/** * @Route("/login", name="login") */ public function loginAction(Request $request) { $user = $this->getUser(); if ($user instanceof UserInterface) { return $this->redirectToRoute('homepage'); } /** @var AuthenticationException $exception */ $exception = $this->get('security.authentication_utils') ->getLastAuthenticationError(); return $this->render('default/login.html.twig', [ 'error' => $exception ? $exception->getMessage() : NULL, ]); } |
定义了/login路由,这个action只是负责显示一个基本的登录表单,用户不登录。twig模板如下:
1 2 3 4 5 6 7 8 9 10 |
{{ error }} <form action="{{ path('login') }}" method="POST"> <label for="username">Username</label> <input type="text" name="username" class="form-control" id="username" placeholder="Username"> <label for="password">Password</label> <input type="password" name="password" class="form-control" id="password" placeholder="Password"> <button type="submit">Login</button> </form> |
到目前为止,没啥特别的。只是一个简单的表单html。
Guard Authenticator 服务
我们在安全配置文件中引用了我们的Guard authenticator服务。让我们确保在sservices.yml中有这个服务定义:
1 2 3 4 |
services: form_authenticator: class: AppBundle\Security\FormAuthenticator arguments: ["@router"] |
这个服务引用了FormAuthenticator类:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
namespace AppBundle\Security; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; class FormAuthenticator extends AbstractGuardAuthenticator { /** * @var \Symfony\Component\Routing\RouterInterface */ private $router; /** * Default message for authentication failure. * * @var string */ private $failMessage = 'Invalid credentials'; /** * Creates a new instance of FormAuthenticator */ public function __construct(RouterInterface $router) { $this->router = $router; } /** * {@inheritdoc} */ public function getCredentials(Request $request) { if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) { return; } return array( 'username' => $request->request->get('username'), 'password' => $request->request->get('password'), ); } /** * {@inheritdoc} */ public function getUser($credentials, UserProviderInterface $userProvider) { if (!$userProvider instanceof InMemoryUserProvider) { return; } try { return $userProvider->loadUserByUsername($credentials['username']); } catch (UsernameNotFoundException $e) { throw new CustomUserMessageAuthenticationException($this->failMessage); } } /** * {@inheritdoc} */ public function checkCredentials($credentials, UserInterface $user) { if ($user->getPassword() === $credentials['password']) { return true; } throw new CustomUserMessageAuthenticationException($this->failMessage); } /** * {@inheritdoc} */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { $url = $this->router->generate('homepage'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); $url = $this->router->generate('login'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function start(Request $request, AuthenticationException $authException = null) { $url = $this->router->generate('login'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function supportsRememberMe() { return false; } } |
尽管他看起来似乎很多,其实不然。让我们一步一步去了解发生了什么事情。
首先,这个文件就在我们bundle的Security文件夹下,这个是一个个人习惯的选择,你随意。然后,我们继承AbstractGuardAuthenticator,因为他已经实现了GuardAuthenticatorInterface接口所有必须要实现接口的方法。如果我们需要一个特定的token类来完成我们的验证,我们仅仅需要实现这个接口极其createAuthenticatedToken方法。现在我们不需要。
实现这个接口的方法是问题的关键,验证都在这里,这个包括当一个用户访问到授权或者拒绝访问所有流程。
我们从这个getCredentials()方法开始,得到每个请求。这个方法的目的是从请求获取凭证数据并返回,或者返回null(会拒绝访问,或者允许其他验证器提供凭证,或者调用start()方法)。如果是这个案例:凭证是通过/login的提交post数据,我们会获得到提交的用户名和密码,并返回一个数组。
如果getCredentials()方法没有返回空,则顺利进入下一个方法getUser().getUser()负责加载用户,主要根据前面getCredentials()方法获得的凭证来获得用户。使用默认的用户提供器(我们案例里的InMemory提供器),基于用户名返回要加载的用户。我们也可以在这里返回null,他会触发验证失败,我们也可以选择CustomUserMessageAuthenticationException去指定我们自定义的失败消息。
如果上面用户被获取到并返回,这个checkCredentials方法就开始生效。这个方法的目的是核查传入的凭证和找到的用户是否匹配。更上面一样的道理,我们返回null或者抛出一个异常,验证失败。
此时,如果凭证匹配用户就可以登陆了。在这种情况下,onAuthenticationSuccess()方法被调用,他能做我们想做的事情。例如我们的案例,重定向到主页上就死一个很好的主意。相反的,如果验证失败,onAuthenticationFailure()方法就会被调用。我们需要做的就是重定向到/login页面,我们会把最后验证的错误信息放在session中,在表单模板显示出来。
start方法是Guard系统(或者应用)的切入点。这个方法是当用户尝试访问一个需要验证的页面时,他没有通过getCredentials()方法返回有效凭证,就会调用这个方法。在我们案例中就是如果有人试图访问主页,getCredentials()获取不到有用的请求,返回null,没有凭证。我们就将他重定向到/login页面,让他登录后访问主页。
让我们想象一下其他例子:基于token的验证。在这种情况下,每个请求需要包含一个token,来验证用户。这意味着getCredentials()将总能返回凭证。如果没有,start()方法将返回一个响应,拒绝访问。
最后,一个方法是负责标记remember-me功能的。在我们的例子中,没有使用它,所以返回false。关于Remember-Me的更多信息请查看symfony cookbook。
总结
我们现在使用Guard组件创建了一个全功能的登录系统。还有我们上面刚刚提到的token验证的例子,他与我们今天文章主要介绍的一起工作。可以存在多个身份验证器:
1 2 3 4 5 |
guard: authenticators: - form_authenticator - token_authenticator entry_point: form_authenticator |
如果我们配置多个验证器,我们需要去指定哪一个是entry point(当一个用户尝试访问一个资源但是没有提供凭证,start()方法将会被调用).
也就是说,我们创建多个验证器,我们提交的数据符合其中的一个即可通过验证,如果都不符合,会调用entry_point指定验证器的start()方法。
注意:Guard并不替换任何的symfony验证,只是补充。现有的将会继续工作。例如,form_login或者simple_form,我们之前使用过的,他们都将继续工作。