chleniang/cls-token

TP6/TP8 Token Auth

v1.1.3 2025-03-12 06:10 UTC

This package is auto-updated.

Last update: 2025-03-12 06:17:11 UTC


README

适用于 ThinkPHP6 / ThinkPHP8Token 登录认证

需求

  • php : ^8.0.2
  • topthink/framework : ^6.0|^8.0
  • topthink/think-migration : ^3.1

特点

  • 简单配置,应对 多应用/多模块 场景
  • 多种存储类型适应更多需求
  • 面对复杂场景可自定义相关存储属性满足所有需求
  • 数据库迁移命令行方便快捷

安装

# composer安装
composer require chleniang/cls-token

# 针对有些国内镜像源找不到 chleniang/cls-token 包的情况
# 可先将设置的国内镜像源取消,使用默认官方仓库可正常安装(有时候国内速度较慢)
#   取消命令:   composer config -g --unset repos.packagist

# 在项目的 config/console.php 配置文件中添加指令,以便后继使用生成数据表迁移文件指令
# 如果是使用redis,不使用数据表(或者是手动生成token记录表的__表创建方法见后序章节),不用添加此指令
return [
    // 指令定义
    'commands' => [
        ......
        // 生成数据库迁移文件的指令
        "token:create-db-table" => \chleniang\ClsToken\command\CreateDbTable::class,
    ],
];

配置

composer 安装后会为项目自动生成 config/cls_token.php 主配置文件。

// config/cls_token.php
// ==== 配置说明 ====

/*
 * cls-token 配置文件
 * 如果是多应用,可在不同的 应用目录/config/cls-token.php 中覆盖配置
 *      举例: 使用了数据库方式保存,
 *             在 admin应用中 token表是 "admin_token"
 *             在 api应用中 token表是 "user_token"
 *             此时就可在两个应用下配置各自的表名(**前提是有对应的token表)
 *
 *          或者是使用了redis方式保存,
 *             在 admin应用中 可设置"admin:"前缀
 *             在 api应用中 可设置"api:"前缀
 *             避免在使用 id 标识时 id 可能重复的问题
 *
 * TP8支持多模块方式,可在 "存储标识" 配置中覆盖"公共配置项"(或者叫"基础配置项")
 *
 *      **如果使用数据库存储token记录,需要创建自己的token表(参见README)
 *      **如果使用Redis存储token记录,且TP缓存也使用Redis类型时: 建议不要将token的Redis.select(数据库序号) 设置成与 TP 缓存相同的数据库序号,否则在清除缓存时,会将token记录一并清除.
 *
 */
return [
    // 默认存储标识 (stores配置项中的键名)
    //      为 "rds" 时,使用下方 stores --> "rds" 相应配置存储
    //      为 "db" 时,使用下方 stores --> "db" 相应配置存储
    "default"         => "rds",

    // 存储配置信息
    "stores"          => [
        // 键名即为 存储标识,可自定义
        "rds" => [
            // 存储类型 "redis" / "database"
            "type"       => "redis",
            "host"       => env("cls_token.redis_host", "127.0.0.1"),
            "port"       => env("cls_token.redis_port", 6379),
            "password"   => env("cls_token.redis_password", ""),
            "select"     => env("cls_token.redis_select", 1),
            "timeout"    => env("cls_token.redis_timeout", 0),
            "persistent" => env("cls_token.persistent", false),
            // 不同应用可设置不同前缀,避免id重复
            "prefix"     => env("cls_token.redis_prefix", "app_name:"),

            // 覆盖公共配置项
            // "expire"     => 3600,  // 当前这个存储标识中的 令牌-过期时长 是 3600秒
        ],
        "db"     => [
            // 存储类型 "redis" / "database"
            "type"        => "database",
            // 数据库连接标识(TP database配置文件中 connections 配置项中的键名)
            //      默认"" 使用默认数据库连接
            "connection"  => "",
            // 保存token数据的表名(不含前缀)
            "token_table" => "admin_token",
        ],
    ],

    // ======= 以下都是公共配置(或者叫基础配置__除 "default" "stores" 之外的配置项),可被"存储标识"中的配置覆盖

    // 令牌存储时加密算法 (默认"" 不加密)
    //     可用加密算法为 hash_hmac()方法可用的算法,可用hash_hmac_algos()获取算法列表
    //     常用的有 "sha256" / "md5" / "ripemd160" / "haval160,4"
    "save_algo"       => "",
    // 令牌存储时的加密密钥(盐),如改变所有已存储token将失效
    "save_secret_key" => 'cls_token_sec_us77@sudf91!hjVbd9$u7',

    // 令牌-过期时长(秒)
    "expire"          => 60,

    // 刷新令牌-是否启用  true:启用(默认) / false:不使用刷新令牌
    "refresh_token"   => true,
    // 刷新令牌-过期时长(秒) 启用刷新令牌时才有用,过期时长应远大于token的过期时长
    "refresh_expire"  => 3600,

    // 鉴权模式(token及refresh_token都受此影响)
    //      可取值:  1 / 2 / 3
    //         1:检查 token / refresh_token 的值以及是否过期
    //         2:在1的基础上,同时检查 来访user_agent与登录时创建的记录是否一致
    //         3:在2的基础上,同时检查 来访ip与创建记录的是否一致(慎用;移动应用中ip可能会随时变)
    "check_mode"      => 2,
];

