Symfony 4 登录表单:身份验证成功,但重定向后身份验证立即丢失 [英] Symfony 4 login form : authenticating successfully, but authentication immediately lost after redirect

查看:31
本文介绍了Symfony 4 登录表单:身份验证成功,但重定向后身份验证立即丢失的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我按照这个表单登录设置文档构建了一个登录表单.>

这在本地主机上运行良好但在生产服务器上运行正常.

在 localhost 和 prod 上,身份验证成功开始

  1. Guard 认证成功
  2. 保护身份验证器设置成功响应
  3. 在会话中存储安全令牌
  4. 匹配路由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 重定向到 

但是在本地主机中,我被正确重定向到后台:

在生产环境中,改为:

  • 跳过步骤:读取现有的安全令牌
  • 不会按预期刷新用户
  • 相反,它使用匿名令牌填充 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 中.

  1. 我清理目录:sudo rm -r var/sessions/prod/sess_*
  2. 我打开 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";}

  3. 我去登录页面.与新 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";}

  4. 我使用正确的值登录.这会创建 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_ 的文件,位于 /var/cache//sessions或者,如果 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

  1. Guard authentication successful
  2. Guard authenticator set success response
  3. Stored the security token in the session
  4. 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.

  1. I clean the dir : sudo rm -r var/sessions/prod/sess_*
  2. 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";}
    

  3. 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";}
    

  4. 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