移动浏览器上的AngularJS和ASP.Net WebAPI社交登录

时间:2016-01-14 17:05:31

标签: javascript asp.net angularjs asp.net-web-api

我正在使用AngularJS和ASP.Net WebAPI关注社交登录这篇文章(非常好):

ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app

实际上,当您通过桌面浏览器(即Chrome,FF,IE,Edge)运行社交登录时,代码可以正常工作。社交登录在新窗口(不是标签)中打开,您可以使用您的Google或Facebook帐户,一旦您通过其中任何一个登录,您将被重定向到回调页面(authComplete.html),并且回调页面有一个JS文件定义(authComplete.js),它将关闭窗口并在父窗口上执行命令。

angularJS控制器,它调用外部登录URL并在桌面浏览器上打开一个弹出窗口(而非标签):

loginController.js

'use strict';
app.controller('loginController', ['$scope', '$location', 'authService', 'ngAuthSettings', function ($scope, $location, authService, ngAuthSettings) {

    $scope.loginData = {
        userName: "",
        password: "",
        useRefreshTokens: false
    };

    $scope.message = "";

    $scope.login = function () {

        authService.login($scope.loginData).then(function (response) {

            $location.path('/orders');

        },
         function (err) {
             $scope.message = err.error_description;
         });
    };

    $scope.authExternalProvider = function (provider) {

        var redirectUri = location.protocol + '//' + location.host + '/authcomplete.html';

        var externalProviderUrl = ngAuthSettings.apiServiceBaseUri + "api/Account/ExternalLogin?provider=" + provider
                                                                    + "&response_type=token&client_id=" + ngAuthSettings.clientId
                                                                    + "&redirect_uri=" + redirectUri;
        window.$windowScope = $scope;

        var oauthWindow = window.open(externalProviderUrl, "Authenticate Account", "location=0,status=0,width=600,height=750");
    };

    $scope.authCompletedCB = function (fragment) {

        $scope.$apply(function () {

            if (fragment.haslocalaccount == 'False') {

                authService.logOut();

                authService.externalAuthData = {
                    provider: fragment.provider,
                    userName: fragment.external_user_name,
                    externalAccessToken: fragment.external_access_token
                };

                $location.path('/associate');

            }
            else {
                //Obtain access token and redirect to orders
                var externalData = { provider: fragment.provider, externalAccessToken: fragment.external_access_token };
                authService.obtainAccessToken(externalData).then(function (response) {

                    $location.path('/orders');

                },
             function (err) {
                 $scope.message = err.error_description;
             });
            }

        });
    }
}]);

authComplete.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>

</head>
<body>
    <script src="scripts/authComplete.js"></script>
</body>
</html>

authComplete.js

window.common = (function () {
    var common = {};

    common.getFragment = function getFragment() {
        if (window.location.hash.indexOf("#") === 0) {
            return parseQueryString(window.location.hash.substr(1));
        } else {
            return {};
        }
    };

    function parseQueryString(queryString) {
        var data = {},
            pairs, pair, separatorIndex, escapedKey, escapedValue, key, value;

        if (queryString === null) {
            return data;
        }

        pairs = queryString.split("&");

        for (var i = 0; i < pairs.length; i++) {
            pair = pairs[i];
            separatorIndex = pair.indexOf("=");

            if (separatorIndex === -1) {
                escapedKey = pair;
                escapedValue = null;
            } else {
                escapedKey = pair.substr(0, separatorIndex);
                escapedValue = pair.substr(separatorIndex + 1);
            }

            key = decodeURIComponent(escapedKey);
            value = decodeURIComponent(escapedValue);

            data[key] = value;
        }

        return data;
    }

    return common;
})();

var fragment = common.getFragment();
window.location.hash = fragment.state || '';
window.opener.$windowScope.authCompletedCB(fragment);
window.close();

我遇到的问题是,当我在移动设备(Safari,Chrome for Mobile)上运行应用程序时,社交登录窗口将在新选项卡和JS功能中打开,该功能旨在将片段传回主应用程序窗口不执行nad新选项卡不关闭。

您实际上可以通过应用程序在桌面和移动浏览器上尝试此行为:

http://ngauthenticationapi.azurewebsites.net/

到目前为止,我在此上下文中尝试的是在登录控制器中,我修改了函数,以便外部登录URL在同一窗口中打开:

$scope.authExternalProvider = function (provider) {
        var redirectUri = location.protocol + '//' + location.host + '/authcomplete.html';
        var externalProviderUrl = ngAuthSettings.apiServiceBaseUri + "api/Account/ExternalLogin?provider=" + provider
                                                                                                                                + "&response_type=token&client_id=" + ngAuthSettings.clientId
                                                                                                                                + "&redirect_uri=" + redirectUri;
        window.location = externalProviderUrl;
};

并修改authComplete.js common.getFragment函数以返回登录页面,方法是将社交登录提供的访问令牌作为查询字符串附加:

common.getFragment = function getFragment() {
        if (window.location.hash.indexOf("#") === 0) {
                var hash = window.location.hash.substr(1);
                var redirectUrl = location.protocol + '//' + location.host + '/#/login?ext=' + hash;
                window.location = redirectUrl;
        } else {
                return {};
        }
};

