【问题标题】:How to implement FosOAuthServerBundle to secure a REST API?如何实施 FosOAuthServerBundle 来保护 REST API?
【发布时间】:2014-02-11 02:37:57
【问题描述】:

我想使用 FOSOAuthServerBundle 提供一个受 OAuth2 保护的 RESTful API,但我不确定我必须做什么。

我按照from the documentation 的基本步骤操作,但有些东西丢失了,我找不到我需要的完整示例。

所以,我尽力了解this example of implementation(我找到的唯一一个),但仍有一些我不明白的地方。

首先,为什么我们需要 API 中的登录页面?假设我的客户端是 iPhone 或 Android 应用程序,我在应用程序上看到了登录页面的兴趣,但我认为客户端只需从 API 调用 Web 服务来获取其令牌,我错了吗?那么如何通过 REST 端点实现自动化和令牌提供呢?

然后,文档告诉编写此防火墙:

oauth_authorize:
    pattern:    ^/oauth/v2/auth
    # Add your favorite authentication process here

而且我不知道如何添加身份验证过程。我应该自己写一个吗,例如关注this tutorial 还是我完全错了?

在全球范围内,在文档中的五个步骤之后,有人可以花时间解释提供 OAuth2 安全 RESTful API 所需的过程吗?会很不错...


@Sehael 回答后编辑:

在完美之前我还有一些问题......

这里的“客户”代表什么?例如,我应该为 iPhone 应用程序创建一个客户端,而为 Android 应用程序创建另一个客户端吗?我是否必须为每个想要使用 API 的实例创建一个新客户端?在这种情况下,最佳做法是什么?

与你不同,我不使用前端网站的 OAuth 过程,而是使用“经典”的 symfony 方式。你觉得这很奇怪,还是很正常?

refresh_token 有什么用处?怎么用?

我尝试测试受 OAuth 保护的新服务。我使用了支持 OAuth 1.0 的 POSTman chrome 扩展,OAuth2 看起来是否像 OAuth1 足以用 POSTman 进行测试?有一个我不知道如何填写的“秘密令牌”字段。如果我不能,我会很高兴看到您提出的 (@Sehael) PHP 课程。 / 编辑:好的,我想我找到了这个答案。我刚刚将 access_token 添加为 GET 参数,并将令牌作为值。它似乎正在工作。不幸的是,我必须对捆绑代码进行反向工程才能找到它,而不是在文档中阅读它。

无论如何,非常感谢!

