AngularJS ui-router登录认证

我是AngularJS的新手,我有点困惑如何在以下场景中使用angular-"ui-router":

我正在构建一个由两个部分组成的web应用程序。第一部分是带有登录和注册视图的主页,第二部分是仪表板(成功登录后)。

我已经为home section创建了一个index.html,它的angular app和ui-router配置来处理/login/signup视图, 还有另一个文件dashboard.html用于仪表板部分及其app和ui-router配置,以处理许多子视图

现在我完成了仪表板部分,但不知道如何将这两个部分与它们不同的角度应用程序结合起来。我如何让主应用重定向到仪表板应用?

181797 次浏览

我认为你需要一个service来处理身份验证过程(及其存储)。

在这个服务中,你需要一些基本的方法:

  • isAuthenticated()
  • login()
  • logout()
  • 等等……

这个服务应该被注入到每个模块的控制器中:

  • 在仪表板部分,使用此服务检查用户是否已通过身份验证(service.isAuthenticated()方法)。如果不是,重定向到/login
  • 在登录部分,只使用表单数据通过service.login()方法验证用户

这种行为的一个很好的健壮的例子是项目angular-app,特别是它的安全模块,它基于很棒的HTTP认证拦截器模块

希望这能有所帮助

我正在制作一个更好的演示,并将其中一些服务清理成一个可用的模块,但以下是我想到的。这是一个复杂的过程,需要解决一些注意事项,所以请坚持下去。你需要把它分解成几个部分。

看看这个砰的一声

首先,您需要一个服务来存储用户的标识。我称之为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委托。所以我们必须在两个地方进行认证检查:一次是根据你的身份承诺在resolve中解析,这涵盖了你的应用程序第一次加载时,一次是在$stateChangeStart中(如果解析已经完成),这涵盖了你在状态间导航的任何时间。

好的,到目前为止我们做了什么?

  1. 如果用户登录,我们检查应用程序何时加载。
  2. 我们跟踪关于登录用户的信息。
  3. 对于需要用户登录的状态,我们将它们重定向到登录状态。
  4. 如果他们没有访问权限,我们会将他们重定向到拒绝访问状态。
  5. 如果我们需要用户登录,我们有一种机制可以将用户重定向到他们所请求的原始状态。
  6. 我们可以将用户签出(需要与管理您的认证票的任何客户端或服务器代码相一致)。
  7. 我们需要在用户每次重新加载浏览器或点击链接时将他们送回登录页面。

我们将何去何从?您可以将您的州组织成需要登录的区域。你可以通过将dataroles添加到这些状态(或者它们的父状态,如果你想使用继承)来要求已验证/授权用户。在这里,我们将资源限制为admin:

.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角色成员。我们讨论过的所有授权事宜都将由此展开。

下面是我们如何摆脱无限路由循环,并且仍然使用$state.go而不是$location.path

if('401' !== toState.name) {
if (principal.isIdentityResolved()) authorization.authorize();
}

我创建了这个模块,以帮助使这个过程的一块蛋糕

你可以这样做:

$routeProvider
.state('secret',
{
...
permissions: {
only: ['admin', 'god']
}
});

或者还

$routeProvider
.state('userpanel',
{
...
permissions: {
except: ['not-logged-in']
}
});

它是全新的,但值得一看!

https://github.com/Narzerus/angular-permission < a href = " https://github.com/Narzerus/angular-permission " > < / >

在我看来,到目前为止发布的解决方案是不必要的复杂。有一个更简单的方法。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中的承诺,然后将他们重定向到登录页面。

事情是这样的:

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()
}
}
}
)

与第一个解决方案不同,这个解决方案实际上阻止了目标状态的加载。

最简单的解决方案是使用$stateChangeStartevent.preventDefault()在用户未经过身份验证时取消状态更改,并将他重定向到登录页面的身份验证状态。

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');
}
});
}]
);

我有另一个解决方案:当你登录时只有你想要显示的内容时,这个解决方案非常有效。定义一个规则,你检查你是否登录,它不是白名单路由的路径。

$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 */ }代替

首先,你需要一个服务,你可以注入到你的控制器,有一些应用程序身份验证状态的想法。使用本地存储持久化认证细节是一种很好的方法。

接下来,您需要在状态更改之前检查身份验证状态。因为你的应用程序有一些页面需要验证,而另一些则不需要,创建一个检查验证的父路由,并使所有其他需要验证的页面成为该父路由的子页面。

最后,您需要一些方法来判断当前登录的用户是否可以执行某些操作。这可以通过在您的认证服务中添加“can”函数来实现。Can有两个参数: - action - required -(即'manage_dashboards'或'create_new_dashboard') - object - optional -正在操作的对象。例如,如果您有一个仪表盘对象,您可能希望检查是否仪表盘。ownerId === loggedInUser.id。(当然,从客户端传递的信息永远不应该被信任,在将其写入数据库之前,您应该始终在服务器上验证这一点)。< / p >
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');
}
}
}]

**免责声明:上述代码是伪代码,并不能保证**

我想分享另一个使用ui路由器1.0.0.X的解决方案

如您所知,stateChangeStart和statechangessuccess现在已弃用。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文档中找到的一些代码构建了这个示例。我对uirouter很陌生,但它很好用。

希望它能帮助到任何人。

使用$http Interceptor

通过使用$http拦截器,您可以将头信息发送到后端或其他方式,并以这种方式进行检查。

关于美元http拦截器的好文章

例子:

$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函数中。