Symfony 3.2:通过不同的属性验证用户

时间:2017-02-10 12:40:20

标签: authentication symfony

我想实现以下目标:

在我的安装中有两个捆绑包,ApiBundle和BackendBundle。用户在BackendBundle中定义,但我可以稍后将它们放在UserBundle中。

ApiBundle基本上为控制器提供api方法,例如getSomething()

BackendBundle具有用户实体,服务和一些视图,如登录表单和后端视图。从后端控制器我想访问某些api方法。

将从外部请求其他api方法。将通过curl请求Api方法。

我希望为这两个目的拥有不同的用户。 User类实现UserInterface并具有$username$password$apiKey等属性。

现在基本上我想通过登录表单提供一个用户名和密码的身份验证方法,以及另一种通过外部curl来调用api的身份验证方法,只需要apiKey。

在这两种情况下,经过身份验证的用户都应该可以访问不同的资源。

到目前为止,我的security.yml看起来像这样:

providers:
    chain_provider:
        chain:
            providers: [db_username, db_apikey]
    db_username:
        entity:
            class: BackendBundle:User
            property: username
    db_apikey:
        entity:
            class: BackendBundle:User
            property: apiKey

encoders:
    BackendBundle\Entity\User:
        algorithm: bcrypt
        cost: 12

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    main:
        anonymous: ~
        form_login:
            login_path: login
            check_path: login
            default_target_path: backend
            csrf_token_generator: security.csrf.token_manager
        logout:
            path:   /logout
            target: /login
        provider: chain_provider

access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/api, roles: ROLE_API }
    - { path: ^/backend, roles: ROLE_BACKEND } 

问题1:如何实现同一实体的用户可以进行不同的身份验证并访问某些资源?所需的行为是使用用户名/密码进行身份验证或仅使用apikey进行身份验证。

问题2:如果请求者未经过正确的身份验证,而不是返回登录表单的视图,那么api方法如何返回json?例如。如果有人请求{ 'error': 'No access' },我想返回/api/getSomething而不是登录表单的html,当然如果有人请求/backend/someroute我想要显示登录表单。

非常感谢每一位帮助! :)

<小时/> symfony文档说:

  

防火墙的主要工作是配置用户的身份验证方式。他们会使用登录表吗? HTTP基本认证? API令牌?以上所有?

我认为我的问题基本上是,如何同时进行登录表单和api令牌身份验证。

所以也许,我需要这样的东西:http://symfony.com/doc/current/security/guard_authentication.html#frequently-asked-questions

1 个答案:

答案 0 :(得分:2)

问题1 :当您只想通过apiKey对用户进行身份验证时,那么最好的解决方案就是实现自己的用户提供商。解决方案在Symfony doc:http://symfony.com/doc/current/security/api_key_authentication.html

中有详细描述

编辑 - 您可以拥有任意数量的用户提供商,如果一个用户提供商失败,那么另一个用户提供商就会发挥作用 - 此处描述https://symfony.com/doc/current/security/multiple_user_providers.html

下面是ApiKeyAuthenticator的代码,它获取令牌并调用ApiKeyUserProvider来查找/获取用户。如果找到用户,则提供给Symfony安全性。 ApiKeyUserProvider需要UserRepository来进行用户操作 - 我确定你有一个,否则就写了。

代码未经过测试,因此可能需要进行一些调整。

让我们开始工作:

的src / BackendBundle /安全性/ ApiKeyAuthenticator.php

namespace BackendBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\HttpFoundation\JsonResponse;

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    protected $httpUtils;

    public function __construct(HttpUtils $httpUtils)
    {
        $this->httpUtils = $httpUtils;
    }   

    public function createToken(Request $request, $providerKey)
    {
        //use this only if you want to limit apiKey authentication only for certain url
        //$targetUrl = '/login/check';
        //if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) {
        //    return;
        //}

        // get an apikey from authentication request
        $apiKey = $request->query->get('apikey');
        // or if you want to use an "apikey" header, then do something like this:
        // $apiKey = $request->headers->get('apikey');

        if (!$apiKey) {
            //You can return null just skip the authentication, so Symfony
            // can fallback to another authentication method, if any.
            return null;
            //or you can return BadCredentialsException to fail the authentication
            //throw new BadCredentialsException();
        }

        return new PreAuthenticatedToken(
            'anon.',
            $apiKey,
            $providerKey
        );
    }

    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
    }

    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        if (!$userProvider instanceof ApiKeyUserProvider) {
            throw new \InvalidArgumentException(
                sprintf(
                    'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
                    get_class($userProvider)
                )
            );
        }

        $apiKey = $token->getCredentials();
        $username = $userProvider->getUsernameForApiKey($apiKey);

        if (!$username) {
            // CAUTION: this message will be returned to the client
            // (so don't put any un-trusted messages / error strings here)
            throw new CustomUserMessageAuthenticationException(
                sprintf('API Key "%s" does not exist.', $apiKey)
            );
        }

        $user = $userProvider->loadUserByUsername($username);

        return new PreAuthenticatedToken(
            $user,
            $apiKey,
            $providerKey,
            $user->getRoles()
        );
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        // this contains information about *why* authentication failed
        // use it, or return your own message
        return new JsonResponse(//$exception, 401);
    }
}

的src / BackendBundle /安全性/ ApiKeyUserProvider.php

namespace BackendBundle\Security;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;

use BackendBundle\Entity\User;
use BackendBundle\Entity\UserORMRepository;

class ApiKeyUserProvider implements UserProviderInterface
{
    private $userRepository;

    public function __construct(UserORMRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function getUsernameForApiKey($apiKey)
    {
        //use repository method for getting user from DB by API key
        $user = $this->userRepository->...
        if (!$user) {
            throw new UsernameNotFoundException('User with provided apikey does not exist.');
        }

        return $username;
    }

    public function loadUserByUsername($username)
    {
        //use repository method for getting user from DB by username
        $user = $this->userRepository->...
        if (!$user) {
            throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $username));
        }
        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Expected an instance of ..., but got "%s".', get_class($user)));
        }
        if (!$this->supportsClass(get_class($user))) {
            throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userRepository->getClassName(), get_class($user)));
        }
        //use repository method for getting user from DB by ID
        if (null === $reloadedUser = $this->userRepository->findUserById($user->getId())) {
            throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
        }
        return $reloadedUser;
    }

    public function supportsClass($class)
    {
        $userClass = $this->userRepository->getClassName();
        return ($userClass === $class || is_subclass_of($class, $userClass));
    }
} 

服务定义:

services:
    api_key_user_provider:
        class: BackendBundle\Security\ApiKeyUserProvider
    apikey_authenticator:
        class: BackendBundle\Security\ApiKeyAuthenticator
        arguments: ["@security.http_utils"]
        public:    false

最后安全提供程序配置:

providers:
    chain_provider:
        chain:
            providers: [api_key_user_provider, db_username]
    api_key_user_provider:
        id: api_key_user_provider
    db_username:
        entity:
            class: BackendBundle:User
            property: username

我鼓励您更多地学习Symfony文档,对身份验证过程,用户实体,用户提供商等有很好的解释。

问题2 :您可以通过定义自己的访问被拒绝处理程序来为访问被拒绝事件实现不同的响应类型:

namespace BackendBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;

class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
    public function handle(Request $request, AccessDeniedException $accessDeniedException)
    {
        $route = $request->get('_route');
        if ($route == 'api')) {
            return new JsonResponse($content, 403);
        } elseif ($route == 'backend')) {
            return new Response($content, 403);
        } else {
            return new Response(null, 403);
        }
    }
}