# cls-token **Repository Path**: chleniang/cls-token ## Basic Information - **Project Name**: cls-token - **Description**: 适用于 ThinkPHP6 / ThinkPHP8 的 Token 登录认证 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-13 - **Last Updated**: 2025-03-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: token, TP8, tp6, 登录认证 ## README # TP6/TP8 Token > 适用于 `ThinkPHP6` / `ThinkPHP8` 的 `Token` 登录认证 ## 需求 - `php : ^8.0.2` - `topthink/framework : ^6.0|^8.0` - `topthink/think-migration : ^3.1` ## 特点 - 简单配置,应对 多应用/多模块 场景 - 多种存储类型适应更多需求 - 面对复杂场景可自定义相关存储属性满足所有需求 - 数据库迁移命令行方便快捷 ## 安装 ```shell # 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` 主配置文件。 > ```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` ,在存储时会将 `token` 及 `refresh_token` 加密后存储,存储的值与返回的值是不一样的。 - 方法定义 ```php 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` :使用配置中的默认配置项) - 返回值:`数组` > 返回值示例: > > ```php > [ > "token" => "2x2b94ac......", > "expire" => 60, // 配置的过期时长 > "refresh_token" => "557d0e9f......", // 如果启用刷新令牌会有此项 > "refresh_expire" => 3600, // 如果启用刷新令牌会有此项,配置的刷新令牌过期时长 > ] > ``` - 使用示例 ```php 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()` 令牌校验 > 来访请求如果需要进行身份认证,使用此方法;一般用于中间件; - 方法定义 ```php 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` ;否则抛出异常。 > - 使用示例 ```php 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` ,在存储时会将 `token` 及 `refresh_token` 加密后存储,存储的值与返回的值是不一样的。 - 方法定义 ```php public function updateToken( string $refreshToken, int|string $userIdentifier, string|null $storeKey = null ): array ``` - 参数说明 - `$refreshToken` `{string}` 刷新令牌字符串 - `$userIdentifier` `{int|string}` 用户唯一标识(`ID / UUID`) - `$storeKey` `{string|null}` 存储标识(`默认null` :使用配置中的默认配置项) - 返回值:`数组` > 返回值示例: > > ```php > [ > "token" => "nwx94ac......", // 新生成的access令牌 > "expire" => 60, // 配置的过期时间 > "refresh_token" => "557d0e9f......", // 跟提交的刷新令牌一样,原样返回 > "refresh_expire" => 3600, // 配置的刷新令牌过期时间 > ] > ``` - 使用示例 ```php 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记录` - 方法定义 ```php public function delete( int|string $userIdentifier, string|null $storeKey = null ): bool ``` - 参数说明 - `$userIdentifier` `{int|string}` 用户唯一标识(`ID / UUID`) - `$storeKey` `{string|null}` 存储标识(`默认null` :使用配置中的默认配置项) - 返回值:`bool` > 删除成功: `true` 删除失败:`false` > - 使用示例 ```php // **删除记录前应使用 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记录` 信息 - 方法定义 ```php public function get( int|string $userIdentifier, string|null $storeKey = null ): array ``` - 参数说明 - `$userIdentifier` `{int|string}` 用户唯一标识(`ID / UUID`) - `$storeKey` `{string|null}` 存储标识(`默认null` :使用配置中的默认配置项) - 返回值:数组 > ** 没找到记录返回"空数组"; > > 返回值示例: > > ```php > [ > "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令牌最后更新时间 > ] > ``` - 使用示例 ```php 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()` > > - 如果项目只会用到这几个方法,建议直接使用上节中的"快捷方法"(几个方法也都可指定"存储标识") ```php 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` 的前缀;如果要用此方法,须将此方法作为"存储对象"的第一调用方法,其他方法用链式调用接在此方法后边。 > 默认存储时会取配置中的前缀; > > 此方法可设置自定义前缀以便与配置中的区分; - 方法定义 ```php public function setPrefix( string $prefix ): $this ``` - 参数说明 - `$prefix` `{string}` 自定义前缀字符串 - 返回值 `$this` > 返回的是当前类实例对象本身,以便做链式调用 > > ** 须将此方法作为"存储对象"的第一调用方法,其他方法接在此方法后边。 - 使用示例 ```php 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表名` 以便与配置中的区分; - 方法定义 ```php public function setTokenTable( string $tableName ): $this ``` - 参数说明 - `$tableName` `{string}` 自定义 `token记录表名`(不含前缀) - 返回值 `$this` > 返回的是当前类实例对象本身,以便做链式调用 > > ** 须将此方法作为"存储对象"的第一调用方法,其他方法接在此方法后边。 - 使用示例 ```php 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` 实现; > > 本命令行提供有完善提示、帮助信息,以方便更多童鞋使用。 ```shell # 可能过以下命令查看可用命令列表 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)已存在相应的迁移文件 ```shell [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 索引` 。 ```sql # 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 ```php 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 - [木兰宽松许可证,第2版 (Mulan PSL v2)](http://license.coscl.org.cn/MulanPSL2)