年后开始尝试用ionic来做个移动App,之前也列出了需要解决的问题,今天算是把UI都搞的差不多了,接着就把权限认证也搞上吧。
一通Google+乱问搜罗到了这篇文章,先不管行不行,直接翻译了再说(按照自己的意思翻译的),不行咱们再找别的方案。
原文地址 https://devdactic.com/user-auth-angularjs-ionic/
原作者说:在Ionic+AngularJS做的Hybrid App中实现登陆和用户认证系统是大家遇到的最难的问题之一,他之前写过一个简单的登陆simple login with Ionic and AngularJS居然是每天访问最多的(反正我没看到,这里是作者写的文章里被浏览最多的)。所以作者又有了奉献精神,当当当,写出了这篇完整的权限认证系统!在此小弟感谢,要不您的文章我还得再花几天时间捣鼓! O(∩_∩)O
认证系统嘛,从以下三个角度来一一解决
- 用户登陆和会话保持(后端基于token认证)。
- 基于路由的授权控制
- 会话失效,重新登陆
这里提供了一份完整的源代码 点击下载,一个基于Ionic框架开发的,但大多数只是AngularJS逻辑,所以不论你想要用到AngularJS App 还是 Hybrid App都没问题,接着往下看吧。还有需要一个后端程序,这是原作者关于后端写的一篇文章 RESTful API User Authentication with Node.js and AngularJS。
准备工作
首先创建个Ionic项目,关于Ionic的开发环境可以看Ionic+Angularjs+PhoneGap开发环境搭建
$ ionic start devdactic-auth blank
$ cd devdactic-auth
$ bower install angular-mocks --save # 经测试这玩意不用装,没啥用,就是模拟请求的。
打开项目,找到www/js目录,创建3个文件,分别是controllers.js services.js和constants.js。
之后在index.html引入
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>
<script src="js/constants.js"></script>
<!--
如果直接源码下载angular-mocks.js
并且放到了www/lib/ionic/js/angular/angular-mocks.js
那么就不用在此引入。
-->
<script src="lib/angular-mocks/angular-mocks.js"></script>
还要修改www/js/app.js文件,引入angular-mocks模块
angular.module('starter', ['ionic', 'ngMockE2E'])
再创建5个HTML文件放到 www/templates目录(没有此目录自行创建),分别是 login.html, main.html, dashboard.html, public.html, admin.html。
OK,准备工作就这些,下面开始愉(苦)快(逼)的编码吧。
在APP里创建一些常量
在创建route之前,需要先定义一些在全局常量,这样比用事件广播或者字符串定义的角色要简单。 编辑www/js/constants.js文件
angular.module('starter')
.constant('AUTH_EVENTS', {
notAuthenticated: 'auth-not-authenticated',
notAuthorized: 'auth-not-authorized'
})
.constant('USER_ROLES', {
admin: 'admin_role',
public: 'public_role'
});
很简单,现在我们就可以在程序中使用这些常量了。并且看到了这个 AngularJS中的身份验证应用程序的技术, 感谢!
开始创建视图
程序包含一个登录页面和3个tab选项的布局,这些tab页都是需要身份验证,所以没登录前不能访问任何tab页面。就算在浏览器里运行,也不能直接通过URL来访问这些tab页面,这样才是确保你应用是最安全的。
编辑www/index.html的body标签
<body ng-app="starter" ng-controller="AppCtrl">
<ion-nav-bar class="bar-balanced">
</ion-nav-bar>
<ion-nav-view></ion-nav-view>
</body>
我们有一个导航栏(ion-nav-bar标签)和导航视图(ion-nav-view)元素将显示视图连接到路由,为了让程序尽快运行就先在创建个全部空的controllers。
编辑www/js/controllers.js
angular.module('starter')
.controller('AppCtrl', function() {})
.controller('LoginCtrl', function() {})
.controller('DashCtrl', function() {});
接着是登录页面,就创建两个input+一个登录按钮。
编辑www/templates/login.html
<ion-view view-title="Sign-In" name="login-view">
<ion-content class="padding">
<div class="list list-inset">
<label class="item item-input">
<input type="text" placeholder="Username" ng-model="data.username">
</label>
<label class="item item-input">
<input type="password" placeholder="Password" ng-model="data.password">
</label>
</div>
<button class="button button-block button-positive" ng-click="login(data)">Login</button>
</ion-content>
</ion-view>
下一个是登陆后的抽象视图,用于登录后的选项卡,并负责加载视图(这个只可意会,白话文我不太会讲,可能没太理解,有大神讲的比较清晰的跟俺留个言呗)。
这里解释一下angular-ui-router的 abstract(抽象视图)属性
我的理解是abstract抽象视图不能直接被激活,但是可以设置为被激活的子节点。
就像你在A页面填入一些内容,但内容太多,有一项需要新打开一个B页面去选择,如果现在直接跳转到B页面,填写完成后再回到A页面,你就会发现一些问题(像A页面原来填写内容没有了),那么这个时候你需要设置B页面为
abstract: ture就不会再存在此问题了。
编辑www/templates/main.html
<ion-view>
<ion-tabs class="tabs-icon-top tabs-balanced">
<ion-tab title="Dashboard" icon="ion-home" href="#/main/dash">
<ion-nav-view name="dash-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="Public" icon="ion-earth" href="#/main/public">
<ion-nav-view name="public-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="Secret" icon="ion-nuclear" href="#/main/admin">
<ion-nav-view name="admin-tab"></ion-nav-view>
</ion-tab>
</ion-tabs>
</ion-view>
当用户登陆后,将跳转到Dashboard页面,页面中包含一个提示文字,一个注销按钮和3个其他请求的按钮。现在还不能运行,因为控制器和services.js还没写好(快点呗,真啰嗦,天朝程序猿就喜欢简单粗暴直接的),但是view写好了,所以我们尽快看下我们程序吧(您也知道要快点了。。)
编辑www/templates/dashboard.html
<ion-view view-title="Dashboard" name="dashboard-view">
<ion-nav-buttons side="right">
<button class="button icon-left ion-log-out button-stable" ng-click="logout()">Logout</button>
</ion-nav-buttons>
<ion-content class="padding">
<div class="row">
<div class="col text-center">
<h3 class="title">Welcome !</h3>
This could be your Dashboard.
<br><br><br>
<button class="button button-full button-positive" ng-click="performValidRequest()">
Make Valid Request
</button>
<button class="button button-full button-energized" ng-click="performUnauthorizedRequest()">
Make Request for unauthorized resource
</button>
<button class="button button-full button-assertive" ng-click="performInvalidRequest()">
Make Request without valid token
</button>
<br><br>
</div>
</div>
</ion-content>
</ion-view>
其他两个视图(public.html, admin.html)是基于用于角色保护的,所以视图只包含你看到的。
编辑 www/templates/public.html
<ion-view view-title="Public" name="dashboard-view">
<ion-content class="padding">
任何登陆的人都能看到!
</ion-content>
</ion-view>
最后是这个www/templates/admin.html,并且只有管理员才能看到!
<ion-view view-title="Admin" name="dashboard-view">
<ion-content class="padding">
管理员才能看到的地方!
</ion-content>
</ion-view>
全部view就这些了,到现在还没啥特别的,就是一般程序。主要还是后面的程序,咱们继续,下面是路由,算是跟程序的一个照面。
实现基本路由
这个demo的路由不是很复杂,一个登陆页面,一个有三个选项卡的页面,其中main这个抽象路由是抽象路线的模板,还有3个main.*对应的是3个选项页面。
这里主要介绍一个地方是37-39行,链接管理员选项卡。这里做了权限控制,只有当用户USER_ROLES.admin时才可以访问这些页面。现在还不能正常运行,咱们接着往下走。
编辑www/js/app.js
.config(function ($stateProvider, $urlRouterProvider, USER_ROLES) {
$stateProvider
.state('login', {
url: '/login',
templateUrl: 'templates/login.html',
controller: 'LoginCtrl'
})
.state('main', {
url: '/',
abstract: true,
templateUrl: 'templates/main.html'
})
.state('main.dash', {
url: 'main/dash',
views: {
'dash-tab': {
templateUrl: 'templates/dashboard.html',
controller: 'DashCtrl'
}
}
})
.state('main.public', {
url: 'main/public',
views: {
'public-tab': {
templateUrl: 'templates/public.html'
}
}
})
.state('main.admin', {
url: 'main/admin',
views: {
'admin-tab': {
templateUrl: 'templates/admin.html'
}
},
data: {
authorizedRoles: [USER_ROLES.admin]
}
});
// Thanks to Ben Noblet!
$urlRouterProvider.otherwise(function ($injector, $location) {
var $state = $injector.get("$state");
$state.go("main.dash");
});
})
现在程序里已经进入了angular-mocks,所以可以添加很多东西到程序。添加以下内容到www/js/app.js,咱们运行看看结果。(译者去掉所有angular-mocks的东西就成功了)
.run(function($httpBackend){
$httpBackend.whenGET(/templates\/\w+.*/).passThrough();
})

