Symfony 7:如何为登录路由绕过身份验证?
Symfony 7 API自定义身份验证实现方案
问题核心
当前实现存在以下关键问题:
- 登录路由
/api/login未正确绕过防火墙拦截 - 认证器逻辑混乱,同时处理登录与Token校验导致冲突
- 安全配置中防火墙规则与访问控制未正确匹配
- Token校验环节缺少标准格式处理,且认证成功后未生成有效Token
正确实现步骤
1. 安全配置修正(config/packages/security.yaml)
拆分防火墙规则,确保登录路由公开访问,主防火墙处理其他API路由:
security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # 启用公开访问角色 role_hierarchy: PUBLIC_ACCESS: [] firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # 登录路由单独配置,允许公开访问 api_login: pattern: ^/api/login stateless: true security: false # 主防火墙处理所有API路由(排除登录) main: pattern: ^/api lazy: true stateless: true custom_authenticators: - App\Security\ApiTokenAuthenticator access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_USER }
2. 拆分认证逻辑:登录控制器与Token校验器分离
登录控制器(src/Controller/UserController.php)
专注处理登录请求,生成加密Token返回客户端:
<?php namespace App\Controller; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class UserController { private $entityManager; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } #[Route('/api/login', name: 'api_login', methods: ['POST'])] public function login(Request $request): JsonResponse { $requestData = json_decode($request->getContent(), true); if (!isset($requestData['name'])) { return new JsonResponse(['error' => 'Name字段必填'], Response::HTTP_BAD_REQUEST); } $name = $requestData['name']; // 可选:验证用户是否存在 $user = $this->entityManager->getRepository(\App\Entity\User::class)->findOneBy(['name' => $name]); if (!$user) { return new JsonResponse(['error' => '用户不存在'], Response::HTTP_UNAUTHORIZED); } // 生成加密Token $encryptedToken = $this->encrypt($name); return new JsonResponse(['token' => $encryptedToken]); } private function encrypt(string $name): string { $secretKey = $_ENV['APP_SECRET']; $key = openssl_digest($secretKey, 'SHA256', true); $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC"); $iv = openssl_random_pseudo_bytes($ivlen); $ciphertextRaw = openssl_encrypt($name, $cipher, $key, OPENSSL_RAW_DATA, $iv); $hmac = hash_hmac('sha256', $ciphertextRaw, $key, true); return base64_encode($iv . $hmac . $ciphertextRaw); } }
API Token认证器(src/Security/ApiTokenAuthenticator.php)
专门处理除登录外的API路由Token校验:
<?php namespace App\Security; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\Security\Core\Authentication\Token\PostAuthenticationToken; 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\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; class ApiTokenAuthenticator implements AuthenticatorInterface { private $entityManager; public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; } public function supports(Request $request): ?bool { // 仅处理非登录的API路由 $route = $request->attributes->get('_route'); return str_starts_with($route, 'api_') && $route !== 'api_login'; } public function authenticate(Request $request): Passport { $authHeader = $request->headers->get('Authorization'); if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) { throw new UnauthorizedHttpException('Bearer', 'Authorization header缺失或格式错误'); } $token = substr($authHeader, 7); // 移除Bearer前缀 $decryptedName = $this->decrypt($token); if (!$decryptedName) { throw new UnauthorizedHttpException('Bearer', '无效或过期的Token'); } // 加载并验证用户 $userBadge = new UserBadge($decryptedName, function ($username) { $user = $this->entityManager->getRepository(\App\Entity\User::class)->findOneBy(['name' => $username]); if (!$user) { throw new AuthenticationException('用户不存在'); } return $user; }); return new Passport($userBadge); } private function decrypt(string $token): string { $secretKey = $_ENV['APP_SECRET']; $key = openssl_digest($secretKey, 'SHA256', true); $c = base64_decode($token); $ivlen = openssl_cipher_iv_length($cipher = "AES-128-CBC"); if (strlen($c) < $ivlen + 32) { return ''; } $iv = substr($c, 0, $ivlen); $hmac = substr($c, $ivlen, 32); $ciphertextRaw = substr($c, $ivlen + 32); $originalPlaintext = openssl_decrypt($ciphertextRaw, $cipher, $key, OPENSSL_RAW_DATA, $iv); $calcmac = hash_hmac('sha256', $ciphertextRaw, $key, true); return hash_equals($hmac, $calcmac) ? $originalPlaintext : ''; } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // 认证成功后继续处理请求,无需返回自定义响应 return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return new JsonResponse(['error' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED); } public function createToken(Passport $passport, string $firewallName): TokenInterface { // 生成认证Token,用于后续权限校验 return new PostAuthenticationToken( $passport->getUser(), $firewallName, $passport->getUser()->getRoles() ); } }
关键注意事项
- Token格式:客户端请求需使用
Authorization: Bearer <token>标准格式 - 用户实体:确保
App\Entity\User实现UserInterface及PasswordAuthenticatedUserInterface接口 - 无状态配置:主防火墙设置
stateless: true,避免生成会话 - 环境变量:确认
.env文件中APP_SECRET已正确配置 - 错误处理:所有认证异常返回JSON格式错误信息,便于客户端处理
内容的提问来源于stack exchange,提问作者Kaveh Mohammadi
相关产品推荐
相关产品推荐

