本文原文:JSON Web Token Tutorial: An Example in Laravel and AngularJS
前言
这是一篇介绍JSON Web Token(JWT)的文章,虽然可能用到的例子和Laravel和AngularJS有关,但知道了原理便能写出适用于自己的。同时,由于目前个人用的后台一直是java,前端也没用过AngularJS,vue也是最近才开始学,所以Laravel和AngularJS部分 并不十分了解,若有错误,欢迎及时提出。
文章内容
随着单页应用程序,移动应用程序和RESTful API服务的日益普及,Web开发人员编写后端代码的方式发生了重大变化。 使用像AngularJS和BackboneJS这样的技术, 我们不再花费大量的时间来构建标记,而是构建前端应用程序使用的api。我们的后端更多地关注业务逻辑和数据,而演示逻辑被专门转移到前端或移动应用。这些变化导致了在现代应用程序中实现身份验证的新方式。
认证是任何Web应用程序中最重要的部分之一。 几十年来, Cookie和基于服务器的认证(感觉应该是常见的session)是最简单的解决方案。然而在现代移动端和单页应用程序处理身份认证可能是很棘手的,需要更好的解决方案。目前,API的认证问题最有名的解决方案是OAuth 2.0和JSON Web Token(JWT)。
什么是 JSON WEB TOKEN(JWT)
JSON Web TOKEN(JWT)是通过发送数字签名进行验证和信任信息的一种规范,是一个开放的标准( RFC 7519 )。它包含一个紧凑且URL安全的JSON对象,该对象通过加密签名来验证其真实性,如果负载(Payload )包含敏感信息,也可以对其进行加密。
由于其结构紧凑,JWT通常用于HTTP Authorization头或URL查询参数。
JSON Web Token的结构
JWT实际上是一个使用.
分隔的多个base64url编码的字符串组成的一个新字符串。它由三部分组成:头部(Header)、负载(Payload)与签名(Signature)。
实例:
代码语言:javascript复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
Header-头部
标题包含token的元数据,最小限度地包含签名的类型和加密算法。(您可以使用JSON格式化工具来优化 JSON对象。)
例:
代码语言:javascript复制{
“alg”: “HS256”,
“typ”: “JWT”
}
该JWT头部声明编码对象是一个JSON Web令牌,并且使用HMAC SHA-256算法进行签名。
将其进行base64编码,我们就有了JWT的第一部分。
代码语言:javascript复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload (Claims)-负载
在JWT的上下文中,一个声明(claim )可以被定义为关于实体(通常是用户)的声明(statement ),以及有关token本身额外的元数据。Claim 包含我们要传输的信息以及服务器可以使用它来正确处理身份验证。我们可以提供多种claim,包括 registered claim names, public claim names and private claim names。
注:对于registered claim names,英文原文中使用的是registered ,jwt.io和查看的一些中文介绍中均用的是Reserved,故下文中均用Reserved代替英文原文中关于registered claim names部分。
即:Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved , public 和 private .
Reserved claims
这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用。包含:
- iss:token签发者
- exp:token过期时间戳
- sub:token面向的用户/token的主题
- aud:token接收方
- iat:签发时间
- nbf:“Not before”,JWT不能接受处理的时间
- jti: JWT ID claim,为JWT提供唯一的标识符
Public claims
根据需要定义自己的字段,注意应该避免冲突。通过使用URI或URN命名避免发送者和接收方不属于封闭网络时 JWT中的命名冲突。
一个public claim name的例子是https://www.toptal.com/jwt_claims/is_admin
,最佳做法是描述声明的位置放置一个文件,并让其文档可以被可以被引用。
Private claims
这些是自定义的字段,可以用来在双方之间交换信息。
可用于JWT仅在已知系统(如企业内部)之间的封闭环境中进行交换的地方。我们可以自定义自己的 claims,如user IDs, user roles, 或者其他任何信息。
使用这些声明名称(claim-names)在封闭或私有系统之外可能具有冲突的语义含义,因此请谨慎使用。
非常需要注意的是,我们希望保持尽可能小的web token,因此尽量仅将必要的数据放在public and private claims中。
例:
代码语言:javascript复制{
“iss”: “toptal.com”,
“exp”: 1426420800,
“https://www.toptal.com/jwt_claims/is_admin”: true,
“company”: “Toptal”,
“awesome”: true
}
这个payload实例中有两个reserved claims, 一个public claim 和两个 private claims。将其进行base64编码,我们就有了JWT的第二部分。
代码语言:javascript复制eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0
Signature-签名
JWT标准遵循JSON Web签名(JWS)规范来生成最终签名的token。它通过组合编码的JWT头(header) 和编码的JWT负载(Payload ) 并使用强加密算法(如HMAC SHA-256)来生成签名。签名的密钥由服务器持有,因此它将能够验证现有的token并签署(颁发/生成)新的token。
代码语言:javascript复制HMACSHA256(
base64UrlEncode(header) "."
base64UrlEncode(payload),
secret)
这给了我们JWT的最后一部分。
代码语言:javascript复制yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
JWT的安全与加密
为了防止中间人(man-in-the-middle)攻击,使用TLS/SSL与JWT结合是至关重要的。在大多数情况下,如果包含敏感信息,加密JWT payload就足够了。但是,如果我们要添加额外的保护层,可以使用JSON Web Encryption(JWE)规范对JWT payload进行加密。
当然,如果我们想避免使用JWE的额外开销,另一个选择是将敏感信息保留在我们的数据库中,并且在需要访问敏感数据时,使用我们的token进行额外的API调用。
为什么需要Web Tokens?
在我们可以看到使用token认证的所有优点之前,我们必须看看过去认证的方式。
基于服务器的身份验证
通常为Session和cookie。
由于HTTP协议是无状态的,因此需要有一种存储用户信息的机制,以及登录后每个后续请求对用户进行身份验证的方法。大多数网站使用Cookie来存储用户的会话ID(session ID)。
它的工作原理
浏览器向包含用户身份和密码的服务器发出POST请求。服务器使用在用户浏览器上设置的cookie进行响应,并包含用于标识用户的会话ID。
在每个后续请求中,由于用户数据存储在服务器上,服务器需要找到该会话并对其进行反序列化。
基于服务器的认证的缺点
- 难以扩展:服务器需要为用户创建一个会话并将其保存在服务器上的某个位置。这可以在内存或数据库中完成。如果我们有一个分布式系统,我们必须确保我们使用一个不耦合到应用服务器的单独的会话存储。
- 跨源请求共享(CORS):当使用AJAX调用从另一个域(跨域,Cross-origin)获取资源时,我们可能会遇到禁止请求的问题,因为默认情况下,HTTP请求不包括跨域(Cross-origin)请求的Cookie 。
- 与Web框架耦合:当使用基于服务器的身份验证时,我们用在我们的框架的身份验证方案,在使用不同编程语言编写的不同Web框架之间共享会话数据是非常困难的,甚至是不可能的。
基于token的身份验证
基于token的认证是无状态的,因此不需要在会话中存储用户信息。这使我们能够扩展我们的应用程序,而不必担心用户登录的位置。我们可以轻松地使用相同的token从除了我们登录的域之外的域中获取安全资源。
JSON Web Token 的工作原理
浏览器或移动客户端向包含用户登录信息的认证服务器发出请求。认证服务器生成新的JWT access token并将其返回给客户端。在对受限资源的每次请求时,客户端都会在查询字符串(the query string)或Authorization
头(header)中发送access token。然后,服务器验证令牌,如果它有效,则将安全资源返回给客户机。
基于token认证的优点
无状态,易于扩展:token包含用于标识用户的所有信息,从而消除了对会话状态的需要(即,无需会话状态)。如果我们使用负载均衡配置,我们可以将用户传递给任何服务器,而不是仅被绑定在我们登陆的那台服务器上。
可重用性:我们可以拥有许多独立的服务器,在多个平台和域(domains)上运行,重复使用相同的令牌来验证用户。很容易构建与其他应用程序共享权限的应用程序。
安全性:由于我们没有使用cookies,我们不必再防御网站的跨站点请求伪造(CSRF)攻击。如果我们必须在其中提供任何敏感信息,我们还应该使用JWE加密我们的token,并通过HTTPS传输我们的令牌以防止中间人(man-in-the-middle)的袭击。
性能:没有服务器端查找可以在每个请求上查找和反序列化会话。我们唯一要做的就是计算HMAC SHA-256来验证token并解析其内容。
使用Laravel 5和AngularJS的JSON Web Token示例
(译注:由于对Laravel和AngularJS不熟悉,这里的以英文原文为准,同时若发现这里有错误,欢迎随时提出。 )
在本教程中,我将演示如何使用两个流行的Web技术实现JSON Web Token的基本身份验证:Laravel 5用于后端代码,AngularJS用于前端单页面应用程序(SPA)示例。(您可以在这里找到整个演示文稿,以及此GitHub存储库中的源代码,以便您可以遵循本教程。)
该JSON Web Token示例不会使用任何类型的加密来确保在claims中传送的信息的机密性。实际上,这通常是可以的,因为TLS / SSL会加密请求。然而,如果token将包含敏感信息,如用户的社会安全号码,则也应使用JWE进行加密。
Laravel后端示例
我们将使用Laravel来处理用户注册,将用户数据保留到数据库,并提供一些需要认证的受限数据,以供Angular应用程序使用。我们将创建一个示例API子域,以模拟跨域( Cross-origin)资源共享(CORS)。
安装和项目引导(Installation and Project Bootstrapping)
为了使用Laravel,我们必须在我们的机器上安装Composer软件包管理器。我建议进行Laravel开发时使用 Laravel Homestead pre-packaged “box” of Vagrant (注:感觉是通过 Laravel 安装工具安装 Laravel)。无论我们的操作系统如何,它都为我们提供了完整的开发环境。
引导(Bootstrap )我们Laravel应用程序的最简单方法是使用 Composer 下载 Laravel 安装包:
代码语言:javascript复制composer global require "laravel/installer=~1.1"
现在我们已经准备好一切通过运行laravel new jwt
创建一个新的Laravel项目。
有关此过程的任何问题,请参阅官方Laravel文档。
在我们创建了基本的Laravel 5应用程序之后,我们需要设置我们的Homestead.yaml
,它将为我们的本地环境配置文件夹映射和域配置。
Homestead.yaml
文件示例:
---
ip: "192.168.10.10"
memory: 2048
cpus: 1
authorize: /Users/ttkalec/.ssh/public.psk
keys:
- /Users/ttkalec/.ssh/private.ppk
folders:
- map: /coding/jwt
to: /home/vagrant/coding/jwt
sites:
- map: jwt.dev
to: /home/vagrant/coding/jwt/public
- map: api.jwt.dev
to: /home/vagrant/coding/jwt/public
variables:
- key: APP_ENV
value: local
当我们使用 vagrant up
命令启动我们的Vagrant box并使用 vagrant ssh
登陆后,我们跳转到事先定义好的项目目录。在上面的例子中,这将是/home/vagrant/coding/jwt
。我们现在可以运行php artisan migrate
命令,以便在我们的数据库中创建必要的用户表。
安装Composer依赖
幸运的是,有一个Laravel开发者的社区,并拥有许多优秀的软件包,可以供我们重用和扩展我们的应用程序。这个例子中,我们将使用 tymon/jwt-auth
,一个由Sean Tymon开发的用于在服务端处理token的,和barryvdh/laravel-cors
,一个由 Barry vd. Heuvel开发的用于处理CORS。
jwt-auth
在我们 composer.json
中 Require the tymon/jwt-auth
package并且更新我们的依赖。
composer require tymon/jwt-auth 0.5.*
添加 JWTAuthServiceProvider
到我们 app/config/app.php
的providers array中。
'TymonJWTAuthProvidersJWTAuthServiceProvider'
接下来,在 app/config/app.php
文件中的 aliases
数组中,我们添加 JWTAuth
facade.
'JWTAuth' => 'TymonJWTAuthFacadesJWTAuth'
最后,我们将通过下面的命令发布软件包的配置: php artisan config:publish tymon/jwt-auth
。
JSON Web tokens 通过秘钥加密。我们可以使用php artisan jwt:generate
命令生成该密钥。它将被放置在我们的config/jwt.php
文件中。然而,在生产环境中,我们不想在配置文件中使用我们的密码或API密钥。相反,我们应该将它们放在服务器环境变量中,并使用该env
函数在配置文件中引用它们。例如:
'secret' => env('JWT_SECRET')
我们可以在Github上找到关于这个软件包和所有配置设置的更多信息。
laravel-cors
在我们composer.json
中Require the barryvdh/laravel-cors
package 并更新我们的依赖。
composer require barryvdh/laravel-cors 0.4.x@dev
添加CorsServiceProvider
到我们的app/config/app.php
的providers array中。
'BarryvdhCorsCorsServiceProvider'
然后添加中间件(middleware )到我们的app/Http/Kernel.php
。
'BarryvdhCorsMiddlewareHandleCors'
通过使用 php artisan vendor:publish
命令发布这配置到 一个本地config/cors.php
文件中。
一个cors.php
文件配置示例:
return [
'defaults' => [
'supportsCredentials' => false,
'allowedOrigins' => [],
'allowedHeaders' => [],
'allowedMethods' => [],
'exposedHeaders' => [],
'maxAge' => 0,
'hosts' => [],
],
'paths' => [
'v1/*' => [
'allowedOrigins' => ['*'],
'allowedHeaders' => ['*'],
'allowedMethods' => ['*'],
'maxAge' => 3600,
],
],
];
路由和处理HTTP请求
为了简洁起见,我将把我所有的代码放在route.php文件中,该文件负责Laravel路由和委托请求给控制器。我们通常会创建专门的控制器来处理我们所有的HTTP请求,并保持我们的代码模块化和干净。
我们将使用我们的AngularJS SPA视图
代码语言:javascript复制Route::get('/', function () {
return view('spa');
});
用户注册
当我们使用用户名和密码向/signup
创建一个POST
请求时,我们将尝试创建一个新用户并将其保存到数据库。创建用户后,将创建一个JWT并通过JSON响应返回。
Route::post('/signup', function () {
$credentials = Input::only('email', 'password');
try {
$user = User::create($credentials);
} catch (Exception $e) {
return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT);
}
$token = JWTAuth::fromUser($user);
return Response::json(compact('token'));
});
用户登录
当我们使用用户名和密码向/signin
发出码POST
请求,我们验证该用户是否存在,并通过JSON响应返回一个JWT。
Route::post('/signin', function () {
$credentials = Input::only('email', 'password');
if ( ! $token = JWTAuth::attempt($credentials)) {
return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED);
}
return Response::json(compact('token'));
});
在同一个域上获取限制资源
用户登录后,我们可以获取受限制的资源。我创建了一个/restricted
模拟需要经过身份验证的用户的资源的路由。为了做到这一点,请求Authorization
头(header )或查询字符串(query string )需要提供JWT用于后端进行验证。
Route::get('/restricted', [
'before' => 'jwt-auth',
function () {
$token = JWTAuth::getToken();
$user = JWTAuth::toUser($token);
return Response::json([
'data' => [
'email' => $user->email,
'registered_at' => $user->created_at->toDateTimeString()
]
]);
}
]);
在这个例子中,我通过'before' => 'jwt-auth'
.使用了 jwt-auth
包中提供的jwt-auth
中间件。该中间件用于过滤请求并验证JWT token。如果token无效,不存在或过期,则中间件将抛出一个可以捕获的异常。
在Laravel 5中,我们可以使用app/Exceptions/Handler.php
文件捕获异常。使用render
函数,我们可以基于抛出的异常创建HTTP响应。
public function render($request, Exception $e)
{
if ($e instanceof TymonJWTAuthExceptionsTokenInvalidException)
{
return response(['Token is invalid'], 401);
}
if ($e instanceof TymonJWTAuthExceptionsTokenExpiredException)
{
return response(['Token has expired'], 401);
}
return parent::render($request, $e);
}
如果用户认证并且token有效,我们可以通过JSON安全地将受限数据返回到前端。
从API子域中获取限制资源(跨域问题)
在下面JSON web token实例中,我们将采用不同的token验证方法。不同于使用jwt-auth
中间件,我们将手动处理异常。当我们向一个API 服务器( server),如 api.jwt.dev/v1/restricted
发出POST
请求时,我们正在进行跨域请求,并且必须在后端启用CORS。幸运的是,我们已经在config/cors.php
文件中配置了CORS 。
Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () {
Route::get('/restricted', function () {
try {
JWTAuth::parseToken()->toUser();
} catch (Exception $e) {
return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED);
}
return ['data' => 'This has come from a dedicated API subdomain with restricted access.'];
});
});
AngularJS前端示例
我们使用AngularJS作为前端,依赖Laravel后端身份验证服务器的API调用进行用户身份验证和样本数据以及用于提供跨域示例数据的API服务器。一旦我们进入我们项目的主页,后端将提供resources/views/spa.blade.php
视图用来引导Angular应用程序。
这是Angular应用程序的文件夹结构:
代码语言:javascript复制public/
|-- css/
`-- bootstrap.superhero.min.css
|-- lib/
|-- loading-bar.css
|-- loading-bar.js
`-- ngStorage.js
|-- partials/
|-- home.html
|-- restricted.html
|-- signin.html
`-- signup.html
`-- scripts/
|-- app.js
|-- controllers.js
`-- services.js
引导Angular应用程序
spa.blade.php
包含运行应用程序所需的基本要素。我们将使用Twitter Bootstrap进行样式化,以及Bootswatch的自定义主题。在进行AJAX调用时,要获得一些视觉反馈,我们将使用angular-loading-bar script来拦截XHR请求并创建一个加载栏。 在<head>中,我们需要添加如下样式文件(即,开头要引入的css文件):
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/bootstrap.superhero.min.css">
<link rel="stylesheet" href="/lib/loading-bar.css">
我们标记的footer 包含对库的引用,以及Angular模块,控制器和服务的自定义脚本。(即,在最后的<.body> 之前引入js文件):
代码语言:javascript复制<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script>
<script src="/lib/ngStorage.js"></script>
<script src="/lib/loading-bar.js"></script>
<script src="/scripts/app.js"></script>
<script src="/scripts/controllers.js"></script>
<script src="/scripts/services.js"></script>
</body>
我们使用AngularJS的 ngStorage
库,将token保存到浏览器的本地存储中,以便我们可以通过Authorization
头(header) 在每个请求上发送它。
在生产环境中,当然,我们会缩小并组合所有的脚本文件(js文件)和样式表(css文件),以提高性能。
我已经使用Bootstrap创建了一个导航栏,它将根据用户的登录状态更改相应链接的可见性。登录状态由控制器作用域中的token变量决定。
代码语言:javascript复制<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">JWT Angular example</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li>
<li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li>
<li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li>
<li data-ng-show="token"><a ng-click="logout()">Logout</a></li>
</ul>
</div>
路由
我们有一个名为app.js
的文件负责配置我们所有的前端路由。
angular.module('app', [
'ngStorage',
'ngRoute',
'angular-loading-bar'
])
.constant('urls', {
BASE: 'http://jwt.dev:8000',
BASE_API: 'http://api.jwt.dev:8000/v1'
})
.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {
$routeProvider.
when('/', {
templateUrl: 'partials/home.html',
controller: 'HomeController'
}).
when('/signin', {
templateUrl: 'partials/signin.html',
controller: 'HomeController'
}).
when('/signup', {
templateUrl: 'partials/signup.html',
controller: 'HomeController'
}).
when('/restricted', {
templateUrl: 'partials/restricted.html',
controller: 'RestrictedController'
}).
otherwise({
redirectTo: '/'
});
我们可以看到我们已经定义了4个由 HomeController
或 RestrictedController
处理的路由。每个路线都对应于部分HTML视图。我们还定义了两个常量,其中包含我们对后端的HTTP请求的URL。
请求拦截器
AngularJS的$ http服务允许我们与后端通信并发出HTTP请求。在我们的例子中,Authorization
如果用户被认证,我们要拦截每个HTTP请求并注入一个包含我们的JWT 的头。我们也可以使用拦截器来创建一个全局的HTTP错误处理程序。这是我们的拦截器的一个例子,它们在浏览器的本地存储中可用时注入一个token。
$httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.token) {
config.headers.Authorization = 'Bearer ' $localStorage.token;
}
return config;
},
'responseError': function (response) {
if (response.status === 401 || response.status === 403) {
$location.path('/signin');
}
return $q.reject(response);
}
};
}]);
控制器
在controllers.js
文件中,我们定义了两个控制器,为我们的应用程序:HomeController
和RestrictedController
。
HomeController
处理登录,注册和注销功能。它将用户名和密码数据从登录表单和注册表单传递Auth
到向后端发送HTTP请求的服务。然后将token保存到本地存储,或者显示错误消息,具体取决于后端的响应。
angular.module('app')
.controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth',
function ($rootScope, $scope, $location, $localStorage, Auth) {
function successAuth(res) {
$localStorage.token = res.token;
window.location = "/";
}
$scope.signin = function () {
var formData = {
email: $scope.email,
password: $scope.password
};
Auth.signin(formData, successAuth, function () {
$rootScope.error = 'Invalid credentials.';
})
};
$scope.signup = function () {
var formData = {
email: $scope.email,
password: $scope.password
};
Auth.signup(formData, successAuth, function () {
$rootScope.error = 'Failed to signup';
})
};
$scope.logout = function () {
Auth.logout(function () {
window.location = "/"
});
};
$scope.token = $localStorage.token;
$scope.tokenClaims = Auth.getTokenClaims();
}])
RestrictedController
表现方式相同,只是它通过使用服务getRestrictedData
和getApiData
函数来获取数据Data
。
.controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) {
Data.getRestrictedData(function (res) {
$scope.data = res.data;
}, function () {
$rootScope.error = 'Failed to fetch restricted content.';
});
Data.getApiData(function (res) {
$scope.api = res.data;
}, function () {
$rootScope.error = 'Failed to fetch restricted API content.';
});
}]);
仅当用户进行身份验证成功后,后端才负责提供受限制的数据。这意味着为了响应受限数据,对该数据的请求需要在其Authorization
头(header)或查询字符串(query string)内包含一个有效的JWT 。如果不是这样,服务器将使用401未经授权的错误状态代码进行响应。
认证服务
Auth服务负责登录并向后端注册HTTP请求。如果请求成功,则响应包含签名token,然后将其解码,并将附带的token声明(claims )信息保存到tokenClaims
变量中。这通过getTokenClaims
功能传递给控制器。
angular.module('app')
.factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) {
function urlBase64Decode(str) {
var output = str.replace('-', ' ').replace('_', '/');
switch (output.length % 4) {
case 0:
break;
case 2:
output = '==';
break;
case 3:
output = '=';
break;
default:
throw 'Illegal base64url string!';
}
return window.atob(output);
}
function getClaimsFromToken() {
var token = $localStorage.token;
var user = {};
if (typeof token !== 'undefined') {
var encoded = token.split('.')[1];
user = JSON.parse(urlBase64Decode(encoded));
}
return user;
}
var tokenClaims = getClaimsFromToken();
return {
signup: function (data, success, error) {
$http.post(urls.BASE '/signup', data).success(success).error(error)
},
signin: function (data, success, error) {
$http.post(urls.BASE '/signin', data).success(success).error(error)
},
logout: function (success) {
tokenClaims = {};
delete $localStorage.token;
success();
},
getTokenClaims: function () {
return tokenClaims;
}
};
}
]);
数据服务
这是一个简单的服务,它向认证服务器以及API服务器发出一些虚拟受限数据的请求。它发出请求,并将成功和错误回调委托给控制器。
代码语言:javascript复制angular.module('app')
.factory('Data', ['$http', 'urls', function ($http, urls) {
return {
getRestrictedData: function (success, error) {
$http.get(urls.BASE '/restricted').success(success).error(error)
},
getApiData: function (success, error) {
$http.get(urls.BASE_API '/restricted').success(success).error(error)
}
};
}
]);
结论
基于token的身份验证使我们能够构建不绑定到特定认证方案的解耦系统。令牌可能在任何地方生成,并在使用相同密钥(secret key)签署token的任何系统上使用。他们已准备就绪,并不要求我们使用Cookie。
JSON Web Token可以在所有流行的编程语言中工作,并且迅速普及。它们由Google,Microsoft和Zendesk等公司支持。互联网工程任务组(IETF)的标准规范仍在草案版本中,未来可能略有变动。
还有很多关于JWT的内容,例如如何处理安全细节,以及在token过期时刷新令牌,但上述示例应演示使用JSON Web Token的基本用法,更重要的是显示优势。
参考资料
Introduction to JSON Web Tokens
JWT 简介
JSON Web Token - 在Web应用间安全地传递信息
待延伸
OAuth 2.0