在登录控制器中,我添加了一个解析查询字符串的函数,并尝试调用$ scope.authCompletedCB(fragment)函数,如:

var vm = this;
var fragment = null;

vm.testFn = function (fragment) {
        $scope.$apply(function () {

                if (fragment.haslocalaccount == 'False') {

                        authenticationService.logOut();

                        authenticationService.externalAuthData = {
                                provider: fragment.provider,
                                userName: fragment.external_user_name,
                                externalAccessToken: fragment.external_access_token
                        };

                        $location.path('/associate');

                }
                else {
                        //Obtain access token and redirect to orders
                        var externalData = { provider: fragment.provider, externalAccessToken: fragment.external_access_token };
                        authenticationService.obtainAccessToken(externalData).then(function (response) {

                                $location.path('/home');

                        },
                 function (err) {
                         $scope.message = err.error_description;
                 });
                }

        });
}

init();

function parseQueryString(queryString) {
        var data = {},
                pairs, pair, separatorIndex, escapedKey, escapedValue, key, value;

        if (queryString === null) {
                return data;
        }

        pairs = queryString.split("&");

        for (var i = 0; i < pairs.length; i++) {
                pair = pairs[i];
                separatorIndex = pair.indexOf("=");

                if (separatorIndex === -1) {
                        escapedKey = pair;
                        escapedValue = null;
                } else {
                        escapedKey = pair.substr(0, separatorIndex);
                        escapedValue = pair.substr(separatorIndex + 1);
                }

                key = decodeURIComponent(escapedKey);
                value = decodeURIComponent(escapedValue);

                data[key] = value;
        }

        return data;
}

function init() {
        var idx = window.location.hash.indexOf("ext=");

        if (window.location.hash.indexOf("#") === 0) {
                fragment = parseQueryString(window.location.hash.substr(idx));
                vm.testFn(fragment);
        }
}

但显然这给了我一个与棱角分相关的错误(我现在还不知道):

https://docs.angularjs.org/error/$rootScope/inprog?p0=$digest

所以,在这个阶段,对我来说几乎是一个死胡同。

任何想法或意见都将受到高度赞赏。

格拉西亚斯!

更新:我设法解决了关于rootcope被抛出的Angular错误,但遗憾的是,解决这个问题并不能解决主要问题。如果我尝试在我的应用程序所在的同一浏览器选项卡上打开社交登录,Google可以登录并返回应用程序并传递所需的令牌。这是一个与Facebook不同的故事,在开发人员的工具控制台中,有一个警告似乎阻止Facebook显示登录页面。

实际上,打开新窗口(或标签页)的原始方法是前进的方法,但为移动浏览器修复相同的方法似乎变得更具挑战性。

1 个答案:

答案 0 :(得分:4)

在桌面上,当auth窗口弹出(而不是tab)时,它将opener属性设置为打开此弹出窗口的窗口,在移动设备上,如你所说,它不是弹出窗口但是一个新标签。当在浏览器中打开新标签页时,opener属性为null,所以实际上您有一个例外:

window.opener.$windowScope.authCompletedCB

因为你不能引用空值($windowScope)的window.opener属性所以在这之后的每一行代码都不会被执行 - 这就是为什么窗口没有在移动设备上关闭的原因。

解决方案

authComplete.js文件中,而不是尝试拨打电话 window.opener.$windowScope.authCompletedCB并传递用户的片段,使用authComplete.html将片段保存在localStorage或cookie中(在JSON.stringify()的所有页面与您的应用程序相同的原点之后)只需使用window.close()关闭窗口。

loginController.js中,为100ms之类的内容生成一个$interval来检查localStorage或cookie中的值(不要忘记清除$scope时的间隔是$destroy),如果存在碎片,您可以使用存储中的JSON.parse解析其值,将其从存储中删除,并使用解析后的值调用$scope.authCompletedCB

更新 - 添加了代码示例

<强> authComplete.js

...
var fragment = common.getFragment();
// window.location.hash = fragment.state || '';
// window.opener.$windowScope.authCompletedCB(fragment);
localStorage.setItem("auth_fragment", JSON.stringify(fragment))
window.close();

<强> loginController.js

app.controller('loginController', ['$scope', '$interval', '$location', 'authService', 'ngAuthSettings',
function ($scope, $interval, $location, authService, ngAuthSettings) {

    ...

    // check for fragment every 100ms
    var _interval = $interval(_checkForFragment, 100);

    function _checkForFragment() {
        var fragment = localStorage.getItem("auth_fragment");
        if(fragment && (fragment = JSON.parse(fragment))) {

            // clear the fragment from the storage
            localStorage.removeItem("auth_fragment");

            // continue as usual
            $scope.authCompletedCB(fragment);

            // stop looking for fragmet
            _clearInterval();
        }
    }

    function _clearInterval() {
        $interval.cancel(_interval);
    }

    $scope.$on("$destroy", function() {
        // clear the interval when $scope is destroyed
        _clearInterval();
    });

}]);