其实 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
<?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
<?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:
<?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 信息,一切就搞定了
# 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
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.

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人
6 Comments
xiaobai
9月 5, 2019 在 4:02 下午大佬你好- -我又来问问题了
结合文中和官方文档, 我已经搞定了,但是在checkCredentials 中我直接 return true, 是不能算作登陆吗? 我并没有看到用户登陆的状态- – 我是新手,比较懵
Chris Yue
9月 7, 2019 在 6:05 下午算作登录啊,然后就该执行 onAuthenticationSuccess 了,只要执行了 onAuthenticationSuccess 就表示已经成功登录了,然后做一些登录成功之后该干的事情,比如保存用户会话啥的,你可以确认一下 onAuthenticationSuccess 到底执行没有。
xiaobai
9月 5, 2019 在 1:28 下午大佬你好,按照官方文档中的来, 设置的请求头,sf接收不到, 如果是这样的话,我是得设置参数来进行是否登陆判断吗
小白
9月 5, 2019 在 12:42 上午The service “App\Security\TokenAuthenticator” has a dependency on a non-existent parameter “client_id”.
大佬,跟着做为啥提示不存在参数
fsing
5月 23, 2019 在 4:42 下午感谢您的分享,有一个问题,如果用在后台登录里面,每一次请求都要带上用户名密码吗?能不能在某个地方判断用户是否已经登录,就不用再去执行后面的语句了呢?
Chris Yue
5月 24, 2019 在 8:23 上午当然可以,credential 不一定非得是单一方式。
你可以让 credential 既可能返回用户登录信息又可能返回会话信息,再根据 credential 的不同,走不同的身份认证流程就好了。