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并在桌面浏览器上打开弹出窗口(而不是tab):
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,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 = {},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显示登录页面.
解决方法
window.opener.$windowScope.authCompletedCB
因为你不能引用null值(window.opener)的$windowScope属性所以在这之后的每一行代码都不会被执行 – 这就是为什么窗口没有在移动设备上关闭.
一个办法
在您的authComplete.js文件中,而不是尝试调用
window.opener.$windowScope.authCompletedCB并传递用户的片段,使用JSON.stringify()将片段保存在localStorage或cookie中(在authComplete.html的所有页面与应用程序的源相同之后)然后使用window.close()关闭窗口.
在loginController.js中,为100ms之类的内容创建一个$interval来检查localStorage或cookie中的值(当$scope为$destroy时不要忘记清除时间间隔),如果存在afragment你可以解析使用存储中的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','$interval',$interval,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(); }); }]);