2018年9月

一个用于 JWT 编码和界面的简单 PHP 库,符合 RFC 7519 规范。

功能支持

功能支持情况算法支持情况
Sign(JWT注册)支持HS256支持
Verify(JWT验证)支持HS384支持
iss check(签发者检查)支持HS512支持
sub check(jwt所面向的用户)不支持RS256支持
aud check(接收者检查)不支持RS384支持
exp check(过期时间检查)支持RS512支持
nbf check(在此时间前JWT不可用)支持ES256不支持
iat check(JWT签发时间检查)支持ES384不支持
jti check(jwt的唯一标识,回避重放攻击)不支持ES512不支持

安装

使用 Composer 管理你的依赖,下载 PHP-JWT:

composer require firebase/php-jwt

示例

<?php
use \Firebase\JWT\JWT;

$key = "example_key";
$token = array(
    "iss" => "http://example.org",
    "aud" => "http://example.com",
    "iat" => 1356999524,
    "nbf" => 1357000000
);

/**
 * 重要:
 * 你必须为你的应用程序指定一个支持的算法。查阅
 * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
 * 了解一系列遵循规范的算法。
 */
$jwt = JWT::encode($token, $key);
$decoded = JWT::decode($jwt, $key, array('HS256'));

print_r($decoded);

/*
 注意: 目前它会是一个对象,而不是关联数组。如果要获取关联数组,你可以这样操作:
*/

$decoded_array = (array) $decoded;

/**
 * 当注册服务器和验证服务器之前存在一些时钟偏差时,你可以添加一个时间余地(leeway)。
 * 建议 leeway 最好不要超过一分钟。
 *
 * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
 */
JWT::$leeway = 60; // $leeway 是以秒为单位的
$decoded = JWT::decode($jwt, $key, array('HS256'));

?>

示例,使用了 RS256 (openssl)

<?php
use \Firebase\JWT\JWT;

$privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC8kGa1pSjbSYZVebtTRBLxBz5H4i2p/llLCrEeQhta5kaQu/Rn
vuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t0tyazyZ8JXw+KgXTxldMPEL9
5+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4ehde/zUxo6UvS7UrBQIDAQAB
AoGAb/MXV46XxCFRxNuB8LyAtmLDgi/xRnTAlMHjSACddwkyKem8//8eZtw9fzxz
bWZ/1/doQOuHBGYZU8aDzzj59FZ78dyzNFoF91hbvZKkg+6wGyd/LrGVEB+Xre0J
Nil0GReM2AHDNZUYRv+HYJPIOrB0CRczLQsgFJ8K6aAD6F0CQQDzbpjYdx10qgK1
cP59UHiHjPZYC0loEsk7s+hUmT3QHerAQJMZWC11Qrn2N+ybwwNblDKv+s5qgMQ5
5tNoQ9IfAkEAxkyffU6ythpg/H0Ixe1I2rd0GbF05biIzO/i77Det3n4YsJVlDck
ZkcvY3SK2iRIL4c9yY6hlIhs+K9wXTtGWwJBAO9Dskl48mO7woPR9uD22jDpNSwe
k90OMepTjzSvlhjbfuPN1IdhqvSJTDychRwn1kIJ7LQZgQ8fVz9OCFZ/6qMCQGOb
qaGwHmUK6xzpUbbacnYrIM6nLSkXgOAwv7XXCojvY614ILTK3iXiLBOxPu5Eu13k
eUz9sHyD6vkgZzjtxXECQAkp4Xerf5TGfQXGXhxIX52yH+N2LtujCdkQZjXAsGdm
B2zNzvrlgRmgBrklMTrMYgm1NPcW+bRLGcwgW2PTvNM=
-----END RSA PRIVATE KEY-----
EOD;

$publicKey = <<<EOD
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
-----END PUBLIC KEY-----
EOD;

$token = array(
    "iss" => "example.org",
    "aud" => "example.com",
    "iat" => 1356999524,
    "nbf" => 1357000000
);

$jwt = JWT::encode($token, $privateKey, 'RS256');
echo "Encode:\n" . print_r($jwt, true) . "\n";

$decoded = JWT::decode($jwt, $publicKey, array('RS256'));

/*
 注意: 目前它会是一个对象,而不是关联数组。如果要获取关联数组,你可以这样操作:
*/

$decoded_array = (array) $decoded;
echo "Decode:\n" . print_r($decoded_array, true) . "\n";
?>

测试

使用 phpunit 来运行测试:

