AngularJS ui-router 登录认证
Posted
技术标签:
【中文标题】AngularJS ui-router 登录认证【英文标题】:AngularJS ui-router login authentication 【发布时间】:2014-04-27 13:18:14 【问题描述】:我是 AngularJS 的新手,我对如何在以下场景中使用 angular-"ui-router" 感到有些困惑:
我正在构建一个由两个部分组成的 Web 应用程序。第一部分是主页及其登录和注册视图,第二部分是仪表板(成功登录后)。
我已经为主页部分创建了一个index.html
及其角度应用程序和ui-router
配置来处理/login
和/signup
视图,
还有另一个文件dashboard.html
用于仪表板部分及其应用程序和ui-router
配置来处理许多子视图。
现在我完成了仪表板部分,但不知道如何将这两个部分与它们不同的 Angular 应用程序结合起来。如何让主应用重定向到仪表板应用?
【问题讨论】:
你能和我们分享一些代码吗? @Chancho 我认为这与代码无关,我真的不知道我应该分享什么代码。 是的,请分享代码,非常笼统的问题... 【参考方案1】:我正在制作一个更好的演示,并将其中一些服务整理成一个可用的模块,但这就是我想出的。这是一个解决一些警告的复杂过程,所以请坚持下去。您需要将其分解为几部分。
Take a look at this plunk.
首先,您需要一个服务来存储用户的身份。我称之为principal
。可以检查用户是否登录,并且根据请求,它可以解析代表有关用户身份的基本信息的对象。这可以是您需要的任何内容,但基本要素是显示名称、用户名、可能是电子邮件以及用户所属的角色(如果这适用于您的应用程序)。 Principal 也有进行角色检查的方法。
.factory('principal', ['$q', '$http', '$timeout',
function($q, $http, $timeout)
var _identity = undefined,
_authenticated = false;
return
isIdentityResolved: function()
return angular.isDefined(_identity);
,
isAuthenticated: function()
return _authenticated;
,
isInRole: function(role)
if (!_authenticated || !_identity.roles) return false;
return _identity.roles.indexOf(role) != -1;
,
isInAnyRole: function(roles)
if (!_authenticated || !_identity.roles) return false;
for (var i = 0; i < roles.length; i++)
if (this.isInRole(roles[i])) return true;
return false;
,
authenticate: function(identity)
_identity = identity;
_authenticated = identity != null;
,
identity: function(force)
var deferred = $q.defer();
if (force === true) _identity = undefined;
// check and see if we have retrieved the
// identity data from the server. if we have,
// reuse it by immediately resolving
if (angular.isDefined(_identity))
deferred.resolve(_identity);
return deferred.promise;
// otherwise, retrieve the identity data from the
// server, update the identity object, and then
// resolve.
// $http.get('/svc/account/identity',
// ignoreErrors: true )
// .success(function(data)
// _identity = data;
// _authenticated = true;
// deferred.resolve(_identity);
// )
// .error(function ()
// _identity = null;
// _authenticated = false;
// deferred.resolve(_identity);
// );
// for the sake of the demo, fake the lookup
// by using a timeout to create a valid
// fake identity. in reality, you'll want
// something more like the $http request
// commented out above. in this example, we fake
// looking up to find the user is
// not logged in
var self = this;
$timeout(function()
self.authenticate(null);
deferred.resolve(_identity);
, 1000);
return deferred.promise;
;
])
其次,您需要一个服务来检查用户想要进入的状态,确保他们已登录(如有必要;不需要登录、密码重置等),然后进行角色检查 (如果你的应用需要这个)。如果他们未通过身份验证,请将其发送到登录页面。如果它们已通过身份验证,但未通过角色检查,请将它们发送到拒绝访问页面。我将此服务称为authorization
。
.factory('authorization', ['$rootScope', '$state', 'principal',
function($rootScope, $state, principal)
return
authorize: function()
return principal.identity()
.then(function()
var isAuthenticated = principal.isAuthenticated();
if ($rootScope.toState.data.roles
&& $rootScope.toState
.data.roles.length > 0
&& !principal.isInAnyRole(
$rootScope.toState.data.roles))
if (isAuthenticated)
// user is signed in but not
// authorized for desired state
$state.go('accessdenied');
else
// user is not authenticated. Stow
// the state they wanted before you
// send them to the sign-in state, so
// you can return them when you're done
$rootScope.returnToState
= $rootScope.toState;
$rootScope.returnToStateParams
= $rootScope.toStateParams;
// now, send them to the signin state
// so they can log in
$state.go('signin');
);
;
])
现在您需要做的就是收听ui-router
的$stateChangeStart
。这使您有机会检查当前状态、他们想要进入的状态,并插入您的授权检查。如果失败,您可以取消路由转换,或更改为其他路由。
.run(['$rootScope', '$state', '$stateParams',
'authorization', 'principal',
function($rootScope, $state, $stateParams,
authorization, principal)
$rootScope.$on('$stateChangeStart',
function(event, toState, toStateParams)
// track the state the user wants to go to;
// authorization service needs this
$rootScope.toState = toState;
$rootScope.toStateParams = toStateParams;
// if the principal is resolved, do an
// authorization check immediately. otherwise,
// it'll be done when the state it resolved.
if (principal.isIdentityResolved())
authorization.authorize();
);
]);
如果您已经通过身份验证,跟踪用户身份的棘手部分是查找它(例如,您在上一个会话之后访问该页面,并在 cookie 中保存了一个身份验证令牌,或者您可能很难刷新页面,或从链接拖放到 URL 上)。由于ui-router
的工作方式,您需要在身份验证检查之前进行一次身份解析。您可以使用状态配置中的resolve
选项来执行此操作。我有一个所有状态都继承自的站点的父状态,这会强制在其他任何事情发生之前解决主体。
$stateProvider.state('site',
'abstract': true,
resolve:
authorize: ['authorization',
function(authorization)
return authorization.authorize();
]
,
template: '<div ui-view />'
)
这里还有一个问题...resolve
只被调用一次。一旦您对身份查找的承诺完成,它就不会再次运行解析委托。因此,我们必须在两个地方进行身份验证检查:一次根据您的身份承诺在 resolve
中解析,这涵盖了您的应用程序首次加载时;如果解析已经完成,则在 $stateChangeStart
中解析一次,涵盖任何时间您可以在各个州之间导航。
好的,那么到目前为止我们做了什么?
-
如果用户已登录,我们会检查应用何时加载。
我们跟踪有关登录用户的信息。
对于需要用户登录的状态,我们将他们重定向到登录状态。
如果他们没有访问权限,我们会将他们重定向到拒绝访问状态。
如果我们需要用户登录,我们有一种机制可以将用户重定向回他们请求的原始状态。
我们可以让用户退出(需要与管理您的身份验证票证的任何客户端或服务器代码一致)。
我们不需要在用户每次重新加载浏览器或点击链接时将其送回登录页面。
我们从这里去哪里?好吧,您可以将您的状态组织成需要登录的区域。您可以通过将data
和roles
添加到这些状态(或它们的父级,如果您想使用继承)来要求经过身份验证/授权的用户。在这里,我们将资源限制为管理员:
.state('restricted',
parent: 'site',
url: '/restricted',
data:
roles: ['Admin']
,
views:
'content@':
templateUrl: 'restricted.html'
)
现在您可以逐个州地控制用户可以访问的路线。还有其他顾虑吗?也许根据他们是否登录只改变视图的一部分?没问题。将principal.isAuthenticated()
甚至principal.isInRole()
与可以有条件地显示模板或元素的多种方式中的任何一种结合使用。
首先,将principal
注入控制器或其他任何东西,并将其粘贴到范围内,以便您可以在视图中轻松使用它:
.scope('HomeCtrl', ['$scope', 'principal',
function($scope, principal)
$scope.principal = principal;
);
显示或隐藏元素:
<div ng-show="principal.isAuthenticated()">
I'm logged in
</div>
<div ng-hide="principal.isAuthenticated()">
I'm not logged in
</div>
等等等等。无论如何,在您的示例应用程序中,您将拥有一个主页状态,该状态将允许未经身份验证的用户访问。他们可以有指向登录或注册状态的链接,或者将这些表单内置到该页面中。什么都适合你。
仪表板页面都可以从要求用户登录的状态继承,例如,成为User
角色成员。我们讨论过的所有授权内容都将从那里流出。
【讨论】:
谢谢,这真的帮助我把自己的代码放在一起。附带说明,如果您遇到无限路由循环(UI 路由器错误),请尝试使用$location.path
而不是 $state.go
。
这是一个很好的答案,它对我帮助很大。当我在我的控制器中设置 user = principal 并尝试在我的视图中调用 say user.identity().name 以获取当前登录的用户名时,我似乎只得到了承诺对象 then: fn, catch: fn, finally : 返回,而不是实际的 _identity 对象。如果我使用 user.identity.then(fn(user)) 我可以获得用户对象,但这似乎有很多视图代码我错过了什么吗?
@Ir1sh 我将首先在控制器中解析身份并将其分配给您的then
函数中的$scope.user
。您仍然可以在视图中引用user
;解决后,视图将被更新。
@HackedByChinese 我认为您的演示不再有效。
@jvannistelrooy 我在使用 go() 时遇到了问题,但是在将它放入 then 之后,在调用像 $q.when(angular.noop).then(function()$state.go('myState')
这样的 noop 函数之后,一切都按预期工作。如果我在另一个状态转换未完成时调用$state.go
,那么它将不起作用(我认为这就是它不起作用的原因)。【参考方案2】:
在我看来,到目前为止发布的解决方案是不必要的复杂。有一个更简单的方法。 documentation of ui-router
表示听 $locationChangeSuccess
并使用 $urlRouter.sync()
检查状态转换、暂停或恢复状态。但即使这样实际上也行不通。
但是,这里有两个简单的选择。选择一个:
解决方案 1:监听 $locationChangeSuccess
你可以收听$locationChangeSuccess
,你可以在那里执行一些逻辑,甚至是异步逻辑。基于该逻辑,您可以让函数返回 undefined,这将导致状态转换继续正常进行,或者如果需要对用户进行身份验证,您可以执行$state.go('logInPage')
。这是一个例子:
angular.module('App', ['ui.router'])
// In the run phase of your Angular application
.run(function($rootScope, user, $state)
// Listen to '$locationChangeSuccess', not '$stateChangeStart'
$rootScope.$on('$locationChangeSuccess', function()
user
.logIn()
.catch(function()
// log-in promise failed. Redirect to log-in page.
$state.go('logInPage')
)
)
)
请记住,这实际上并不会阻止目标状态的加载,但如果用户未经授权,它会重定向到登录页面。没关系,因为真正的保护是在服务器上,无论如何。
解决方案2:使用状态resolve
在此解决方案中,您使用ui-router
resolve feature。
如果用户未通过身份验证,您基本上会拒绝resolve
中的承诺,然后将他们重定向到登录页面。
事情是这样的:
angular.module('App', ['ui.router'])
.config(
function($stateProvider)
$stateProvider
.state('logInPage',
url: '/logInPage',
templateUrl: 'sections/logInPage.html',
controller: 'logInPageCtrl',
)
.state('myProtectedContent',
url: '/myProtectedContent',
templateUrl: 'sections/myProtectedContent.html',
controller: 'myProtectedContentCtrl',
resolve: authenticate: authenticate
)
.state('alsoProtectedContent',
url: '/alsoProtectedContent',
templateUrl: 'sections/alsoProtectedContent.html',
controller: 'alsoProtectedContentCtrl',
resolve: authenticate: authenticate
)
function authenticate($q, user, $state, $timeout)
if (user.isAuthenticated())
// Resolve the promise successfully
return $q.when()
else
// The next bit of code is asynchronously tricky.
$timeout(function()
// This code runs after the authentication promise has been rejected.
// Go to the log-in page
$state.go('logInPage')
)
// Reject the authentication promise to prevent the state from loading
return $q.reject()
)
与第一个解决方案不同,此解决方案实际上阻止了目标状态的加载。
【讨论】:
@FredLackey 说未经身份验证的用户在state A
。他们单击链接转到protected state B
,但您想将他们重定向到logInPage
。如果没有$timeout
,ui-router
将简单地停止所有状态转换,因此用户将卡在state A
。 $timeout
允许 ui-router
首先阻止初始转换到 protected state B
,因为解析被拒绝,完成后,它重定向到 logInPage
。
authenticate
函数实际调用在哪里?
@Imray authenticate
函数作为参数传递给ui-router
。您不必自己调用它。 ui-router
调用它。
你为什么使用'$locationChangeSuccess'而不是'$stateChangeStart'?
@PeterDraexDräxler 我主要关注文档。使用$stateChangeStart
有没有发现什么不同?【参考方案3】:
最简单的解决方案是使用$stateChangeStart
和event.preventDefault()
在用户未通过身份验证时取消状态更改,并将他重定向到登录页面的auth状态。
angular
.module('myApp', [
'ui.router',
])
.run(['$rootScope', 'User', '$state',
function ($rootScope, User, $state)
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams)
if (toState.name !== 'auth' && !User.authenticaded())
event.preventDefault();
$state.go('auth');
);
]
);
【讨论】:
如果 User.authenticated() 是异步调用,我认为这不会起作用。那是每个人都追求的圣杯。例如,如果每个状态 except "login" 都是安全的,我想确认用户仍然在 BEFORE 加载任何状态之前通过身份验证。使用解析很糟糕,因为它们只解析一次,并且为了防止加载子状态,您必须将解析注入 EVERY CHILD。 authenticated 在我的情况下不是异步调用:` this.authenticated = function() if (this.currentAccountID !== null) return true; 返回假; ; ` 根据:***.com/a/38374313/849829,“运行”远高于“服务”,因此存在问题。检查本地存储的身份验证状态似乎是一个好方法。【参考方案4】:我认为您需要一个处理身份验证过程(及其存储)的service
。
在这个服务中你需要一些基本的方法:
isAuthenticated()
login()
logout()
等等...
这个服务应该被注入到你每个模块的控制器中:
在您的仪表板部分,使用此服务检查用户是否已通过身份验证(service.isAuthenticated()
方法)。如果没有,重定向到 /login
在您的登录部分,只需使用表单数据通过您的service.login()
方法对用户进行身份验证
这个行为的一个很好的例子是angular-app,特别是它的security module,它基于很棒的HTTP Auth Interceptor Module
希望对你有帮助
【讨论】:
【参考方案5】:我创建了这个模块来帮助完成这个过程
您可以执行以下操作:
$routeProvider
.state('secret',
...
permissions:
only: ['admin', 'god']
);
也可以
$routeProvider
.state('userpanel',
...
permissions:
except: ['not-logged-in']
);
它是全新的,但值得一试!
https://github.com/Narzerus/angular-permission
【讨论】:
什么阻止我在运行时编辑源代码并删除你的“管理员”|| “上帝”并继续? 我希望任何需要授权的数据请求也在服务器上得到验证。 这并不意味着安全,客户端授权永远不会因为您可以随时更改值。您甚至可以拦截来自服务器端的响应并将其评估为“已授权”客户端的权限/授权点是为了避免让用户出于 ux 目的做禁止的事情。例如,如果您正在处理仅管理员操作,即使用户恶意欺骗客户端允许向服务器发送受限请求,服务器仍会返回 401 响应。这当然始终是正在实施的 api 的责任@BenRipley 确实 对 Rafael 的问题做出了很好的回应。始终保护 api,因为前端几乎是最容易进行逆向工程和欺骗的东西。 这个历史问题已经解决了很长一段时间@Bohdan。即使使用 ui-router extras,您也可以安全地使用它。【参考方案6】:我想分享另一个使用 ui 路由器 1.0.0.X 的解决方案
您可能知道,stateChangeStart 和 stateChangeSuccess 现在已被弃用。 https://github.com/angular-ui/ui-router/issues/2655
您应该使用 $transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html
这就是我实现它的方式:
首先我有 AuthService 一些有用的功能
angular.module('myApp')
.factory('AuthService',
['$http', '$cookies', '$rootScope',
function ($http, $cookies, $rootScope)
var service = ;
// Authenticates throug a rest service
service.authenticate = function (username, password, callback)
$http.post('api/login', username: username, password: password)
.success(function (response)
callback(response);
);
;
// Creates a cookie and set the Authorization header
service.setCredentials = function (response)
$rootScope.globals = response.token;
$http.defaults.headers.common['Authorization'] = 'Bearer ' + response.token;
$cookies.put('globals', $rootScope.globals);
;
// Checks if it's authenticated
service.isAuthenticated = function()
return !($cookies.get('globals') === undefined);
;
// Clear credentials when logout
service.clearCredentials = function ()
$rootScope.globals = undefined;
$cookies.remove('globals');
$http.defaults.headers.common.Authorization = 'Bearer ';
;
return service;
]);
那么我就有了这样的配置:
angular.module('myApp', [
'ui.router',
'ngCookies'
])
.config(['$stateProvider', '$urlRouterProvider',
function ($stateProvider, $urlRouterProvider)
$urlRouterProvider.otherwise('/resumen');
$stateProvider
.state("dashboard",
url: "/dashboard",
templateUrl: "partials/dashboard.html",
controller: "dashCtrl",
data:
authRequired: true
)
.state("login",
url: "/login",
templateUrl: "partials/login.html",
controller: "loginController"
)
])
.run(['$rootScope', '$transitions', '$state', '$cookies', '$http', 'AuthService',
function ($rootScope, $transitions, $state, $cookies, $http, AuthService)
// keep user logged in after page refresh
$rootScope.globals = $cookies.get('globals') || ;
$http.defaults.headers.common['Authorization'] = 'Bearer ' + $rootScope.globals;
$transitions.onStart(
to: function (state)
return state.data != null && state.data.authRequired === true;
, function ()
if (!AuthService.isAuthenticated())
return $state.target("login");
);
]);
你可以看到我用了
data:
authRequired: true
标记只有经过身份验证才能访问的状态。
然后,在 .run 上,我使用转换来检查经过身份验证的状态
$transitions.onStart(
to: function (state)
return state.data != null && state.data.authRequired === true;
, function ()
if (!AuthService.isAuthenticated())
return $state.target("login");
);
我使用 $transitions 文档中的一些代码构建了这个示例。我对 ui 路由器很陌生,但它可以工作。
希望它可以帮助任何人。
【讨论】:
这对于那些使用较新路由器的人来说非常有用。谢谢!【参考方案7】:这是我们如何摆脱无限路由循环并仍然使用$state.go
而不是$location.path
if('401' !== toState.name)
if (principal.isIdentityResolved()) authorization.authorize();
【讨论】:
有谁知道为什么当使用上述接受的答案/设置时,地址栏不再显示 url 和所有片段和查询字符串参数?由于实现这一点,地址栏不再允许为我们的应用添加书签。 这不应该是对现有答案之一的评论吗?因为OP中没有这样的代码,甚至不清楚这是指哪个答案/什么代码【参考方案8】:我有另一个解决方案:当您登录时只有想要显示的内容时,该解决方案非常有效。定义一个规则,您可以在其中检查您是否已登录,而不是白名单路由的路径。
$urlRouterProvider.rule(function ($injector, $location)
var UserService = $injector.get('UserService');
var path = $location.path(), normalized = path.toLowerCase();
if (!UserService.isLoggedIn() && path.indexOf('login') === -1)
$location.path('/login/signin');
);
在我的示例中,我询问我是否未登录,并且我要路由的当前路由不是 `/login' 的一部分,因为我的白名单路由如下
/login/signup // registering new user
/login/signin // login to app
所以我可以立即访问这两条路线,如果您在线,将检查所有其他路线。
这是我的登录模块的整个路由文件
export default (
$stateProvider,
$locationProvider,
$urlRouterProvider
) =>
$stateProvider.state('login',
parent: 'app',
url: '/login',
abstract: true,
template: '<ui-view></ui-view>'
)
$stateProvider.state('signin',
parent: 'login',
url: '/signin',
template: '<login-signin-directive></login-signin-directive>'
);
$stateProvider.state('lock',
parent: 'login',
url: '/lock',
template: '<login-lock-directive></login-lock-directive>'
);
$stateProvider.state('signup',
parent: 'login',
url: '/signup',
template: '<login-signup-directive></login-signup-directive>'
);
$urlRouterProvider.rule(function ($injector, $location)
var UserService = $injector.get('UserService');
var path = $location.path();
if (!UserService.isLoggedIn() && path.indexOf('login') === -1)
$location.path('/login/signin');
);
$urlRouterProvider.otherwise('/error/not-found');
() => /* code */
是 ES6 语法,请改用 function() /* code */
【讨论】:
【参考方案9】:使用 $http 拦截器
通过使用 $http 拦截器,您可以将标头发送到后端或以其他方式进行检查。
$http interceptors 上的精彩文章
例子:
$httpProvider.interceptors.push(function ($q)
return
'response': function (response)
// TODO Create check for user authentication. With every request send "headers" or do some other check
return response;
,
'responseError': function (reject)
// Forbidden
if(reject.status == 403)
console.log('This page is forbidden.');
window.location = '/';
// Unauthorized
else if(reject.status == 401)
console.log("You're not authorized to view this page.");
window.location = '/';
return $q.reject(reject);
;
);
把它放在你的 .config 或 .run 函数中。
【讨论】:
【参考方案10】:首先,您需要一个可以注入到控制器中的服务,该服务对应用身份验证状态有所了解。使用本地存储保留身份验证详细信息是一种不错的方法。
接下来,您需要在状态更改之前检查身份验证的状态。由于您的应用程序有一些页面需要进行身份验证,而另一些页面则不需要,请创建一个检查身份验证的父路由,并使所有其他需要相同身份的页面成为该父级的子级。
最后,您需要一些方法来判断您当前登录的用户是否可以执行某些操作。这可以通过向您的身份验证服务添加“can”功能来实现。可以接受两个参数: - 操作 - 必需 - (即“manage_dashboards”或“create_new_dashboard”) - 对象 - 可选 - 正在操作的对象。例如,如果您有一个仪表板对象,您可能想要检查是否仪表板.ownerId === loggedInUser.id。 (当然,从客户端传递的信息永远不应该被信任,并且在将其写入数据库之前,您应该始终在服务器上验证这一点)。
angular.module('myApp', ['ngStorage']).config([
'$stateProvider',
function(
$stateProvider
)
$stateProvider
.state('home', ...) //not authed
.state('sign-up', ...) //not authed
.state('login', ...) //not authed
.state('authed', ...) //authed, make all authed states children
.state('authed.dashboard', ...)
])
.service('context', [
'$localStorage',
function(
$localStorage
)
var _user = $localStorage.get('user');
return
getUser: function()
return _user;
,
authed: function()
return (_user !== null);
,
// server should return some kind of token so the app
// can continue to load authenticated content without having to
// re-authenticate each time
login: function()
return $http.post('/login.json').then(function(reply)
if (reply.authenticated === true)
$localStorage.set(_userKey, reply.user);
);
,
// this request should expire that token, rendering it useless
// for requests outside of this session
logout: function()
return $http.post('logout.json').then(function(reply)
if (reply.authenticated === true)
$localStorage.set(_userKey, reply.user);
);
,
can: function(action, object)
if (!this.authed())
return false;
var user = this.getUser();
if (user && user.type === 'admin')
return true;
switch(action)
case 'manage_dashboards':
return (user.type === 'manager');
return false;
])
.controller('AuthCtrl', [
'context',
'$scope',
function(
context,
$scope
)
$scope.$root.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams)
//only require auth if we're moving to another authed page
if (toState && toState.name.indexOf('authed') > -1)
requireAuth();
);
function requireAuth()
if (!context.authed())
$state.go('login');
]
** 免责声明:以上代码为伪代码,不提供任何保证**
【讨论】:
以上是关于AngularJS ui-router 登录认证的主要内容,如果未能解决你的问题,请参考以下文章