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

Posted

技术标签:

【中文标题】移动浏览器上的 AngularJS 和 ASP.Net WebAPI 社交登录【英文标题】:AngularJS and ASP.Net WebAPI Social Login on a Mobile Browser 【发布时间】:2016-04-20 03:03:01 【问题描述】:

我正在关注这篇关于使用 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),它将关闭窗口并在父窗口上执行命令。

调用外部登录 url 并在桌面浏览器上打开弹出窗口(不是选项卡)的 angularJS 控制器:

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)上运行应用程序时,社交登录窗口会在新选项卡中打开,而 JS 函数旨在将片段传回主应用程序窗口未执行且新选项卡未关闭。

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

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

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

任何想法或意见将不胜感激。

感谢!

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

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

【问题讨论】:

【参考方案1】:

在桌面上,当身份验证窗口弹出(不是选项卡)时,它会将opener 属性设置为在移动设备上打开此弹出窗口的窗口,正如您所说,它不是弹出窗口,而是新的标签。当在浏览器中打开一个新选项卡时,opener 属性为null,所以实际上你在这里有一个例外:

window.opener.$windowScope.authCompletedCB

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

解决方案

在您的 authComplete.js 文件中,而不是尝试调用 window.opener.$windowScope.authCompletedCB 并传递用户的片段,使用 JSON.stringify() 将片段保存在 localStorage 或 cookie 中(毕竟 authComplete.html 的所有页面与您的应用程序位于同一来源),然后使用 @ 关闭窗口987654331@.

loginController.js 中,将$interval 设为100 毫秒,以检查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();
    );

]);

【讨论】:

我会在当天晚些时候花时间测试一下。 你不需要,localStorage是浏览器中HTML5 API的一部分,实际上它在window上,你可以在任何地方使用window.localStorage访问它 刚刚试了一下,我得到了一些 rootscope 错误 Error: $rootScope:inprog Action Already In Progress 从 authCompletedCB 中移除 $rootScope.$apply 还有一些。遇到一些 JS 错误:TypeError: _interval.cancel is not a function

以上是关于移动浏览器上的 AngularJS 和 ASP.Net WebAPI 社交登录的主要内容,如果未能解决你的问题,请参考以下文章

iOS 移动浏览器和 OS X Safari 上的 Flexbox 奇怪行为

是否可以在 AngularJS、JavaScript 和 JQuery 中跟踪移动/桌面模型?

ng-click 在移动设备上的 Angularjs 问题

某些 Mac 机器浏览器上的 Angularjs CORS 问题

Angularjs 计算不适用于 iPad / iPhone 上的 safari

使用 Angularjs 和 Laravel 注销会话过期