Firebase Auth JS/PHP

6
我被委托基于firebase为Android应用程序构建Web界面。我有一些与数据库交互的端点(Cloud functions)。要访问这些端点,我需要使用电子邮件和密码对用户进行身份验证[1],检索accessToken[2],并使用Authorization: Bearer {accessToken}标头授权每个请求到端点。
我使用php,并且很难理解如何在我的应用程序中管理经过身份验证的用户。
简而言之,请查看我在php中的最终解决方案。https://dev59.com/mK3la4cB1Zd3GeqPStDC#52119600 我通过ajax在php会话中传输accessToken,以签署cURL请求到端点。显然,没有其他方法可以使用firebase JS auth(据我所知[4])。
我的问题是:仅将accessToken保存在php会话中,并通过ajax POST请求与每个页面加载进行比较是否足够?处理php的更强大的策略是什么?
编辑:一个用户指出,使用JWT令牌的经典php会话没有太多意义,我研究了这个主题。因此,关于Firebase - 这是需要考虑的东西吗?https://firebase.google.com/docs/auth/admin/manage-cookies

Firebase Auth为依赖于会话cookie的传统网站提供服务器端会话cookie管理。与可能需要重定向机制才能在到期时更新会话cookie的客户端短暂ID令牌相比,此解决方案具有几个优点:

以下是我的内容:
1. 登录页面
如Firebase示例中所述[3]
function initApp() {

  firebase.auth().onAuthStateChanged(function (user) {
    if (user) {
      // User is signed in.

      // obtain token, getIdToken(false) = no forced refresh
      firebase.auth().currentUser.getIdToken(false).then(function (idToken) {

        // Send token to your backend via HTTPS
        $.ajax({
          type: 'POST',
          url: '/auth/check',
          data: {'token': idToken},
          complete: function(data){
            // data = {'target' => '/redirect/to/route'}
            if(getProperty(data, 'responseJSON.target', false)){
              window.location.replace(getProperty(data, 'responseJSON.target'));
            }
          }
        });
        // ...
      }).catch(function (error) {
        console.log(error);
      });


    } else {
      // User Signed out
      $.ajax({
        type: 'POST',
        url: '/auth/logout',

        complete: function(data){
          // data = {'target' => '/redirect/to/route'}
          if(getProperty(data, 'responseJSON.target', false)){
            // don't redirect to itself
            // logout => /
            if(window.location.pathname != getProperty(data, 'responseJSON.target', false)){
              window.location.replace(getProperty(data, 'responseJSON.target'));
            }
          }
        }
      });

      // User is signed out.
    }

  });
}

window.onload = function () {
  initApp();
};

2. 一个PHP控制器来处理认证请求

public function auth($action)
{

  switch($action) {
    // auth/logout
    case 'logout':

      unset($_SESSION);
      // some http status header and mime type header
      echo json_encode(['target' => '/']); // / => index page
    break;

    case 'check':

      // login.
      if(! empty($_POST['token']) && empty($_SESSION['token'])){

        // What if I send some bogus data here? The call to the Endpoint later would fail anyway
        // But should it get so far?

        $_SESSION['token'] = $_POST['token'];

        // send a redirect target back to the JS
        echo json_encode(['target' => '/dashboard']);
        break;
      }


      if($_POST['token'] == $_SESSION['token']){
        // do nothing;
        break;
      }
    break;
  }
}

3. 主控制器

// pseudo code
class App
{
  public function __construct()
  {
    if($_SESSION['token']){
      $client = new \GuzzleHttp\Client();
      // $user now holds all custom access rights within the app.
      $this->user = $client->request(
        'GET', 
        'https://us-centralx-xyz.cloudfunctions.net/user_endpoint',
        ['headers' => 
                [
                    'Authorization' => "Bearer {$_SESSION['token']}"
                ]
            ]
        )->getBody()->getContents();
    }else{
      $this->user = null;
    }
  }

  public function dashboard(){
    if($this->user){
      var_dump($this->user);
    }else{
      unset($_SESSION);
      // redirect to '/' 
    }
  }
}

注意:我知道这个sdk https://github.com/kreait/firebase-php,我在那里阅读了很多问题和在SO上的帖子,但是我感到困惑,因为有谈论完全管理员权限等内容,而我只与建立在Firebase之上的端点进行交互(加上Firebase身份验证和Firestore)。而且我仍然在使用php 5.6 :-/。
感谢您的时间!
[1]:https://firebase.google.com/docs/auth/web/password-auth [2]:https://firebase.google.com/docs/reference/js/firebase.User#getIdToken [3]:https://github.com/firebase/quickstart-js/blob/master/auth/email-password.html [4]:https://github.com/kreait/firebase-php/issues/159#issuecomment-360225655
3个回答

