<?php
declare(strict_types=1);
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdmin
* +----------------------------------------------------------------------
* | 版权所有 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | 官方网站: https://thinkadmin.top
* +----------------------------------------------------------------------
* | 开源协议 ( https://mit-license.org )
* | 免责声明 ( https://thinkadmin.top/disclaimer )
* | 会员特权 ( https://thinkadmin.top/vip-introduce )
* +----------------------------------------------------------------------
* | gitee 代码仓库:https://gitee.com/zoujingli/ThinkAdmin
* | github 代码仓库:https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
namespace think\admin\extend;
use think\admin\Controller;
use think\admin\Exception;
use think\admin\Library;
* 接口 JWT 接口扩展.
* @class JwtExtend
* @method static bool isRejwt() 是否输出令牌
* @method static array getInData() 获取输入数据
*/
class JwtExtend
{
private const header = ['typ' => 'JWT', 'alg' => 'HS256'];
private const signTypes = [
'HS256' => 'sha256',
'HS384' => 'sha384',
'HS512' => 'sha512',
];
* 获取原会话标签.
* @var string
*/
public static $sessionId = '';
* 是否返回令牌.
* @var bool
*/
private static $rejwt = false;
* 当前请求数据.
* @var array
*/
private static $input = [];
* 兼容历史方法.
* @return array|string
* @throws Exception
*/
public static function __callStatic(string $method, array $arguments)
{
switch ($method) {
case 'isRejwt':
return self::$rejwt;
case 'getInData':
return self::$input;
case 'getToken':
return self::token(...$arguments);
case 'verifyToken':
return self::verify(...$arguments);
default:
throw new Exception("method not exists: JwtExtend::{$method}()");
}
}
* 生成 jwt token.
* @param array $data jwt 载荷 格式如下非必须
* {
* "iss": "http://example.org", // 签发者(Issuer),JWT的签发者
* "sub": "1234567890", // 主题(Subject),JWT所面向的用户
* "aud": "http://example.com", // 受众(Audience),接收JWT的一方
* "exp": 1625174400, // 过期时间(Expiration time),JWT的过期时间戳
* "iat": 1625138400, // 签发时间(Issued at),JWT的签发时间戳
* "nbf": 1625138400, // 生效时间(Not Before),JWT的生效时间戳
* "...": ... // 其他扩展内容
* }
* @param ?string $jwtkey 签名密钥
* @param ?bool $rejwt 输出令牌
*/
public static function token(array $data = [], ?string $jwtkey = null, ?bool $rejwt = null): string
{
$jwtkey = self::jwtkey($jwtkey);
if (is_bool($rejwt)) {
self::$rejwt = $rejwt;
}
[$fields, $payload] = [['iss', 'sub', 'aud', 'exp', 'iat', 'nbf'], ['iat' => time()]];
foreach ($data as $k => $v) {
if (in_array($k, $fields)) {
$payload[$k] = $v;
unset($data[$k]);
}
}
$data['.ssid'] = self::withSess();
if (empty($data['.ssid'])) {
unset($data['.ssid']);
}
$payload['enc'] = CodeExtend::encrypt(json_encode($data, JSON_UNESCAPED_UNICODE), $jwtkey);
$two = CodeExtend::enSafe64(json_encode($payload, JSON_UNESCAPED_UNICODE));
$one = CodeExtend::enSafe64(json_encode(self::header, JSON_UNESCAPED_UNICODE));
return "{$one}.{$two}." . self::withSign("{$one}.{$two}", self::header['alg'], $jwtkey);
}
* 验证 token 是否有效, 默认验证 exp,nbf,iat 时间.
* @param string $token 加密数据
* @param ?string $jwtkey 签名密钥
* @throws Exception
*/
public static function verify(string $token, ?string $jwtkey = null): array
{
$tokens = explode('.', $token);
if (count($tokens) != 3) {
throw new Exception('数据解密失败!', 0, []);
}
[$base64header, $base64payload, $signature] = $tokens;
$header = json_decode(CodeExtend::deSafe64($base64header), true);
if (empty($header['alg'])) {
throw new Exception('数据解密失败!', 0, []);
}
$jwtkey = self::jwtkey($jwtkey);
if (self::withSign("{$base64header}.{$base64payload}", $header['alg'], $jwtkey) !== $signature) {
throw new Exception('验证签名失败!', 0, []);
}
$payload = json_decode(CodeExtend::deSafe64($base64payload), true);
if (isset($payload['iat']) && $payload['iat'] > time()) {
throw new Exception('服务器时间验证失败!', 0, $payload);
}
if (isset($payload['exp']) && $payload['exp'] < time()) {
throw new Exception('服务器时间验证失败!', 0, $payload);
}
if (isset($payload['nbf']) && $payload['nbf'] > time()) {
throw new Exception('不接收处理该TOKEN', 0, $payload);
}
if (isset($payload['enc'])) {
$extra = json_decode(CodeExtend::decrypt($payload['enc'], $jwtkey), true);
if (!empty($extra['.ssid'])) {
self::$sessionId = $extra['.ssid'];
}
unset($payload['enc'], $extra['.ssid']);
}
return self::$input = array_merge($payload, $extra ?? []);
}
* 获取 JWT 密钥.
*/
public static function jwtkey(?string $jwtkey = null): string
{
try {
if (!empty($jwtkey)) {
return $jwtkey;
}
$jwtkey = config('app.jwtkey');
if (!empty($jwtkey)) {
return $jwtkey;
}
$jwtkey = sysconf('data.jwtkey|raw');
if (!empty($jwtkey)) {
return $jwtkey;
}
$jwtkey = md5(uniqid(strval(rand(1000, 9999)), true));
sysconf('data.jwtkey', $jwtkey);
return $jwtkey;
} catch (\Exception $exception) {
trace_file($exception);
return 'thinkadmin';
}
}
* 输出模板变量.
*/
public static function fetch(Controller $class, array $vars = [])
{
$ignore = array_keys(get_class_vars(Controller::class));
foreach ($class as $name => $value) {
if (!in_array($name, $ignore)) {
if (is_array($value) || is_numeric($value) || is_string($value) || is_bool($value) || is_null($value)) {
$vars[$name] = $value;
}
}
}
$class->success('获取变量成功!', $vars);
}
* 获取原会话标识.
*/
private static function withSess(): string
{
if (!isset(Library::$sapp->session)) {
return self::$sessionId = '';
}
return self::$sessionId = Library::$sapp->session->getId();
}
* 生成数据签名.
* @param string $input 为 base64UrlEncode(header).".".base64UrlEncode(payload)
* @param string $alg 算法方式
* @param ?string $key 签名密钥
*/
private static function withSign(string $input, string $alg = 'HS256', ?string $key = null): string
{
return CodeExtend::enSafe64(hash_hmac(self::signTypes[$alg], $input, self::jwtkey($key), true));
}
}