看了好多无感刷新Token的方法, 处理的都不尽如人意, 全都要折腾前端, 自己处理完不行吗
前端跟往常一样只需要存一个token
后端所有Jwt生成的真实token都存到Redis,随机字符串做key返回给前端存起来做token(以下称作虚拟token)
前端虚拟token不过期真实token过期:直接调用JWT重新签发新的真实Token存起来,前端虚拟token不变
有效期内访问可以延长有效期,有效期外重新登陆
别有事没事就跑去折腾前端
文件JWTPro.PHP
<?php
use think\cache\driver\Redis;
/**
* @author 木头人下士
* @since 2021-09-03
* 自动延期Token
* 用户登录互斥
* Class JwtPro
*/
class JwtPro {
protected $multi_login = true;//是否开启多地登录
protected $prefix = 'JWToken_';//key前缀 服务器只存在单系统可以不改,多系统必须修改,否则可能会有意料之外的后果!!!!!!
protected $pc_expired_time = 60 * 60 * 24;//电脑端过期时长
protected $mobile_expired_time = 60 * 60 * 24 * 7;//手机端过期时长
/**
* 获取手机端token
* @param $uid
* @return false|string
* @since 2021-09-03
* @author 木头人下士
*/
public function getMobileToken($uid){
return $this->getToken($uid,$this->mobile_expired_time);
}
/**
* 获取电脑端token
* @param $uid
* @return false|string
* @since 2021-09-03
* @author 木头人下士
*/
public function getPcToken($uid){
return $this->getToken($uid,$this->pc_expired_time);
}
/**
* 验证手机端token
* @since 2021-09-03
* @author 木头人下士
*/
public function checkMobileToken($token){
return $this->checkToken($token,$this->mobile_expired_time);
}
/**
* 验证pc端token
* @since 2021-09-03
* @author 木头人下士
*/
public function checkPcToken($token){
return $this->checkToken($token,$this->pc_expired_time);
}
/**
* 获取JWToken
* @author 木头人下士
* @since 2021-09-03
* @param $uid string|integer 需要加密的字段
* @param $time integer 过期时间 默认两小时
*/
public function getToken($uid,$time = 60 * 60 * 2,$random = ''){
try {
//获取redis实例
$redis = $this->checkRedis();
//拼接key
$key = $this->prefix.$uid;
//设置token
$token = $this->setToken($key,$time);
//如果token不存在
$data['token'] = $token;
//随机串
$data['random'] = $random == '' ? get_random_str(7,5) : $random;
$redis->set($key,$data);
//设置过期时间秒数 到期自动删除 如果不是单点登录 则按照最后一个登录的token设置
$redis->expire($key,$time);
return $this->encode($uid,$time,$data['random']);
}catch (Exception $e){
echo $e->getMessage();
exit;
}
}
/**
* 验证token
* @author 木头人下士
* @since 2021-09-03
* @param $token string
*/
public function checkToken($token,$time){
try {
//Redis实例
$redis = $this->checkRedis();
//解密token
$decode_str = $this->decode($token);
//分割解密串获取uid跟过期时间
$tokenArr = explode(',',$decode_str);
if($tokenArr[0] == '@shb@'){
$uid = $tokenArr[1];
$exp_time = $tokenArr[2];
//判断随机串是否正确
$random = $tokenArr[3] ?? '';
//拼接key
$key = $this->prefix.$uid;
if($redis->EXISTS($key)){
//获取token
$data = $redis->get($key);
//验证
$check = $this->checkJWToken($data['token']);
if($check){
if(!$this->multi_login && isset($data['random']) && $data['random'] != $random){
return message('您的账号已在别处登录',false,null,401);
}
//先续期
$this->getToken($uid,$time,$random);
//验证成功
$checkArr = explode($this->prefix,$check);
return $checkArr[1];
}
//token验证失败
return false;
}
//不存在也是过期
return false;
}
//token格式不正确
return false;
}catch (Exception $e){
echo $e->getMessage();
exit;
}
}
/**
* @author 木头人下士
* @since 2021-09-03
* 设置真实token
*/
protected function setToken($key,$time){
//JWT实例
$jwt = new Jwt();
//获取token 这边多设置一个小时,防止虚拟token验证成功,但真实token失效的问题 机智~~~
return $jwt->getToken($key,$time + 60 * 60);
}
/**
* 使用JWT验证JWToken
* @param $token
* @return bool|mixed
*/
protected function checkJWToken($token){
$jwt = new Jwt();
//验证
return $jwt->verifyToken($token);
}
/**
* 验证Redis是否可用
* @return Redis
*/
protected function checkRedis(){
//redis实例
$redis = new Redis();
//测试redis连接
$redis->connect(env('cache.HOST'), env('cache.PORT'));
$conn = $redis->ping();
if (!$conn){
echo '请开启Redis拓展';
exit;
}
return $redis;
}
/**
* 加密
*/
protected function encode($data,$time,$random,$key = "#s21M"){
$exp_time = time() + $time;
$value = "@shb@,{$data},{$exp_time},{$random}";
return openssl_encrypt($value, 'des-ecb', $key);
}
/**
* 解密
*/
protected function decode($data,$key = "#s21M"){
return openssl_decrypt($data, 'des-ecb', $key);
}
}
文件JWT.PHP
<?php
/**
* JWT鉴权
* @since 2020/11/14
* Class Jwt
* @package App\Helpers
*/
class Jwt
{
// 头部
private static $header = [
'alg' => 'HS256', //生成signature的算法
'typ' => 'JWT' //JWT 令牌统一写为JWT
];
// 使用HMAC生成信息摘要时所使用的密钥
private static $key = 'dasdsadsafeb7f0a5ede98cdca2';
/**
* alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
* @return string
* @since 2020/11/14
*/
public function getHeader()
{
return self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
}
/**
* Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据
* JWT 规定了7个官方字段,供选用,这里可以存放私有信息,比如uid
* @param $uid
* @return string
* @since 2020/11/14
*/
public function getPayload($uid,$time)
{
$payload = [
'iss' => 'jwt_yh', //签发人
'exp' => time() + $time, //过期时间
'sub' => 'YH', //主题
'aud' => 'every', //受众
'nbf' => time(), //生效时间,该时间之前不接收处理该Token
'iat' => time(), //签发时间
'jti' => 10001, //编号(JWT ID用于标识该JWT)
'uid' => $uid, //私有信息,uid
];
return self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
}
/**
* 获取Token
* @param array $payload jwt载荷
* 格式如下非必须:
* [
* 'iss'=>'jwt_admin', //该JWT的签发者
* 'iat'=>time(), //签发时间
* 'exp'=>time()+7200, //过期时间
* 'nbf'=>time()+60, //该时间之前不接收处理该Token
* 'sub'=>'www.admin.com', //面向的用户
* 'jti'=>md5(uniqid('JWT').time()) //该Token唯一标识
* ]
* @return bool|string 返回结果
* @since 2020/11/14
*/
public function getToken($uid,$time = 60 * 60 * 24 * 7)
{
// 获取JWT头
$header = self::getHeader();
// 获取JWT有效载荷
$payload = self::getPayload($uid,$time);
// JWT头拼接JWT有效载荷
$raw = $header . '.' . $payload;
// Token字符串
$token = $raw . '.' . self::signature($raw, self::$key, self::$header['alg']);
// 返回Token
return $token;
}
/**
* 验证token是否有效,默认验证exp,nbf,iat时间
* @param string $token token字符串
* @return bool|mixed 返回结果
* @since 2020/11/14
*/
public function verifyToken(string $token)
{
if (!$token) {
return false;
}
$tokens = explode('.', $token);
if (count($tokens) != 3) {
return false;
}
list($base64header, $base64payload, $sign) = $tokens;
// 获取jwt算法
$base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
if (empty($base64decodeheader['alg'])) {
return false;
}
//签名验证
if (self::signature($base64header . '.' . $base64payload, self::$key, $base64decodeheader['alg']) !== $sign) {
return false;
}
$payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);
//签发时间大于当前服务器时间验证失败
if (isset($payload['iat']) && $payload['iat'] > time()) {
return false;
}
//过期时间小宇当前服务器时间验证失败
if (isset($payload['exp']) && $payload['exp'] < time()) {
return false;
}
//该nbf时间之前不接收处理该Token
if (isset($payload['nbf']) && $payload['nbf'] > time()) {
return false;
}
return $payload['uid'];
}
/**
* base64UrlEncode编码实现
* @param string $str 需要编码的字符串
* @return mixed 返回结果
* @since 2020/11/14
*/
private static function base64UrlEncode(string $str)
{
return str_replace('=', '', strtr(base64_encode($str), '+/', '-_'));
}
/**
* base64UrlDecode解码实现
* @param string $str 需要解码的字符串
* @return bool|string 返回结果
* @since 2020/11/14
*/
private static function base64UrlDecode(string $str)
{
$remainder = strlen($str) % 4;
if ($remainder) {
$addlen = 4 - $remainder;
$str .= str_repeat('=', $addlen);
}
return base64_decode(strtr($str, '-_', '+/'));
}
/**
* HMACSHA256签名实现
* @param string $str 待签名字符串
* @param string $key 加密密钥
* @param string $alg 算法方式
* @return mixed 返回结果
* @since 2020/11/14
*/
private static function signature(string $str, string $key, string $alg)
{
$alg_config = array(
'HS256' => 'sha256'
);
return self::base64UrlEncode(hash_hmac($alg_config[$alg], $str, $key, true));
}
}