介绍
JWT,JSON Web Token,开放的、行业标准(RFC 7519),用于网络应用环境间安全传递声明。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所须的声明信息。
特点:
跨语言:支持主流语言
自包含:包含必要的所有信息,如用户信息和签名等
易传递:很方便通过HTTP头部传递
具体来说:
JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
JWT 不加密的情况下,不能将秘密数据写入 JWT
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
JWT的最大缺点:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或更改 token 的权限。即,一旦 JWT 签发,在到期之前就会始终有效,除非服务器部署额外的逻辑
JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
为减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
引言
web应用中请求、响应是在网络中传输的,我们通常都通过接口api方式来调用,这样请求、响应参数如果暴露在外面,很容易被黑客利用,模拟访问对服务器进行攻击,我们使用JWT对请求或响应数据进行加密处理,这样黑客就不容易破解,调用的api就不能正常通过,一定程度的保护了服务器的安全访问。
参考
JWT-token—前后端分离架构的api安全问题 | CN-SEC 中文网
使用
我们通过如下内容,从安装、到一个实例场景的实现,使用程序框架tp6来实现这个场景,来说明JWT的使用。
安装
我们使用程序框架为TP6,安装方法有2种:1.composer方式;2.自定义JWT
1.composer方式
关于composer的安装与使用,笔者不做过多讲解,搜索笔者的相关《composer...指南》即可。
我们编辑composer.json文件中加入如下依赖包引入
"firebase/php-jwt": "^6.1.0"
然后执行composer update,就将jwt依赖包引入。
2.自定义JWT
我们熟悉的JWT的程序包及相关原理后,自行封装符合JWT规范的程序即可。它的优点是可以根据需要自行扩展逻辑,而第三方依赖包一般可能封装固定,不易扩展修改,不易调试等。
如图,extend目录下的Jwt.php就是自定义的依赖包。
示例需求场景
应用中,我们用户登录认证后返回加密的用户认证信息token,后续的接口访问携带token信息,服务端可以解密token,还原用户认证信息,方便用户接口正常访问,并保证数据安全。另外,jwt可能用于其他业务场景,我们通过不同业务场景,返回不同的加密信息,传递不同的接口参数,保证整体框架的通用和灵活。
封装JWT
jwt依赖包
class Jwt
{
/**
* 头部
* @var array
*/
private $header;
/**
* 使用HMAC生成信息摘要时所使用的密钥
* @var string
*/
private $key;
/**
* 该JWT的签发者
* @var string
*/
private $iss;
/**
* 签发时间
* @var int
*/
private $iat;
/**
* 过期时间
* @var int
*/
private $exp;
/**
* @var int
* 该时间之前不接收处理该Token
*/
private $nbf;
/**
* 面向的用户
* @var string
*/
private $sub;
/**
* 该Token唯一标识
* @var string
*/
private $jti;
/**
* 自定义数据
* @var mixed
*/
private $claim;
/**
* 实例
*/
private static $instance;
private function __construct() {
$this->header = array(
'alg' => 'HS256', //生成signature的算法
'typ' => 'JWT' //类型
);
$this->key = '111sfssdfsd';//使用HMAC生成信息摘要时所使用的密钥
$this->iss = 'yunyan.org';//该JWT的签发者
$this->iat = time();//签发时间
$this->exp = time() + 86400;//过期时间
$this->nbf = time() + 0;//该时间之前不接收处理该Token
$this->sub = 'yunyan.jwt.com';//面向的用户
$this->jti = md5(uniqid('JWT') . time());//该Token唯一标识
}
/**
* 获取对象实例
* @return instance
*/
public static function getInstance() {
if(!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* @param string $key
*/
public function setKey(string $key): void
{
$this->key = $key;
}
...
jwt服务类封装
<?php
namespace app\service;
use think\facade\Config;
class JwtService {
/**
* 生成token
* @param array $data
* @param int $exp 过期时间
* @param string $scene 场景 秘钥加上场景, 对应前后端隔离的效果
* @return string
*/
public function genToken($data = [], $exp = 0, $scene = '') {
$jwt = \Jwt::getInstance();
if ($exp > 0) {
$jwt->setExp(time() + $exp);//过期时间为1天
} else {
$jwt->setExp(time() + 60 * 60 * 24);// 默认过期时间为1天
}
$jwt->setKey(Config::get('app.jwt_key') . $scene);
$jwt->setClaim($data);//存储数据
//生成token并获取
return $jwt->getToken();
}
/**
* 验证解码token
* @param $token
* @param $scene
* @return array
*/
public function decodeToken($token, $scene) {
if (empty($token)) {
return ['code'=>-1,'msg'=>'token为空'];
}
$jwt = \Jwt::getInstance();
$jwt->setKey(Config::get('app.jwt_key') . $scene);
$verifyResult = $jwt->verifyToken($token);
if (!$verifyResult) {
return ['code'=>-2,'msg'=>'token验证失败'];
} else {
$data=$jwt->getClaim();
return ['code'=>0,'msg'=>'successs','data'=>$data];
}
}
}
JwtService类封装了生成token、解析token的这2个方法,其中调用了基础依赖库jwt的算法,用来token的生成和解析,其中参数$scene为业务场景参数,这样我们可以为不同业务场景生成相应的token,用于不同的安全处理。
相关配置文件参数
config/app.php
<?php
return [
...
'jwt_key'=>'sem_0fNgssdfsdfr6@B*2YjZp8P',
'jwt_scene_apiLogin'=>'api_login'
...
控制器login.php,包含登录成功后的生成token的调用,
<?php
namespace app\api\controller;
use app\model\UserModel;
use app\service\JwtService;
use think\facade\Config;
use think\Exception;
class Login extends BaseApi{
// 授权登录接口 http://localhost:7000/api/login/authLogin
public function authLogin() {
$currtime = time();
try {
$username = input('username');
$password = input('password');
if (empty($username) || empty($password)) {
throw new Exception("用户名或者密码不能为空", parent::FAIL_CODE);
}
$user = UserModel::where('username', $username)->find();
if (empty($user)) {
throw new Exception("用户不存在", parent::FAIL_CODE);
}
// 验证密码
if ($user['password'] != password_enc($password)) {
throw new Exception("密码错误", parent::FAIL_CODE);
}
$jwt = new JwtService();
unset($user['password']);
$datas=['user'=>$user];
$token=$jwt->genToken($datas,60*60*12,Config::get('app.jwt_scene_apiLogin'));
$datas=['token'=>$token,'uid'=>$user['id']];
return json_response(parent::SUCCESS_CODE, '登录成功', $datas);
} catch (Exception $e) {
return json_response($e->getCode(), $e->getMessage(), ['line' => $e->getLine()]);
}
}
}
上述代码,控制器接口调用jwtservice类生成登录场景的token,前端调用此接口后返回的token可以缓存下来,用于其他接口的调用,token定义了一定的实效时间,过期会失效。
我们生成路由接口,本地调试:http://localhost:7000/api/login/authLogin
我们用postman测试,
接口:http://localhost:7000/api/login/authLogin
传入相应参数username、password
返回如下:
我们得到一个token参数,前端接受缓存处理即可
控制器user.php,包含token解析,得到token中包含的用户认证信息
<?php
namespace app\api\controller;
use app\Request;
use app\service\JwtService;
use think\facade\Config;
class User extends BaseApi{
public function getOne(Request $request){
$token=$request->header('authorization');//$request->param('token');
$jwt=new JwtService();
$deDatas=$jwt->decodeToken($token,Config::get('app.jwt_scene_apiLogin'));
if($deDatas['code']==0){
$userInfo= $deDatas['data'];
return json_response(parent::SUCCESS_CODE, 'success', $userInfo);
}else{
return json_response($deDatas['code'], $deDatas['msg']);
}
}
}
控制器调用接口,通过request的header头包含特定参数authorization,里面放的是token数据(前端缓存上次登录认证后的),调用jwtservice类的解析token方法,得到用户认证信息返回即可。
我们用postman测试,
接口:http://localhost:7000/api/user/getOne
传入请求头的authorization参数token
如下:
这样我们通过此接口中携带的token参数,才能正常访问接口,得到相应的数据,这样接口访问才是比较安全的。