后端添加用户认证服务
存储服务端token或者session-id使用localStorage,请求时把token放在http的头$http.defaults.headers.common['X-Auth-Token'] = 'token'
编辑services.js
angular.module('starter')
.service('AuthService', function($q, $http, USER_ROLES) {
var LOCAL_TOKEN_KEY = '';
var username = '';
var isAuthenticated = false; // 是否已授权
var role = '';
var authToken;
function loadUserCredentials() {
var token = window.localStorage.getItem(LOCAL_TOKEN_KEY);
if (token) {
useCredentials(token);
}
}
function storeUserCredentials(token) {
window.localStorage.setItem(LOCAL_TOKEN_KEY, token);
useCredentials(token);
}
function useCredentials(token) {
username = token.split('.')[0];
isAuthenticated = true;
authToken = token;
if (username == 'admin') {
role = USER_ROLES.admin
}
if (username == 'user') {
role = USER_ROLES.public
}
// Set the token as header for your requests!
$http.defaults.headers.common['X-Auth-Token'] = token;
}
function destroyUserCredentials() {
authToken = undefined;
username = '';
isAuthenticated = false;
$http.defaults.headers.common['X-Auth-Token'] = undefined;
window.localStorage.removeItem(LOCAL_TOKEN_KEY);
}
var login = function(name, pw) {
return $q(function(resolve, reject) {
if ((name == 'admin' && pw == '1') || (name == 'user' && pw == '1')) {
// Make a request and receive your auth token from your server
storeUserCredentials(name + '.yourServerToken');
resolve('Login success.');
} else {
reject('Login Failed.');
}
});
};
var logout = function() {
destroyUserCredentials();
};
var isAuthorized = function(authorizedRoles) {
if (!angular.isArray(authorizedRoles)) {
authorizedRoles = [authorizedRoles];
}
return (isAuthenticated && authorizedRoles.indexOf(role) !== -1);
};
loadUserCredentials();
return {
login: login,
logout: logout,
isAuthorized: isAuthorized,
isAuthenticated: function() {return isAuthenticated;},
username: function() {return username;},
role: function() {return role;}
};
})
像之前说的,我们不能检查token是否有效,所以我们设置一个拦截器,当用一个无效的token请求,拦截器会注意到$http响应的错误,并且播放一条消息到app。
所以还要在services.js添加以下内容。
.factory('AuthInterceptor', function ($rootScope, $q, AUTH_EVENTS) {
return {
responseError: function (response) {
$rootScope.$broadcast({
401: AUTH_EVENTS.notAuthenticated,
403: AUTH_EVENTS.notAuthorized
}[response.status], response);
return $q.reject(response);
}
};
})
.config(function ($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
});
虽然这个请求是全局的配置,但依然需要控制器来配合
.controller('AppCtrl', function($scope, $state, $ionicPopup, AuthService, AUTH_EVENTS) {
$scope.username = AuthService.username();
$scope.$on(AUTH_EVENTS.notAuthorized, function(event) {
var alertPopup = $ionicPopup.alert({
title: 'Unauthorized!',
template: 'You are not allowed to access this resource.'
});
});
$scope.$on(AUTH_EVENTS.notAuthenticated, function(event) {
AuthService.logout();
$state.go('login');
var alertPopup = $ionicPopup.alert({
title: 'Session Lost!',
template: 'Sorry, You have to login again.'
});
});
$scope.setCurrentUsername = function(name) {
$scope.username = name;
};
})
notAuthorized事件告知用户权限不足,notAuthenticated事件是token过期需要登录等。之后又设置了个当前用户,这可以使用所有的子范围!
之后是LoginCtrl
.controller('LoginCtrl', function($scope, $state, $ionicPopup, AuthService) {
$scope.data = {};
$scope.login = function(data) {
AuthService.login(data.username, data.password).then(function(authenticated) {
$state.go('main.dash', {}, {reload: true});
$scope.setCurrentUsername(data.username);
}, function(err) {
var alertPopup = $ionicPopup.alert({
title: 'Login failed!',
template: 'Please check your credentials!'
});
});
};
})
接着是DashCtrl
.controller('DashCtrl', function($scope, $state, $http, $ionicPopup, AuthService) {
$scope.logout = function() {
AuthService.logout();
$state.go('login');
};
$scope.performValidRequest = function() {
$http.get('http://localhost:8100/valid').then(
function(result) {
$scope.response = result;
});
};
$scope.performUnauthorizedRequest = function() {
$http.get('http://localhost:8100/notauthorized').then(
function(result) {
// No result here..
}, function(err) {
$scope.response = err;
});
};
$scope.performInvalidRequest = function() {
$http.get('http://localhost:8100/notauthenticated').then(
function(result) {
// No result here..
}, function(err) {
$scope.response = err;
});
};
});
拦截所有改变的状态
现在没有任何检查用户的状态,这就意味着用户可以改变其URL来访问想要访问的页面。这时需要添加路由更改事件.
编辑 app.js 最下方添加
.run(function ($rootScope, $state, AuthService, AUTH_EVENTS) {
$rootScope.$on('$stateChangeStart', function (event,next, nextParams, fromState) {
if ('data' in next && 'authorizedRoles' in next.data) {
var authorizedRoles = next.data.authorizedRoles;
if (!AuthService.isAuthorized(authorizedRoles)) {
event.preventDefault();
$state.go($state.current, {}, {reload: true});
$rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
}
}
if (!AuthService.isAuthenticated()) {
if (next.name !== 'login') {
event.preventDefault();
$state.go('login');
}
}
});
});
译者试验了,的确很好用,但是有些地方并不合意。待俺先加上后端服务,修改修改再上来更新吧!!