前言
这篇本应是几周前发的,无奈中间遭遇了搬家阿里云以及备案,只好拖到今天。这是一篇介绍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 , 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中。
例:
| { “iss”: “toptal.com”, “exp”: 1426420800, “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
文件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | --- 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
文件配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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="/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变量决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <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
的文件负责配置我们所有的前端路由。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { }) .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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | $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保存到本地存储,或者显示错误消息,具体取决于后端的响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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
功能传递给控制器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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
来自 https://windcoder.com/json-web-tokenjwtjiaochengyigejiyularavelheangularjsdelizi