You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:45:59