Symfony 4 登录表单:身份验证成功,但重定向后身份验证立即丢失 [英] Symfony 4 login form : authenticating successfully, but authentication immediately lost after redirect
问题描述
我按照这个表单登录设置文档构建了一个登录表单.>
这在本地主机上运行良好但在生产服务器上运行正常.
在 localhost 和 prod 上,身份验证成功开始
- Guard 认证成功
- 保护身份验证器设置成功响应
- 在会话中存储安全令牌
匹配路由easyadmin
### var/log/prod.log 输出信息级别[2019-07-05 10:28:46] request.INFO:匹配路由app_login".{"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"POST"} [][2019-07-05 10:28:46] security.DEBUG:检查守卫身份验证凭据.{"firewall_key":"main","authenticators":1} [][2019-07-05 10:28:46] security.DEBUG:检查对守卫身份验证器的支持.{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG:在防护认证器上调用 getCredentials().{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG:将守卫令牌信息传递给 GuardAuthenticationProvider {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] php.INFO:用户弃用:Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder"类自 Symfony 4.3 起已弃用,使用Symfony\Component\Security\Core\Encoder\NativePasswordEncoder"代替.{"exception":"[object] (ErrorException(code: 0): User Deprecated: The \"Symfony\\Component\\Security\\Core\\Encoder\\BCryptPasswordEncoder\" class is deprecated from Symfony 4.3, use \"Symfony\\Component\\Security\\Core\\Encoder\\NativePasswordEncoder\" 代替.在/var/www/clients/client0/web4/web/vendor/symfony/security-core/Encoder/BCryptPasswordEncoder.php:14)"} [][2019-07-05 10:28:46] security.INFO: Guard 认证成功!{"token":"[object] (Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user=\"myemail@gmail.com\", authenticated=true, roles=\"ROLE_EDITOR, ROLE_USER\"))","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG:保护身份验证器设置成功响应.{"response":"[object] (Symfony\\Component\\HttpFoundation\\RedirectResponse: HTTP/1.0 302 Found\r\nCache-Control: no-cache, private\r\nDate: Fri, 05 Jul 2019 10:格林威治标准时间 28:46\r\n位置:/backoffice\r\n\r\n\n\n \n \n <body>\n 重定向到
但是在本地主机中,我被正确重定向到后台:
- 从会话中读取现有的安全令牌
用户已从用户提供程序重新加载
### var/log/prod.log(以下几行,本地主机)[2019-07-05 10:19:29] security.DEBUG:从会话中读取现有的安全令牌.{"key":"_security_main","token_class":"Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken"} [][2019-07-05 10:19:29] security.DEBUG:用户是从用户提供程序重新加载的.{"provider":"Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider","username":"raoux.thierry@free.fr"} [][2019-07-05 10:19:29] security.DEBUG:检查守卫身份验证凭据.{"firewall_key":"main","authenticators":1} [][2019-07-05 10:19:29] security.DEBUG:检查对守卫身份验证器的支持.{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:19:29] security.DEBUG:Guard 验证器不支持该请求.{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:19:29] cache.INFO:获取锁,现在计算项目easyadmin.processed_config"{"key":"easyadmin.processed_config"} []
在生产环境中,改为:
- 跳过步骤:读取现有的安全令牌
- 不会按预期刷新用户
- 相反,它使用匿名令牌填充 TokenStorage
访问被拒绝并返回登录 URL
### var/log/prod.log(以下相同的行,但来自生产服务器)[2019-07-05 10:28:46] security.DEBUG:检查守卫身份验证凭据.{"firewall_key":"main","authenticators":1} [][2019-07-05 10:28:46] security.DEBUG:检查对守卫身份验证器的支持.{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG:Guard 验证器不支持该请求.{"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.INFO:使用匿名令牌填充 TokenStorage.[] [][2019-07-05 10:28:46] security.DEBUG:访问被拒绝,用户未完全通过身份验证;重定向到身份验证入口点.{"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at/var/www/clients/client0/web4/web/vendor/symfony/security-http/Firewall/AccessListener.php:72)"} [][2019-07-05 10:28:46] security.DEBUG:调用身份验证入口点.[] [][2019-07-05 10:28:46] request.INFO:匹配路由app_login".{"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"GET"} []
security.yaml
安全性:编码器:应用\实体\用户:算法:bcrypt供应商:app_user_provider:实体:类:应用\实体\用户属性:电子邮件防火墙:开发:模式:^/(_(分析器|wdt)|css|图像|js)/安全:假主要的:匿名:真实警卫:验证器:- App\Security\LoginFormAuthenticator登出:路径:app_logout访问控制:- { path: ^/backoffice, roles: ROLE_EDITOR} # requires_channel: https
routes.yaml
管理员:路径:/后台控制器:EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController
登录表单身份验证器
//使用...类 LoginFormAuthenticator 扩展了 AbstractFormLoginAuthenticator{使用 TargetPathTrait;私人 $entityManager;私人 $urlGenerator;私人 $csrfTokenManager;私人 $passwordEncoder;公共函数 __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder){$this->entityManager = $entityManager;$this->urlGenerator = $urlGenerator;$this->csrfTokenManager = $csrfTokenManager;$this->passwordEncoder = $passwordEncoder;}公共函数支持(请求 $request){return 'app_login' === $request->attributes->get('_route')&&$request->isMethod('POST');}公共函数 getCredentials(Request $request){$凭据= ['电子邮件' =>$request->request->get('email'),'密码' =>$request->request->get('password'),'csrf_token' =>$request->request->get('_csrf_token'),];$request->getSession()->set(安全::LAST_USERNAME,$credentials['email']);返回 $credentials;}公共函数 getUser($credentials, UserProviderInterface $userProvider){$token = new CsrfToken('authenticate', $credentials['csrf_token']);if (!$this->csrfTokenManager->isTokenValid($token)) {抛出新的 InvalidCsrfTokenException();}$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);如果(!$用户){//由于自定义错误而导致身份验证失败throw new CustomUserMessageAuthenticationException('找不到邮箱.');}返回 $user;}公共函数 checkCredentials($credentials, UserInterface $user){返回 $this->passwordEncoder->isPasswordValid($user, $credentials['password']);}公共函数 onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey){if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {返回新的重定向响应($targetPath);}return new RedirectResponse($this->urlGenerator->generate('admin'));}受保护的函数 getLoginUrl(){返回 $this->urlGenerator->generate('app_login');}}
安全控制器
//使用...类 SecurityController 扩展了 AbstractController{/*** @Route("/login", name="app_login")*/公共功能登录(AuthenticationUtils $authenticationUtils):响应{//如果有,则获取登录错误$error = $authenticationUtils->getLastAuthenticationError();//用户最后输入的用户名$lastUsername = $authenticationUtils->getLastUsername();返回 $this->render('安全/登录.html.twig',['last_username' =>$last用户名,'错误' =>$错误,]);}/*** @Route("/logout", name="app_logout")* @return \Symfony\Component\HttpFoundation\RedirectResponse*/公共函数注销(){返回 $this->redirectToRoute('home');}}//... 跳过了忘记密码和重置密码方法
php bin/console debug:config security
输出
具有别名security"的扩展的当前配置==========================================================安全:编码器:应用\实体\用户:算法:bcrypt哈希算法:sha512密钥长度:40ignore_case: 假encode_as_base64: 真迭代次数:5000成本:空内存成本:空时间成本:空线程:空供应商:app_user_provider:实体:类:应用\实体\用户属性:电子邮件manager_name: 空防火墙:开发:模式:^/(_(分析器|wdt)|css|图像|js)/安全:假方法: { }user_checker:security.user_checker无状态:假logout_on_user_change: 真主要的:匿名的:秘密:空警卫:验证器:- App\Security\LoginFormAuthenticator入口点:空登出:路径:app_logoutcsrf_parameter: _csrf_tokencsrf_token_id:注销目标:/invalidate_session: 真删除cookies: { }处理程序:{}方法: { }安全性:真实user_checker:security.user_checker无状态:假logout_on_user_change: 真访问控制:——路径:^/后台角色:- ROLE_EDITORrequires_channel: 空主机:空端口:空ips:{}方法: { }allow_if: 空access_decision_manager:策略:肯定allow_if_all_abstain: 假allow_if_equal_granted_denied: 真access_denied_url: 空session_fixation_strategy:迁移hide_user_not_found: 真always_authenticate_before_granting: 假擦除凭证:真role_hierarchy: { }
编辑 2
AS @Arno 评论说,我编辑了 framework.yaml
以将会话保存在 var/
目录中,我可以检查此步骤是否在没有权限问题的情况下工作,每次我点击登录表单,写了一个sess_文件.
值得说的是,如果我发表评论:
access_control:- { 路径:^/odelices_admin,角色:ROLE_USER}
我可以访问后台.
编辑 3:会话行为
所以现在会话被保存到 var/sessions/prod 中.
- 我清理目录:
sudo rm -r var/sessions/prod/sess_*
我打开 Chrome 和 url,它设置了一个 PHPSSID cookie,其值与第一个 sess_xyz 文件相同:
_sf2_attributes|a:2:{s:19:"_csrf/https-contact";s:43:"Oq-QpN21bI_BUDcVbv0ocyrYsTzQo3aJr80QAk2AR7w";s:19:"_csrf";s-43:"Oq-QpN21bI_BUDcVbv0ocyrYsTzQo3aJr80QAk2AR7w":"z_L4TG7Wg0jydwl5VabfJMx0NBhQgeasuAiqxksLvD8";}_sf2_meta|a:3:{s:1:"u";i:1562668584;s:1:"c";i:15626:s:"l";s0";}
我去登录页面.与新 sess_xyz 文件关联的新 PHPSSID 值:
_sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";}_sf2_meta:1:3:"{;i:1562668662;s:1:"c";i:1562668662;s:1:"l";s:1:"0";}
我使用正确的值登录.这会创建 3 个新的 ssid_xyz 文件.
# 第一个显示用户以正确的角色登录等等_sf2_attributes|a:3:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";s:23:"_security.last_username";s:come@mail.come;s:14:"_security_main";s:799:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":718:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":6:{s:19:"^@App\Entity\User^@id";i:1;s:22:"^@App\Entity\User^@email";s:21:"user_email@gmail.com";s:22:"^@App\Entity\User^@roles";a:1:{i:0;s:11:"ROLE_EDITOR";}s:25:"^@App\Entity\User^@password";s:60:"$2y$13$cXaR7Ss.kTH1U.T/Rzi6m.ALsKwWCLDcO5/OIeRDAq02iylmf4us6";s:21:"^@App\Entity\User^@name";s:7:"Thierry";s:13:"^@*^@resetToken";N;}i:1;b:1;i:2;a:2:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:11:"ROLE_EDITOR";}i:1;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:2:{i:0;s:11:"ROLE_EDITOR";i:1;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}# 第二个...是空的# 第三个指的是后台网址_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:42:"https://mywebsite.com/backoffice";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}# 最后一个和第3点类似,在登录之前,只是ssid值不同,在Chrome上设置了一个对应的cookie_sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"3UC5dCRrahc2qhdZ167Jg4HKTJCexf8PFlefibTVpYk";}_sf2_meta|a:3:{s:1:1816i6;:"c";i:1562668713;s:1:"l";s:1:"0";}
编辑 4:用户实体
命名空间 App\Entity;使用 Doctrine\ORM\Mapping 作为 ORM;//使用 Symfony\Component\Security\Core\User\EquatableInterface;使用 Symfony\Component\Security\Core\User\UserInterface;/*** @ORM\Entity(repositoryClass="App\Repository\UserRepository")*/类 User 实现 UserInterface # , EquatableInterface{/*** @ORM\Id()* @ORM\GeneratedValue()* @ORM\Column(type="整数")*/私人 $id;/*** @ORM\Column(type="string", length=180, unique=true)*/私人 $email;/*** @ORM\Column(type="json")*/私人 $roles = [];/*** @var string 散列密码* @ORM\Column(type="string")*/私人 $password;/*** @ORM\Column(type="string", length=255)*/私人 $name;/*** @var string le token qui servira lors de l'oubli de mot de passe* @ORM\Column(type="string", length=255, nullable=true)*/受保护的 $resetToken;/* 公共函数 __construct($username, $password, 数组 $roles){$this->username = $username;$this->password = $password;$this->roles = $roles;}*/公共函数 getId(): ?int{返回 $this->id;}公共函数 getEmail(): ?string{返回 $this->email;}公共函数 setEmail(string $email): self{$this->email = $email;返回 $this;}/*** 代表该用户的视觉标识符.** @see 用户界面*/公共函数 getUsername(): 字符串{return (string) $this->email;}/*** @see 用户界面*/公共函数 getRoles(): 数组{$roles = $this->roles;//保证每个用户至少有 ROLE_USER$roles[] = 'ROLE_USER';返回 array_unique($roles);}公共函数 setRoles(array $roles): self{$this->roles = $roles;返回 $this;}/*** @see 用户界面*/公共函数 getPassword(): 字符串{return (string) $this->password;}公共函数 setPassword(string $password): self{$this->password = $password;返回 $this;}/*** @see 用户界面*/公共函数 getSalt(){//在 security.yaml 中使用bcrypt"算法时不需要}/*** @see 用户界面*/公共函数eraseCredentials(){//如果您存储了任何关于用户的临时敏感数据,请在此处清除//$this->plainPassword = null;}公共函数 getName(): ?string{返回 $this->name;}公共函数 setName(string $name): self{$this->name = $name;返回 $this;}/*** @return 字符串*/公共函数 getResetToken(): 字符串{返回 $this->resetToken;}/*** @param 字符串 $resetToken*/公共函数 setResetToken(?string $resetToken): void{$this->resetToken = $resetToken;}公共函数 __toString() {返回 $this->getName() ;}/* 公共函数 isEqualTo(UserInterface $user){if ($this->password !== $user->getPassword()) {返回假;}if ($this->email !== $user->getUsername()) {返回假;}返回真;}*/}
堆栈
Debian Stretch,Nginx + Varnish :Nginx 处理 443 个请求,将它们作为缓存代理传递给 Varnish,后者传递缓存对象或将请求传递到 8083 端口上的 nginx 后端.这对于具有类似登录逻辑的另一个应用程序来说就像一个魅力(唯一的区别是有问题的应用程序重定向到 easyadmin 而不是自定义管理员),所以我认为它与堆栈无关.
虚拟主机
server { # 此块仅将 www 重定向到非 www听 aaa.bbb.ccc.ddd:443 ssl;server_name www.somewebsite.com;ssl_protocols TLSv1 TLSv1.1 TLSv1.2;ssl_certificate/var/www/clients/client0/web4/ssl/somewebsite.com-le.crt;ssl_certificate_key/var/www/clients/client0/web4/ssl/somewebsite.com-le.key;返回 301 https://somewebsite.com$request_uri;}server { # 此块将 ssl 请求重定向到 Varnish听 aaa.bbb.ccc.ddd:443 ssl;server_name somewebsite.com;ssl_protocols TLSv1 TLSv1.1 TLSv1.2;ssl_certificate/var/www/clients/client0/web4/ssl/somewebsite.com-le.crt;ssl_certificate_key/var/www/clients/client0/web4/ssl/somewebsite.com-le.key;地点/{# 将请求传递给 Varnish.proxy_pass http://127.0.0.1;# 向下游服务器传递一些标头,以便它可以识别主机.proxy_set_header 主机 $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;# 告诉任何网络应用会话是 HTTPS.proxy_set_header X-Forwarded-Proto https;proxy_redirect 关闭;}}server { # 现在发送到后端听听 aaa.bbb.ccc.ddd:8083;server_name somewebsite.com;根/var/www/somewebsite.com/web/public;地点/{try_files $uri/index.php$is_args$args;}位置 ~ ^/index\.php(/|$) {fastcgi_pass 127.0.0.1:8998;fastcgi_split_path_info ^(.+\.php)(/.*)$;包括 fastcgi_params;fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;fastcgi_param DOCUMENT_ROOT $realpath_root;内部的;}位置 ~ \.php$ {返回404;}error_log/var/log/ispconfig/httpd/somewebsite.com/error.log;access_log/var/log/ispconfig/httpd/somewebsite.com/access.log 合并;位置 ~/\.{否认一切;}位置 ^~/.well-known/acme-challenge/{access_log off;log_not_found 关闭;root/usr/local/ispconfig/interface/acme/;自动索引关闭;try_files $uri $uri/=404;}位置 =/favicon.ico {log_not_found 关闭;access_log off;最大到期;}位置 =/robots.txt {允许所有;log_not_found 关闭;access_log off;}}
<小时>
这可能与某些目录的权限有关吗?HTTPS?易管理员?我如何确保安全令牌存储在会话中,即使它被记录为存储?我还尝试将 access_control 更改为角色 ROLE_USER 以便任何经过身份验证的用户都可以访问.没办法.
非常感谢任何帮助.
这里是我以更结构化的方式发表的评论,以便它可以帮助其他人在 Symfony 中遇到身份验证问题.
确保会话已保存
默认情况下,每个会话都保存为一个名为 sess_
的文件,位于
或者,如果 framework.session.handler
设置为 null
,则在 php.ini
中由 save_path
定义.明确配置您的会话目录并确保在您登录时创建了一个会话文件.如果没有,请检查该文件夹的权限.
# app/config/config.yml (Symfony 3)# config/packages/framework.yaml (Symfony 4)框架:会议:handler_id: 'session.handler.native_file'save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
参见https://symfony.com/doc/current/session.html#configuration
确保会话正确并被使用
当您登录时,应创建一个具有新 ID 的会话.检查文件的内容.它应该在防火墙名称(例如 main)下包含您的序列化用户,包括您的标识符(例如电子邮件)和您的用户角色(例如 ROLE_USER).此处的问题可能是由错误的身份验证、安全配置或序列化引起的.
_sf2_attributes|a:3:{s:18:"_csrf/authenticate";s:43:"n2oap401u4P4O7m_IhPODZ6Bz7EHl-DDsHxBEl-fhxc"s:name"s:2310:"foo@bar.de";s:14:"_security_main";s:545:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":464:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":4:{s:19:";App\Entity\Userid";i:1;s:22:"App\Entity\Useremail";s:10:"foo@bar.de";s:22:"App\Entity\Userroles";a:0:{}s:25:"App\Entity\Userpassword";s:60:"$2y$13$qwbtasafa58lPonX6B5a9eV4lziF7EZWP8NFLAe3blpCJVhQgPVOS";}i:1;b:1;i:1;{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:1:{i:0;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1563015142;s:1:"c";i:1563015142;s:1:"t;l";s:1:"0";}
在登录时检查您的浏览器中是否设置了具有相同 ID 的 cookie.cookie 的名称由 php.ini
中的 session.name
定义,默认为 PHPSESSID
.它应该与您提出的每个请求一起发送(例如 Cookie: PHPSESSID=lpcf79ff8jdv2iigsgvepnr9bb
).如果存在正确的会话,但您的浏览器中有不同的 cookie,则您可能会在成功重定向后立即注销.
确保用户正确刷新
会话 ID 应仅在您的用户更改时(例如在登录和注销时)更改.如果它在正常请求后发生变化(例如,您立即退出)或者您的会话似乎被忽略,则问题可能是 Symfony 认为您的用户已更改.这可能是由于错误的(反)序列化或比较造成的.
默认情况下,Symfony 使用会话中 getPassword()
、getUsername()
和 getSalt()
的序列化结果进行比较针对用户提供者(例如数据库)提供的用户.如果这些值中的任何一个发生变化,您将被注销(参见 https://symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session).
因此,您应该确保例如提供的用户您的数据库是正确的,并且与会话中的反序列化用户相匹配.确保相关字段正确序列化.如果您实现了 Serializable
接口,请确保您的 serialize()
方法与您的 unserialize()
匹配.如果您实现了 EquatableInterface
,请确保您的 isEqualTo()
方法正常工作.不过这两个接口都是可选的,因此您可以考虑删除它们以进行调试.
I built a login form following this form login setup doc.
This is working fine on localhost but not on the production server.
On both localhost and prod, authentication begins successfully
- Guard authentication successful
- Guard authenticator set success response
- Stored the security token in the session
Matched route "easyadmin
### var/log/prod.log output with info level [2019-07-05 10:28:46] request.INFO: Matched route "app_login". {"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"POST"} [] [2019-07-05 10:28:46] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [] [2019-07-05 10:28:46] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Calling getCredentials() on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Passing guard token information to the GuardAuthenticationProvider {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] php.INFO: User Deprecated: The "Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder" class is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\NativePasswordEncoder" instead. {"exception":"[object] (ErrorException(code: 0): User Deprecated: The \"Symfony\\Component\\Security\\Core\\Encoder\\BCryptPasswordEncoder\" class is deprecated since Symfony 4.3, use \"Symfony\\Component\\Security\\Core\\Encoder\\NativePasswordEncoder\" instead. at /var/www/clients/client0/web4/web/vendor/symfony/security-core/Encoder/BCryptPasswordEncoder.php:14)"} [] [2019-07-05 10:28:46] security.INFO: Guard authentication successful! {"token":"[object] (Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user=\"myemail@gmail.com\", authenticated=true, roles=\"ROLE_EDITOR, ROLE_USER\"))","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Guard authenticator set success response. {"response":"[object] (Symfony\\Component\\HttpFoundation\\RedirectResponse: HTTP/1.0 302 Found\r\nCache-Control: no-cache, private\r\nDate: Fri, 05 Jul 2019 10:28:46 GMT\r\nLocation: /backoffice\r\n\r\n<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"refresh\" content=\"0;url=/backoffice\" />\n\n <title>Redirecting to /backoffice</title>\n </head>\n <body>\n Redirecting to <a href=\"/backoffice\">/backoffice</a>.\n </body>\n</html>)","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Remember me skipped: it is not configured for the firewall. {"authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: The "App\Security\LoginFormAuthenticator" authenticator set the response. Any later authenticator will not be called {"authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Stored the security token in the session. {"key":"_security_main"} [] [2019-07-05 10:28:46] request.INFO: Matched route "easyadmin". {"route":"easyadmin","route_parameters":{"_controller":"Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction","path":"/backoffice/","permanent":true,"scheme":null,"httpPort":80,"httpsPort":443,"_route":"easyadmin"},"request_uri":"https://example.com/backoffice","method":"GET"} []
But while in localhost, I am correctly redirected to the backoffice :
- Read existing security token from the session
User was reloaded from a user provider
### var/log/prod.log (following lines, localhost) [2019-07-05 10:19:29] security.DEBUG: Read existing security token from the session. {"key":"_security_main","token_class":"Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken"} [] [2019-07-05 10:19:29] security.DEBUG: User was reloaded from a user provider. {"provider":"Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider","username":"raoux.thierry@free.fr"} [] [2019-07-05 10:19:29] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [] [2019-07-05 10:19:29] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:19:29] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:19:29] cache.INFO: Lock acquired, now computing item "easyadmin.processed_config" {"key":"easyadmin.processed_config"} []
In prod environment, instead :
- it skips step : reading existing security token
- does not refresh user as expected
- instead it populates the TokenStorage with an anonymous Token
Acces denied and back to login url
### var/log/prod.log (same following lines, but from production server) [2019-07-05 10:28:46] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [] [2019-07-05 10:28:46] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [] [2019-07-05 10:28:46] security.INFO: Populated the TokenStorage with an anonymous Token. [] [] [2019-07-05 10:28:46] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /var/www/clients/client0/web4/web/vendor/symfony/security-http/Firewall/AccessListener.php:72)"} [] [2019-07-05 10:28:46] security.DEBUG: Calling Authentication entry point. [] [] [2019-07-05 10:28:46] request.INFO: Matched route "app_login". {"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"GET"} []
security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
access_control:
- { path: ^/backoffice, roles: ROLE_EDITOR} # requires_channel: https
routes.yaml
admin:
path: /backoffice
controller: EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController
LoginFormAuthenticator
// use...
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('admin'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
Security controller
// use...
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'security/login.html.twig',
[
'last_username' => $lastUsername,
'error' => $error,
]
);
}
/**
* @Route("/logout", name="app_logout")
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function logout()
{
return $this->redirectToRoute('home');
}
}
//... skipped forgottenPassword and resetPassword methods
EDIT:
php bin/console debug:config security
output
Current configuration for extension with alias "security"
=========================================================
security:
encoders:
App\Entity\User:
algorithm: bcrypt
hash_algorithm: sha512
key_length: 40
ignore_case: false
encode_as_base64: true
iterations: 5000
cost: null
memory_cost: null
time_cost: null
threads: null
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
manager_name: null
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
methods: { }
user_checker: security.user_checker
stateless: false
logout_on_user_change: true
main:
anonymous:
secret: null
guard:
authenticators:
- App\Security\LoginFormAuthenticator
entry_point: null
logout:
path: app_logout
csrf_parameter: _csrf_token
csrf_token_id: logout
target: /
invalidate_session: true
delete_cookies: { }
handlers: { }
methods: { }
security: true
user_checker: security.user_checker
stateless: false
logout_on_user_change: true
access_control:
-
path: ^/backoffice
roles:
- ROLE_EDITOR
requires_channel: null
host: null
port: null
ips: { }
methods: { }
allow_if: null
access_decision_manager:
strategy: affirmative
allow_if_all_abstain: false
allow_if_equal_granted_denied: true
access_denied_url: null
session_fixation_strategy: migrate
hide_user_not_found: true
always_authenticate_before_granting: false
erase_credentials: true
role_hierarchy: { }
EDIT 2
AS @Arno commented, I edited framework.yaml
to save sessions in var/
directory and I can check that this step works without permissions issues, each time I hit the login form, a sess_ file is written.
Worth saying that if I comment :
access_control:
- { path: ^/odelices_admin, roles: ROLE_USER}
I can access backoffice.
EDIT 3 : session behavior
So now sessions are saved into var/sessions/prod.
- I clean the dir :
sudo rm -r var/sessions/prod/sess_*
I open Chrome and the url, it sets a PHPSSID cookie with the same value as a first sess_xyz file :
_sf2_attributes|a:2:{s:19:"_csrf/https-contact";s:43:"Oq-QpN21bI_BUDcVbv0ocyrYsTzQo3aJr80QAk2AR7w";s:19:"_csrf/https-booking";s:43:"z_L4TG7Wg0jydwl5VabfJMx0NBhQgeasuAiqxksLvD8";}_sf2_meta|a:3:{s:1:"u";i:1562668584;s:1:"c";i:1562668584;s:1:"l";s:1:"0";}
I go to login page. New PHPSSID value associated with a new sess_xyz file :
_sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";}_sf2_meta|a:3:{s:1:"u";i:1562668662;s:1:"c";i:1562668662;s:1:"l";s:1:"0";}
I log in with correct values. This creates 3 new ssid_xyz files.
# 1st one shows user logged in with correct roles and so on _sf2_attributes|a:3:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";s:23:"_security.last_username";s:21:"user_email@gmail.com";s:14:"_security_main";s:799:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":718:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":6:{s:19:"^@App\Entity\User^@id";i:1;s:22:"^@App\Entity\User^@email";s:21:"user_email@gmail.com";s:22:"^@App\Entity\User^@roles";a:1:{i:0;s:11:"ROLE_EDITOR";}s:25:"^@App\Entity\User^@password";s:60:"$2y$13$cXaR7Ss.kTH1U.T/Rzi6m.ALsKwWCLDcO5/OIeRDAq02iylmf4us6";s:21:"^@App\Entity\User^@name";s:7:"Thierry";s:13:"^@*^@resetToken";N;}i:1;b:1;i:2;a:2:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:11:"ROLE_EDITOR";}i:1;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:2:{i:0;s:11:"ROLE_EDITOR";i:1;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";} # 2nd one ...is empty # 3rd one refers to backoffice url _sf2_attributes|a:1:{s:26:"_security.main.target_path";s:42:"https://mywebsite.com/backoffice";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";} # last one is similar to point 3, before logging, only ssid value differs, and a corresponding cookie is set on Chrome _sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"3UC5dCRrahc2qhdZ167Jg4HKTJCexf8PFlefibTVpYk";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}
EDIT 4 : User Entity
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
// use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface # , EquatableInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @var string le token qui servira lors de l'oubli de mot de passe
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $resetToken;
/*public function __construct($username, $password, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->roles = $roles;
}*/
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getResetToken(): string
{
return $this->resetToken;
}
/**
* @param string $resetToken
*/
public function setResetToken(?string $resetToken): void
{
$this->resetToken = $resetToken;
}
public function __toString() {
return $this->getName() ;
}
/* public function isEqualTo(UserInterface $user)
{
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->email !== $user->getUsername()) {
return false;
}
return true;
}*/
}
Stack
Debian Stretch, Nginx + Varnish : Nginx handles 443 requests, pass them to Varnish as a cache proxy, which delivers cached objects or pass requests to nginx backend on 8083 port. This is working like a charm for another app with similar login logic (the lone difference is the buggy one redirects to easyadmin instead of a custom admin), so I don't think it is related to the stack.
vhost
server { # this block only redirects www to non www
listen aaa.bbb.ccc.ddd:443 ssl;
server_name www.somewebsite.com;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_certificate /var/www/clients/client0/web4/ssl/somewebsite.com-le.crt;
ssl_certificate_key /var/www/clients/client0/web4/ssl/somewebsite.com-le.key;
return 301 https://somewebsite.com$request_uri;
}
server { # this block redirects ssl requests to Varnish
listen aaa.bbb.ccc.ddd:443 ssl;
server_name somewebsite.com;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_certificate /var/www/clients/client0/web4/ssl/somewebsite.com-le.crt;
ssl_certificate_key /var/www/clients/client0/web4/ssl/somewebsite.com-le.key;
location / {
# Pass the request on to Varnish.
proxy_pass http://127.0.0.1;
# Pass some headers to the downstream server, so it can identify the host.
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Tell any web apps that the session is HTTPS.
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
}
}
server { # now sent to backend
listen aaa.bbb.ccc.ddd:8083;
server_name somewebsite.com;
root /var/www/somewebsite.com/web/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass 127.0.0.1:8998;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
internal;
}
location ~ \.php$ {
return 404;
}
error_log /var/log/ispconfig/httpd/somewebsite.com/error.log;
access_log /var/log/ispconfig/httpd/somewebsite.com/access.log combined;
location ~ /\. {
deny all;
}
location ^~ /.well-known/acme-challenge/ {
access_log off;
log_not_found off;
root /usr/local/ispconfig/interface/acme/;
autoindex off;
try_files $uri $uri/ =404;
}
location = /favicon.ico {
log_not_found off;
access_log off;
expires max;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
}
Could this be related to permissions on some dir ? HTTPS ? EasyAdmin ? How can I make sure the security token was stored in the session, even it is logged as stored ? I also tried to change access_control to role ROLE_USER so that any authenticated user should access. No way.
Any help is really appreciated.
So here are my comments in a more structured way, so that it might help someone else having problems with authentication in Symfony.
Make sure sessions are saved
By default, each session is saved as a file with the name sess_<id>
in <project_dir>/var/cache/<env>/sessions
or as defined by save_path
in your php.ini
if framework.session.handler
is set to null
. Configure your session directory explicitly and make sure a session file is created when you log in. If not, check the permissions for that folder.
# app/config/config.yml (Symfony 3)
# config/packages/framework.yaml (Symfony 4)
framework:
session:
handler_id: 'session.handler.native_file'
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
Cf. https://symfony.com/doc/current/session.html#configuration
Make sure sessions are correct and used
When you login, a session with a new ID should be created. Check the content of the file. It should contain your serialized user under the firewall name (e.g. main), including your identifier (e.g. email) and your user role(s) (e.g. ROLE_USER). A problem here could be caused by faulty authentication, security config, or serialization.
_sf2_attributes|a:3:{s:18:"_csrf/authenticate";s:43:"n2oap401u4P4O7m_IhPODZ6Bz7EHl-DDsHxBEl-fhxc";s:23:"_security.last_username";s:10:"foo@bar.de";s:14:"_security_main";s:545:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":464:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":4:{s:19:"App\Entity\Userid";i:1;s:22:"App\Entity\Useremail";s:10:"foo@bar.de";s:22:"App\Entity\Userroles";a:0:{}s:25:"App\Entity\Userpassword";s:60:"$2y$13$qwbtasafa58lPonX6B5a9eV4lziF7EZWP8NFLAe3blpCJVhQgPVOS";}i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:1:{i:0;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1563015142;s:1:"c";i:1563015142;s:1:"l";s:1:"0";}
Check that a cookie with the same ID is set in your browser on login. The name of the cookie is defined by session.name
in your php.ini
, by default it is PHPSESSID
. It should be sent with every request you make (e.g. Cookie: PHPSESSID=lpcf79ff8jdv2iigsgvepnr9bb
). If the correct session exists, but you have a different cookie in your browser, you might have been immediately logged out after a success redirect.
Make sure the user is refreshed properly
The session ID should only change when your user changes (e.g. on login and logout). If it changes after normal requests (e.g. you are immediately logged out) or your session seems to be ignored, the problem might be that Symfony considers your user changed. This can be caused by faulty (de)serialization or comparison.
By default, Symfony uses the serialized results of getPassword()
, getUsername()
, and getSalt()
from the session to compare against the user provided by the user provider (e.g. the database). If any of those values changes, you are logged out (cf. https://symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session).
Thus, you should make sure that the user provided by e.g. your database is correct and matches the deserialized user from the session. Make sure the relevant fields are properly serialized. If you implement the Serializable
interface, make sure your serialize()
method matches your unserialize()
. If you implement EquatableInterface
, make sure your isEqualTo()
method works correctly. Both of those interfaces are optional though, so you might consider to remove them for debugging purposes.
这篇关于Symfony 4 登录表单:身份验证成功,但重定向后身份验证立即丢失的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!