JWT.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. <?php
  2. /**
  3. *-------------------------------------------------------------------------s*
  4. *
  5. *-------------------------------------------------------------------------h*
  6. * @copyright Copyright (c) 2015-2099 Shopwwi Inc. (http://www.shopwwi.com)
  7. *-------------------------------------------------------------------------o*
  8. * @license http://www.shopwwi.com s h o p w w i . c o m
  9. *-------------------------------------------------------------------------p*
  10. * @link http://www.shopwwi.com
  11. *-------------------------------------------------------------------------w*
  12. * @since shopwwi
  13. *-------------------------------------------------------------------------w*
  14. */
  15. namespace Shopwwi\WebmanAuth;
  16. use Firebase\JWT\BeforeValidException;
  17. use Firebase\JWT\ExpiredException;
  18. use Firebase\JWT\JWT as jwtMan;
  19. use Firebase\JWT\Key;
  20. use Firebase\JWT\SignatureInvalidException;
  21. use Shopwwi\WebmanAuth\Exception\JwtTokenException;
  22. use Shopwwi\WebmanAuth\Facade\Str;
  23. use support\Redis;
  24. use UnexpectedValueException;
  25. class JWT
  26. {
  27. /**
  28. * access_token. refresh_token.
  29. */
  30. const REFRESH = 2, ACCESS = 1;
  31. /**
  32. * 自动报错
  33. * @var bool
  34. */
  35. protected $guard = 'user';
  36. protected $config = [];
  37. protected $redis = false;
  38. protected $redis_prefix = '';
  39. /**
  40. * 构造方法
  41. * @access public
  42. */
  43. public function __construct()
  44. {
  45. $_config = config('plugin.shopwwi.auth.app.jwt');
  46. if (empty($_config)) {
  47. throw new JwtTokenException('The configuration file is abnormal or does not exist');
  48. }
  49. $this->config = $_config;
  50. if ($_config['redis']) {
  51. $this->redis = true;
  52. $this->redis_prefix = $_config['redis_prefix'] ?? '';
  53. }
  54. }
  55. /**
  56. * 设置角色
  57. * @param string $guard
  58. * @return $this
  59. */
  60. public function guard($guard = 'user')
  61. {
  62. $this->guard = $guard;
  63. return $this;
  64. }
  65. /**
  66. * 生成令牌
  67. * @param array $extend
  68. * @param int $access_exp
  69. * @param int $refresh_exp
  70. * @return mixed
  71. */
  72. public function make(array $extend, int $access_exp = 0, int $refresh_exp = 0)
  73. {
  74. $exp = $access_exp > 0 ? $access_exp : $this->config['access_exp'];
  75. $refreshExp = $refresh_exp > 0 ? $refresh_exp : $this->config['refresh_exp'];
  76. $payload = self::payload($extend, $exp, $refreshExp);
  77. $secretKey = self::getPrivateKey();
  78. $accessToken = self::makeToken($payload['accessPayload'], $secretKey, $this->config['algorithms']);
  79. $refreshSecretKey = self::getPrivateKey(self::REFRESH);
  80. $refreshToken = self::makeToken($payload['refreshPayload'], $refreshSecretKey, $this->config['algorithms']);
  81. //获取主键
  82. $idKey = config("plugin.shopwwi.auth.app.guard.{$this->guard}.key");
  83. //redis 开启
  84. if ($this->redis) {
  85. $this->setRedis($extend[$idKey], $accessToken, $refreshToken, $exp, $refreshExp);
  86. //存储session
  87. session()->set("token_{$this->guard}", $accessToken);
  88. } else {
  89. //存储session
  90. session()->set("token_{$this->guard}", $accessToken);
  91. }
  92. return json_decode(json_encode([
  93. 'token_type' => 'Bearer',
  94. 'expires_in' => $exp,
  95. 'refresh_expires_in' => $refreshExp,
  96. 'access_token' => $accessToken,
  97. 'refresh_token' => $refreshToken,
  98. ]));
  99. }
  100. /**
  101. * 刷新token值
  102. * @return object
  103. */
  104. public function refresh(int $accessTime = 0): ?object
  105. {
  106. $token = $this->getTokenFormHeader();
  107. $tokenPayload = (array)self::verifyToken($token, self::REFRESH);
  108. $tokenPayload['exp'] = time() + ($accessTime > 0 ? $accessTime : $this->config['access_exp']);
  109. $secretKey = $this->getPrivateKey();
  110. $newToken = $this->makeToken($tokenPayload, $secretKey, $this->config['algorithms']);
  111. $tokenObj = json_decode(json_encode(['access_token' => $newToken]));
  112. if ($this->redis) {
  113. //获取主键
  114. $idKey = config("plugin.shopwwi.auth.app.guard.{$this->guard}.key");
  115. $this->setRedis($tokenPayload['extend']->$idKey, $tokenObj->access_token, $token, $this->config['access_exp'], $this->config['refresh_exp']);
  116. }
  117. return $tokenObj;
  118. }
  119. /**
  120. * 获取token信息
  121. * @return mixed|string|null
  122. */
  123. protected function getTokenFormHeader()
  124. {
  125. $header = request()->header('Authorization', '');
  126. $token = request()->input('_token');
  127. if (Str::startsWith($header, 'Bearer ')) {
  128. $token = Str::substr($header, 7);
  129. }
  130. if (!empty($token) && Str::startsWith($token, 'Bearer ')) {
  131. $token = Str::substr($token, 7);
  132. }
  133. $token = $token ?? session("token_{$this->guard}", null);
  134. if (empty($token)) {
  135. $token = null;
  136. $fail = new JwtTokenException('尝试获取的Authorization信息不存在');
  137. $fail->setCode(401);
  138. throw $fail;
  139. }
  140. return $token;
  141. }
  142. /**
  143. * @desc: 验证令牌
  144. * @param string|null $token
  145. * @param int $tokenType
  146. * @return object
  147. * @throws JwtTokenException
  148. */
  149. public function verify(string $token = null, int $tokenType = self::ACCESS): ?object
  150. {
  151. $token = $token ?? $this->getTokenFormHeader();
  152. return $this->verifyToken($token, $tokenType);
  153. }
  154. /**
  155. * 验证token值
  156. * @param string $token
  157. * @param int $tokenType
  158. * @return object
  159. */
  160. public function verifyToken(string $token, int $tokenType): ?object
  161. {
  162. $secretKey = self::ACCESS == $tokenType ? $this->getPublicKey($this->config['algorithms']) : $this->getPublicKey($this->config['algorithms'], self::REFRESH);
  163. jwtMan::$leeway = 60;
  164. try {
  165. // v5.5.1 return (array) JWT::decode($token, $secretKey, [$this->config['algorithms']]);
  166. $tokenPayload = jwtMan::decode($token, new Key($secretKey, $this->config['algorithms']));
  167. if ($tokenPayload->guard != $this->guard) {
  168. throw new SignatureInvalidException('无效令牌');
  169. }
  170. //redis 开启
  171. if ($this->redis) {
  172. //获取主键
  173. $idKey = config("plugin.shopwwi.auth.app.guard.{$this->guard}.key");
  174. $this->checkRedis($tokenPayload->extend->$idKey, $token, $tokenType);
  175. }
  176. return $tokenPayload;
  177. } catch (SignatureInvalidException $e) {
  178. throw new JwtTokenException('身份验证令牌无效', 401);
  179. } catch (BeforeValidException $e) { // 签名在某个时间点之后才能用
  180. throw new JwtTokenException('身份验证令牌尚未生效', 403);
  181. } catch (ExpiredException $e) { // token过期
  182. throw new JwtTokenException('身份验证会话已过期,请重新登录!', 402);
  183. } catch (UnexpectedValueException $unexpectedValueException) {
  184. throw new JwtTokenException('获取扩展字段不正确', 401);
  185. } catch (\Exception $exception) {
  186. throw new JwtTokenException($exception->getMessage(), 401);
  187. }
  188. }
  189. /**
  190. * 获取扩展字段.
  191. * @param string|null $token
  192. * @param int $tokenType
  193. * @return object
  194. * @throws JwtTokenException
  195. */
  196. public function getTokenExtend(string $token = null, int $tokenType = self::ACCESS): ?object
  197. {
  198. return $this->verify($token, $tokenType);
  199. }
  200. /**
  201. * 生成token值
  202. * @param array $payload
  203. * @param string $secretKey
  204. * @param string $algorithms
  205. * @return string
  206. */
  207. public function makeToken(array $payload, string $secretKey, string $algorithms): string
  208. {
  209. try {
  210. return jwtMan::encode($payload, $secretKey, $algorithms);
  211. } catch (ExpiredException $e) { //签名不正确
  212. throw new JwtTokenException('签名不正确', 401);
  213. } catch (\Exception $e) { //其他错误
  214. throw new JwtTokenException('其它错误', 401);
  215. }
  216. }
  217. /**
  218. * 获取加载体
  219. * @param array $extend
  220. * @param int $access_exp
  221. * @param int $refresh_exp
  222. * @return array
  223. */
  224. public function payload(array $extend, int $access_exp = 0, int $refresh_exp = 0): array
  225. {
  226. $basePayload = [
  227. 'iss' => $this->config['iss'],
  228. 'iat' => time(),
  229. 'exp' => time() + $access_exp,
  230. 'extend' => $extend,
  231. 'guard' => $this->guard
  232. ];
  233. $resPayLoad['accessPayload'] = $basePayload;
  234. $basePayload['exp'] = time() + $refresh_exp;
  235. $resPayLoad['refreshPayload'] = $basePayload;
  236. return $resPayLoad;
  237. }
  238. /**
  239. * 根据签名算法获取【公钥】签名值
  240. * @param string $algorithm
  241. * @param int $tokenType
  242. * @return string
  243. */
  244. protected function getPublicKey(string $algorithm, int $tokenType = self::ACCESS): string
  245. {
  246. switch ($algorithm) {
  247. case 'HS256':
  248. $key = self::ACCESS == $tokenType ? $this->config['access_secret_key'] : $this->config['refresh_secret_key'];
  249. break;
  250. case 'RS512':
  251. case 'RS256':
  252. $key = self::ACCESS == $tokenType ? $this->config['access_public_key'] : $this->config['refresh_public_key'];
  253. break;
  254. default:
  255. $key = $this->config['access_secret_key'];
  256. }
  257. return $key;
  258. }
  259. /**
  260. * 根据签名算法获取【私钥】签名值
  261. * @param int $tokenType
  262. * @return mixed
  263. */
  264. protected function getPrivateKey(int $tokenType = self::ACCESS): string
  265. {
  266. switch ($this->config['algorithms']) {
  267. case 'HS256':
  268. $key = self::ACCESS == $tokenType ? $this->config['access_secret_key'] : $this->config['refresh_secret_key'];
  269. break;
  270. case 'RS512':
  271. case 'RS256':
  272. $key = self::ACCESS == $tokenType ? $this->config['access_private_key'] : $this->config['refresh_private_key'];
  273. break;
  274. default:
  275. $key = $this->config['access_secret_key'];
  276. }
  277. return $key;
  278. }
  279. /**
  280. * 退出登入
  281. */
  282. public function logout($all = false)
  283. {
  284. $token = $this->getTokenFormHeader();
  285. $tokenPayload = self::verifyToken($token, self::ACCESS);
  286. //redis 开启
  287. if (isset($this->config['redis']) && $this->config['redis']) {
  288. //获取主键
  289. $idKey = config("plugin.shopwwi.auth.app.guard.{$this->guard}.key");
  290. $id = $tokenPayload->extend->$idKey;
  291. if ($all) {
  292. Redis::hDel("{$this->redis_prefix}token_{$this->guard}", $id);
  293. } else {
  294. $list = Redis::hGet("{$this->redis_prefix}token_{$this->guard}", $id);
  295. if ($list) {
  296. $tokenList = unserialize($list);
  297. foreach ($tokenList as $key => $val) {
  298. if ($val['accessToken'] == $token) {
  299. unset($tokenList[$key]);
  300. }
  301. }
  302. if (count($tokenList) == 0) {
  303. Redis::hDel("{$this->redis_prefix}token_{$this->guard}", $id);
  304. } else {
  305. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize($tokenList));
  306. }
  307. }
  308. }
  309. }
  310. //清理session数据
  311. session()->forget("token_{$this->guard}");
  312. }
  313. /**
  314. * 写入redis
  315. * @param int $id
  316. * @param $accessToken
  317. * @param $refreshToken
  318. * @param $accessExp
  319. * @param $refreshExp
  320. */
  321. protected function setRedis(int $id, $accessToken, $refreshToken, $accessExp, $refreshExp)
  322. {
  323. $list = Redis::hGet("{$this->redis_prefix}token_{$this->guard}", $id);
  324. $clientType = strtolower(request()->input('client_type', 'web'));
  325. $defaultList = [
  326. 'accessToken' => $accessToken,
  327. 'refreshToken' => $refreshToken,
  328. 'clientType' => $clientType,
  329. 'accessExp' => $accessExp,
  330. 'refreshExp' => $refreshExp,
  331. 'refreshTime' => time(),
  332. 'accessTime' => time(),
  333. ];
  334. if ($list != null) {
  335. $tokenList = unserialize($list);
  336. $maxNum = config("plugin.shopwwi.auth.app.guard.{$this->guard}.num");
  337. if (is_array($tokenList)) {
  338. if ($maxNum === -1) { //不限制
  339. $match = false;
  340. foreach ($tokenList as &$item) {
  341. if ($item['refreshToken'] === $refreshToken) {
  342. $match = true;
  343. $item['accessToken'] = $accessToken;
  344. $item['accessExp'] = $accessExp;
  345. $item['accessTime'] = $defaultList['accessTime'];
  346. break;
  347. }
  348. }
  349. !$match && $tokenList[] = $defaultList;
  350. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize($tokenList));
  351. } elseif ($maxNum === 0) { // 只允许一个终端
  352. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize([$defaultList]));
  353. } elseif ($maxNum > 0) { // 限制同一终端使用个数
  354. $clientTypeNum = 0;
  355. $index = -1;
  356. foreach ($tokenList as $key => $val) {
  357. if ($val['clientType'] == $clientType) {
  358. $clientTypeNum++;
  359. $index < 0 && $index = $key;
  360. }
  361. }
  362. if ($index >= 0 && $clientTypeNum >= $maxNum) {
  363. unset($tokenList[$index]);
  364. }
  365. $match = false;
  366. foreach ($tokenList as &$item) {
  367. if ($item['refreshToken'] === $refreshToken) {
  368. $match = true;
  369. $item['accessToken'] = $accessToken;
  370. $item['accessExp'] = $accessExp;
  371. $item['accessTime'] = $defaultList['accessTime'];
  372. break;
  373. }
  374. }
  375. !$match && $tokenList[] = $defaultList;
  376. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize($tokenList));
  377. }
  378. //清理过期token
  379. $this->clearExpRedis($id);
  380. }
  381. } else {
  382. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize([$defaultList]));
  383. }
  384. }
  385. /**
  386. * 清理过期token
  387. * @param int $id
  388. */
  389. public function clearExpRedis(int $id)
  390. {
  391. $list = Redis::hGet("{$this->redis_prefix}token_{$this->guard}", $id);
  392. if ($list) {
  393. $tokenList = unserialize($list);
  394. $refresh = false;
  395. foreach ($tokenList as $key => $val) {
  396. if (($val['refreshTime'] + $val['refreshExp']) < time()) {
  397. unset($tokenList[$key]);
  398. $refresh = true;
  399. }
  400. }
  401. if (count($tokenList) == 0) {
  402. Redis::hDel("{$this->redis_prefix}token_{$this->guard}", $id);
  403. } else {
  404. if ($refresh) {
  405. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize($tokenList));
  406. }
  407. }
  408. }
  409. }
  410. /**
  411. * 验证token是否存在
  412. * @param int $id
  413. * @param string $token
  414. * @param int $tokenType
  415. */
  416. public function checkRedis(int $id, string $token, int $tokenType = self::ACCESS)
  417. {
  418. $list = Redis::hGet("{$this->redis_prefix}token_{$this->guard}", $id);
  419. if ($list != null) {
  420. $tokenList = unserialize($list);
  421. $checkToken = false;
  422. $expireToken = false;
  423. foreach ($tokenList as $key => $val) {
  424. if ($tokenType == self::REFRESH && $val['refreshToken'] == $token) {
  425. if (\bcadd($val['refreshTime'], $val['refreshExp'], 0) < time()) {
  426. unset($tokenList[$key]);
  427. } else {
  428. $checkToken = true;
  429. }
  430. }
  431. if ($tokenType == self::ACCESS && $val['accessToken'] == $token) {
  432. if (\bcadd($val['accessTime'], $val['accessExp'], 0) < time()) {
  433. $expireToken = true;
  434. } else {
  435. $checkToken = true;
  436. }
  437. }
  438. }
  439. if (count($tokenList) == 0) {
  440. Redis::hDel("{$this->redis_prefix}token_{$this->guard}", $id);
  441. } else {
  442. Redis::hSet("{$this->redis_prefix}token_{$this->guard}", $id, serialize($tokenList));
  443. }
  444. if (!$checkToken) {
  445. if ($expireToken) {
  446. throw new ExpiredException('无效');
  447. } else {
  448. throw new SignatureInvalidException('无效');
  449. }
  450. }
  451. } else {
  452. throw new SignatureInvalidException('无效');
  453. }
  454. }
  455. /**
  456. * 动态方法 直接调用is方法进行验证
  457. * @access public
  458. * @param string $method 方法名
  459. * @param array $args 调用参数
  460. * @return bool
  461. */
  462. public function __call(string $method, array $args)
  463. {
  464. if ('is' == strtolower(substr($method, 0, 2))) {
  465. $method = substr($method, 2);
  466. }
  467. $args[] = lcfirst($method);
  468. return call_user_func_array([$this, 'is'], $args);
  469. }
  470. }