【实践】手把手带你实现JWT登录鉴权
前言
何为JWT呢?
JWT的全称是JSON Web Token,他是一种基于JSON的用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 载荷(payload):也就是消息体和签名(signature);他是一种用于身份提供者和服务提供者双方之间传递安全信息简洁的、URL安全的表述性声明规范。是一个为分布式应用环境间传递身份信息而执行的一种基于JSON的开放标准(RFC 7519),他定义了一种简洁的,自包含的方法用于通信双方之间以json对象的形式安全地传递信息。因为有数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或RSA的公私秘钥对其进行签名。
JWT的原则是在服务器身份验证之后返回给用户的JSON对象如下所示:
{
"UserName": "micai",
"Role": "Admin",
"Expire": "2022-08-30 22:15:56"
}
RFC 7519 对 JWT 做的较为正式的定义如下:
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)
JWT的特点
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
JWT有何用处,解决了什么问题?
了解jwt的作用前,必须得了解jwt出现前,使用的session认证证模式;众所周知,http协议本身是一种无状态的协议,当用户向我们的服务器提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再次进行用户认证才行,因为根据http协议,服务器并不能知道是哪个用户发出的请求,所以为了让我们的服务器中的应用能识别是哪个用户发过来的请求,我们只能在服务器中存储一份用户的登录信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时把客户端保存的cookie发送给我们的服务器,这样我们的服务器就能够识别这个请求到底是来自哪个用户的,这就是传统的基于session认证。session具体的请求过程如下:
1.用户向服务器发送用户名和密码,服务器进行验证
2.验证通过后,相关数据(如用户角色,登录时间等)将保存在当前会话中
3.服务器会向用户返回session_id,客户端(浏览器)会把session信息写入本地的Cookie
4.用户的后续的每次请求都会从Cookie中取出session_id传给服务器
5.服务器收到session_id并对比之前保存的数据,确认用户的身份
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,服务器的开销增加,独立的服务器已无法承载更多的用户,这时候基于session认证的问题就会暴露出来,而且当应用扩展到分布式架构时需要解决session共享的问题,又因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击.
这时候就该JWT上场拯救了,JWT是目前最流行的跨域身份验证解决方案,基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,一旦用户完成了登陆,在接下来的每个请求中都会包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限的验证.由于它的开销非常小,可以轻松的在不同域名的系统中传递,这也为应用的扩展提供了便利.目前在单点登录(SSO)中应用比较广泛.
大致流程上是这样的:
用户使用用户名密码来请求服务器
服务器进行验证用户的信息
服务器通过验证发送给用户一个token
客户端存储token,并在每次请求时附送上这个token
服务端验证token,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *
。
JWT的构成
jwt大概长下面这样:这里使用的是HS256进行加密.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iui_t-W9qSIsImlhdCI6MTY2MTg3MTk3OX0.fa9akf1yhTjH_ovdW5Q0kuY8yDmaItpBGKABKKWFp5Q
红色部分为JWT头,紫色部分为有效载荷,蓝色部分为签名
下面截图中的结果是使用https://jwt.io进行解析的:
JWT头(header)
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
有效载荷(payload)
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
{
"sub": "1234567890",
"name": "迷彩",
"iat": 1661871979
}
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"sub": "1234567890",
"name": "迷彩",
"status": true
}
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON对象也使用Base64 URL算法转换为字符串保存
签名哈希(signature)
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret)
如下图:
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
Base64URL算法
作为令牌的JWT可以放在URL中(例如api.micai/?token=xxxxxx). Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法
JWT认证流程
综上所述.jwt鉴权验证的流程可总结为以下几点:
在JWT头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
在载荷中声明用户信息,同时还有一些其他的内容.再次使用json 把载荷部分进行转化,转化为字符串
使用在header中声明的加密算法和每个项目随机生成的secret来进行加密.把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串.这个字符串是独一无二的
解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密
注:解密的时候没有使用数据库,仅仅使用的是secret进行解密,所以JWT的secret一定要保管好!
JWT登录鉴权的实现
这里会使用到一个jwt相关的库:lcobucci/jwt,该库的地址为:https://github.com/lcobucci/jwt
lcobucci/jwt是一个与框架无关的 PHP 库,允许您基于RFC 7519发布、解析和验证 JSON Web 令牌
如果使用composer require lcobucci/jwt
安装最新版本有如下问题可降低版本安装
我这里安装的是3.0的版本
composer require lcobucci/jwt 3.*
安装完成之后你在后台就可以看到composer.json多了lcobucci
vendor文件下也会多了lcobucci
的文件夹
这里使用了composer进行包管理,好处就是不管我们需要加载多少第三库,我们只需要使用require 'vendor/autoload.php';
进行加载即可,实际使用到的库composer会帮我们自动加载
我们接下来看看具体实现:
如果你在使用lcobucci时出现以下告警提示,那是因为你使用的版本中已经启用了相关的用法,你可以使用新的方式,或者将lcobucci的版本降低到3.3或以下
本文使用的是3.4.6版本.
代码实现
相关类的使用说明:
Lcobucci\JWT\Validation\Constraint\IdentifiedBy: 验证jwt id是否匹配
Lcobucci\JWT\Validation\Constraint\IssuedBy: 验证签发人参数是否匹配
Lcobucci\JWT\Validation\Constraint\PermittedFor: 验证受众人参数是否匹配
Lcobucci\JWT\Validation\Constraint\RelatedTo: 验证自定义cliam参数是否匹配
Lcobucci\JWT\Validation\Constraint\SignedWith: 验证令牌是否已使用预期的签名者和密钥签名
Lcobucci\JWT\Validation\Constraint\StrictValidAt: ::验证存在及其有效性的权利要求中的iat,nbf和exp(支持余地配置
Lcobucci\JWT\Validation\Constraint\LooseValidAt: 验证的权利要求iat,nbf和exp,当存在时(支持余地配置)
在项目根目录新建testjwt.php文件,引入的相关的类
<?php
namespace Wmtest;
error_reporting(0);//屏蔽提示
require 'vendor/autoload.php';//引入自动加载
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\Plain;
use Lcobucci\JWT\Validation\Constraint\IdentifiedBy;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\PermittedFor;
use Lcobucci\JWT\ValidationData;
创建Token类,并实现token的生成和验证功能
<?php
class Token
{
protected $issuer = "http://micai.com";
protected $audience = "http://micai.io";
protected $id = "5t6y9400453";
// jwt_sercetkey一定要保管好不要泄露,这里使用随机密码生成器搞一个https://suijimimashengcheng.bmcx.com/
protected static $jwt_sercetkey = "1hKopf4l9FqCRxmw6D1KXSN8fei8EbFsTL73Kja2pQmwv3Xv";
/**
* 签发token
*/
public function getToken()
{
$time = time();
$config = self::getConfig();
assert($config instanceof Configuration);
/*
iss 【issuer】签发人(可以是,发布者的url地址)
sub 【subject】该JWT所面向的用户,用于处理特定应用,不是常用的字段
aud 【audience】受众人(可以是客户端的url地址,用作验证是否是指定的人或者url)
exp 【expiration】 该jwt销毁的时间;unix时间戳
nbf 【not before】 该jwt的使用时间不能早于该时间;unix时间戳
iat 【issued at】 该jwt的发布时间;unix 时间戳
jti 【JWT ID】 该jwt的唯一ID编号
*/
$token = $config->builder()
->issuedBy($this->issuer) // iss 【issuer】签发人(可以是,发布者的url地址)
->permittedFor($this->audience) // aud 【audience】受众人(可以是客户端的url地址,用作验证是否是指定的人或者url)
->identifiedBy($this->id, true) // jti 【JWT ID】 该jwt的唯一ID编号
->issuedAt($time) // iat 【issued at】 该jwt的发布时间;unix 时间戳
->canOnlyBeUsedAfter($time + 1) // nbf 【not before】 该jwt的使用时间不能早于该时间;unix时间戳即生效时间
->expiresAt($time + 3600) // exp 【expiration】 该jwt销毁的时间;unix时间戳
->withClaim('uid', 999) // 用户id
->withClaim('username', "Micai") // 用户名
->getToken($config->signer(), $config->signingKey()); // 生成签名
return $token->toString(); //返回签名字符串
}
/**
* 验证 jwt token 并返回其中的用户id
*/
public function verifyToken_other($token)
{
try {
$config = self::getConfig(); //获取配置
assert($config instanceof Configuration);
$token = $config->parser()->parse($token);//解析
assert($token instanceof Plain);
//验证jwt id是否匹配
$validate_jwt_id = new IdentifiedBy($this->id);
//验证签发人url是否正确
$validate_issued = new IssuedBy($this->issuer);
//验证客户端url是否匹配
$validate_aud = new PermittedFor($this->audience);
$config->setValidationConstraints($validate_jwt_id, $validate_issued, $validate_aud);
$constraints = $config->validationConstraints();
if (!$config->validator()->validate($token, ...$constraints))
{
die("无效token!");
}
}
catch (\Exception $e)
{
die("错误:" . $e->getMessage());
}
$jwtContent = $token->claims(); // 这是jwt token中存储的所有信息
print_r($jwtContent);
return json_encode(array('uid'=>$jwtContent->get("uid"),'username'=>$jwtContent->get("username")),JSON_UNESCAPED_UNICODE); // 获取uid和username
}
/**
* 加密解密使用的配置
* @return Configuration
*/
public static function getConfig()
{
$configuration = Configuration::forSymmetricSigner(
new Sha256(), //除了256你还可以使用384,512的加密算法
InMemory::base64Encoded(self::$jwt_sercetkey) //这里可重写
);
return $configuration;
}
/**
* 另一种验证方法,但是已经弃用
* verify token
*/
public function verifyToken($token)
{
$token = (new Parser())->parse((string)$token);
//验证token
$data = new ValidationData();
$data->setIssuer($this->issuer);//验证的签发人
$data->setAudience($this->audience);//验证的接收人
$data->setId($this->id);//验证token标识
if (!$token->validate($data)) {
//token验证失败
die("无效token!");
}
$jwtContent = $token->claims(); // 这是jwt token中存储的所有信息
print_r($jwtContent);
return json_encode(array('uid'=>$jwtContent->get("uid"),'username'=>$jwtContent->get("username")),JSON_UNESCAPED_UNICODE); // 获取uid和username
}
}
验证Token类
<?php
namespace Wmtest;
require 'testjwt.php';
$new = new Token();
$token = $new->getToken();
echo '获取的token如下:<br>';
print_r($token);
?>
执行结果如下:
获取的token如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjV0Nnk5NDAwNDUzIn0.eyJpc3MiOiJodHRwOlwvXC9taWNhaS5jb20iLCJhdWQiOiJodHRwOlwvXC9taWNhaS5pbyIsImp0aSI6IjV0Nnk5NDAwNDUzIiwiaWF0IjoxNjYxODgxNjkxLCJuYmYiOjE2NjE4ODE2OTIsImV4cCI6MTY2MTg4NTI5MSwidWlkIjo5OTksInVzZXJuYW1lIjoiTWljYWkifQ.qUupkPdNKlK3n7bQIYjpKJfSbro0gW6RxPHWEf9U7EI
使用上面生成的token,获取用户信息
<?php
namespace Wmtest;
require 'testjwt.php';
$token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjV0Nnk5NDAwNDUzIn0.eyJpc3MiOiJodHRwOlwvXC9taWNhaS5jb20iLCJhdWQiOiJodHRwOlwvXC9taWNhaS5pbyIsImp0aSI6IjV0Nnk5NDAwNDUzIiwiaWF0IjoxNjYxODgxNjkxLCJuYmYiOjE2NjE4ODE2OTIsImV4cCI6MTY2MTg4NTI5MSwidWlkIjo5OTksInVzZXJuYW1lIjoiTWljYWkifQ.qUupkPdNKlK3n7bQIYjpKJfSbro0gW6RxPHWEf9U7EI';
$new = new Token();
echo '验证token结果如下:<br>';
$arr = $new->verifyToken_other($token);
print_r("<br>获取用户id和用户名:".$arr);
执行结果如下:
获取用户id和用户名:{"uid":999,"username":"Micai"}
- 点赞
- 收藏
- 关注作者
评论(0)