$ pear install PHPUnit
$ phpunit --configuration phpunit.xml.dist
PHPUnit 3.7.10 by Sebastian Bergmann.
.....
Time: 0 seconds, Memory: 2.50Mb
OK (5 tests, 5 assertions)

关于私钥的补充说明

如果你的私钥包含 \n 字符,为了能正确地转义,请确保使用双引号("")将其包裹,而不是单引号('')。

License

3-Clause BSD.


JWT 验证过程中的各种异常整理

基本用法

<?php
use \Firebase\JWT\JWT; //导入JWT
class MainController extends Controller
{

    public function verification()
    {
        $key = '344'; //key要和签发的时候一样

        $jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC93d3cuaGVsbG93ZWJhLm5ldCIsImF1ZCI6Imh0dHA6XC9cL3d3dy5oZWxsb3dlYmEubmV0IiwiaWF0IjoxNTI1MzQwMzE3LCJuYmYiOjE1MjUzNDAzMTcsImV4cCI6MTUyNTM0NzUxNywiZGF0YSI6eyJ1c2VyaWQiOjEsInVzZXJuYW1lIjoiXHU2NzRlXHU1YzBmXHU5Zjk5In19.Ukd7trwYMoQmahOAtvNynSA511mseA2ihejoZs7dxt0"; //签发的Token
        try {
                   JWT::$leeway = 60;//当前时间减去60,把时间留点余地
                   $decoded = JWT::decode($jwt, $key, ['HS256']); //HS256方式,这里要和签发的时候对应
                   $arr = (array)$decoded;
                   print_r($arr);
            } catch(\Firebase\JWT\SignatureInvalidException $e) {  //签名不正确
                echo $e->getMessage();
            }catch(\Firebase\JWT\BeforeValidException $e) {  // 签名在某个时间点之后才能用
                echo $e->getMessage();
            }catch(\Firebase\JWT\ExpiredException $e) {  // token过期
                echo $e->getMessage();
           }catch(Exception $e) {  //其他错误
                echo $e->getMessage();
            }
        //Firebase定义了多个 throw new,我们可以捕获多个catch来定义问题,catch加入自己的业务,比如token过期可以用当前Token刷新一个新Token
    }
}

在src目录下的JWT.php中的方法 decode中,可以看到多个自定义 throw new 抛出异常:

/**
 * 将 JWT 字符串解码成一个 PHP 对象。
 *
 * @param string        $jwt            The JWT
 * @param string|array  $key            The key, or map of keys.
 *                                      如果使用的是非对称算法,则是公钥
 * @param array         $allowed_algs   一系列被支持的验证算法
 *                                      被支持的算法有 'HS256', 'HS384', 'HS512' and 'RS256'
 *
 * @return object 将 JWT 的载荷以 PHP 对象形式返回
 *
 * @throws UnexpectedValueException     JWT 无效
 * @throws SignatureInvalidException    由于签名验证失败,JWT 无效
 * @throws BeforeValidException         尝试在 nbf 前使用 JWT
 * @throws BeforeValidException         尝试在 iat 之前使用 JWT
 * @throws ExpiredException             JWT 已过期,由 exp 定义的过期时间
 *
 * @uses jsonDecode
 * @uses urlsafeB64Decode
 */
public static function decode($jwt, $key, array $allowed_algs = array())
{
    $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
    if (empty($key)) {
        // Key 不能为空
        throw new InvalidArgumentException('Key may not be empty');
    }
    $tks = explode('.', $jwt);
    if (count($tks) != 3) {
        // 错误的分段数
        throw new UnexpectedValueException('Wrong number of segments');
    }
    list($headb64, $bodyb64, $cryptob64) = $tks;
    if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
        // 无效的 header 编码 
        throw new UnexpectedValueException('Invalid header encoding');
    }
    if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
        // 无效的 claims 编码
        throw new UnexpectedValueException('Invalid claims encoding');
    }
    if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
        // 无效的 signature 编码
        throw new UnexpectedValueException('Invalid signature encoding');
    }
    if (empty($header->alg)) {
        // 算法为空
        throw new UnexpectedValueException('Empty algorithm');
    }
    if (empty(static::$supported_algs[$header->alg])) {
        // 未被支持的算法
        throw new UnexpectedValueException('Algorithm not supported');
    }
    if (!in_array($header->alg, $allowed_algs)) {
        // 算法未被允许使用
        throw new UnexpectedValueException('Algorithm not allowed');
    }
    if (is_array($key) || $key instanceof \ArrayAccess) {
        if (isset($header->kid)) {
            if (!isset($key[$header->kid])) {
                // "kid" 无效,不能查找正确的 key
                throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
            }
            $key = $key[$header->kid];
        } else {
            // "kid" 为空,不能查找正确的 key
            throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
        }
    }
    // 检查 signature
    if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
        // 签名验证失败
        throw new SignatureInvalidException('Signature verification failed');
    }
    // 检查 nbf 是否已定义。已定义则可以使用该 token;否则还为到可用时间,终止。
    if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
        // 不能在该时间前处理该 token
        throw new BeforeValidException(
            'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
        );
    }
    // 检查在“此刻”之前已创建的 token。防止使用那些在稍后才能使用的 token (没有正确使用 nbf)        if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
        // 不能在该时间前处理该 token
        throw new BeforeValidException(
            'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
        );
    }
    // 检查 token 是否已过期
    if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
        // token 已过期
        throw new ExpiredException('Expired token');
    }
    return $payload;
}

