JSON Web Token(JWT)教程:一个基于Laravel和AngularJS的例子

2018-09-20 10:35:22 浏览数 (1)

本文原文: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 , publicprivate .

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文件示例:

代码语言:javascript复制
---
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并且更新我们的依赖。

代码语言:javascript复制
composer require tymon/jwt-auth 0.5.*

添加 JWTAuthServiceProvider 到我们 app/config/app.php  的providers array中。

代码语言:javascript复制
'TymonJWTAuthProvidersJWTAuthServiceProvider'

接下来,在 app/config/app.php 文件中的 aliases 数组中,我们添加 JWTAuth facade.

代码语言:javascript复制
'JWTAuth' => 'TymonJWTAuthFacadesJWTAuth'

最后,我们将通过下面的命令发布软件包的配置: php artisan config:publish tymon/jwt-auth

JSON Web tokens 通过秘钥加密。我们可以使用php artisan jwt:generate命令生成该密钥。它将被放置在我们的config/jwt.php文件中。然而,在生产环境中,我们不想在配置文件中使用我们的密码或API密钥。相反,我们应该将它们放在服务器环境变量中,并使用该env函数在配置文件中引用它们。例如:

代码语言:javascript复制
'secret' => env('JWT_SECRET')

我们可以在Github上找到关于这个软件包和所有配置设置的更多信息。

laravel-cors

在我们composer.json 中Require the barryvdh/laravel-cors package 并更新我们的依赖。

代码语言:javascript复制
composer require barryvdh/laravel-cors 0.4.x@dev

添加CorsServiceProvider到我们的app/config/app.php的providers array中。

代码语言:javascript复制
'BarryvdhCorsCorsServiceProvider'

然后添加中间件(middleware )到我们的app/Http/Kernel.php

代码语言:javascript复制
'BarryvdhCorsMiddlewareHandleCors'

通过使用 php artisan vendor:publish 命令发布这配置到 一个本地config/cors.php 文件中。

一个cors.php文件配置示例:

代码语言:javascript复制
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响应返回。

代码语言:javascript复制
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。

代码语言:javascript复制
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用于后端进行验证。

代码语言:javascript复制
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响应。

代码语言:javascript复制
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 。

代码语言:javascript复制
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文件):

代码语言:javascript复制
<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的文件负责配置我们所有的前端路由。

代码语言:javascript复制
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。

代码语言:javascript复制
$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文件中,我们定义了两个控制器,为我们的应用程序:HomeControllerRestrictedController

HomeController处理登录,注册和注销功能。它将用户名和密码数据从登录表单和注册表单传递Auth到向后端发送HTTP请求的服务。然后将token保存到本地存储,或者显示错误消息,具体取决于后端的响应。

代码语言:javascript复制
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表现方式相同,只是它通过使用服务getRestrictedDatagetApiData函数来获取数据Data

代码语言:javascript复制
 .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功能传递给控制器。

代码语言:javascript复制
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

0 人点赞