chleniang / cls-token
TP6/TP8 Token Auth
Requires
- php: >=8.0.2
- topthink/framework: ^6.0|^8.0
- topthink/think-migration: ^3.1
README
适用于
ThinkPHP6
/ThinkPHP8
的Token
登录认证
需求
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
,在存储时会将token
及refresh_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
,在存储时会将token
及refresh_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
常见报错
在使用创建迁移文件命令时提示:该表(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'=>'令牌无效,无法删除']);
}
}