8
我必须承认,Firebase文档和示例以及不同的服务的复杂性让我感到困惑,以至于我认为仅通过JavaScript才能实现Web身份验证。那是错误的。至少对于我的情况来说,我只需要使用电子邮件和密码登录检索Json Web Token(JWT),以便签署所有调用Firebase云函数的请求。与通过奇怪的Ajax请求或通过JavaScript设置令牌cookie相比,我只需要调用Firebase Auth REST API即可。
这里是一个使用Fatfree框架的最小示例: 登录表单
<form action="/auth" method="post">
    <input name="email">
    <input name="password">
    <input type="submit">
</form>

路由

$f3->route('POST /auth', 'App->auth');

控制器

class App
{
    function auth()
    {
        $email = $this->f3->get('POST.email');
        $password = $this->f3->get('POST.password');

        $apiKey = 'API_KEY'; // see https://firebase.google.com/docs/web/setup

        $auth = new Auth($apiKey);
        $result = $auth->login($email,$password);

        if($result['success']){
            $this->f3->set('COOKIE.token',$result['idToken']);
            $this->f3->reroute('/dashboard');
        }else{
            $this->f3->clear('COOKIE.token');
            $this->f3->reroute('/');
        }
    }
}

<?php
use GuzzleHttp\Client;

class Auth
{

    protected $apiKey;

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

    public function login($email,$password)
    {

        $client = new Client();
        // Create a POST request using google api
        $key = $this->apiKey;
        $responsee = $client->request(
            'POST',
            'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=' . $key,
            [
                'headers' => [
                    'content-type' => 'application/json',
                    'Accept' => 'application/json'
                ],
                'body' => json_encode([
                    'email' => $email,
                    'password' => $password,
                    'returnSecureToken' => true
                ]),
                'exceptions' => false
            ]
        );

        $body = $responsee->getBody();
        $js = json_decode($body);

        if (isset($js->error)) {
            return [
                'success' => false,
                'message' => $js->error->message
            ];
        } else {
            return [
                'success' => true,
                'localId' => $js->localId,
                'idToken' => $js->idToken,
                'email' => $js->email,
                'refreshToken' => $js->refreshToken,
                'expiresIn' => $js->expiresIn,
            ];

        }

    }

}

Credits


我遇到了完全相同的问题 - 这真是太有帮助了,而且完美地解决了。如果可以的话,我会点赞两次 :) 谢谢。 - Katai

2
似乎@Chad K正在引导你走上正确的轨道(cookie和ajax - 冠军的早餐... :),不过我想分享一下我的代码,这是从我的工作系统中提取的(当然还有一些“隐私”方面的东西!)
查找/****类型的注释以获取需要自己设置的内容(您可能还想以其他方式执行一些firebase操作 - 请参阅文档...)
LOGIN.php页面(我发现总的来说将其分开更简单 - 请查看说明以了解原因...)
<script>
    /**** I picked this up somewhere off SO - kudos to them - I use it a lot!.... :) */
        function setCookie(name, value, days = 7, path = '/') {
            var expires = new Date(Date.now() + days * 864e5).toUTCString();
            document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=' + path;
        }

        function getCookie(c_name) {
            if (document.cookie.length > 0) {
                c_start = document.cookie.indexOf(c_name + "=");
                if (c_start !== -1) {
                    c_start = c_start + c_name.length + 1;
                    c_end = document.cookie.indexOf(";", c_start);
                    if (c_end === -1) {
                        c_end = document.cookie.length;
                    }
                    return unescape(document.cookie.substring(c_start, c_end));
                }
            }
            return "";
        }
    </script>
    <script>
        var config = {
            apiKey: "your_key",
            authDomain: "myapp.firebaseapp.com",
            databaseURL: "https://myapp.firebaseio.com",
            projectId: "myapp",
            storageBucket: "myapp.appspot.com",
            messagingSenderId: "the_number"
        };
        firebase.initializeApp(config);
    </script>
