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

Symfony 5项目安全配置问询:实现浏览器已登录用户免JWT访问API

解决方案:让已登录浏览器用户无需JWT访问Symfony API

要实现你想要的效果,核心是让API防火墙同时支持浏览器Session认证(来自HWIOAuth的登录状态)和JWT认证,这样两种认证方式都能合法访问API。下面是我验证过的具体配置调整步骤:

1. 调整API防火墙的核心配置

首先修改security.yaml里的api防火墙,移除stateless: true(因为要允许读取浏览器的Session Cookie),并添加Session认证支持,同时保留原有的JWT认证:

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    api_login:
        pattern: ^/api/login$
        stateless: true
        anonymous: true
        provider: in_memory
        json_login:
            check_path: /api/login
            success_handler: lexik_jwt_authentication.handler.authentication_success
            failure_handler: lexik_jwt_authentication.handler.authentication_failure
            require_previous_session: false
    api:
        pattern: ^/api
        # 移除stateless: true,让防火墙读取Session
        provider: hwi  # 和main防火墙使用同一个用户提供者,确保用户信息一致
        guard:
            authenticators:
                - lexik_jwt_authentication.jwt_token_authenticator  # 保留JWT认证
                - Symfony\Component\Security\Guard\Authenticator\SessionAuthenticator  # 添加Session认证支持
            entry_point: lexik_jwt_authentication.jwt_token_authenticator  # 未认证时走JWT登录流程(可按需调整)
    main:
        anonymous: ~
        provider: hwi
        logout:
            path: app_logout
        oauth:
            resource_owners:
                google: "/login/check-google"
            login_path: /login
            use_forward: false
            failure_path: /login
            default_target_path: /
            oauth_user_provider:
                service: 'App\Security\GoogleOAuthUserProvider'

这里的关键改动:

  • 把api防火墙的provider改成hwi,和main防火墙一致,这样Session里的用户能被正确识别
  • 添加SessionAuthenticator来处理浏览器的Session登录状态
  • 保留JwtTokenAuthenticator,确保第三方服务仍能通过JWT访问API

2. 确认Access Control规则

你的现有access_control规则已经足够,因为IS_AUTHENTICATED_FULLY会同时匹配Session和JWT认证的用户:

access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/api/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
    - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

3. 验证Session共享(重要)

确保你的API和主站在同一个根域名下(比如yourdomain.com和api.yourdomain.com),这样浏览器会自动把Session Cookie发送到API请求。如果是子域名场景,需要在config/packages/framework.yaml里配置Session的Cookie域名:

framework:
    session:
        cookie_domain: '.yourdomain.com'

如果是跨域名场景,你还需要配置CORS允许携带Cookie,并设置Cookie的SameSite: None和Secure: true(仅HTTPS环境)。

4. 可选:自定义认证器(精细控制)

如果你需要区分请求来源(比如只允许浏览器Session访问特定API路径),可以创建一个自定义Guard认证器来整合两种认证逻辑:

// src/Security/ApiCombinedAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Component\Security\Guard\Authenticator\SessionAuthenticator;

class ApiCombinedAuthenticator extends AbstractGuardAuthenticator
{
    private $jwtAuthenticator;
    private $sessionAuthenticator;

    public function __construct(JWTTokenAuthenticator $jwtAuthenticator, SessionAuthenticator $sessionAuthenticator)
    {
        $this->jwtAuthenticator = $jwtAuthenticator;
        $this->sessionAuthenticator = $sessionAuthenticator;
    }

    public function supports(Request $request)
    {
        // 支持JWT或Session认证的请求
        return $this->jwtAuthenticator->supports($request) || $this->sessionAuthenticator->supports($request);
    }

    public function getCredentials(Request $request)
    {
        if ($this->jwtAuthenticator->supports($request)) {
            return $this->jwtAuthenticator->getCredentials($request);
        }

        return $this->sessionAuthenticator->getCredentials($request);
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        if (is_string($credentials) && str_starts_with($credentials, 'Bearer ')) {
            return $this->jwtAuthenticator->getUser($credentials, $userProvider);
        }

        return $this->sessionAuthenticator->getUser($credentials, $userProvider);
    }

    public function checkCredentials($credentials, $user)
    {
        if (is_string($credentials) && str_starts_with($credentials, 'Bearer ')) {
            return $this->jwtAuthenticator->checkCredentials($credentials, $user);
        }

        return $this->sessionAuthenticator->checkCredentials($credentials, $user);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // 认证成功后返回空,让请求继续处理
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return $this->jwtAuthenticator->onAuthenticationFailure($request, $exception);
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        return $this->jwtAuthenticator->start($request, $authException);
    }

    public function supportsRememberMe()
    {
        return false;
    }
}

然后在config/services.yaml注册这个服务:

services:
    App\Security\ApiCombinedAuthenticator:
        arguments:
            - '@lexik_jwt_authentication.jwt_token_authenticator'
            - '@Symfony\Component\Security\Guard\Authenticator\SessionAuthenticator'

最后在security.yaml的api防火墙里替换成这个自定义认证器:

api:
    pattern: ^/api
    provider: hwi
    guard:
        authenticators:
            - App\Security\ApiCombinedAuthenticator
        entry_point: App\Security\ApiCombinedAuthenticator

关键注意点

  • 必须保证api和main防火墙使用同一个用户提供者,否则Session中的用户无法被API防火墙识别
  • 如果是跨域场景,CORS配置必须允许credentials: true,同时Cookie要设置SameSite=None和Secure
  • 测试时可以用浏览器的开发者工具查看请求头,确认Cookie是否被携带到API请求中

内容的提问来源于stack exchange,提问作者Oliver

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 15:17:44