使用-快捷方法

直接使用配置信息快速调用相关操作

调用门面类 \chleniang\ClsToken\facade\Token 的静态方法即可

原始类为 \chleniang\ClsToken\Token

Token::buildToken()

生成 token记录

在登录成功后,依据登录用户标识 (一般为 ID / UUID ) 创建 token记录

注意:如果配置中指定了 save_algo ,在存储时会将 tokenrefresh_token 加密后存储,存储的值与返回的值是不一样的。

  • 方法定义

    public function buildToken(
        int|string  $userIdentifier,
        array       $userExInfo = [],
        string|null $storeKey = null
    ): array
    
  • 参数说明

    • $userIdentifier {int|string} 用户唯一标识(ID / UUID
    • $userExInfo {array} token记录中要保存的用户其他信息(不要太多)
    • $storeKey {string|null} 存储标识(默认null :使用配置中的默认配置项)
  • 返回值:数组

    返回值示例:

    [
         "token" => "2x2b94ac......",
         "expire" => 60,  // 配置的过期时长
         "refresh_token" => "557d0e9f......",  // 如果启用刷新令牌会有此项
         "refresh_expire" => 3600,  // 如果启用刷新令牌会有此项,配置的刷新令牌过期时长
    ]
    
  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    // ... 用户登录提交 账号/密码 验证通过
    //     可获取到相应用户标识($userID / $userUUID)及 相关用户其他信息(在token记录中,用户其他信息可存可不存)
      
    // 按默认存储标识保存生成的 token记录
    $token = Token::buildToken($userID,$userExInfo);
      
    // 指定存储标识:token记录存储到 配置文件中 存储标识为 'db' 的存储器中
    $token = Token::buildToken($userID,$userExInfo,'db');
      
    // 此时的 $token 就拿到了生成的 token 及 expire 过期时长
    //   (如果开启了刷新令牌还会得到刷新令牌相关数据)
    // 将 $token 及 用户标识($userID / $userUUID) 返回给前端,
    // 以后访问携带 用户标识 及 token 即可进行身份认证
      
    

Token::check()

令牌校验

来访请求如果需要进行身份认证,使用此方法;一般用于中间件;

  • 方法定义

    public function check(
        string      $token,
        int|string  $userIdentifier,
        string      $checkType = Constant::CHECK_TYPE_ACCESS,
        string|null $storeKey = null
    ): bool
    
  • 参数说明

    • $token {string} 待验证的令牌字符串
    • $userIdentifier {int|string} 用户唯一标识(ID / UUID
    • $checkType {string} 待校验令牌类型 "access"(默认) / "refresh"
    • $storeKey {string|null} 存储标识(默认null :使用配置中的默认配置项)
  • 返回值:true / 抛异常

    校验通过返回 true ;否则抛出异常。

  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    // ... 用户提交的某个请求,需要登录身份认证,此时就可使用 check()方法
    //     一般在中间件中进行认证
      
    // 假设每次请求都会将 token 保存在请求头 x-token 中;用户标识保存在 x-uid 中
    // 如果涉及跨域问题,请先解决,否则可能无法拿到这两个请求头相关数据
      
      
    $accessToken = request()->header('x-token','');
    $userID = request()->header('x-uid','');
    // access令牌校验
    try{
        $checkRes = Token::check($accessToken,$userID);
        if($checkRes !== true){
            throw new \chleniang\ClsToken\exception\ClsTokenValidateException();
        }
    }
    catch (\Exception $e) {
        // 校验不通过,响应相关提示
        return json(['msg'=>'令牌无效,请重新登录/刷新令牌(如果使用刷新令牌的话)']);
    }
      
      
      
    $refreshToken = request()->header('x-refresh-token','');
    $userID = request()->header('x-uid','');
    // refresh刷新令牌校验
    try{
        $checkRes = Token::check($refreshToken,$userID,TokenConstant::CHECK_TYPE_REFRESH);
        if($checkRes !== true){
            throw new \chleniang\ClsToken\exception\ClsTokenValidateException();
        }
    }
    catch (\Exception $e) {
        // 校验不通过,响应相关提示
        return json(['msg'=>'刷新令牌无效,只能重新登录']);
    }
      
    // 补充:以上两个都是使用默认存储标识,特殊情况也可使用第四个参数,指定存储标识
    // 例:只有全局配置文件(默认存储是"rds"),没有应用配置文件,但当前token记录用的是数据库存储,此时就可指定存储标识"db"即可
      
    

Token::updateToken()

刷新令牌

只有在使用刷新令牌的方式下此方法才有意义;

通常 access令牌 过期时间较短,当前端收到 access令牌已失效 的响应后,可携带 refresh令牌 调用此方法以更新 access令牌

注意:如果配置中指定了 save_algo ,在存储时会将 tokenrefresh_token 加密后存储,存储的值与返回的值是不一样的。

  • 方法定义

      
    public function updateToken(
        string      $refreshToken,
        int|string  $userIdentifier,
        string|null $storeKey = null
    ): array
    
  • 参数说明

    • $refreshToken {string} 刷新令牌字符串
    • $userIdentifier {int|string} 用户唯一标识(ID / UUID
    • $storeKey {string|null} 存储标识(默认null :使用配置中的默认配置项)
  • 返回值:数组

    返回值示例:

    [
         "token" => "nwx94ac......",   // 新生成的access令牌
         "expire" => 60,  // 配置的过期时间
         "refresh_token" => "557d0e9f......",  // 跟提交的刷新令牌一样,原样返回
         "refresh_expire" => 3600,  // 配置的刷新令牌过期时间
    ]
    
  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    // 用户在收到 access令牌过期的响应后,可发送刷新令牌的请求
      
    $refreshToken = request()->header('x-refresh-token','');
    $userID = request()->header('x-uid','');
    try{
        // updateToken() 方法内部会对提交的 refresh令牌进行校验,校验不通过抛出异常
        $tokenRes = Token::updateToken($refreshToken,$userID);
        return json([
            'msg' => '刷新令牌成功',
            'data' => $tokenRes,
            'code' => 0,
        ]);
    }
    catch (\Exception $e) {
        // 刷新失败,响应相关提示
        return json(['msg'=>'刷新失败,请重新登录']);
    }
      
    

Token::delete()

删除记录

删除指定用户标识的 token记录

  • 方法定义

    public function delete(
        int|string  $userIdentifier,
        string|null $storeKey = null
    ): bool
    
  • 参数说明

    • $userIdentifier {int|string} 用户唯一标识(ID / UUID
    • $storeKey {string|null} 存储标识(默认null :使用配置中的默认配置项)
  • 返回值:bool

    删除成功: true 删除失败:false

  • 使用示例

    // **删除记录前应使用 check() 校验请求合法性
    use chleniang\ClsToken\facade\Token;
      
    $accessToken = request()->header('x-token','');
    $userID = request()->header('x-uid','');
    // access令牌校验
    try{
        $checkRes = Token::check($accessToken,$userID);
        if($checkRes !== true){
            throw new \chleniang\ClsToken\exception\ClsTokenValidateException();
        }
        // 校验通过,删除记录
        Token::delete($userID);
    }
    catch (\Exception $e) {
        // 校验不通过,响应相关提示
        return json(['msg'=>'令牌无效,无法删除']);
    }
        
    

Token::get()

获取记录信息

获取指定用户标识的 token记录 信息

  • 方法定义

    public function get(
        int|string  $userIdentifier,
        string|null $storeKey = null
    ): array
    
  • 参数说明

    • $userIdentifier {int|string} 用户唯一标识(ID / UUID
    • $storeKey {string|null} 存储标识(默认null :使用配置中的默认配置项)
  • 返回值:数组

    ** 没找到记录返回"空数组";

    返回值示例:

    [
        "user_identifier" => "3", // 用户标识统一按字符存储,如果需要自行转换为数字
        "token" => "65e992d6......", // access令牌
        "expire" => 60, // access令牌过期时长(配置中的值);
        "refresh_token" => "aef3815d......", // 刷新令牌;如果未启用刷新令牌,返回空字符串
        "refresh_expire" => 3600, // 刷新令牌过期时长(配置中的值);如未启用刷新令牌,返回0
        "ex_info" => [  // 生成token记录时传入的附加信息
           "name" => "zhang3",
           "age" => 33,
        ],
        "user_agent" => "d1xc9e5a......",  // 生成token记录时来访UA(散列码)
        "ip" => "192.168.0.66",  // 生成token记录时来访IP
        "update_time" => 1723106409, // access令牌最后更新时间
    ]
    
  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    $userID = request()->header('x-uid','');
    $tokenInfo = Token::get($userID);
    if(!empty($tokenInfo)){
        // token记录信息
        var_dump($tokenInfo);
    }
    

使用-存储对象用法

如果当前项目中只涉及一个 token类型,或者需求中只需要用到 buildToken() check() updateToken() delete() get() 这些公用方法,直接使用"快捷方法"即可;

使用"存储对象"的主要目的是在一些较为复杂的情形下,可以对当前对象指定一些特殊属性,以应对复杂业务场景:

(比如:针对某些 token 记录,没有配置对应的存储标识,此时要想正确使用,就需要用到 存储对象 的特殊方法 >>> database类型setTokenTable()方法 / redis类型setPrefix()方法等 );

使用 Token::store($storeKey) 方法可取得存储驱动的对象实例 ;

  • 参数 $storeKey 为"存储标识";可以为空(取默认存储标识)

可以使用此存储对象调用本驱动类型特有的一些方法;

公用方法

获取存储驱动对象实例后,可调用此对象实例的方法

  • 可调用方法有5个"快捷方法":

    buildToken() check() updateToken() delete() get()

  • 如果项目只会用到这几个方法,建议直接使用上节中的"快捷方法"(几个方法也都可指定"存储标识")

use chleniang\ClsToken\facade\Token;

// 获取存储驱动对象(默认存储标识);
$storeObj = Token::store();

// 指定存储标识;
// $storeObj = Token::store('rds');

$storeObj->buildToken(...);
$storeObj->check(...);
$storeObj->updateToken(...);
$storeObj->delete(...);
$storeObj->get(...);

Redis存储类型-特有方法

setPrefix()

设置 存储KEY 的前缀;如果要用此方法,须将此方法作为"存储对象"的第一调用方法,其他方法用链式调用接在此方法后边。

默认存储时会取配置中的前缀;

此方法可设置自定义前缀以便与配置中的区分;

  • 方法定义

    public function setPrefix(
        string $prefix
    ): $this
    
  • 参数说明

    • $prefix {string} 自定义前缀字符串
  • 返回值 $this

    返回的是当前类实例对象本身,以便做链式调用

    ** 须将此方法作为"存储对象"的第一调用方法,其他方法接在此方法后边。

  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    // 假设当前应用中有一个 后台管理员的 token记录,同时还要对前台会员进行 token记录
    // 两个都用的是 id 作为用户标识,如果都使用默认配置中的前缀,有可能会造成存储KEY冲突问题,
    // 此时就可以针对其中一个自定义一个其他的前缀
      
    // 前台用户的使用默认配置方式
    Token::buildToken(...);
    Token::check(...);
      
    // 针对后台管理员的自定义前缀
    $storeObj = Token::store('rds')->setPrefix('Manager_admin:');
    $storeObj->buildToken(...);
    $storeObj->check(...);
      
    

Database存储类型-特有方法

setTokenTable()

设置 token记录表名 ;如果要用此方法,须将此方法作为"存储对象"的第一调用方法,其他方法接在此方法后边。

默认存储时会取配置中的 token_table 作为表名;

此方法可设置自定义 token表名 以便与配置中的区分;

  • 方法定义

    public function setTokenTable(
        string $tableName
    ): $this
    
  • 参数说明

    • $tableName {string} 自定义 token记录表名(不含前缀)
  • 返回值 $this

    返回的是当前类实例对象本身,以便做链式调用

    ** 须将此方法作为"存储对象"的第一调用方法,其他方法接在此方法后边。

  • 使用示例

    use chleniang\ClsToken\facade\Token;
      
    // 假设当前应用中有一个 后台管理员的 token记录表,同时还要对前台会员进行 token记录表
    // 两个表都用的是 id 作为用户标识,如果都使用默认配置,就只能从配置中指定的表取记录,
    // 此时就需要针对其中一个指定表名
      
    // 前台用户的使用默认配置(配置中的 token_table 就是前台用户的记录表)
    Token::buildToken(...);
    Token::check(...);
      
    // 针对后台管理员的指定表名
    $storeObj = Token::store('db')->setTokenTable('admin_token');
    $storeObj->buildToken(...);
    $storeObj->check(...);
      
    

数据库存储-生成数据表

如果需要用数据库(database)存储 token记录 ,需要有对应的数据表;

在此 cls-token 提供了命令行功能,方便大家使用;

方式1: 命令行方式

本功能使用 ThinkPHP 命令行功能 + topthink/think-migration 实现;

本命令行提供有完善提示、帮助信息,以方便更多童鞋使用。

# 可能过以下命令查看可用命令列表
php think

# 回显信息包含有以下内容表明cls-token安装成功,可使用命令生成数据表
... 
token
  token:create-db-table  创建token表数据库迁移文件
...

# 查看token:create-db-table帮助信息,有详细参数及示例说明
php think token:create-db-table -h

# 几个示例---------------

# 创建不启用刷新令牌的"user_token1"表:
php think token:create-db-table user_token1

# 创建启用刷新令牌的"user_token2"表:
php think token:create-db-table user_token2 --refresh-token

# 强制创建启用刷新令牌的"user_token3"表:
php think token:create-db-table user_token3 -r -f


# ====几个常用 migrate 命令========
# 查看migration状态:
#	显示 "UP" 的为已经执行过迁移的
#	显示 "DOWN" 的为还没有执行过迁移的,等待执行的
php think migrate:status

# 执行迁移(所有待迁移版本全部执行):
php think migrate:run

# 指定版本号执行迁移: 
#	命令参数值 "20240812083514" 为迁移文件版本号(也是迁移文件最前边的时间戳)
#   会执行迁移到此版本(包含此版本)
php think migrate:run -t 20240812083514

# 回滚(所有版本全部回滚):
php think migrate:rollback

# 指定版本号回滚: 
#	命令参数值 "20240812083514" 为迁移文件版本号(也是迁移文件最前边的时间戳)
#   会回滚到此版本(此版本状态还是"UP",将此版本之前的全部回退)
php think migrate:rollback -t 20240812083514

常见报错

  1. 在使用创建迁移文件命令时提示:该表(xxx)已存在相应的迁移文件

      [InvalidArgumentException]
      该表(user_token1)已存在相应的迁移文件
       >>> 20240812083514_create_token_table_user_token.php;
      可使用migrate相关命令查看状态,回滚并删除相应迁移文件后重试。
    
    • 原因:之前已经创建过 user_token 的迁移文件,可以查看当前项目的 项目根目录/database/migrations/ 目录,其中应该有上面提示信息中提到的对应文件。
    • 解决办法:先要用 php think migrate:status 查看迁移文件状态,以确定是否能直接删除该文件(还是说要先回滚再删除;或者说不能删除)

方式2: 手动创建数据表

如果不想使用 think-migration ,也可直接创建数据表;

直接使用以下 SQL 创建即可;

注意:更换成自己的表名;根据需要决定是否需要使用 refresh_token_index 索引

# DROP TABLE IF EXISTS `cls_admin_token1`;
CREATE TABLE `cls_admin_token1`  (
  `user_identifier` varchar(128) NOT NULL DEFAULT '' COMMENT '登录用户唯一标识(通常为用户表的id/uuid)',
  `token` varchar(500) NOT NULL DEFAULT '' COMMENT 'token',
  `expire` int NOT NULL DEFAULT 0 COMMENT 'token过期时间',
  `refresh_token` varchar(500) NOT NULL DEFAULT '' COMMENT '刷新令牌',
  `refresh_expire` int NOT NULL DEFAULT 0 COMMENT '刷新令牌过期时间',
  `ex_info` varchar(2000) NOT NULL DEFAULT '' COMMENT '登录用户其他信息(JSON字符串,不要放太多信息)',
  `user_agent` varchar(128) NOT NULL DEFAULT '' COMMENT '登录时的User-Agent(md5后的)',
  `ip` varchar(128) NOT NULL DEFAULT '' COMMENT '登录时的IP(注意移动应用后续来访IP可能会变,作校验时自行决定是否校验此字段)',
  `update_time` int NOT NULL DEFAULT 0 COMMENT '创建/更新token的时间戳',
  PRIMARY KEY (`user_identifier`) USING BTREE COMMENT '用户标识id/uuid作为主键',
  # INDEX `refresh_token_index`(`refresh_token` ASC) USING BTREE COMMENT '可根据是否使用 refresh_token 使用/不使用 此索引',
  INDEX `token_index`(`token` ASC) USING BTREE
) ENGINE = InnoDB COMMENT = '// 用户登录token表';

DEMO

use chleniang\ClsToken\facade\Token;

public function login(){
    // ... 用户登录提交 账号/密码 验证通过
    //     可获取到相应用户标识($userID / $userUUID)及 相关用户其他信息$userExInfo
    
    $username = $this->request->param('name','');
    $passwd = $this->request->param('password','');
    
    $info = UserModel::field(['id','username','password',...])
        ->where('username','=',$username)
        ->findOrEmpty();
    
    if($info->isEmpty()){
        return json([
            'msg'=>"无此用户",
            'data' => [],
            'code' => 0
        ]);
    }
    if(md5($passwd) !== $info['password']){
        return json([
            'msg'=>"密码有误",
            'data' => [],
            'code' => 0
        ]);
    }

    $userID = $info['id'];
    $userExInfo = [
        'name' => $info['nickname'],
    ];
    // 按默认存储标识保存生成的 token记录
    $tokenRes = Token::buildToken($userID, $userExInfo);
    return json([
        'msg' => '登录成功',
        'data' => $tokenRes,
        'code' => 0,
    ]);
}

// 登录鉴权,一般在中间件中实现
public function loginCheck(){
    $accessToken = request()->header('x-token','');
    $userID = request()->header('x-uid','');
    // access令牌校验
    try{
        $checkRes = Token::check($accessToken,$userID);
        if($checkRes !== true){
            throw new \chleniang\ClsToken\exception\ClsTokenValidateException();
        }
    }
    catch (\Exception $e) {
        // 校验不通过,响应相关提示
        return json(['msg'=>'令牌无效,请重新登录/刷新令牌(如果使用刷新令牌的话)']);
    }
}

// 退出时一般要删除token记录(删除前也要验证身份)
// 也可直接将logout方法调用放在登录鉴权中间件之后
public function logout(){
    $accessToken = request()->header('x-token','');
    $userID = request()->header('x-uid','');
    // access令牌校验
    try{
        $checkRes = Token::check($accessToken,$userID);
        if($checkRes !== true){
            throw new \chleniang\ClsToken\exception\ClsTokenValidateException();
        }
        // 校验通过,删除记录
        Token::delete($userID);
    }
    catch (\Exception $e) {
        // 校验不通过,响应相关提示
        return json(['msg'=>'令牌无效,无法删除']);
    }
}

Lisence