【问题讨论】:

    标签: symfony oauth-2.0 fosoauthserverbundle


    【解决方案1】:

    我还发现文档可能有点混乱。但是经过几个小时的尝试,我在this blog 的帮助下弄明白了(更新 - 博客不再存在,改为 Internet 存档)。在您的情况下,您不需要 ^/oauth/v2/auth 的防火墙条目,因为这是用于授权页面。您需要记住 oAuth 能够做什么......它不仅仅用于 REST api。但是,如果您想要保护 REST api,则不需要它。这是我的应用程序中的示例防火墙配置:

    firewalls:
    
        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false
    
        api_firewall:
            pattern: ^/api/.*
            fos_oauth: true
            stateless: true
            anonymous: false
    
        secure_area:
            pattern:    ^/
            fos_oauth: true
            form_login:
                provider: user_provider 
                check_path: /oauth/v2/auth_login_check
                login_path: /oauth/v2/auth_login
            logout:
                path:   /logout
                target: /
            anonymous: ~
    
    access_control:
        - { path: ^/oauth/v2/auth_login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
    

    请注意,您需要定义一个用户提供者。如果您使用 FOSUserBundle,则已经为您创建了一个用户提供程序。就我而言,我自己做了一个服务,并用它创建了一个服务。

    在我的 config.yml 中:

    fos_oauth_server:
        db_driver: orm
        client_class:        BB\AuthBundle\Entity\Client
        access_token_class:  BB\AuthBundle\Entity\AccessToken
        refresh_token_class: BB\AuthBundle\Entity\RefreshToken
        auth_code_class:     BB\AuthBundle\Entity\AuthCode
        service:
            user_provider: platform.user.provider
            options:
                supported_scopes: user
    

    我还应该提到,您在数据库中创建的表(access_token、client、auth_code、refresh_token)需要具有比文档中显示的更多的字段...

    访问令牌表: id(int), client_id(int), user_id(int), token(string), scope(string), expires_at(int)

    客户端表: id(int)、random_id(string)、secret(string)、redirect_urls(string)、allowed_grant_types(string)

    授权码表: id(int)、client_id(int)、user_id(int)

    刷新令牌表: id(int), client_id(int), user_id(int), token(string), expires_at(int), scope(string)

    这些表将存储 oAuth 所需的信息,因此请更新您的 Doctrine 实体,使其与上述 db 表匹配。

    然后您需要一种方法来实际生成密钥和 client_id,这就是文档的“创建客户端”部分的用武之地,尽管它不是很有帮助...

    /src/My/AuthBundle/Command/CreateClientCommand.php 创建一个文件(您需要创建Command 文件夹)这段代码来自我上面链接的文章,并展示了一个可以放入该文件的示例:

    <?php
    # src/Acme/DemoBundle/Command/CreateClientCommand.php
    namespace Acme\DemoBundle\Command;
    
    use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
    use Symfony\Component\Console\Input\InputArgument;
    use Symfony\Component\Console\Input\InputOption;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;
    
    class CreateClientCommand extends ContainerAwareCommand
    {
        protected function configure()
        {
            $this
                ->setName('acme:oauth-server:client:create')
                ->setDescription('Creates a new client')
                ->addOption(
                    'redirect-uri',
                    null,
                    InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                    'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.',
                    null
                )
                ->addOption(
                    'grant-type',
                    null,
                    InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                    'Sets allowed grant type for client. Use this option multiple times to set multiple grant types..',
                    null
                )
                ->setHelp(
                    <<<EOT
                        The <info>%command.name%</info>command creates a new client.
    
    <info>php %command.full_name% [--redirect-uri=...] [--grant-type=...] name</info>
    
    EOT
                );
        }
    
        protected function execute(InputInterface $input, OutputInterface $output)
        {
            $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
            $client = $clientManager->createClient();
            $client->setRedirectUris($input->getOption('redirect-uri'));
            $client->setAllowedGrantTypes($input->getOption('grant-type'));
            $clientManager->updateClient($client);
            $output->writeln(
                sprintf(
                    'Added a new client with public id <info>%s</info>, secret <info>%s</info>',
                    $client->getPublicId(),
                    $client->getSecret()
                )
            );
        }
    }
    

    然后要实际创建client_id和secret,从命令行执行这个命令(这会将必要的id和东西插入数据库):

    php app/console acme:oauth-server:client:create --redirect-uri="http://clinet.local/" --grant-type="password" --grant-type="refresh_token" --grant-type="client_credentials"

    注意acme:oauth-server:client:create 可以是您在CreateClientCommand.php 文件中使用$this-&gt;setName('acme:oauth-server:client:create') 实际命名命令的任何名称。

    获得 client_id 和 secret 后,您就可以进行身份​​验证了。在浏览器中发出如下请求:

    http://example.com/oauth/v2/token?client_id=[CLIENT_ID_YOU GENERATED]&amp;client_secret=[SECRET_YOU_GENERATED]&amp;grant_type=password&amp;username=[USERNAME]&amp;password=[PASSWORD]

    希望它对你有用。肯定有很多要配置的,只需尝试一步一步地进行。

    我还编写了一个简单的 PHP 类来使用 oAuth 调用我的 Symfony REST api,如果您认为这很有用,请告诉我,我可以传递它。

    更新

    回答您的进一步问题:

    “客户”在同一个博客上进行了描述,只是一篇不同的文章。阅读此处的Clients and Scopes 部分,它应该为您阐明什么是客户。就像文章中提到的那样,您不需要每个用户都有一个客户端。如果您愿意,您可以为所有用户提供一个客户端。

    我实际上也在为我的前端站点使用经典的 Symfony 身份验证,但将来可能会改变。所以把这些东西放在脑海里总是好的,但我不会说把这两种方法结合起来很奇怪。

    当 access_token 已过期并且您希望在不重新发送用户凭据的情况下请求新的 access_token 时使用 refresh_token。相反,您发送刷新令牌并获得一个新的 access_token。这对于 REST API 来说并不是必需的,因为单个请求可能不会花费足够长的时间来使 access_token 过期。

    oAuth1 和 oAuth2 非常不同,所以我认为您使用的方法不起作用,但我从未尝试过。但只是为了测试,你可以发出普通的 GET 或 POST 请求,只要你在 GET 查询字符串中传递access_token=[ACCESS_TOKEN](实际上是所有类型的请求)。

    但无论如何,这是我的课。我使用了一个配置文件来存储一些变量,我没有实现删除的能力,但这并不太难。

    class RestRequest{
        private $token_url;
        private $access_token;
        private $refresh_token;
        private $client_id;
        private $client_secret;
    
        public function __construct(){
            include 'config.php';
            $this->client_id = $conf['client_id'];
            $this->client_secret = $conf['client_secret']; 
            $this->token_url = $conf['token_url'];
    
            $params = array(
                'client_id'=>$this->client_id,
                'client_secret'=>$this->client_secret,
                'username'=>$conf['rest_user'],
                'password'=>$conf['rest_pass'],
                'grant_type'=>'password'
            );
    
            $result = $this->call($this->token_url, 'GET', $params);
            $this->access_token = $result->access_token;
            $this->refresh_token = $result->refresh_token;
        }
    
        public function getToken(){
            return $this->access_token;
        }
    
        public function refreshToken(){
            $params = array(
                'client_id'=>$this->client_id,
                'client_secret'=>$this->client_secret,
                'refresh_token'=>$this->refresh_token,
                'grant_type'=>'refresh_token'
            );
    
            $result = $this->call($this->token_url, "GET", $params);
    
            $this->access_token = $result->access_token;
            $this->refresh_token = $result->refresh_token;
    
            return $this->access_token;
        }
    
        public function call($url, $method, $getParams = array(), $postParams = array()){
            ob_start();
            $curl_request = curl_init();
    
            curl_setopt($curl_request, CURLOPT_HEADER, 0); // don't include the header info in the output
            curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1); // don't display the output on the screen
            $url = $url."?".http_build_query($getParams);
            switch(strtoupper($method)){
                case "POST": // Set the request options for POST requests (create)
                    curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
                    curl_setopt($curl_request, CURLOPT_POST, 1); // set request type to POST
                    curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
                    break;
                case "GET": // Set the request options for GET requests (read)
                    curl_setopt($curl_request, CURLOPT_URL, $url); // request URL and params
                    break;
                case "PUT": // Set the request options for PUT requests (update)
                    curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
                    curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT"); // set request type
                    curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
                    break;
                case "DELETE":
    
                    break;
                default:
                    curl_setopt($curl_request, CURLOPT_URL, $url);
                    break;
            }
    
            $result = curl_exec($curl_request); // execute the request
            if($result === false){
                $result = curl_error($curl_request);
            }
            curl_close($curl_request);
            ob_end_flush();
    
            return json_decode($result);
        }
    }
    

    然后要使用该类,只需:

    $request = new RestRequest();
    $insertUrl = "http://example.com/api/users";
    $postParams = array(
        "username"=>"test",
        "is_active"=>'false',
        "other"=>"3g12g53g5gg4g246542g542g4"
    );
    $getParams = array("access_token"=>$request->getToken());
    $response = $request->call($insertUrl, "POST", $getParams, $postParams);
    

    【讨论】:

    • 非常感谢@Sehael,真的很有帮助!我有我的客户,我可以按用户生成令牌。但是我编辑了这个问题,因为我还有一些最后的(我希望的)审讯。
    • 非常感谢!你救了我;)
    • @Sehael 您能否描述一下如何为 oauth 和网络登录用户提供有效的 ^/api URL。当我使用 access_token - 它可以工作,但是对于在浏览器中登录的用户,它会失败并出现 401 错误
    • 在您的主 routing.yml 文件中,您需要做的就是添加一个具有不同前缀的新定义。在此示例中,/api 下的所有内容都受 oauth 保护,但如果您添加指向相同控制器的新路由定义(具有不同前缀),则可以以不同方式保护它(如表单登录)
    猜你喜欢
    • 2015-05-13
    • 2019-08-21
    • 2019-04-02
    • 2021-05-16
    • 2019-06-03
    • 2017-04-15
    • 2011-04-23
    • 2019-09-28
    相关资源
    最近更新 更多