本文已于2017年5月11日更新,以反映对Auth0 API的重要更改。
单页应用程序的身份验证可能会很棘手。 在许多情况下,SPA体系结构涉及具有一个独立的前端应用程序和一个类似AngularJS的框架,以及一个单独的后端,该后端充当数据API来馈送前端。 在这些情况下,在大多数往返应用程序中完成的传统的基于会话的身份验证就不够了。 基于会话的身份验证对于这种体系结构有很多问题,但最大的可能是它向API引入了状态 ,而REST的宗旨之一就是事物保持无状态 。 另一个考虑因素是,如果您想使用相同的数据API作为移动应用程序的后端,则基于会话的身份验证将不起作用。
JSON Web令牌
为了解决这些限制,我们可以使用JSON Web令牌(JWT)向我们的单页应用程序添加身份验证。 JWT是一个开放标准,它为我们提供了一种方法,可以验证从前端AngularJS应用到后端API的请求。 JWT不仅仅是一个令牌。 JWT的最大优点之一是它们包含一个数据有效载荷,该载荷可以具有我们定义的声明形式的任意JSON数据。 由于JWT是使用服务器上的秘密进行数字签名的,因此我们可以放心,它们不会被篡改,并且有效载荷中的数据在到达后端之前无法更改。
Angular应用程序的身份验证
JWT是向我们的AngularJS应用添加身份验证的理想解决方案。 从API访问安全端点所需要做的就是将用户的JWT保存在本地存储中,然后在发出HTTP请求时将其作为Authorization
标头发送。 如果用户的JWT无效或根本没有JWT,则他们访问受保护资源的请求将被拒绝,并且他们将得到一个错误。
不幸的是,这仅仅是在AngularJS应用中处理身份验证的最低要求。 如果我们完全关心用户体验,则需要做其他一些事情来确保我们的应用程序表现出预期的效果。 我们要:
- 有条件地显示或隐藏某些元素,具体取决于用户是否具有有效的JWT(例如:“ 登录”和“ 注销”按钮)
- 保护未经身份验证的用户无法访问的某些路由
- 如果用户状态更改(如果其JWT过期)或注销,则更新用户界面
在本文中,我们将在AngularJS应用中从头到尾实现身份验证,甚至将创建一个小型NodeJS服务器,以了解如何对受保护的资源进行记录。 关于建立用户数据库和发行JWT的细节很多,因此,我们将不用Auth0 (我为之工作的公司)来自己做,而是自己做。 Auth0为多达7,000个活动用户提供了免费计划 ,这为我们在许多实际应用中提供了足够的空间。 我们还将看到如何轻松添加登录框,甚至将社交身份验证与Auth0结合使用。
在我们开始之前,如果您想在AngularJS上复习一下,请在SitePoint Premium上查看使用AngularJS构建应用程序 。
要查看本教程的所有代码,请查看repo 。
注册Auth0
本教程首先需要的是Auth0帐户。 注册帐户时,您需要为您的应用提供一个域名,以后不能更改。 由于您可以在同一个帐户下拥有多个应用程序,因此如何命名域取决于您的情况。 在大多数情况下,最好使用与您的组织相关的名称来命名它,例如公司名称。 如果可行,您也可以使用应用程序的名称-由您自己决定。 您的Auth0域采用的格式为your-domain.auth0.com
并在配置Auth0工具时使用,我们将在下面看到。
注册后,系统会询问您要对应用程序进行哪种身份验证。 可以保留默认值,因为以后可以更改它们。
注册后,转到仪表板进行检查。 如果您点击左侧栏中的“ 客户”链接,则会看到您的帐户是使用Default App创建的。 单击默认应用程序以查看您的凭据和其他详细信息。
马上,我们应该填写我们的“ 允许的来源”和“ 允许的回调URL” 。 此字段用于告诉Auth0允许哪些域发出对用户进行身份验证的请求,以及在进行身份验证后我们可以重定向到哪些域。 在本教程中,我们将使用http-sever ,其默认来源为http://localhost:8080
。
接下来,由于我们正在构建将与API后端通信的“单页应用”,因此,我们还要构建一个API客户端。 单击主菜单中的API链接。 在此处,单击创建API按钮,将出现一个对话框,要求您填写有关API的一些信息。 您只需要提供Name和Identifier即可 。 记下标识符,因为这是将用作您的API的受众标识符的值。 将签名算法保留为RS256 。
借助Auth0的免费计划,我们可以使用两个社交身份提供商,例如Google,Twitter,Facebook等。 要使它们正常工作,我们要做的就是翻转开关,这可以在仪表板的“ 连接” >“ 社交”链接中完成。
安装依赖项并配置Auth0
我们需要此应用程序的许多软件包,其中一些由Auth0作为开源模块提供。 如果您已分叉GitHub存储库 ,则只需运行bower install
即可安装所有必需的依赖项。 安装依赖项后,您将需要全局安装http-server
模块。 为此,请输入以下命令:
# To serve the app (if not already installed)
npm install -g http-server
最后,要启动并运行该应用程序,只需从终端或命令行界面执行http-server
命令。
接下来,让我们设置app.js
和index.html
文件以引导应用程序。 此时,我们可以让Angular从安装的依赖项中知道我们需要访问哪些模块。
// app.js
(function () {
'use strict';
angular
.module('app', ['auth0.auth0', 'angular-jwt', 'ui.router'])
.config(config);
config.$inject = ['$stateProvider', '$locationProvider', 'angularAuth0Provider', '$urlRouterProvider', 'jwtOptionsProvider'];
function config($stateProvider, $locationProvider, angularAuth0Provider, $urlRouterProvider, jwtOptionsProvider) {
$stateProvider
.state('home', {
url: '/home',
controller: 'HomeController',
templateUrl: 'components/home/home.html',
controllerAs: 'vm'
})
// Initialization for the angular-auth0 library
angularAuth0Provider.init({
clientID: AUTH0_CLIENT_ID, // Your Default Client ID
domain: AUTH0_DOMAIN, // Your Auth0 Domain
responseType: 'token id_token',
redirectUri: AUTH0_CALLBACK_URL, // Your Callback URL
audience: AUTH0_API_AUDIENCE, // The API Identifier value you gave your API
});
// Configure a tokenGetter so that the isAuthenticated
// method from angular-jwt can be used
jwtOptionsProvider.config({
tokenGetter: function() {
return localStorage.getItem('id_token');
}
});
$urlRouterProvider.otherwise('/home');
// Remove the ! from the hash so that
// auth0.js can properly parse it
$locationProvider.hashPrefix('');
}
})();
在这里,我们使用仪表板中的凭据从auth0-angular配置了authProvider
。 当然,您需要使用自己的凭据替换示例中的值。 我们还创建一个app.run.js
文件并粘贴以下代码:
// app.run.js
(function () {
'use strict';
angular
.module('app')
.run(function ($rootScope, authService) {
// Put the authService on $rootScope so its methods
// can be accessed from the nav bar
$rootScope.auth = authService;
// Process the auth token if it exists and fetch the profile
authService.handleParseHash();
});
})();
一旦用户成功通过身份验证,此功能将执行的工作是解析哈希以提取与回调id_token
返回的access_token
和id_token
。 在实际的应用程序中,您可能有一条特定的路径来处理此问题,例如/callback
但是对于我们的简单演示,只要刷新应用程序,就可以运行该程序。
access_token
将被发送到您的后端API,并且此令牌将经过验证以确保正确的访问。 另一方面, id_token
用于前端客户端,并保存客户端的用户数据。
<!-- index.html -->
<html>
<head>
<title>AngularJS Authentication</title>
<!-- Viewport settings-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- Basic style -->
<link href="bower_components/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
<style>
.navbar{
margin-bottom: 0;
border-radius: 0;
}
</style>
</head>
<body>
<div ng-app="app">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" ui-sref="home">Auth0</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li ng-if="!auth.isAuthenticated()"><a ng-click="auth.login()">Log In</a></li>
<li ng-if="auth.isAuthenticated()"><a ng-click="auth.logout()">Log Out</a></li>
</ul>
</div>
</div>
</nav>
<div ui-view></div>
</div>
<script type="text/javascript" src="auth0-variables.js"></script>
<script type="text/javascript" src="bower_components/angular/angular.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script type="text/javascript" src="bower_components/auth0.js/build/auth0.js"></script>
<script type="text/javascript" src="bower_components/angular-auth0/dist/angular-auth0.js"></script>
<script type="text/javascript" src="bower_components/angular-jwt/dist/angular-jwt.js"></script>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript" src="app.run.js"></script>
<script type="text/javascript" src="components/home/home.controller.js"></script>
<script type="text/javascript" src="components/auth/auth.service.js"></script>
</body>
</html>
现在,我们已经为应用程序设置奠定了基础。 页面顶部有一个简单的工具栏,允许用户登录。 您会在底部看到很多尚未创建的导入。 我们将在下一部分中开始构建它们。
建立首页
应用程序中有几个地方可以放置验证控件。 我们可以使用sidenav,navbar,模式或什至这三者的混合。 为了简单起见,我们已经在工具栏中放置了一个登录按钮,但是为了获得更好的用户体验,我们也将其添加到主视图中。 如果我们查看app.js
文件,我们将看到home组件将位于components/home
目录中,因此接下来使用home.controller.js
文件和UI的home.html
文件创建此目录。 我们的用户界面如下所示:
<!-- home.html -->
<div class="jumbotron">
<h2 class="text-center"><img src="https://cdn.auth0.com/styleguide/1.0.0/img/badge.svg"></h2>
<h2 class="text-center">Home</h2>
<div class="text-center" ng-if="!vm.auth.isAuthenticated()">
<p>You are not yet authenticated. <a href="javascript:;" ng-click="vm.auth.login()">Log in.</a></p>
</div>
<div class="text-center" ng-if="vm.auth.isAuthenticated()">
<p>Thank you for logging in! <a href="javascript:;" ng-click="vm.auth.logout()">Log out.</a></p>
</div>
<div class="text-center">
<a ng-click="vm.getMessage()">Get Message</a> <span style="padding: 0 50px;">|</span>
<a ng-click="vm.getSecretMessage()">Get Secret Message</a>
<br />
<p>{{vm.message}}</p>
</div>
</div>
对于我们的home.controller.js
文件,我们将具有以下代码:
// home.controller.js
(function () {
'use strict';
angular
.module('app')
.controller('HomeController', homeController);
homeController.$inject = ['authService', '$http'];
function homeController(authService, $http) {
var vm = this;
vm.auth = authService;
vm.getMessage = function() {
$http.get('http://localhost:3001/api/public').then(function(response) {
vm.message = response.data.message;
});
}
// Makes a call to a private endpoint.
// We will append our access_token to the call and the backend will
// verify that it is valid before sending a response.
vm.getSecretMessage = function() {
$http.get('http://localhost:3001/api/private', {headers : {
Authorization: 'Bearer ' + localStorage.getItem('access_token')
}}).then(function(response) {
vm.message = response.data.message;
}).catch(function(error){
vm.message = "You must be logged in to access this resource."
});
}
}
})();
从我们的家庭控制器,我们将调用我们的API服务。 我们将有两个API调用,一个用于任何人都可以访问的公共API路由,一个用于只能由登录用户成功访问的受保护路由。 如果其中一些代码还没有意义,那是可以的。 创建身份验证服务时,我们将在下一部分中更深入地介绍。
创建身份验证服务
到目前为止,我们已经多次引用了身份验证服务,但实际上并没有构建它。 接下来,让我们来照顾它。 身份验证服务将负责登录用户,管理身份验证状态等。 创建一个名为auth
的新目录,并在其中创建文件auth.service.js
。 我们的身份验证服务将如下所示:
// auth.service.js
(function () {
'use strict';
angular
.module('app')
.service('authService', authService);
authService.$inject = ['$state', 'angularAuth0', 'authManager'];
function authService($state, angularAuth0, authManager) {
// When a user calls the login function they will be redirected
// to Auth0's hosted Lock and will provide their authentication
// details.
function login() {
angularAuth0.authorize();
}
// Once a user is successfuly authenticated and redirected back
// to the AngularJS application we will parse the hash to extract
// the idToken and accessToken for the user.
function handleParseHash() {
angularAuth0.parseHash(
{ _idTokenVerification: false },
function(err, authResult) {
if (err) {
console.log(err);
}
if (authResult && authResult.idToken) {
setUser(authResult);
}
});
}
// This function will destroy the access_token and id_token
// thus logging the user out.
function logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
}
// If we can successfuly parse the id_token and access_token
// we wil store them in localStorage thus logging the user in
function setUser(authResult) {
localStorage.setItem('access_token', authResult.accessToken);
localStorage.setItem('id_token', authResult.idToken);
}
// This method will check to see if the user is logged in by
// checking to see whether they have an id_token stored in localStorage
function isAuthenticated() {
return authManager.isAuthenticated();
}
return {
login: login,
handleParseHash: handleParseHash,
logout: logout,
isAuthenticated: isAuthenticated
}
}
})();
身份验证服务非常简单。 我们具有处理应用程序登录和注销以及检查用户是否登录的功能。我们的应用程序现在应该可以运行了。 让我们继续并访问localhost:8080 ,看看我们的应用程序正在运行。
如果一切顺利,您应该会看到Angular应用程序已加载,并且您将处于注销状态。
由于我们尚未部署服务器,因此底部的两个链接目前无法使用。 我们将很快执行此操作,但是为了确保我们的应用正常运行,请尝试登录。 单击导航栏或页面主要内容中的登录链接,您将被重定向到Auth0域上的登录页面。
在这里,您可以使用已设置的任何连接登录,甚至可以注册一个新帐户。 但是,如果需要登录,您将被重定向回localhost:8080
AngularJS应用程序,但是这次您将处于登录状态。
优秀的。 在演示的最后一部分,让我们编写一个简单的Node服务器来处理我们的API调用。
创建NodeJS服务器
现在,让我们快速设置一个NodeJS服务器,以便我们可以发出请求! 创建一个名为server
的新目录,然后安装一些依赖项。
mkdir server && cd server
npm init
npm install express express-jwt cors jkws-rsa
安装后,创建一个使用express-jwt中间件的express应用。 您需要您的Auth0 API信息。 由于我们早先已经创建了API,因此请进入您的信息中心 ,找到该API,然后复制其受众价值。 看一下下面的实现:
// server/server.js
var express = require('express');
var app = express();
var jwt = require('express-jwt');
var jwks = require('jwks-rsa');
var cors = require('cors');
app.use(cors());
var authCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://{YOUR-AUTH0-DOMAIN}.auth0.com/.well-known/jwks.json"
}),
audience: '{YOUR-AUTH0-API-AUDIENCE}', // Paste your API audience here.
issuer: "https://{YOUR-AUTH0-DOMAIN}.auth0.com/",
algorithms: ['RS256']
});
app.get('/api/public', function(req, res) {
res.json({ message: "Hello from a public endpoint! You don't need to be authenticated to see this." });
});
// For the private call we are using the authCheck middleware to validate the token
app.get('/api/private', authCheck, function(req, res) {
res.json({ message: "Hello from a private endpoint! You DO need to be authenticated to see this." });
});
app.listen(3001);
console.log('Listening on http://localhost:3001');
除非发送了有效的JWT,否则express-jwt中间件用于防止访问端点。 然后,我们只需将中间件作为第二个参数传入即可将中间件应用于我们想要保护的任何路由,就像我们在此处为private
路由所做的那样。
发出API请求
使用命令node server.js
在新的控制台窗口/选项卡中启动服务器
现在,如果我们现在进入AngularJS应用程序,然后单击“ 获取消息”按钮,我们将看到一条消息,显示“您来自公共端点……”。 单击旁边的“ 获取机密消息”按钮,您应该看到显示“来自私有端点的Hello ...”消息。 这是因为我们之前登录过,而您仍处于登录状态。
让我们看看当您没有登录并尝试访问该秘密消息时会发生什么。 单击导航栏或主要内容中的注销按钮。 注销后,单击“ 获取机密消息”按钮,这次将显示另一条消息,提示您必须经过身份验证才能访问端点。
有关Auth0的更多信息
Auth0还使我们可以轻松地将其他现代身份验证功能添加到我们的应用程序,包括单点登录 ,无密码登录和多因素身份验证 。
我们也不仅限于使用NodeJS作为后端。 有许多其他可用的SDK,包括:
还有一些SDK可用于移动开发,以简化身份验证:
包起来
从AngularJS应用向API添加身份验证并向其发送经过身份验证的请求相对容易,但要确保用户体验正确,还涉及很多步骤。 Auth0在身份验证方面为我们带来了繁重的工作,因为我们不必担心保留自己的用户数据库,也不需要放入自己的登录框。
根据我的经验,在Angular 2应用程序中实现身份验证要容易得多,因为我们需要担心的事情更少。 如果您对Angular 2感兴趣,则可以查看有关如何构建具有身份验证功能的应用程序的示例 ,并查看Auth0文档 。
From: https://www.sitepoint.com/easy-angularjs-authentication-with-auth0/