由于此库不支持subaudjti检查,在完成上述操作后,需按自己的需要对这三者进行检查操作。

如果你没有开启OpenSSL的话我们的不能使用ssl功能,如果我们发邮箱使用ssl加密就无法使用了,但要开启它是非常的简单的,具体如下.

XAMPP打开OpenSSL方法:

(1)检查X:/xampp/php/ext目录下是否存在php_openssl.dll文件,如果不存在就先去PHP官网下载放入.

(2)打开X:/xampp/php/php.ini文件,查找extension=php_openssl.dll,如果找到了,去掉前面的分号;

如果没找到就在extension=php_curl.dll的下一行添加如下代码:

extension=php_openssl.dll

然后重启Apache就行了.


编辑 ..xampp/apache/bin/php.ini 文件,找到 “;extension=php_openssl.dll” (去掉前面的;号注释)

我的 XAMPP 没有找到这句话 ,直接添加 extension=php_openssl.dll 大概988行
另外,需要配置 httpd-ssl.conf 文件(*xamppapacheconfextrahttpd-ssl.conf)
大概86行 配置 DocumentRoot 和 ServerName ,改成自己定义的(如果没有更改默认配置的话就不用再配置了)

JWT

其他文档参考:http://blog.pkcms.cn/php-jwt-token/

用于处理 JSON Web Token 和 JSON Web Signature(PHP 5.5+) 的库。基于 current draft 实现。

安装

此包可以在 Packagist 找到,你可以使用 Composer 进行安装。

composer require lcobucci/jwt

依赖

  • PHP 5.5+
  • OpenSSL Extension

参考:https://yunye.pw/index.php/archives/6.html

基础用法

创建 JWT

只使用 builder 来新建一个 JWT/JWS tokens:

use Lcobucci\JWT\Builder;

$token = (new Builder())->setIssuer('http://example.com') // 设置发行人 (iss claim)
                        ->setAudience('http://example.org') // 设置接收人 (aud claim)
                        ->setId('4f1g23a12aa', true) // 设置 id (jti claim), 复制用做 header 元素
                        ->setIssuedAt(time()) // 设置 token 生成的时间 (iat claim)
                        ->setNotBefore(time() + 60) // 设置在60秒内该token无法使用 (nbf claim)
                        ->setExpiration(time() + 3600) // 设置过期时间 (exp claim)
                        ->set('uid', 1) // 给token设置一个id Configures a new claim, called "uid"
                        ->getToken(); // 获取生成的 token

$token->getHeaders(); // 获取 token 的 header 信息
$token->getClaims(); // 获取 token 的 claims

echo $token->getHeader('jti'); // will print "4f1g23a12aa"
echo $token->getClaim('iss'); // will print "http://example.com"
echo $token->getClaim('uid'); // will print "1"
echo $token; // The string representation of the object is a JWT string (pretty easy, right?)

分析 token 字符串

使用 parser 根据 JWT 字符串新建 token(继续使用上述 token 作为示例):

use Lcobucci\JWT\Parser;

$token = (new Parser())->parse((string) $token); // 分析字符串 
$token->getHeaders(); // 获取 token header
$token->getClaims(); // 获取 token claims

echo $token->getHeader('jti'); // will print "4f1g23a12aa"
echo $token->getClaim('iss'); // will print "http://example.com"
echo $token->getClaim('uid'); // will print "1"

验证

