为避免在使用JWT的时候,Token过期后,会自动退出系统回到登录页面,最好是采用双Token的机制;具体过程简单描述一下:
- 用户登录,系统返回两个令牌,AccessToken和RefreshToken,AccessToken是资源访问令牌,有效期较短;RefreshToken是刷新令牌,有效期较长。
- 用户通过自动在Header传递AccessToken。申请资源访问,直到AccessToken过期。
- AccessToken过期后,前端自动使用RefreshToken向服务器申请新的AccessToken
- 客户端使用新的AccessToken请求资源,直到RefreshToken失效
Jwt 4.0以上版本的封装网上的参考比较少,在这里,提供一份简单的封装,至于双令牌的具体实现,后面再陆续分享。
前提条件:PHP 7.4版本及以上,lcobucci/jwt 4.1.5
封装类文件:utils/JwtTools.php
issuedBy = config('system.jwt_issued_by') : null;
config('system.jwt_permitted_for') ? $this->permittedFor = config('system.jwt_permitted_for') : null;
config('system.jwt_secrect') ? $this->secrect = config('system.jwt_secrect') : null;
$this->issuedAt = new \DateTimeImmutable();
$this->expiresAtAccess = $this->issuedAt->modify(config('system.jwt_expires_at_access') ? config('system.jwt_expires_at_access') : '+1 minute');
$this->expiresAtRefresh = $this->issuedAt->modify(config('system.jwt_expires_at_refresh') ? config('system.jwt_expires_at_refresh') : '+5 minute');
}
/**
* 生成Jwt配置对象
* @return Configuration
*/
private function createJwt(){
return Configuration::forSymmetricSigner(new Sha256(),InMemory::base64Encoded($this->secrect));
}
/**
* 生成Token
* @param array $bind 必须存在字段 uid
* @param string $type
* @return string
*/
public function getToken(array $bind=[], $type = 'Access'){
$config = $this->createJwt();
$builder = $config->builder();
// 访问Token可以携带用户信息,刷新Token只携带用户编号
if(is_array($bind) && !empty($bind)){
foreach ($bind as $k => $v){
$builder->withClaim($k,$v);
}
$builder->withClaim('scopes',$type == 'Access' ? 'Access' : 'Refresh');
}
$token = $builder
->issuedBy($this->issuedBy)
->permittedFor($this->permittedFor)
->issuedAt($this->issuedAt)
->canOnlyBeUsedAfter($this->issuedAt->modify('+1 second'))
->expiresAt($type == 'Access' ? $this->expiresAtAccess : $this->expiresAtRefresh)
->getToken($config->signer(),$config->signingKey());
return $token->toString();
}
/**
* 校验Token
* @param $token
* @return bool
*/
public function verify($token){
$config = $this->createJwt();
try {
$token = $config->parser()->parse($token);
assert($token instanceof UnencryptedToken);
} catch (\Exception $e){
\think\facade\Log::error('令牌解析失败[1]:'.$e->getMessage());
return ['status'=>1,'msg'=>'令牌解析错误'];
}
// 验证签发端是否匹配
$validate_issued = new IssuedBy($this->issuedBy);
$config->setValidationConstraints($validate_issued);
$constraints = $config->validationConstraints();
try {
$config->validator()->assert($token,...$constraints);
} catch (RequiredConstraintsViolated $e){
\think\facade\Log::error('令牌验证失败[2]:' . $e->getMessage());
return ['status'=>2,'msg'=>'签发错误'];
}
//验证客户端是否匹配
$validate_permitted_for = new PermittedFor($this->permittedFor);
$config->setValidationConstraints($validate_permitted_for);
$constraints = $config->validationConstraints();
try {
$config->validator()->assert($token,...$constraints);
} catch (RequiredConstraintsViolated $e){
\think\facade\Log::error('令牌验证失败[3]:' . $e->getMessage());
return ['status'=>3,'msg'=>'客户端错误'];
}
// 验证是否过期
$timezone = new \DateTimeZone('Asia/Shanghai');
$time = new SystemClock($timezone);
$validate_exp = new StrictValidAt($time);
$config->setValidationConstraints($validate_exp);
$constraints = $config->validationConstraints();
try {
$config->validator()->assert($token,...$constraints);
} catch (RequiredConstraintsViolated $e){
\think\facade\Log::error('令牌验证失败[4]:' . $e->getMessage());
return ['status'=>4,'msg'=>'已过期'];
}
// 验证令牌是否已使用预期的签名者和密钥签名
$validate_signed = new SignedWith(new Sha256(),InMemory::base64Encoded($this->secrect));
$config->setValidationConstraints($validate_signed);
$constraints = $config->validationConstraints();
try {
$config->validator()->assert($token,...$constraints);
} catch (RequiredConstraintsViolated $e){
\think\facade\Log::error('令牌验证失败[5]:' . $e->getMessage());
return ['status'=>5,'msg'=>'签名错误'];
}
return ['status'=>0,'msg'=>'验证通过'];
}
/**
* 获取token的载体内容
* @param $token
* @return mixed
*/
public function getTokenContent($token){
$config = $this->createJwt();
try {
$decode_token = $config->parser()->parse($token);
$claims = json_decode(base64_decode($decode_token->claims()->toString()),true);
} catch (\Exception $e){
throw new ValidateException($e->getMessage());
}
return $claims;
}
}
配套配置文件:config/system.php
'Rapid_Development_System',
// 是否开启验证码
'verify_status' => false,
// JWT配置
'jwt_issued_by' => 'rds.server',
'jwt_permitted_for' => 'rds.client',
'jwt_secrect' => 'aHR0cDovL3Jkcy5yYWlzZWluZm8uY24=',
'jwt_expires_at_access' => '+5 minute',
'jwt_expires_at_refresh' => '+30 minute',
];
测试类文件:app/admin/controller/JwtTest.php
request->param('type','Access');
$jwtTools = new JwtTools();
$token = $jwtTools->getToken(['uid'=>1],$type);
return json(['status'=>200,'data'=>$token]);
}
/**
* 提取Token内容
* @return \think\response\Json
*/
public function getContent(){
$token = $this->request->header('AccessToken');
if($token){
$jwtTools = new JwtTools();
$content = $jwtTools->getTokenContent($token);
} else {
$content = '无有效令牌';
}
return json(['status'=>200,'data'=>$content]);
}
/**
* 验证令牌
* @return \think\response\Json
*/
public function verifyToken(){
$token = $this->request->header('AccessToken');
if($token){
$jwtTools = new JwtTools();
$content = $jwtTools->verify($token);
} else {
$content = '无有效令牌';
}
return json(['status'=>200,'data'=>$content['msg']]);
}
/**
* 登录后生成访问令牌和刷新令牌
* @return \think\response\Json
*/
public function getTokens(){
$jwtTools = new JwtTools();
$payload = [
'uid' => [
'user_id' => 100,
'username' => 'Tome',
'sex' => 2,
]
];
$accessToken = $jwtTools->getToken($payload,'Access');
$refreshToken = $jwtTools->getToken(['uid'=>100],'Refresh');
$tokens = [
'Access' => $accessToken,
'Refresh'=> $refreshToken
];
return json(['status'=>200,'data'=>$tokens]);
}
/**
* 通过刷新令牌,申请新的访问令牌
* @return \think\response\Json
*/
public function refreshToken(){
$token = $this->request->header('RefreshToken');
$jwtTools = new JwtTools();
if($jwtTools->verify($token)){
$content = $jwtTools->getTokenContent($token);
$accessToken = $jwtTools->getToken(['uid'=>$content['uid']],'Access');
$tokens = [
'Access' => $accessToken,
'Refresh'=> $token
];
return json(['status'=>200,'data'=>$tokens]);
} else {
return json(['status'=>411,'data'=>'刷新令牌无效']);
}
}
}
通过POSTMAN软件,调用测试接口即可!