我正在使用symfony后端实现一个api,为了进行身份验证,我使用内置库进行此处描述的无密码身份验证
https://symfony.com/doc/current/security/login_link.html#1-configure-the-login-link-authenticator
我只实现了登录端点,login_check由库处理。
登录所做的只是生成链接并将其发送到用户邮箱中,如下所示:
<?php
namespace App\Controller\Api;
use App\Entity\User;
use Psr\Log\LoggerInterface;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
class AuthenticationController extends BaseController
{
public function __construct(
LoggerInterface $logger,
protected MailerInterface $mailer,
protected EntityManagerInterface $entityManager
) {
parent::__construct($logger);
}
#[Route(path: '/login', name: 'login', methods: ['POST'])]
public function login(
LoginLinkHandlerInterface $loginLinkHandler,
UserRepository $userRepository,
Request $request
): Response {
// load the user in some way (e.g. using the form input)
$email = $request->getPayload()->get('email');
if (!$this->isValidEmail($email)) {
throw new BadRequestHttpException("Email $email is not valid!");
}
$user = $userRepository->findOneBy(['email' => $email]);
$emailTitle = "Your beligent.io Login link.";
if (!$user) {
$user = new User();
$user->setEmail($email);
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->logger->info("User {$user->getEmail()} created!");
$emailTitle = "Successfully registered to beligent.io, your login link.";
}
// create a login link for $user this returns an instance
// of LoginLinkDetails
$loginLinkDetails = $loginLinkHandler->createLoginLink($user);
$loginLink = $loginLinkDetails->getUrl();
$regex = '/^https?:\/\/[^\/]+(\/.*)$/';
if (!preg_match($regex, $loginLink, $matches)) {
throw new \Exception("Could not decode login link");
}
$feBaseURL = getenv("FRONTEND_URL");
$customLoginLink = $feBaseURL . $matches[1];
$email = (new Email())
->from('[email protected]')
->to($user->getEmail())
->subject($emailTitle)
->text('Myapp.io')
->html("<p>$customLoginLink</p>");
$this->mailer->send($email);
return new Response(null, Response::HTTP_NO_CONTENT);
}
private function isValidEmail(string $email)
{
$emailPattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
return preg_match($emailPattern, $email);
}
}
然后我在电子邮件中收到此链接:
localhost:8888/[email protected]&expires=1718457559&hash=at3wXVy9CFjNZauoIs7Yds85pH4HBQplPUDznaEm1H0~D60nAF03Qti0aU2B2Z5nVMOl_evP1uYHUVXRtHzgea0~
我的配置如下:
security.yaml:
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto"
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
login_link:
check_route: login_check
signature_properties: ["id"]
lifetime: 3600
success_handler: App\Security\Authentication\AuthenticationSuccessHandler
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/login_check, roles: PUBLIC_ACCESS }
- { path: ^/doc, roles: PUBLIC_ACCESS }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
我还使用了nelmio_cors捆绑包,其配置如下:
nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
allow_credentials: true
expose_headers: ['Link']
max_age: 3600
paths:
'^/': null
和frameworks.yml:
framework:
secret: "%env(APP_SECRET)%"
#csrf_protection: true
# Note that the session will be started ONLY if you read or write from it.
session:
cookie_samesite: "none"
cookie_secure: true
当使用poster测试时,一切都很好,但当使用浏览器测试时,尽管服务器响应中包含Set cookie,但该浏览器不会存储PHPSESSID cookie。我使用chrome,在调试模式下打开“应用程序”选项卡时,我看不到存储任何cookie。
以下是调用login_check时的服务器响应:
HTTP/1.1 200 OK
Server: nginx/1.22.1
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.2.20
Cache-Control: max-age=0, must-revalidate, private
Date: Sat, 15 Jun 2024 12:32:13 GMT
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: link
X-Robots-Tag: noindex
Expires: Sat, 15 Jun 2024 12:32:13 GMT
Set-Cookie: PHPSESSID=4f43da26e66330f7b41cf749a5306a93; path=/; httponly; samesite=lax
收到了Set Cookie,但我不明白为什么没有存储此Cookie。
目前我可能错了,但我认为它可能来自于隐式的login_check端点,它可能会把头中的东西搞砸,因为对于单体架构来说,我知道如何在配置中更改它,但我不知道这背后有什么逻辑。
基本上,问题是浏览器不存储会话cookie的最明显原因是什么?
Symfony版本:7
PHP版本:8.2