chleniang/cls-token

TP6/TP8 Token 认证

维护者

详细信息

gitee.com/chleniang/cls-token.git

v1.0.2 2024-08-16 02:32 UTC

This package is auto-updated.

Last update: 2024-09-16 02:45:44 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

# 在项目的 config/console.php 配置文件中添加指令,以便后继使用生成数据表迁移文件指令
return [
    // 指令定义
    'commands' => [
        ......
        // 生成数据库迁移文件的指令
        "token:create-db-table" => \chleniang\ClsToken\command\CreateDbTable::class,
    ],
];

配置

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

如果是多应用项目,可自定义应用下的 应用目录/config/cls_token.php 配置文件,覆盖主配置。

// ==== 配置项说明

// 存储标识 (stores配置项中的键名)
//      "rds" 时使用下方 stores --> "rds"相应配置连接
//      "db" 时使用下方 stores --> "db"相应配置连接
//      "rds_user" 时使用下方 stores --> "rds_user"相应配置连接
//      "db_user" 时使用下方 stores --> "db_user"相应配置连接

// 默认存储标识
"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:"),
    ],
    "db"     => [
        // 存储类型 "redis" / "database"
        "type"        => "database",

        // 数据库连接标识(TP database配置文件中 connections 配置项中的键名)
        //      默认"" 使用默认数据库连接
        "connection"  => "",

        // 保存token数据的表名(不含前缀)
        "token_table" => "admin_token",
    ],
    // 另一个配置,存储标识 "rds_user"
    "rds_user" => [
        // 存储类型 "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"     => "API_SHOP_USER:",
    ],
    "db_user"     => [
        "type"        => "database",
        "connection"  => "",
        "token_table" => "user_token",
    ],
],

// 令牌存储时加密算法 (默认"" 不加密)
//     可用加密算法为 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

buildToken()

生成 token记录

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

注意:如果配置中指定了 save_algo ,在存储时会将 token 加密后存储,存储的值与返回的 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 即可进行身份认证
      
    

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-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"即可
      
    

updateToken()

刷新令牌

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

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

注意:如果配置中指定了 save_algo ,在存储时会将 token 加密后存储,存储的值与返回的 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-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'=>'刷新失败,请重新登录']);
    }
      
    

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'=>'令牌无效,无法删除']);
    }
      
      
    

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::store($storeKey) 方法取得存储驱动的对象;

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

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

公用方法

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

  • 可调用方法有上节"快捷方法"中的5个方法
  • 如果只用到这几个公用方法,建议直接使用上节中的"快捷方法"(几个方法都可指定"存储标识")
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 提供了命令行功能,方便大家使用;

命令行方式

本功能使用 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 查看迁移文件状态,以确定是否能直接删除该文件(还是说要先回滚再删除;或者说不能删除)

直接创建数据表

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

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

注意表名以及是否需要使用 refresh_token 索引。

# 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 '刷新token',
  `refresh_expire` int NOT NULL DEFAULT 0 COMMENT '刷新token过期时间',
  `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 '可根据是否使用 刷新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)
        ->find();
    
    if(empty($info)){
        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'=>'令牌无效,无法删除']);
    }
}

许可证