轻松验证 token 是否有效((继续使用上述 token 作为示例):

use Lcobucci\JWT\ValidationData;

$data = new ValidationData(); // 使用当前时间来验证 (iat, nbf and exp)
$data->setIssuer('http://example.com');
$data->setAudience('http://example.org');
$data->setId('4f1g23a12aa');

var_dump($token->validate($data)); // false, 因为我们创建的 token 在 `time() + 60` 前不能使用

$data->setCurrentTime(time() + 60); // 将验证时候改到 token 可用时间之后

var_dump($token->validate($data)); // true, 因为验证信息与 token 包含的信息一直了

$data->setCurrentTime(time() + 4000); // 将验证时间改到未来

var_dump($token->validate($data)); // false, token 已经过了有效期了

Token 签名

使用签名来验证 token 是否在生成后被修改过。本库实现了 Hmac, RSA 以及 ECDSA signatures (using 256, 384 and 512).

Hmac

Hmac 签名真的用起来很简单:

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;

$signer = new Sha256();

$token = (new Builder())->setIssuer('http://example.com') // Configures the issuer (iss claim)
                        ->setAudience('http://example.org') // Configures the audience (aud claim)
                        ->setId('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item
                        ->setIssuedAt(time()) // Configures the time that the token was issue (iat claim)
                        ->setNotBefore(time() + 60) // Configures the time that the token can be used (nbf claim)
                        ->setExpiration(time() + 3600) // Configures the expiration time of the token (exp claim)
                        ->set('uid', 1) // Configures a new claim, called "uid"
                        ->sign($signer, 'testing') // 使用“testing”作为 key 创建签名
                        ->getToken(); // Retrieves the generated token


var_dump($token->verify($signer, 'testing 1')); // false, key 不一致
var_dump($token->verify($signer, 'testing')); // true, key 值一致

RSA 和 ECDSA

RSA 和 ECDSA 签名是基于公钥和私钥,所以必须使用私钥生成,使用公钥验证:

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Keychain; // just to make our life simpler
use Lcobucci\JWT\Signer\Rsa\Sha256; // 如果在使用 ECDSA 可以使用 Lcobucci\JWT\Signer\Ecdsa\Sha256

$signer = new Sha256();

$keychain = new Keychain();

$token = (new Builder())->setIssuer('http://example.com') // Configures the issuer (iss claim)
                        ->setAudience('http://example.org') // Configures the audience (aud claim)
                        ->setId('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item
                        ->setIssuedAt(time()) // Configures the time that the token was issue (iat claim)
                        ->setNotBefore(time() + 60) // Configures the time that the token can be used (nbf claim)
                        ->setExpiration(time() + 3600) // Configures the expiration time of the token (nbf claim)
                        ->set('uid', 1) // Configures a new claim, called "uid"
                        ->sign($signer,  $keychain->getPrivateKey('file://{path to your private key}')) // 使用你的私钥创建签名                        ->getToken(); // Retrieves the generated token


var_dump($token->verify($signer, $keychain->getPublicKey('file://{path to your public key}'))); // 如果公钥是使用对应的私钥生成的,即为 true

重要的一点:如果你使用 RSA keys ,那么就不必调用 ECDSA signers(and vice-versa), otherwise sign()verify() 会报异常!

Thinkphp5

文档

安装

composer create-project topthink/think 项目名称

Guzzle

PHP HTTP 客户端(CURL 工具)
https://guzzle-cn.readthedocs.io/zh_CN/latest/index.html


PHPMailer

古典的邮件发送库
https://github.com/PHPMailer/PHPMailer
https://www.cnblogs.com/songbo236589/p/8184039.html


ramsey/uuid

UUID 生成库
https://github.com/ramsey/uuid


predis

灵活且功能完整的 redis 客户端
https://github.com/nrk/predis


PhpSpreadsheet

电子表格文件读写库
https://github.com/PHPOffice/PhpSpreadsheet


yidas/phpspreadsheet-helper

PHP Excel Helper

https://github.com/yidas/phpspreadsheet-helper

box/spout

低内存占用的电子表格库

http://opensource.box.com/spout/

snappy

从 URL 或 HTML 页面生成缩略图、快照、PDF
https://github.com/KnpLabs/snappy


qr-code

二维码生成器
https://github.com/endroid/qr-code


easy-sms

短信发送组件
https://github.com/overtrue/easy-sms


rbac

基于角色的访问控制和验证
https://github.com/OWASP/rbac
https://github.com/jackchow123456/jackchow-rbac


CORS

PHP CORS 中间件
https://github.com/medz/cors


think-queue

thinkphp 队列支持
https://github.com/top-think/think-queue


houdunwang/arr

数组增强组件
https://github.com/houdunwang/arr


Gregwar/Captcha

验证码
https://github.com/Gregwar/Captcha


jae-jae/QueryList

一套简洁、优雅、可扩展的PHP采集工具(爬虫),基于phpQuery。
https://github.com/jae-jae/QueryList