其实 Symfony Guard 已经发布很久了,很早就想写一篇相关的教程,趁最近一个项目有用到它,正好可以作为一个实例,跟大家展示一下 Guard 的用法。
在 Symfony2 的年代,我就写过一篇文章介绍 Symfony 的用户登录系统。用户登录系统是一个网站开发必须经历,但又不简单的活,从 Symfony 对登录系统的设计就可以看得出来,四个大组件,才能做好用户登录系统。当然 Symfony 开发组也深知原有的设计太『宏大』,估计一开始就会吓跑很多人,所以后来添加了精简版的用户登录系统 Guard。
简而言之,Guard 就是将原本的登录四大组件变成了一个统一的类来处理,从它的接口 GuardAuthenticatorInterface 就能看出来,这里我将按照执行的顺序,依次说明每个接口的意义。
getCredentials(Request $request)
这是每次请求都会执行的方法,让开发者从 $request
对象中获取登录所需要的讯息,比如常见表单登录的用户名和密码,或者微信登录的 code 参数。返回的结果可以是任意的类型,但一般来说用数组形式就够了。如果此方法返回 null
,那么 GuardAuthenticatorInterface::start
方法将会被执行;如果不是 null
,则 GuardAuthenticatorInterface::getUser
方法将被执行,而 getCredentials
返回的结果将作为 getUser
的第一个参数。
start(Request $request, AuthenticationException $authException = null)
此方法可以理解成传统 Symfony 登录系统的 entry point,即让用户登录的地方。看定义可以发现此方法返回一个 Response,你可以返回带登录表单的页面,或者跳转到微信 OAuth 获取 code 的接口。当用户提交了账号密码,或者微信返回带 code 参数的链接,第二次请求开始,将又从 getCredentials
方法重新开始。
getUser(mixed $credentials, UserProviderInterface $userProvider)
此方法一般来说,都需要利用 UserProvider
的 loadUserByUsername
方法,通过传入 $credentials
里的登录名,或者微信的 openId,返回 User 对象。如果通过用户名找不到对应的 User 对象,既 UserProvider::loadUserByUsername
返回 null
,那么 GuardAuthenticatorInterface::onAuthenticationFailure
方法将会被调用;如果 UserProvider::loadUserByUsername
能返回 User 对象,那么 GuardAuthenticatorInterface::checkCredentials
方法将会被调用,而 $user
对象会被作为第二参数被传入,第一参数仍是 $credentials
。
checkCredentials
此方法将检查用户和 $credentials
是否匹配。比如表单登录,将 $credentials
里的密码信息和 $user
对象里的密码做对比,如果密码不匹配,那么你将在此方法抛出 AuthenticationException 异常或者返回 false
来表示登录失败,从而转向 GuardAuthenticatorInterface::onAuthenticationFailure
方法。
onAuthenticationSuccess
当然,如果 checkCredentials
方法返回 true
(即登录成功),那么 GuardAuthenticatorInterface::onAuthenticationSuccess
方法将会被调用,你可以在此方法做一些登录成功之后的事情。此方法需要返回 Response
或者 null
,如果返回 null
将继续执行当前路径应当执行的代码;如果返回 Response
,则此 Response
会立马发送。比如如果要求用户登录成功都需要返回到首页,那么你就可以在此返回 new RedirectResponse('/')
。
onAuthenticationFailure
最后是登录失败的处理。类似于登录成功,此方法需要返回 Response
,比如显示登录失败的页面,或者继续显示登录表单让用户登录。
还有两个方法,现在先不说。到此是否觉得 Guard 接口的设计让我们对 Symfony 登录思路的理解更加简洁清晰了呢?
把实例给大家展现出来,自己感受感受,利用微信 OAuth2 接口做微信登录。
首先是用户类。跟之前一样,必须实现 Symfony\Component\Security\Core\User\UserInterface
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 | <?php namespace AppBundle; use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface { private $openId; public function __construct($openId) { $this->openId = $openId; } public function getRoles() { } public function getPassword() { } public function getSalt() { } public function getUsername() { return $this->openId; } public function eraseCredentials() { } } |
接下来是 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 | <?php namespace AppBundle; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; class UserProvider implements UserProviderInterface { public function loadUserByUsername($openId) { return new User($openId); } public function refreshUser(UserInterface $user) { return $user; // 关于用户刷新以后专门开一篇说,目前就直接返回 $user } public function supportsClass($class) { return User::class === $class; } } |
一切准备工作就绪,上 Guard:
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 | <?php namespace AppBundle\Security\Guard; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; class Authenticator extends AbstractGuardAuthenticator { private $clientId; private $clientSecret; public function __construct($clientId, $clientSecret) { $this->clientId = $clientId; $this->clientSecret = $clientSecret; } public function getCredentials(Request $request) { if ($request->query->has('code')) { return ['code' => $request->query->get('code')]; } } public function getUser($credentials, UserProviderInterface $userProvider) { $openId = $this->getOpenIdFromCredentials($credentials); return $userProvider->loadUserByUsername($openId); } public function checkCredentials($credentials, UserInterface $user) { return true; // 注意:对 OAuth2 来说到此步已经拿到 openId,其实已经算登录成功了,直接返回 true } public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { return new Response($exception->getMessage(), Response::HTTP_UNAUTHORIZED); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { } public function supportsRememberMe() { return false; } public function start(Request $request, AuthenticationException $authException = null) { $queries = [ 'appid' => $this->clientId, 'redirect_uri' => $request->getUri(), 'response_type' => 'code', 'scope' => 'snsapi_base', ]; $redirectUrl = sprintf('https://open.weixin.qq.com/connect/oauth2/authorize?%s#wechat_redirect', http_build_query($queries)); return new RedirectResponse($redirectUrl); } private function getOpenIdFromCredentials(array $credentials) { $url = sprintf( 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code', $this->clientId, $this->clientSecret, $credentials['code'] ); try { $info = json_decode(file_get_contents($url), true); } catch (\Exception $ex) { throw new AuthenticationException('OAuth server is down', $ex); } if (empty($info['openid'])) { throw new AuthenticationException('OAuth code is not valid'); } return $info['openid']; } } |
最后,配置 services 和 firewall 信息,一切就搞定了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # security.yml security: providers: main: id: AppBundle\UserProvider firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: guard: authenticators: - AppBundle\Security\Guard\Authenticator |
1 2 3 4 5 6 7 8 | serivces: # 这里使用了 Symfony3 的新语法 AppBundle\Security\Guard\Authenticator: arguments: $clientId: '%oauth_client_id%' $clientSecret: '%oauth_client_secret%' # Symfony3 默认开启 autowire 所以此处也不用定义 UserProvider 服务 |
使用 Symfony Guard 做用户登录 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

文章不错,我要帮站长分担建站费!
天使投赏人