JwtToken.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. <?php
  2. namespace app\common\library;
  3. class JwtToken
  4. {
  5. /**
  6. * Generate an HS256 JWT.
  7. *
  8. * @param array $payload
  9. * @param string $secret
  10. * @return string
  11. */
  12. public static function encode(array $payload, $secret)
  13. {
  14. if (empty($secret)) {
  15. throw new \InvalidArgumentException('JWT密钥不能为空');
  16. }
  17. $header = [
  18. 'typ' => 'JWT',
  19. 'alg' => 'HS256',
  20. ];
  21. $segments = [
  22. self::base64UrlEncode(json_encode($header, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
  23. self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
  24. ];
  25. $signature = hash_hmac('sha256', implode('.', $segments), $secret, true);
  26. $segments[] = self::base64UrlEncode($signature);
  27. return implode('.', $segments);
  28. }
  29. /**
  30. * Decode a JWT and return the payload without verifying the signature.
  31. *
  32. * 注意:此方法只做 base64url 解码,不校验签名,返回的数据可能被篡改,
  33. * 仅用于查看内容;做安全判断请使用 verify()。
  34. *
  35. * @param string $jwt
  36. * @return array|null 解析成功返回 payload 数组,token 格式非法时返回 null
  37. */
  38. public static function decode($jwt)
  39. {
  40. if (!is_string($jwt) || $jwt === '') {
  41. return null;
  42. }
  43. $segments = explode('.', $jwt);
  44. if (count($segments) !== 3) {
  45. return null;
  46. }
  47. $payload = json_decode(self::base64UrlDecode($segments[1]), true);
  48. if ($payload === null && json_last_error() !== JSON_ERROR_NONE) {
  49. return null;
  50. }
  51. return $payload;
  52. }
  53. /**
  54. * Verify a JWT's HS256 signature and expiration time.
  55. *
  56. * @param string $jwt
  57. * @param string $secret
  58. * @return array|false 验证通过返回 payload 数组,验签失败/已过期/格式非法时返回 false
  59. * @throws \InvalidArgumentException 密钥为空时抛出,与 encode() 行为一致
  60. */
  61. public static function verify($jwt, $secret)
  62. {
  63. if (empty($secret)) {
  64. throw new \InvalidArgumentException('JWT密钥不能为空');
  65. }
  66. if (!is_string($jwt) || $jwt === '') {
  67. return false;
  68. }
  69. $segments = explode('.', $jwt);
  70. if (count($segments) !== 3) {
  71. return false;
  72. }
  73. list($headerSegment, $payloadSegment, $signatureSegment) = $segments;
  74. // 1. 校验签名(hash_equals 防止时序攻击)
  75. $expectedSignature = hash_hmac('sha256', $headerSegment . '.' . $payloadSegment, $secret, true);
  76. if (!hash_equals(self::base64UrlEncode($expectedSignature), $signatureSegment)) {
  77. return false;
  78. }
  79. // 2. 校验算法声明,防止算法混淆攻击
  80. $header = json_decode(self::base64UrlDecode($headerSegment), true);
  81. if (!is_array($header) || !isset($header['alg']) || $header['alg'] !== 'HS256') {
  82. return false;
  83. }
  84. // 3. 解析 payload 并检查过期时间
  85. $payload = json_decode(self::base64UrlDecode($payloadSegment), true);
  86. if (!is_array($payload)) {
  87. return false;
  88. }
  89. if (isset($payload['exp']) && time() > (int) $payload['exp']) {
  90. return false;
  91. }
  92. return $payload;
  93. }
  94. private static function base64UrlEncode($data)
  95. {
  96. return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  97. }
  98. private static function base64UrlDecode($data)
  99. {
  100. return base64_decode(strtr($data, '-_', '+/'));
  101. }
  102. }