<script src="https://cdn.firebase.com/libs/firebaseui/2.7.0/firebaseui.js"></script>
    <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.7.0/firebaseui.css"/>
    <script type="text/javascript">
        /**** set this url to the 'logged in' page (mine goes to a dashboard) */ 
        var url = 'https://my.app/index.php#dashboard';
        /**** by doing this signOut first, then it is simple to send any 'logout' request in the app to 'login.php' - one page does it.... :) */
        firebase.auth().signOut().then(function () {
        }).catch(function (error) {
            console.log(error);
        });
        var signInFlow = 'popup';
        if (('standalone' in window.navigator)
            && window.navigator.standalone) {
            signInFlow = 'redirect';
        }
        var uiConfig = {
            callbacks: {
                signInSuccessWithAuthResult: function (authResult, redirectUrl) {
                    /**** here you can see the logged in user */
                    var firebaseUser = authResult.user;
                    var credential = authResult.credential;
                    var isNewUser = authResult.additionalUserInfo.isNewUser;
                    var providerId = authResult.additionalUserInfo.providerId;
                    var operationType = authResult.operationType;
                    /**** I like to force emailVerified...... */
                    if (firebaseUser.emailVerified !== true) {
                        firebase.auth().currentUser.sendEmailVerification().then(function () {
                            /**** if using this, you can set up your own usermgmt.php page for the user verifications (see firebase docs) */
                         window.location.replace("https://my.app/usermgmt.php?mode=checkEmail");
                        }).catch(function (error) {
                            console.log("an error has occurred in sending verification email " + error)
                        });
                    }
                    else {
                        var accessToken = firebaseUser.qa;
                        /**** set the Cookie (yes, I found this best, too) */
                        setCookie('firebaseRegistrationID', accessToken, 1);
                            /**** set up the AJAX call to PHP (where you will store this data for later lookup/processing....) - I use "function=....." and "return=....." to have options for all functions and what to select for the return so that ajax.php can be called for 'anything' (you can just call a special page if you like instead of this - if you use this idea, be sure to secure the ajax.php 'function' call to protect from non-authorized use!) */
                            var elements = {
                            function: "set_user_data",
                            user: JSON.stringify(firebaseUser),
                            return: 'page',
                            accessToken: accessToken
                        };
                        $.ajaxSetup({cache: false});
                        $.post("data/ajax.php", elements, function (data) {
                            /**** this calls ajax and gets the 'page' to set (this is from a feature where I store the current page the user is on, then when they log in again here, we go back to the same page - no need for cookies, etc. - only the login cookie is needed (and available for 'prying eyes' to see!) */
                            url = 'index.php#' + data;
                            var form = $('<form method="post" action="' + url + '"></form>');
                            $('body').append(form);
                            form.submit();
                        });
                    }
                    return false;
                },
                signInFailure: function (error) {
                    console.log("error - signInFailure", error);
                    return handleUIError(error);
                },
                uiShown: function () {
                    var loader = document.getElementById('loader');
                    if (loader) {
                        loader.style.display = 'none';
                    }
                }
            },
            credentialHelper: firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM,
            queryParameterForWidgetMode: 'mode',
            queryParameterForSignInSuccessUrl: 'signInSuccessUrl',
            signInFlow: signInFlow,
            signInSuccessUrl: url,
            signInOptions: [
                firebase.auth.GoogleAuthProvider.PROVIDER_ID,
                //     firebase.auth.FacebookAuthProvider.PROVIDER_ID,
                //     firebase.auth.TwitterAuthProvider.PROVIDER_ID,
                {
                    provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
                    requireDisplayName: true,
                    customParameters: {
                        prompt: 'select_account'
                    }
                }
                /*      {
                        provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID,
                        // Invisible reCAPTCHA with image challenge and bottom left badge.
                        recaptchaParameters: {
                          type: 'image',
                          size: 'invisible',
                          badge: 'bottomleft'
                        }
                      }
                */
            ],
            tosUrl: 'https://my.app/login.php'
        };
        var ui = new firebaseui.auth.AuthUI(firebase.auth());
        (function () {
            ui.start('#firebaseui-auth-container', uiConfig);
        })();
    </script>

