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

本文原文: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)。

实例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Header-头部

标题包含token的元数据,最小限度地包含签名的类型和加密算法。(您可以使用JSON格式化工具来优化 JSON对象。)

例:

{
  “alg”: “HS256”,
  “typ”: “JWT”
}

该JWT头部声明编码对象是一个JSON Web令牌,并且使用HMAC SHA-256算法进行签名。

将其进行base64编码,我们就有了JWT的第一部分。

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中。

例:

{
  “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的第二部分。

eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0

Signature-签名

JWT标准遵循JSON Web签名(JWS)规范来生成最终签名的token。它通过组合编码的JWT头(header) 和编码的JWT负载(Payload ) 并使用强加密算法(如HMAC SHA-256)来生成签名。签名的密钥由服务器持有,因此它将能够验证现有的token并签署(颁发/生成)新的token。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

这给了我们JWT的最后一部分。

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 安装包:

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中。

'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'

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

'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'

最后,我们将通过下面的命令发布软件包的配置: 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中。

'Barryvdh\Cors\CorsServiceProvider'

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

'Barryvdh\Cors\Middleware\HandleCors'

通过使用 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视图

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 \Tymon\JWTAuth\Exceptions\TokenInvalidException)
  {
     return response(['Token is invalid'], 401);
  }
  if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException)
  {
     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应用程序的文件夹结构:

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文件):

<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变量决定。

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

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表现方式相同,只是它通过使用服务getRestrictedDatagetApiData函数来获取数据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服务器发出一些虚拟受限数据的请求。它发出请求,并将成功和错误回调委托给控制器。

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 条评论
登录 后参与评论

相关文章

来自专栏前端笔记

【HTTP2.0 协议】就“腾讯云 CDN 针对 HTTP 2.0全面公测”浅谈如何启用 HTTP 2.0协议?

HTTPS 是指超文本传输安全协议(Hypertext Transfer Protocol Secure),是一种在 HTTP 协议基础上进行传输加密的安全协...

5719
来自专栏瞎说开发那些事

linux下利用一次性口令实现 安全管理

2076
来自专栏QQ会员技术团队的专栏

HTTP/2探索第二篇——工具及应用

由于不同环境过于复杂,本文仅基于Mac OS和Linux来讲解工具及应用。 目录结构: HTTP/2环境搭建Step by step wireshark使用 f...

67210
来自专栏Android先生

RxJava2 实战知识梳理(1) - 后台执行耗时操作,实时通知 UI 更新

接触RxJava2已经很久了,也看了网上的很多文章,发现基本都是在对RxJava的基本思想介绍之后,再去对各个操作符进行分析,但是看了之后感觉过了不久就...

1072
来自专栏魏艾斯博客www.vpsss.net

补充记录腾讯云 DNSPod 域名 API 申请 Let’s Encrypt 泛域名 SSL 证书需要注意的几点

1944
来自专栏电光石火

tengine+tomcat+php安装

在安装tengine之前,确认centos环境中有无gcc、pcre、openssl,如果没有按以下命令进行安装 #yum install gcc #yu...

21610
来自专栏FreeBuf

linux下利用一次性口令实现安全管理

作者 黑狐 [译自vpsboard] Linux服务器一直就是以稳定、高效、安全而著称。安全是比较重要的一个环节,这关系到商业机密,更关系到企业的存亡。本文...

2487
来自专栏魏艾斯博客www.vpsss.net

补充记录腾讯云 DNSPod 域名 API 申请 Let’s Encrypt 泛域名 SSL 证书需要注意的几点

刚写完了腾讯云 DNSPod 域名 API 申请 Let’s Encrypt 泛域名 SSL 证书这篇教程,感觉中间有几点是新手需要注意的,申请 SSL 泛域名...

5564
来自专栏逸鹏说道

上传文件的陷阱

0x00 背景 现在很多网站都允许用户上传文件,但他们都没意识到让用户(或攻击者)上传文件(甚至合法文件)的陷阱。 什么是合法文件? 通常,判断文件是否合法会透...

3487
来自专栏阮一峰的网络日志

HTTPS 升级指南

上一篇文章我介绍了 HTTP/2 协议 ,它只有在 HTTPS 环境才会生效。 为了升级到 HTTP/2 协议,必须先启用 HTTPS。如果你不了解 HTTPS...

3355

扫码关注云+社区

领取腾讯云代金券