JWT在web用户身份验证中的应用与实现
如果你喜欢计算机安全的话题,你就会知道最具有讨论和争议的话题之一是用户身份验证。
在这里,您将从新的机制到可用性,找到一个广泛的研究领域。它虽然具有如此大的魅力,但是,令我惊奇的是,JSON Web Token不是一个常谈到的主题,我想它今天该展露光芒了。
我们将看到它是多么容易的被集成在一个API认证机制中。
和会话的对比
曾经有一段时间,验证自己应用程序的唯一方法是通过给定凭证。后来以纯文本的形式提供api并发送凭证是令人难以接受的。关于API Tokens的想法萌生已久,如今,它们已经是常见的做法。
向应用程序,和维护用户的状态与应用程序会话的cookie给定凭证的一些缺点是:
数据是以纯文本的形式存储在服务器上的。尽管数据通常不是存储在一个公共文件夹,任何访问的用户都可以读取会话文件中的内容。
文件系统读/写请求。每次开始一个会话或其数据被修改,服务器需要更新会话。这同样适用于每次应用程序发送一个会话cookie。如果此时有大量的用户访问,你将会与缓慢的服务器断开连接,除非你使用会话存储替代。
分布式/集群应用程序,因为会话文件在默认情况下存储在文件系统,对于需要使用负载平衡器、集群服务器等…的高可用性的应用程序,很难有一个分布式或集群基础设施实现。在这种情况下,不得不探索其他的媒体存储和特殊配置。
当处理限制服务调用的服务api,您将需要在你的每个请求中(在请求头,如 Authorization或在URL查询字符串)添加密钥。API密钥通常依赖于一个集中的机制来控制他们。如果你想使一个API密钥无效,必须在应用层面操作它。
JWT
2010年10月以来,已经有几个建议使用基于JSON的令牌诞生。并提出了在2010年12月JWT或JSON Web标记,有以下特点:
用于空间受限的环境中,如HTTP授权头或查询字符串参数。
在Javascript对象表示法数据传输格式(JSON)
一个JSON数据必须有效负载的Web签名(jw)
使用Base64 URL编码表示
JSON网络签名是被设计来加密数据,带有数字签名、唯一的令牌的内容,这样我们就能够确定我们的数据令牌是否被篡改。
JWTs的使用在单个API密匙有很多好处:
API密钥只是随机字符串,jwt包含信息和元数据,可以描述用户身份,授权数据和令牌在一个时间框架的有效性或域。
jwt不需要一个集中的发行或撤销的权威。
兼容OAUTH2。
JWT可以检查数据。
jwt对已经过期的控制。
2015年5月19日,JWT成为IETF出版RFC 7519.
它看起来像什么?
JWT样子如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E
看起来字符串是随机字符连接在一起,和API密匙并没有什么区别。但是,如果你仔细看,实际上有3个字符串,由一个点分开的性格。
第一个和第二个字符串Base64URL编码的JSON字符串,如果我们解码这些,我们会有以下结果:
{
"alg": "HS256",
"typ": "JWT"
}
{
"iat": 1416929109,
"jti": "aa7f8d0a95c",
"scopes": [
"repo",
"public_repo"
]
}
第一个字符串是jw头,即哪种类型、哪种密码算法用于生成签名和有效载荷类型。第二个字符串是有效载荷,传递了一些标准字段,以及任何你希望发送在令牌的数据。第三个字符串是加密签名,它将会被解码成二进制数据。
有趣的地方在签名即密码算法需要一个密钥,一个只有发行人应用程序必须知道和不应以任何方式披露的字符串。
当应用程序接收到令牌时,它可以根据已经使用密钥加密过的token来验证签名。如果签名验证失败,我们可以肯定,令牌中的数据被篡改,应该丢弃。
你可以看一看jwt.io在那里你可以体验编码和解码jwt。
让我们开始吧
那么,我们又如何把这个应用到一个PHP应用程序?
假设我们有一个登录机制,目前使用会话cookie存储信息在应用程序中用户的登录状态。请注意,JWT并非旨在替cookie。
但是,对于这个示例,我们将有两个服务:
一个基于所提供的用户名和密码生成一个JWT,和另一个通过我们提供一个有效的JWT来获取受保护的资源。
一旦我们登录,我们将能够从应用程序检索一个受保护的资源。
首先,我们安装php-jwt与 composer require firebase/php-jwt。在样例应用程序开发本教程,我也使用zend-config和zend-http,所以如果你想跟着我操作,你可以选择安装下面这些:
composer require firebase/php-jwt:dev-master
composer require zendframework/zend-config:~2.3
composer require zendframework/zend-http:~2.3
这里还有另一个PHP库,jose从namshi,如果你待会想体验一把。
现在,让我们假设登录表单通过AJAX提交数据到我们的JWT服务器,通过数据库对凭证验证,之后确定证书都是有效的,我们必须建立令牌。让我们先建立它作为一个数组:
<?php
require_once('vendor/autoload.php');
/*
* Application setup, database connection, data sanitization and user
* validation routines are here.
*/
$config = Factory::fromFile('config/config.php', true); // Create a Zend Config Object
if ($credentialsAreValid) {
$tokenId = base64_encode(mcrypt_create_iv(32));
$issuedAt = time();
$notBefore = $issuedAt + 10; //Adding 10 seconds
$expire = $notBefore + 60; // Adding 60 seconds
$serverName = $config->get('serverName'); // Retrieve the server name from config file
/*
* Create the token as an array
*/
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'jti' => $tokenId, // Json Token Id: an unique identifier for the token
'iss' => $serverName, // Issuer
'nbf' => $notBefore, // Not before
'exp' => $expire, // Expire
'data' => [ // Data related to the signer user
'userId' => $rs['id'], // userid from the users table
'userName' => $username, // User name
]
];
/*
* More code here...
*/
}
请注意,您可以定义任何你先搞的数据结构,但是有一些字段必须需要保留,如上面使用的:
iat——令牌发放的时间戳。
jti——一个独一无二的字符串,可以用来验证一个令牌,但是违背没有一个集中的发行者权威。
iss——一个字符串包含的名称或标识符发行人的应用程序。可以是一个域名,可以用来丢弃令牌从其他应用程序。
nbf——时间戳标记时应该被认为是有效的。应该等于或大于 iat。在这种情况下,令牌将开始有效10秒
后发布。exp——时的时间戳标记应该不再是有效的。应该大于 iat和 nbf。在这种情况下,令牌将到期后60秒。
那些声称不需要,但将帮助您确定一个令牌的有效性(后面详细讨论)。我们的应用程序内的有效载荷 data要求,我们存储的地方 userId和 userName值。自从JWT可以检查客户端,请记住不要包含任何敏感信息。
将这个数组转化成JWT是超级简单的:
<?php
/*
* Code here...
*/
/*
* Extract the key, which is coming from the config file.
*
* Best suggestion is the key to be a binary string and
* store it in encoded in a config file.
*
* Can be generated with base64_encode(openssl_random_pseudo_bytes(64));
*
* keep it secure! You'll need the exact key to verify the
* token later.
*/
$secretKey = base64_decode($config->get('jwtKey'));
/*
* Encode the array to a JWT string.
* Second parameter is the key to encode the token.
*
* The output string can be validated at http://jwt.io/
*/
$jwt = JWT::encode(
$data, //Data to be encoded in the JWT
$secretKey, // The signing key
'HS512' // Algorithm used to sign the token, see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3
);
$unencodedArray = ['jwt' => $jwt];
echo json_encode($unencodedArray);
JWT::encode()会完成一切工作(将数组转换为JSON,生产头,签署有效负载和编码的最终字符串)。你最好让你的密钥,二进制串,编码保存在一个配置文件并不被披露。让它直接写在代码中不是一个好主意。
既然客户已经令牌,你可以将它存储使用JS或任何你喜欢的机制。下面是一个示例使用jQuery:
$(function(){
var store = store || {};
/*
* Sets the jwt to the store object
*/
store.setJWT = function(data){
this.JWT = data;
}
/*
* Submit the login form via ajax
*/
$("#frmLogin").submit(function(e){
e.preventDefault();
$.post('auth/token', $("#frmLogin").serialize(), function(data){
store.setJWT(data.JWT);
}).fail(function(){
alert('error');
});
});
});
现在让我们用JWT保护机制,检索资源。
当点击“Get resource> >”按钮,如果一切正常,您应该看到一个图像的灰色区域。
让我们用一个ajax调用发送请求的资源服务:
$("#btnGetResource").click(function(e){
e.preventDefault();
$.ajax({
url: 'resource/image',
beforeSend: function(request){
request.setRequestHeader('Authorization', 'Bearer ' + store.JWT);
},
type: 'GET',
success: function(data) {
// Decode and show the returned data nicely.
},
error: function() {
alert('error');
}
});
});
请注意到 beforeSend 选择。我们告诉jQuery,每个请求都是通过这个调用之前,我们需要把Bearer [JWT]格式的JWT设置到 Authorization请求头中 。当我们点击按钮,将产生以下请求:
GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0
现在我们可以看到那些受保护的资源:
这就是我们如何验证令牌的资源服务的过程。
<?php
chdir(dirname(__DIR__));
require_once('vendor/autoload.php');
use Zend\Config\Config;
use Zend\Config\Factory;
use Zend\Http\PhpEnvironment\Request;
/*
* Get all headers from the HTTP request
*/
$request = new Request();
if ($request->isGet()) {
$authHeader = $request->getHeader('authorization');
/*
* Look for the 'authorization' header
*/
if ($authHeader) {
/*
* Extract the jwt from the Bearer
*/
list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s');
if ($jwt) {
try {
$config = Factory::fromFile('config/config.php', true);
/*
* decode the jwt using the key from config
*/
$secretKey = base64_decode($config->get('jwtKey'));
$token = JWT::decode($jwt, $secretKey, array('HS512'));
$asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/'));
/*
* return protected asset
*/
header('Content-type: application/json');
echo json_encode([
'img' => $asset
]);
} catch (Exception $e) {
/*
* the token was not able to be decoded.
* this is likely because the signature was not able to be verified (tampered token)
*/
header('HTTP/1.0 401 Unauthorized');
}
} else {
/*
* No token was able to be extracted from the authorization header
*/
header('HTTP/1.0 400 Bad Request');
}
} else {
/*
* The request lacks the authorization token
*/
header('HTTP/1.0 400 Bad Request');
echo 'Token not found in request';
}
} else {
header('HTTP/1.0 405 Method Not Allowed');
}
为了让事情更容易处理,我使用 Zend\Http\PhpEnvironment\Request提取HTTP请求类型和头部:
$request = new Request();
if ($request->isGet()) { //Will only process HTTP GET requests.
$authHeader = $request->getHeader('authorization');
// ...
现在让我们看看授权头是否有一个JWT字符串:
/*
* Look for the 'authorization' header
*/
if ($authHeader) {
/*
* Extract the JWT from the Bearer
*/
list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s');
// MORE CODE
}
这样的变量 $jwt会有潜在的JWT的内容。
另一个选择你可以选择如果你不想处理HTTP授权头,包括令牌请求的URL参数:
GET /resource.php?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0 HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
现在让我们尝试解码这段JWT。还记得我们之前使用的密钥生成令牌么?这是解码过程的至关重要的一部分:
$secretKey = base64_decode($config->get('jwtKey'));
/*
* decode the JWT using the key from config
*/
$token = JWT::decode($jwt, $secretKey, array('HS512'));
如果过程中解码JWT失败,可能是:
提供的片段的数量不符合如前所述标准的3端格式。
头部或载荷不是一个有效的JSON字符串
签名是无效的,这意味着数据篡改!
nbf声称在JWT中设置一个时间戳,当小于当前时间戳。
iat声称在JWT中设置一个时间戳,当小于当前时间戳。
exp声称在JWT中设置一个时间戳,在当前的时间戳比这更多。
如您所见,JWT有一组将可以它标记为无效的手段,而不需要手动撤销或检查列表中它对应哪个有效的令牌。
如果你想知道关于JWT签名和破坏数据,由于密码消息身份验证代码,这是可能的。
简而言之,任意数据输入以及关键数据的编制了一个独特的“指纹”。这个指纹不能逆转回数据输入和数据输入或没有一点改变关键将产生一个完全不同的指纹。
在这一点上我们可以肯定,JWT是有效的。此外,你可以检查用户令牌是否仍然有效,如果令牌(from the iss claim)是你,或者如果你的令牌有嵌入式许可标志,然后请检查那些反对用户要求执行的行动。
最后,我们请求一个图像lorempixel.combase64编码,并返回一个json响应字符串:
$asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/'));
/*
* return protected asset
*/
header('Content-type: application/json');
echo json_encode([
'img' => $asset
]);
如果你想尝试一个示例应用程序,您可以看看我项目的回购在本文中,按照README的指示,仔细看看代码。
结论
从这里,你可以试着在你的下一个API,也许可以尝试一些其他签名算法来实现jwt,比如使用非对称密钥 RS256或在现有OAUTH2身份认证集成服务器API密匙。你所有的建设性反馈是受欢迎的,当然也包括任何问题或意见。
外文连接:https://www.sitepoint.com/php-authorization-jwt-json-web-tokens/