现在,在您希望用户查看的每个页面上(在我的情况下,所有内容都通过index.php#something进行 - 这使得操作更加简单... :)

 <script src="https://www.gstatic.com/firebasejs/4.12.0/firebase.js"></script>
<script>
    // Initialize Firebase - from https://github.com/firebase/firebaseui-web
    var firebaseUser;
    var config = {
        apiKey: "your_key",
        authDomain: "yourapp.firebaseapp.com",
        databaseURL: "https://yourapp.firebaseio.com",
        projectId: "yourapp",
        storageBucket: "yourapp.appspot.com",
        messagingSenderId: "the_number"
    };
    firebase.initializeApp(config);
    initFBApp = function () {
        firebase.auth().onAuthStateChanged(function (firebaseuser) {
                if (firebaseuser) {
                    /**** here, I have another ajax call that sets up some select boxes, etc. (I chose to call it here, you can call it anywhere...) */
                    haveFBuser();
                    firebaseUser = firebaseuser;
                    // User is signed in.
                    var displayName = firebaseuser.displayName;
                    var email = firebaseuser.email;
                    var emailVerified = firebaseuser.emailVerified;
                    var photoURL = firebaseuser.photoURL;
                    if (firebaseuser.photoURL.length) {
                        /**** set the profile picture (presuming you are showing it....) */
                        $(".profilepic").prop('src', firebaseuser.photoURL);
                    }
                    var phoneNumber = firebaseuser.phoneNumber;
                    var uid = firebaseuser.uid;
                    var providerData = firebaseuser.providerData;
                    var string = "";
                    firebaseuser.getIdToken().then(function (accessToken) {
                        // document.getElementById('sign-in-status').textContent = 'Signed in';
                        // document.getElementById('sign-in').textContent = 'Sign out';
                        /**** set up another ajax call.... - to store things (yes, again.... - though this time it may be due to firebase changing the token, so we need it twice...) */
                        string = JSON.stringify({
                            displayName: displayName,
                            email: email,
                            emailVerified: emailVerified,
                            phoneNumber: phoneNumber,
                            photoURL: photoURL,
                            uid: uid,
                            accessToken: accessToken,
                            providerData: providerData
                        });
                        if (accessToken !== '<?php echo $_COOKIE['firebaseRegistrationID']?>') {
                            console.log("RESETTING COOKIE with new accessToken ");
                            setCookie('firebaseRegistrationID', accessToken, 1);
                            var elements = 'function=set_user_data&user=' + string;
                            $.ajaxSetup({cache: false});
                            $.post("data/ajax.php", elements, function (data) {
                                <?php
                                /**** leave this out for now and see if anything weird happens - should be OK but you might want to use it (refreshes the page when firebase changes things.....  I found it not very user friendly as they reset at 'odd' times....)
                                /*
                            // var url = 'index.php#<?php echo(!empty($user->userNextPage) ? $user->userNextPage : 'dashboard'); ?>';
                            // var form = $('<form action="' + url + '" method="post">' + '</form>');
                            // $('body').append(form);
                            // console.log('TODO - leave this form.submit(); out for now and see if anything weird happens - should be OK');
                            // form.submit();
                            */
                                ?>
                            });
                        }
                    });
                } else {
                    console.log("firebase user CHANGED");
                    document.location.href = "../login.php";
                }
            }, function (error) {
                console.log(error);
            }
        );
    };
    window.addEventListener('load', function () {
        initFBApp();
    });
</script>

希望这能帮到你。这是从我的工作系统中获取的,其中包括我一路上添加的一些额外功能,但大部分都直接来自Firebase,所以你应该能够很好地跟上。似乎比你原来的方式简单得多。

回答晚了,但是你的代码帮助我验证了一些东西,直到我意识到我可以使用Firebase Auth REST API而不需要任何奇怪的ajax调用。 - marcus
是的,正如我的回答所述,其中有一些“额外”的内容,用于在后端存储数据等进行ajax调用。但是,如果您只想进行身份验证,则不需要ajax。最初的问题有点“这里/那里”,因为您不了解可以做什么,这就是为什么我放入了我的“工作代码”示例。很高兴它帮助您找到所需的内容。请考虑添加一个“很有用”的点击(我正在与朋友比赛,“年底前在SO上获得最多积分”,因此每个“它帮助了我”都受到赞赏! :) - Apps-n-Add-Ons

0

在使用令牌时,PHP中不应该使用会话。每个请求都应该在头部发送令牌(或者也可以使用cookie)。

令牌的工作原理如下: 1. 登录后,服务器会生成一个带有一些信息编码的令牌 2. 每次请求时都要发送该令牌

根据令牌中编码的信息,服务器可以获取有关用户的信息。通常,在其中编码了某种类型的用户ID。服务器知道它是有效的令牌,因为它的编码方式。

在需要进行的每个请求中发送令牌,然后在PHP中,您只需将该令牌传递给其他API即可。


这是一个很好的观点,但我认为通过将令牌存储在会话中,我可以更轻松地从我的php应用程序中访问它,而不必与那些ajax调用打交道。不是吗?我的意思是我必须存储它。我可以看到,Firebase JS SDK将信息存储在浏览器本地存储中。 - marcus
如果我将令牌存储从 PHP 会话更改为 cookie(可以使用 JS 和 PHP 访问),这样做是否更好? - marcus
为了正确使用令牌,您需要在每个API调用中包含它。然后,您将像会话一样拥有对它的相同访问权限。获取令牌与获取会话一样容易。试试吧。 - Chad K
令牌可以从前端和后端访问!您可以使用本地存储从JS访问令牌,也可以使用cookie。在我的网站上,我将令牌存储在cookie中,因为这样更容易处理,并且获取安全图像也更容易。这不是推荐的方法,但它非常有效。 - Chad K
感谢@Chad K让我找到了正确的方向。现在我正在更多地了解JWT,对于发生的事情和需要寻找的内容有了更好的认识。 - marcus
没关系,我也曾经对JWT感到困惑。虽然它不是我的最爱,但它确实有效。 - Chad K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接