Sa-Token PHP 使用手册

Sa-Token PHP 使用手册

Sa-Token PHP 完全开发手册(ThinkPHP 8 版)

框架版本: ThinkPHP 8.0+ 项目版本: 基于 pohoc/sa-token 最新源码 PHP 版本: PHP >= 8.1

目录

一、框架概述

1.1 什么是 Sa-Token?

Sa-Token 是一个轻量级权限认证框架,专为 PHP 8.1+ 设计,完美适配 ThinkPHP 8.0+。登录认证只需一行代码:

php
StpUtil::login(10001);

权限校验也只需一行:

php
StpUtil::checkPermission('user:add');

1.2 核心功能矩阵

类别功能说明
登录认证单端登录一个账号只能在一个设备登录
多端登录一个账号可在多个设备同时登录
同端互斥登录同一设备类型只能登录一个
记住我延长登录有效期
踢人下线强制指定用户下线
账号封禁禁止账号登录指定时长
服务封禁封禁特定服务如评论、发帖
权限认证权限校验判断用户是否拥有某权限
角色校验判断用户是否拥有某角色
二级认证敏感操作二次身份验证
身份切换临时切换到其他用户身份
注解式鉴权通过注解声明鉴权规则
Cookie 管理自动写入登录后自动写入 Cookie
自动读取请求时自动读取 Cookie
安全配置Secure/HttpOnly/SameSite
跨域共享多域名间共享 Cookie
Session 会话账号 Session同一账号所有设备共享
Token Session仅当前 Token 独享
自定义 Session任意 Key 的 Session
自动清理过期 Session 自动清理
Token 安全多种风格uuid/simple-uuid/random-32/random-64/random-128/tik
Token 前缀如 Bearer 前缀
Token 加密AES-256/SM4 加密
指纹绑定IP+UA 防止 Token 盗用
黑名单手动拉黑 Token
Refresh Token双 Token 机制AccessToken + RefreshToken
Token 轮换刷新时自动轮换
无感刷新客户端无感知刷新 Token
SSO 单点登录四种模式同域/跨域/前后端分离/无 SDK
OAuth2.0四种授权模式授权码/隐藏式/密码/客户端凭证
OpenID Connect支持 OIDC 协议
安全防护防暴力破解登录失败次数限制
IP 异常检测异地登录检测
设备管理登录设备记录与管理
敏感操作验证OTP/安全令牌

1.3 核心术语

术语说明
StpUtil静态工具类,提供所有认证 API
StpLogic逻辑类,支持多账号体系隔离
SaRouter路由匹配器,用于 URL 拦截鉴权
Token身份凭证,由框架生成和管理
LoginId登录标识,通常为用户 ID
SaTokenDao数据持久层接口,负责所有会话数据的底层写入和读取
Session会话对象,存储当前用户的状态数据
Cookie客户端存储 Token 的机制

二、环境要求与安装

2.1 环境要求

text
PHP >= 8.1
ThinkPHP >= 8.0
ext-openssl(必需)
ext-json(必需)
ext-redis(Redis 模式必需)

2.2 创建项目

bash
# 创建单应用项目
composer create-project topthink/think tp8-sa-token
cd tp8-sa-token

# 或创建多应用项目
composer create-project topthink/think tp8-sa-token
cd tp8-sa-token
composer require topthink/think-multi-app

2.3 安装 Sa-Token

bash
composer require pohoc/sa-token

2.4 安装 Redis 扩展(Redis 模式必需)

bash
composer require predis/predis

2.5 文件清单(使用前需创建)

序号文件路径说明优先级
1config/sa_token.php主配置文件✅ 必须
2config/sa_token_sso.phpSSO 配置⚠️ 按需
3config/sa_token_oauth2.phpOAuth2.0 配置⚠️ 按需
4app/service/PermissionService.php权限数据提供者✅ 必须
5app/middleware/SaTokenMiddleware.php鉴权中间件✅ 推荐
6app/exception/Handler.php异常处理器✅ 推荐
7app/provider/SaTokenProvider.php服务提供者✅ 推荐
8config/middleware.php中间件注册配置✅ 必须
9config/service.php服务提供者注册✅ 必须
10route/app.php路由配置✅ 必须

三、四种部署模式详解

3.1 单应用 + Redis 模式

3.1.1 场景说明

适用于生产环境的单体应用,使用 Redis 作为存储介质,支持数据持久化和分布式部署。

3.1.2 目录结构

text
tp8-sa-token/
├── app/
│   ├── controller/
│   │   ├── AuthController.php
│   │   └── UserController.php
│   ├── middleware/
│   │   └── SaTokenMiddleware.php
│   ├── exception/
│   │   └── Handler.php
│   ├── service/
│   │   └── PermissionService.php
│   └── provider/
│       └── SaTokenProvider.php
├── config/
│   ├── sa_token.php
│   ├── sa_token_sso.php
│   ├── sa_token_oauth2.php
│   ├── middleware.php
│   └── service.php
├── route/
│   └── app.php
├── .env
└── composer.json

3.1.3 第一步:创建配置文件 config/sa_token.php

php
<?php

return [
    // ==================== Token 基础配置 ====================
    // tokenName: Token 名称,同时也是 Cookie 名称
    'tokenName' => 'satoken',
    
    // tokenPrefix: Token 前缀,如 Bearer
    'tokenPrefix' => 'Bearer',
    
    // tokenStyle: Token 风格
    // uuid: UUID 格式 (550e8400-e29b-41d4-a716-446655440000)
    // simple-uuid: 简化 UUID (550e8400e29b41d4a716446655440000)
    // random-32: 32位随机字符串
    // random-64: 64位随机字符串
    // random-128: 128位随机字符串
    // tik: 24位随机字符串
    'tokenStyle' => 'uuid',
    
    // ==================== 有效期配置 ====================
    // timeout: Token 有效期(秒),默认30天,-1代表永不过期
    'timeout' => 86400,
    
    // activityTimeout: 最低活跃频率(秒),-1不限制
    // 设置为正数时,每次请求会刷新 Token 有效期
    'activityTimeout' => -1,
    
    // ==================== Cookie 详细配置 ====================
    // cookieName: Cookie 名称(默认与 tokenName 一致)
    'cookieName' => 'satoken',
    
    // cookieDomain: Cookie 域名,空表示当前域名
    // 设置为 .example.com 可在子域名间共享
    'cookieDomain' => '',
    
    // cookiePath: Cookie 路径,'/' 表示整个网站都有效
    'cookiePath' => '/',
    
    // cookieSecure: 是否仅通过 HTTPS 传输,生产环境建议 true
    'cookieSecure' => false,
    
    // cookieHttpOnly: 是否禁止 JavaScript 读取 Cookie,建议 true 防止 XSS
    'cookieHttpOnly' => true,
    
    // cookieSameSite: CSRF 防护策略
    // Strict: 最安全,Lax: 兼容性更好,None: 需要配合 Secure
    'cookieSameSite' => 'Strict',
    
    // cookieMaxAge: Cookie 在客户端的最大存活时间(秒)
    'cookieMaxAge' => 86400,
    
    // cookiePrefix: Cookie 前缀,如 __Secure- 增强安全性
    'cookiePrefix' => '',
    
    // ==================== 读取/写入配置 ====================
    // isReadHeader: 是否从 Header 读取 Token
    'isReadHeader' => true,
    
    // isReadCookie: 是否从 Cookie 读取 Token
    'isReadCookie' => true,
    
    // isReadBody: 是否从请求体读取 Token
    'isReadBody' => false,
    
    // isWriteCookie: 登录后是否写入 Cookie
    'isWriteCookie' => true,
    
    // isWriteHeader: 登录后是否写入响应头
    'isWriteHeader' => false,
    
    // ==================== 登录策略 ====================
    // concurrent: 是否允许多端同时登录
    'concurrent' => true,
    
    // isShare: 同端是否复用 Token
    'isShare' => true,
    
    // maxLoginCount: 同账号最大登录数,-1不限
    'maxLoginCount' => 12,
    
    // maxTryTimes: 创建 Token 最高循环次数
    'maxTryTimes' => 12,
    
    // ==================== 存储配置(Redis 模式) ====================
    'storage' => 'redis',
    'redis' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'port' => env('REDIS_PORT', 6379),
        'password' => env('REDIS_PASSWORD', ''),
        'db' => env('REDIS_DB', 0),
        'timeout' => 3,
        'prefix' => 'sa_token_',
    ],
    
    // ==================== Session 配置 ====================
    'session' => [
        'timeout' => 86400,
        'activityTimeout' => 1800,
        'maxSize' => 1024,
        'prefix' => 'session_',
    ],
    
    // ==================== 加密配置 ====================
    // cryptoType: 加密类型,intl(国际)/ sm(国密)
    'cryptoType' => 'intl',
    
    // tokenEncrypt: 是否启用 Token 内容加密
    'tokenEncrypt' => false,
    
    // tokenEncryptKey: Token 加密密钥(16/24/32 字节)
    'tokenEncryptKey' => '',
    
    // tokenFingerprint: 是否启用 Token 指纹绑定(IP + User-Agent)
    'tokenFingerprint' => false,
    
    // ==================== Refresh Token ====================
    // refreshToken: 是否启用 RefreshToken
    'refreshToken' => false,
    
    // refreshTokenTimeout: RefreshToken 有效期(秒),默认30天
    'refreshTokenTimeout' => 2592000,
    
    // refreshTokenRotation: 刷新时是否轮换 RefreshToken
    'refreshTokenRotation' => true,
    
    // ==================== 安全配置 ====================
    // antiBruteMaxFailures: 防暴力破解最大失败次数,0不限制
    'antiBruteMaxFailures' => 5,
    
    // antiBruteLockDuration: 锁定持续时间(秒)
    'antiBruteLockDuration' => 600,
    
    // ipAnomalyDetection: IP 异常检测
    'ipAnomalyDetection' => true,
    
    // ipAnomalySensitivity: IP 异常灵敏度(1-5)
    'ipAnomalySensitivity' => 3,
    
    // deviceManagement: 设备管理
    'deviceManagement' => true,
    
    // auditLog: 审计日志
    'auditLog' => true,
    
    // auditLogMaxEntries: 每种事件最大日志条数
    'auditLogMaxEntries' => 1000,
    
    // auditLogTtlDays: 日志保留天数
    'auditLogTtlDays' => 30,
    
    // ==================== JWT 配置 ====================
    'jwtSecretKey' => env('JWT_SECRET_KEY', ''),
    'jwtStateless' => false,
    
    // ==================== 签名配置 ====================
    'signKey' => env('SIGN_KEY', ''),
    'signTimestampGap' => 600,
    'signAlg' => 'sha256',
    
    // ==================== 日志配置 ====================
    'isLog' => env('APP_DEBUG', false),
];

3.1.4 第二步:创建权限数据提供者 app/service/PermissionService.php

php
<?php

declare(strict_types=1);

namespace app\service;

use think\facade\Db;

/**
 * 权限数据提供者
 * 负责从数据库获取用户的权限和角色信息
 */
class PermissionService
{
    /**
     * 获取用户权限码集合
     *
     * @param int $userId 用户ID
     * @return array 权限码列表
     */
    public function getPermissionList(int $userId): array
    {
        $perms = Db::name('user_permission')
            ->where('user_id', $userId)
            ->column('permission_code');
        return $perms ?: [];
    }

    /**
     * 获取用户角色标识集合
     *
     * @param int $userId 用户ID
     * @return array 角色标识列表
     */
    public function getRoleList(int $userId): array
    {
        $roles = Db::name('user_role')
            ->alias('ur')
            ->join('role r', 'ur.role_id = r.id')
            ->where('ur.user_id', $userId)
            ->column('r.role_code');
        return $roles ?: [];
    }

    /**
     * 获取用户所有权限(含角色继承)
     *
     * @param int $userId 用户ID
     * @return array 所有权限码列表
     */
    public function getAllPermissions(int $userId): array
    {
        $roles = $this->getRoleList($userId);
        $perms = [];

        // 从角色继承权限
        if (!empty($roles)) {
            $rolePerms = Db::name('role_permission')
                ->whereIn('role_code', $roles)
                ->column('permission_code');
            $perms = array_merge($perms, $rolePerms);
        }

        // 合并用户直接权限
        $userPerms = $this->getPermissionList($userId);
        $perms = array_merge($perms, $userPerms);

        return array_unique($perms);
    }

    /**
     * 验证用户密码
     *
     * @param string $username 用户名
     * @param string $password 密码
     * @return array|null 用户信息或 null
     */
    public function verifyPassword(string $username, string $password): ?array
    {
        $user = Db::name('user')
            ->where('username', $username)
            ->where('status', 1)
            ->find();

        if ($user && password_verify($password, $user['password'])) {
            return $user;
        }
        return null;
    }

    /**
     * 根据用户ID获取用户信息
     *
     * @param int $userId 用户ID
     * @return array|null 用户信息
     */
    public function getUserById(int $userId): ?array
    {
        return Db::name('user')->find($userId);
    }

    /**
     * 检查用户是否有指定权限
     *
     * @param int $userId 用户ID
     * @param string $permission 权限码
     * @return bool
     */
    public function hasPermission(int $userId, string $permission): bool
    {
        $permissions = $this->getAllPermissions($userId);
        return in_array($permission, $permissions);
    }

    /**
     * 检查用户是否有指定角色
     *
     * @param int $userId 用户ID
     * @param string $role 角色码
     * @return bool
     */
    public function hasRole(int $userId, string $role): bool
    {
        $roles = $this->getRoleList($userId);
        return in_array($role, $roles);
    }
}

3.1.5 第三步:创建服务提供者 app/provider/SaTokenProvider.php

php
<?php

declare(strict_types=1);

namespace app\provider;

use think\Service;
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoRedis;
use SaToken\StpUtil;
use app\service\PermissionService;

class SaTokenProvider extends Service
{
    public function register(): void
    {
        // 1. 加载配置
        $config = config('sa_token');

        // 2. 初始化 Sa-Token
        SaToken::init($config);

        // 3. 设置 Redis 存储驱动
        if ($config['storage'] === 'redis') {
            SaToken::setDao(new SaTokenDaoRedis($config['redis']));
        }

        // 4. 注入权限提供者
        $this->injectPermissionGetter();

        // 5. 注册事件监听器(可选)
        $this->registerListeners();
    }

    protected function injectPermissionGetter(): void
    {
        $service = new PermissionService();

        // 设置权限获取回调
        StpUtil::setPermissionGetter(function($loginId) use ($service) {
            return $service->getAllPermissions((int) $loginId);
        });

        // 设置角色获取回调
        StpUtil::setRoleGetter(function($loginId) use ($service) {
            return $service->getRoleList((int) $loginId);
        });
    }

    protected function registerListeners(): void
    {
        // 注册事件监听器
        // SaToken::addListener(new MySaTokenListener());
    }
}

3.1.6 第四步:创建中间件 app/middleware/SaTokenMiddleware.php

php
<?php

declare(strict_types=1);

namespace app\middleware;

use SaToken\SaRouter;
use SaToken\StpUtil;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use SaToken\Exception\DisableServiceException;
use think\Response;
use think\facade\Log;

class SaTokenMiddleware
{
    public function handle($request, \Closure $next)
    {
        try {
            // ========== 定义路由拦截规则 ==========

            // 1. 登录校验:所有 /api/** 接口需要登录
            // 排除登录、注册、刷新接口
            SaRouter::match('/api/**')
                ->exclude('/api/login', '/api/register', '/api/refresh')
                ->check(function() {
                    StpUtil::checkLogin();
                });

            // 2. 角色校验:/admin/** 需要 admin 角色
            SaRouter::match('/admin/**')
                ->check(function() {
                    StpUtil::checkLogin();
                    StpUtil::checkRole('admin');
                });

            // 3. 权限校验:/api/user/** 需要 user:read 权限
            SaRouter::match('/api/user/**')
                ->check(function() {
                    StpUtil::checkPermission('user:read');
                });

            // 4. 多权限校验:/api/order/** 需要 order:manage 或 order:view
            SaRouter::match('/api/order/**')
                ->check(function() {
                    StpUtil::checkPermissionOr(['order:manage', 'order:view']);
                });

            // 5. GET 请求特殊校验:/api/search/** 需要登录
            SaRouter::match('/api/search/**', 'GET')
                ->check(function() {
                    StpUtil::checkLogin();
                });

            // 6. POST 请求特殊校验:/api/upload/** 需要 upload 权限
            SaRouter::match('/api/upload/**', 'POST')
                ->check(function() {
                    StpUtil::checkPermission('upload');
                });

        } catch (NotLoginException $e) {
            return $this->error(401, '未登录或登录已过期', ['type' => $e->getType()]);
        } catch (NotPermissionException $e) {
            return $this->error(403, '权限不足:' . $e->getPermission());
        } catch (NotRoleException $e) {
            return $this->error(403, '角色不足:' . $e->getRole());
        } catch (DisableServiceException $e) {
            $remaining = StpUtil::getDisableTime();
            return $this->error(403, "账号已被封禁,剩余 {$remaining} 秒");
        } catch (\Exception $e) {
            Log::error('SaToken 鉴权异常:' . $e->getMessage());
            return $this->error(500, '服务器内部错误');
        }

        return $next($request);
    }

    protected function error(int $code, string $msg, array $extra = []): Response
    {
        return json([
            'code' => $code,
            'msg' => $msg,
            'data' => null,
            'extra' => $extra
        ], $code);
    }
}

3.1.7 第五步:注册服务提供者 config/service.php

php
<?php

return [
    'providers' => [
        \app\provider\SaTokenProvider::class,
    ],
];

3.1.8 第六步:注册中间件 config/middleware.php

php
<?php

return [
    // 全局中间件
    'global' => [
        // ...
    ],

    // 别名中间件(用于路由注册)
    'alias' => [
        'sa-token' => \app\middleware\SaTokenMiddleware::class,
    ],
];

3.1.9 第七步:配置路由 route/app.php

php
<?php

use think\facade\Route;

// ========== 公开路由(无需鉴权) ==========
Route::post('api/login', 'Auth/login');
Route::post('api/register', 'Auth/register');
Route::post('api/refresh', 'Auth/refresh');

// ========== 需要鉴权的路由(使用中间件) ==========
Route::group('api', function () {
    Route::get('user/info', 'Auth/info');
    Route::post('user/logout', 'Auth/logout');
    Route::resource('users', 'User');
})->middleware('sa-token');

// ========== 后台管理路由 ==========
Route::group('admin', function () {
    Route::get('/', 'Admin/index');
    Route::get('/users', 'Admin/users');
    Route::get('/settings', 'Admin/settings');
})->middleware('sa-token');

// ========== 公开API ==========
Route::get('api/public', 'Public/index');

3.1.10 第八步:环境变量配置 .env

bash
# Redis 配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# Sa-Token 配置
SA_TOKEN_TIMEOUT=86400
SA_TOKEN_REFRESH_TIMEOUT=2592000

# JWT 配置
JWT_SECRET_KEY=your-jwt-secret

# 签名配置
SIGN_KEY=your-sign-key

# 调试模式(生产环境设为 false)
APP_DEBUG=false

3.1.11 第九步:创建数据库表结构

sql
-- 用户表
CREATE TABLE `user` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL UNIQUE,
    `password` VARCHAR(255) NOT NULL,
    `nickname` VARCHAR(100),
    `email` VARCHAR(100),
    `avatar` VARCHAR(255),
    `status` TINYINT DEFAULT 1,
    `last_login` INT,
    `last_ip` VARCHAR(45),
    `create_time` INT,
    `update_time` INT,
    INDEX `idx_username` (`username`),
    INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 用户权限表
CREATE TABLE `user_permission` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `user_id` INT NOT NULL,
    `permission_code` VARCHAR(50) NOT NULL,
    `create_time` INT,
    UNIQUE KEY `uk_user_perm` (`user_id`, `permission_code`),
    INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户权限表';

-- 角色表
CREATE TABLE `role` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `role_code` VARCHAR(50) NOT NULL UNIQUE,
    `role_name` VARCHAR(100) NOT NULL,
    `description` VARCHAR(255),
    `create_time` INT,
    INDEX `idx_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 用户角色关联表
CREATE TABLE `user_role` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `user_id` INT NOT NULL,
    `role_id` INT NOT NULL,
    `create_time` INT,
    UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
    INDEX `idx_user_id` (`user_id`),
    INDEX `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色权限表
CREATE TABLE `role_permission` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `role_code` VARCHAR(50) NOT NULL,
    `permission_code` VARCHAR(50) NOT NULL,
    `create_time` INT,
    UNIQUE KEY `uk_role_perm` (`role_code`, `permission_code`),
    INDEX `idx_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限表';

-- 审计日志表
CREATE TABLE `audit_log` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `user_id` INT,
    `event_type` VARCHAR(50) NOT NULL,
    `ip` VARCHAR(45),
    `user_agent` VARCHAR(255),
    `details` JSON,
    `create_time` INT,
    INDEX `idx_user_id` (`user_id`),
    INDEX `idx_event_type` (`event_type`),
    INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志表';

3.2 单应用 + 内存模式

3.2.1 场景说明

适用于开发测试环境,使用内存作为存储介质,无需 Redis。

3.2.2 配置文件 config/sa_token.php

php
<?php

return [
    // ==================== Token 基础配置 ====================
    'tokenName' => 'satoken',
    'tokenPrefix' => 'Bearer',
    'tokenStyle' => 'uuid',

    // ==================== 有效期配置 ====================
    'timeout' => 86400,
    'activityTimeout' => -1,

    // ==================== Cookie 配置 ====================
    'cookieName' => 'satoken',
    'cookieDomain' => '',
    'cookiePath' => '/',
    'cookieSecure' => false,
    'cookieHttpOnly' => true,
    'cookieSameSite' => 'Strict',
    'cookieMaxAge' => 86400,

    // ==================== 读取/写入配置 ====================
    'isReadHeader' => true,
    'isReadCookie' => true,
    'isReadBody' => false,
    'isWriteCookie' => true,
    'isWriteHeader' => false,

    // ==================== 登录策略 ====================
    'concurrent' => true,
    'isShare' => true,
    'maxLoginCount' => 12,

    // ==================== 存储配置(内存模式) ====================
    'storage' => 'memory',

    // ==================== Session 配置 ====================
    'session' => [
        'timeout' => 86400,
        'activityTimeout' => 1800,
        'maxSize' => 1024,
    ],

    // ==================== 加密配置 ====================
    'cryptoType' => 'intl',
    'tokenEncrypt' => false,
    'tokenFingerprint' => false,

    // ==================== Refresh Token ====================
    'refreshToken' => false,
    'refreshTokenTimeout' => 2592000,

    // ==================== 安全配置 ====================
    'antiBruteMaxFailures' => 5,
    'antiBruteLockDuration' => 600,
    'ipAnomalyDetection' => true,
    'deviceManagement' => true,
    'auditLog' => true,

    // ==================== JWT 配置 ====================
    'jwtSecretKey' => '',
    'jwtStateless' => false,

    // ==================== 签名配置 ====================
    'signKey' => '',
    'signTimestampGap' => 600,
    'signAlg' => 'sha256',

    'isLog' => true,
];

3.2.3 服务提供者 app/provider/SaTokenProvider.php(内存模式)

php
<?php

declare(strict_types=1);

namespace app\provider;

use think\Service;
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoMemory;
use SaToken\StpUtil;
use app\service\PermissionService;

class SaTokenProvider extends Service
{
    public function register(): void
    {
        $config = config('sa_token');
        SaToken::init($config);

        // ========== 使用内存存储 ==========
        // 所有会话数据存储在 PHP 内存中
        // 应用重启后数据全部丢失
        SaToken::setDao(new SaTokenDaoMemory());

        $service = new PermissionService();
        StpUtil::setPermissionGetter(function($loginId) use ($service) {
            return $service->getAllPermissions((int) $loginId);
        });
        StpUtil::setRoleGetter(function($loginId) use ($service) {
            return $service->getRoleList((int) $loginId);
        });
    }
}

3.2.4 内存模式注意事项

php
// ========== 内存模式特点 ==========
// 1. 应用重启后所有会话丢失,用户需重新登录
// 2. 不支持分布式部署
// 3. 适合开发测试环境
// 4. 数据存储在 SaTokenDaoMemory 类的静态属性中
// 5. 进程内共享,进程重启丢失

// ========== 内存模式适用场景 ==========
// - 本地开发调试
// - 单元测试
// - 演示/原型项目
// - 低并发内部工具

其余文件(app/service/PermissionService.phpapp/middleware/SaTokenMiddleware.phpconfig/service.phpconfig/middleware.phproute/app.php)与 3.1 节相同。

3.3 多应用 + Redis 模式

3.3.1 场景说明

适用于微服务架构模块化中大型项目,多个应用共享 Redis 存储。

3.3.2 启用多应用模式

bash
# 安装多应用扩展
composer require topthink/think-multi-app

# 生成应用目录
php think build

3.3.3 目录结构

text
tp8-sa-token/
├── app/
│   ├── common/
│   │   ├── middleware/
│   │   │   └── SaTokenMiddleware.php
│   │   ├── service/
│   │   │   └── PermissionService.php
│   │   ├── exception/
│   │   │   └── Handler.php
│   │   └── provider/
│   │       └── SaTokenProvider.php
│   ├── api/
│   │   ├── controller/
│   │   │   └── AuthController.php
│   │   ├── config/
│   │   │   └── sa_token.php
│   │   └── route/
│   │       └── app.php
│   ├── admin/
│   │   ├── controller/
│   │   │   └── AuthController.php
│   │   ├── config/
│   │   │   └── sa_token.php
│   │   └── route/
│   │       └── app.php
│   └── provider.php
├── config/
│   └── sa_token.php
└── .env

3.3.4 全局配置 config/sa_token.php

php
<?php

return [
    // ==================== 通用配置 ====================
    'tokenName' => 'satoken',
    'tokenPrefix' => 'Bearer',
    'tokenStyle' => 'uuid',

    // ==================== Cookie 通用配置 ====================
    'cookieDomain' => '',
    'cookiePath' => '/',
    'cookieSecure' => false,
    'cookieHttpOnly' => true,
    'cookieSameSite' => 'Strict',

    // ==================== 存储配置(共享 Redis) ====================
    'storage' => 'redis',
    'redis' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'port' => env('REDIS_PORT', 6379),
        'password' => env('REDIS_PASSWORD', ''),
        'db' => env('REDIS_DB', 0),
        'timeout' => 3,
        // 全局前缀由各应用配置覆盖
    ],

    // ==================== Session 配置(共享) ====================
    'session' => [
        'timeout' => 86400,
        'activityTimeout' => 1800,
        'maxSize' => 1024,
    ],

    // ==================== 安全配置(全局通用) ====================
    'antiBruteMaxFailures' => 5,
    'antiBruteLockDuration' => 600,
    'ipAnomalyDetection' => true,
    'deviceManagement' => true,
    'auditLog' => true,

    // ==================== 各应用差异化配置 ====================
    'apps' => [
        'api' => [
            // API Token 有效期 2 小时
            'timeout' => 7200,
            'refreshToken' => true,
            'refreshTokenTimeout' => 2592000,
            'isReadCookie' => false,
            'isWriteCookie' => false,
            'isReadHeader' => true,
            'isWriteHeader' => true,
            'tokenFingerprint' => true,
            'redis' => ['prefix' => 'sa_token_api_'],
        ],
        'admin' => [
            // Admin Token 有效期 8 小时
            'timeout' => 28800,
            'refreshToken' => true,
            'concurrent' => false,
            'isShare' => false,
            'isReadCookie' => true,
            'isWriteCookie' => true,
            'cookiePath' => '/admin',
            'cookieSecure' => true,
            'tokenFingerprint' => true,
            'redis' => ['prefix' => 'sa_token_admin_'],
        ],
    ],
];

3.3.5 API 应用专属配置 app/api/config/sa_token.php

php
<?php

return [
    'timeout' => 7200,
    'refreshToken' => true,
    'refreshTokenTimeout' => 2592000,
    'refreshTokenRotation' => true,
    'isReadCookie' => false,
    'isWriteCookie' => false,
    'isReadHeader' => true,
    'isWriteHeader' => true,
    'concurrent' => true,
    'tokenFingerprint' => true,
    'redis' => [
        'prefix' => 'sa_token_api_',
    ],
];

3.3.6 Admin 应用专属配置 app/admin/config/sa_token.php

php
<?php

return [
    'timeout' => 28800,
    'refreshToken' => true,
    'concurrent' => false,
    'isShare' => false,
    'isReadCookie' => true,
    'isWriteCookie' => true,
    'cookiePath' => '/admin',
    'cookieSecure' => true,
    'tokenFingerprint' => true,
    'redis' => [
        'prefix' => 'sa_token_admin_',
    ],
];

3.3.7 公共服务提供者 app/common/provider/SaTokenProvider.php

php
<?php

declare(strict_types=1);

namespace app\common\provider;

use think\Service;
use SaToken\SaToken;
use SaToken\StpUtil;
use SaToken\Dao\SaTokenDaoRedis;
use app\common\service\PermissionService;

class SaTokenProvider extends Service
{
    public function register(): void
    {
        $baseConfig = config('sa_token');
        $appName = app()->http->getName() ?: 'api';

        // 获取应用专属配置
        $appConfig = $this->getAppConfig($appName, $baseConfig);

        SaToken::init($appConfig);

        // ========== 使用 Redis 存储 ==========
        $redisConfig = $appConfig['redis'] ?? $baseConfig['redis'] ?? [];
        SaToken::setDao(new SaTokenDaoRedis($redisConfig));

        $this->injectPermission($appName);
    }

    protected function getAppConfig(string $appName, array $baseConfig): array
    {
        $appConfigFile = app()->getAppPath() . $appName . '/config/sa_token.php';
        if (is_file($appConfigFile)) {
            $appConfig = include $appConfigFile;
            return array_merge($baseConfig, $appConfig);
        }

        if (isset($baseConfig['apps'][$appName])) {
            return array_merge($baseConfig, $baseConfig['apps'][$appName]);
        }

        return $baseConfig;
    }

    protected function injectPermission(string $appName): void
    {
        // 不同应用使用不同的权限服务
        $service = match ($appName) {
            'admin' => new \app\admin\service\AdminPermissionService(),
            'api'   => new \app\api\service\ApiPermissionService(),
            default => new PermissionService(),
        };

        StpUtil::setPermissionGetter(function($loginId) use ($service) {
            return $service->getAllPermissions((int) $loginId);
        });

        StpUtil::setRoleGetter(function($loginId) use ($service) {
            return $service->getRoleList((int) $loginId);
        });
    }
}

3.3.8 公共中间件 app/common/middleware/SaTokenMiddleware.php

php
<?php

declare(strict_types=1);

namespace app\common\middleware;

use SaToken\SaRouter;
use SaToken\StpUtil;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use think\Response;
use think\facade\Log;

class SaTokenMiddleware
{
    protected array $appRoutes = [
        'api' => [
            'match' => '/api/**',
            'exclude' => ['/api/login', '/api/register', '/api/refresh'],
            'check' => 'login',
        ],
        'admin' => [
            'match' => '/admin/**',
            'exclude' => ['/admin/login'],
            'check' => ['login', 'role:admin'],
        ],
    ];

    public function handle($request, \Closure $next)
    {
        try {
            $appName = app()->http->getName() ?: 'api';
            $rules = $this->appRoutes[$appName] ?? $this->appRoutes['api'];

            $router = SaRouter::match($rules['match']);
            if (!empty($rules['exclude'])) {
                $router->exclude(...$rules['exclude']);
            }

            $router->check(function() use ($rules) {
                $checks = is_array($rules['check']) ? $rules['check'] : [$rules['check']];
                foreach ($checks as $check) {
                    if ($check === 'login') {
                        StpUtil::checkLogin();
                    } elseif (str_starts_with($check, 'role:')) {
                        StpUtil::checkRole(substr($check, 5));
                    } elseif (str_starts_with($check, 'perm:')) {
                        StpUtil::checkPermission(substr($check, 5));
                    }
                }
            });

        } catch (NotLoginException $e) {
            return json(['code' => 401, 'msg' => '请先登录'], 401);
        } catch (NotPermissionException $e) {
            return json(['code' => 403, 'msg' => '权限不足:' . $e->getPermission()], 403);
        } catch (NotRoleException $e) {
            return json(['code' => 403, 'msg' => '角色不足:' . $e->getRole()], 403);
        } catch (\Exception $e) {
            Log::error('SaToken 鉴权异常:' . $e->getMessage());
            return json(['code' => 500, 'msg' => '服务器内部错误'], 500);
        }

        return $next($request);
    }
}

3.3.9 注册服务提供者 app/provider.php

php
<?php

return [
    'providers' => [
        \app\common\provider\SaTokenProvider::class,
    ],
];

3.3.10 API 应用路由 app/api/route/app.php

php
<?php

use think\facade\Route;

Route::post('login', 'Auth/login');
Route::post('register', 'Auth/register');
Route::post('refresh', 'Auth/refresh');

Route::group(function () {
    Route::get('user/info', 'Auth/info');
    Route::post('user/logout', 'Auth/logout');
    Route::resource('users', 'User');
})->middleware('sa-token');

3.3.11 Admin 应用路由 app/admin/route/app.php

php
<?php

use think\facade\Route;

Route::post('login', 'Auth/login');

Route::group(function () {
    Route::get('dashboard', 'Admin/dashboard');
    Route::get('users', 'Admin/users');
    Route::get('settings', 'Admin/settings');
})->middleware('sa-token');

3.3.12 API 权限服务 app/api/service/ApiPermissionService.php

php
<?php

declare(strict_types=1);

namespace app\api\service;

use think\facade\Db;

class ApiPermissionService
{
    public function getPermissionList(int $userId): array
    {
        return Db::name('user_permission')
            ->where('user_id', $userId)
            ->column('permission_code') ?: [];
    }

    public function getRoleList(int $userId): array
    {
        return Db::name('user_role')
            ->alias('ur')
            ->join('role r', 'ur.role_id = r.id')
            ->where('ur.user_id', $userId)
            ->column('r.role_code') ?: [];
    }

    public function getAllPermissions(int $userId): array
    {
        return $this->getPermissionList($userId);
    }

    public function verifyPassword(string $username, string $password): ?array
    {
        $user = Db::name('user')
            ->where('username', $username)
            ->where('status', 1)
            ->find();

        if ($user && password_verify($password, $user['password'])) {
            return $user;
        }
        return null;
    }
}

3.3.13 Admin 权限服务 app/admin/service/AdminPermissionService.php

php
<?php

declare(strict_types=1);

namespace app\admin\service;

use think\facade\Db;

class AdminPermissionService
{
    public function getPermissionList(int $adminId): array
    {
        $perms = Db::name('admin_permission')
            ->where('admin_id', $adminId)
            ->column('permission_code');
        return $perms ?: [];
    }

    public function getRoleList(int $adminId): array
    {
        $roles = Db::name('admin_role')
            ->alias('ar')
            ->join('role r', 'ar.role_id = r.id')
            ->where('ar.admin_id', $adminId)
            ->column('r.role_code');
        return $roles ?: [];
    }

    public function getAllPermissions(int $adminId): array
    {
        $roles = $this->getRoleList($adminId);
        $perms = [];

        if (!empty($roles)) {
            $rolePerms = Db::name('role_permission')
                ->whereIn('role_code', $roles)
                ->column('permission_code');
            $perms = array_merge($perms, $rolePerms);
        }

        $userPerms = $this->getPermissionList($adminId);
        $perms = array_merge($perms, $userPerms);

        return array_unique($perms);
    }

    public function verifyPassword(string $username, string $password): ?array
    {
        $user = Db::name('admin')
            ->where('username', $username)
            ->where('status', 1)
            ->find();

        if ($user && password_verify($password, $user['password'])) {
            return $user;
        }
        return null;
    }
}

3.4 多应用 + 内存模式

3.4.1 全局配置 config/sa_token.php

php
<?php

return [
    'tokenName' => 'satoken',
    'tokenPrefix' => 'Bearer',
    'tokenStyle' => 'uuid',
    'cookieDomain' => '',
    'cookiePath' => '/',
    'cookieSecure' => false,
    'cookieHttpOnly' => true,
    'cookieSameSite' => 'Strict',

    // ========== 存储配置(内存模式) ==========
    'storage' => 'memory',

    'session' => [
        'timeout' => 86400,
        'activityTimeout' => 1800,
        'maxSize' => 1024,
    ],
    'antiBruteMaxFailures' => 5,
    'antiBruteLockDuration' => 600,
    'ipAnomalyDetection' => true,
    'deviceManagement' => true,
    'auditLog' => true,

    'apps' => [
        'api' => [
            'timeout' => 7200,
            'refreshToken' => true,
            'isReadCookie' => false,
            'isWriteCookie' => false,
            'isReadHeader' => true,
            'isWriteHeader' => true,
            'tokenFingerprint' => true,
        ],
        'admin' => [
            'timeout' => 28800,
            'refreshToken' => true,
            'concurrent' => false,
            'isShare' => false,
            'cookiePath' => '/admin',
            'tokenFingerprint' => true,
        ],
    ],
];

3.4.2 公共服务提供者 app/common/provider/SaTokenProvider.php(内存模式)

php
<?php

declare(strict_types=1);

namespace app\common\provider;

use think\Service;
use SaToken\SaToken;
use SaToken\StpUtil;
use SaToken\Dao\SaTokenDaoMemory;
use app\common\service\PermissionService;

class SaTokenProvider extends Service
{
    public function register(): void
    {
        $baseConfig = config('sa_token');
        $appName = app()->http->getName() ?: 'api';

        $appConfig = $this->getAppConfig($appName, $baseConfig);
        SaToken::init($appConfig);

        // ========== 使用内存存储 ==========
        SaToken::setDao(new SaTokenDaoMemory());

        $this->injectPermission($appName);
    }

    protected function getAppConfig(string $appName, array $baseConfig): array
    {
        $appConfigFile = app()->getAppPath() . $appName . '/config/sa_token.php';
        if (is_file($appConfigFile)) {
            return array_merge($baseConfig, include $appConfigFile);
        }
        if (isset($baseConfig['apps'][$appName])) {
            return array_merge($baseConfig, $baseConfig['apps'][$appName]);
        }
        return $baseConfig;
    }

    protected function injectPermission(string $appName): void
    {
        $service = match ($appName) {
            'admin' => new \app\admin\service\AdminPermissionService(),
            'api'   => new \app\api\service\ApiPermissionService(),
            default => new PermissionService(),
        };

        StpUtil::setPermissionGetter(function($loginId) use ($service) {
            return $service->getAllPermissions((int) $loginId);
        });
        StpUtil::setRoleGetter(function($loginId) use ($service) {
            return $service->getRoleList((int) $loginId);
        });
    }
}

3.5 四种模式完整对比

对比项单应用+Redis单应用+内存多应用+Redis多应用+内存
Redis 依赖✅ 需要❌ 不需要✅ 需要❌ 不需要
数据持久化
分布式支持
多应用支持
应用间共享会话N/AN/A
部署复杂度
性能极高极高
推荐环境生产开发/测试生产/微服务开发/小型项目

四、登录认证

4.1 创建认证控制器 app/controller/AuthController.php

php
<?php

declare(strict_types=1);

namespace app\controller;

use think\facade\Db;
use SaToken\StpUtil;
use app\service\PermissionService;
use think\response\Json;
use SaToken\Security\SaAuditLog;

class AuthController
{
    protected PermissionService $permissionService;

    public function __construct(PermissionService $permissionService)
    {
        $this->permissionService = $permissionService;
    }

    /**
     * 用户登录 - 完整流程演示
     */
    public function login(): Json
    {
        $username = input('post.username', '');
        $password = input('post.password', '');
        $device = input('post.device', 'web');
        $remember = input('post.remember', false);

        if (empty($username) || empty($password)) {
            return json(['code' => 400, 'msg' => '用户名和密码不能为空']);
        }

        // ========== 防暴力破解检查 ==========
        StpUtil::checkAntiBrute($username);

        // ========== 验证用户凭证 ==========
        $user = $this->permissionService->verifyPassword($username, $password);
        if (!$user) {
            StpUtil::recordAntiBruteFailure($username);
            $info = StpUtil::getAntiBruteInfo($username);
            return json([
                'code' => 401,
                'msg' => '用户名或密码错误',
                'remaining' => $info['remainingAttempts'] ?? 0
            ]);
        }

        // 登录成功,清除失败记录
        StpUtil::clearAntiBruteFailure($username);

        // ========== 执行登录 ==========
        // Sa-Token 登录,一行代码搞定
        // 内部自动完成:生成Token、绑定LoginId、创建Session、写入Cookie、触发事件
        StpUtil::login((string) $user['id'], [
            'device' => $device,
            'isLasting' => $remember,
            'extra' => [
                'nickname' => $user['nickname'],
                'ip' => request()->ip(),
                'login_time' => time(),
            ]
        ]);

        // ========== 登录后操作 ==========
        // 1. 存储用户信息到 Session
        StpUtil::setSession('user_info', [
            'id' => $user['id'],
            'username' => $user['username'],
            'nickname' => $user['nickname'],
            'email' => $user['email'] ?? '',
        ]);

        // 2. 存储权限信息到 Session(避免每次查询数据库)
        $permissions = $this->permissionService->getAllPermissions((int) $user['id']);
        $roles = $this->permissionService->getRoleList((int) $user['id']);
        StpUtil::setSession('permissions', $permissions);
        StpUtil::setSession('roles', $roles);

        // 3. 更新数据库登录信息
        Db::name('user')
            ->where('id', $user['id'])
            ->update([
                'last_login' => time(),
                'last_ip' => request()->ip(),
            ]);

        // 4. 记录审计日志
        SaAuditLog::logLogin($user['id'], [
            'ip' => request()->ip(),
            'device' => $device,
        ]);

        // ========== 返回结果 ==========
        return json([
            'code' => 200,
            'msg' => '登录成功',
            'data' => [
                'token' => StpUtil::getTokenValue(),
                'tokenPrefix' => config('sa_token.tokenPrefix'),
                'userId' => $user['id'],
                'nickname' => $user['nickname'],
                'expires' => StpUtil::getTokenTimeout(),
                'permissions' => $permissions,
                'roles' => $roles,
            ]
        ]);
    }

    /**
     * 退出登录
     */
    public function logout(): Json
    {
        // 获取用户信息(用于审计)
        $userId = StpUtil::getLoginId(false);

        // 执行登出
        StpUtil::logout();

        // 记录审计日志
        if ($userId) {
            SaAuditLog::logLogout($userId);
        }

        return json([
            'code' => 200,
            'msg' => '退出成功'
        ]);
    }

    /**
     * 获取当前用户信息
     */
    public function info(): Json
    {
        StpUtil::checkLogin();

        $userId = StpUtil::getLoginId();

        // 从 Session 读取用户信息
        $userInfo = StpUtil::getSession('user_info');
        if (empty($userInfo)) {
            // Session 中没有,从数据库读取
            $user = Db::name('user')->find($userId);
            if (!$user) {
                return json(['code' => 404, 'msg' => '用户不存在']);
            }
            $userInfo = $user;
        }

        // 获取权限和角色
        $permissions = StpUtil::getPermissionList();
        $roles = StpUtil::getRoleList();

        return json([
            'code' => 200,
            'data' => [
                'id' => $userId,
                'username' => $userInfo['username'] ?? '',
                'nickname' => $userInfo['nickname'] ?? '',
                'email' => $userInfo['email'] ?? '',
                'permissions' => $permissions,
                'roles' => $roles,
                'token' => StpUtil::getTokenValue(),
                'tokenTimeout' => StpUtil::getTokenTimeout(),
                'loginDevice' => StpUtil::getLoginDevice(),
                'extra' => StpUtil::getExtra(),
            ]
        ]);
    }

    /**
     * 刷新 Token(启用 RefreshToken 时使用)
     */
    public function refresh(): Json
    {
        if (!config('sa_token.refreshToken')) {
            return json(['code' => 400, 'msg' => 'RefreshToken 未启用']);
        }

        try {
            $newToken = StpUtil::refreshToken();
            return json([
                'code' => 200,
                'msg' => 'Token 刷新成功',
                'data' => [
                    'token' => $newToken,
                    'expires' => StpUtil::getTokenTimeout(),
                ]
            ]);
        } catch (\Exception $e) {
            return json([
                'code' => 401,
                'msg' => '刷新失败:' . $e->getMessage()
            ]);
        }
    }

    /**
     * 注册账号
     */
    public function register(): Json
    {
        $username = input('post.username', '');
        $password = input('post.password', '');
        $nickname = input('post.nickname', '');

        if (empty($username) || empty($password)) {
            return json(['code' => 400, 'msg' => '用户名和密码不能为空']);
        }

        if (strlen($password) < 6) {
            return json(['code' => 400, 'msg' => '密码长度不能少于6位']);
        }

        $exist = Db::name('user')->where('username', $username)->find();
        if ($exist) {
            return json(['code' => 400, 'msg' => '用户名已存在']);
        }

        $userId = Db::name('user')->insertGetId([
            'username' => $username,
            'password' => password_hash($password, PASSWORD_BCRYPT),
            'nickname' => $nickname ?: $username,
            'status' => 1,
            'create_time' => time(),
            'update_time' => time(),
        ]);

        StpUtil::login((string) $userId);
        $token = StpUtil::getTokenValue();

        return json([
            'code' => 200,
            'msg' => '注册成功',
            'data' => [
                'userId' => $userId,
                'token' => $token,
            ]
        ]);
    }
}

4.2 登录参数详解

php
use SaToken\SaLoginParameter;

// ========== 方式一:数组参数 ==========
StpUtil::login(10001, [
    'device' => 'web',          // 设备标识,用于同端互斥
    'isLasting' => true,        // 记住我,延长有效期
    'timeout' => 3600,          // 自定义有效期(秒),覆盖全局配置
    'extra' => [                // 扩展数据,可通过 StpUtil::getExtra() 获取
        'nickname' => '张三',
        'avatar' => 'https://...',
        'role' => 'admin',
    ]
]);

// ========== 方式二:登录参数对象 ==========
$param = SaLoginParameter::create()
    ->setDevice('mobile')
    ->setIsLasting(true)
    ->setTimeout(7200)
    ->setExtra([
        'nickname' => '张三',
        'app_version' => '2.3.1',
    ]);

StpUtil::login(10001, $param);

// ========== 方式三:简单登录(使用默认参数) ==========
StpUtil::login(10001);

4.3 获取当前登录信息

php
// ========== 基础信息 ==========
// 获取当前登录用户 ID
$userId = StpUtil::getLoginId();

// 获取当前 Token 值
$token = StpUtil::getTokenValue();

// 获取 Token 剩余有效期(秒)
$timeout = StpUtil::getTokenTimeout();

// 获取 Token 创建时间
$createTime = StpUtil::getTokenCreateTime();

// 获取 Token 上次活动时间
$lastActivity = StpUtil::getTokenLastActivityTime();

// ========== 扩展信息 ==========
// 获取 Token 扩展信息
$extra = StpUtil::getExtra();
$nickname = StpUtil::getExtra('nickname');

// 更新扩展信息
StpUtil::updateExtra(['nickname' => '李四']);
StpUtil::updateExtra('level', 5);

// ========== 设备信息 ==========
// 获取当前登录设备
$device = StpUtil::getLoginDevice();

// ========== 登录状态 ==========
// 判断是否登录
if (StpUtil::isLogin()) {
    echo '已登录,用户ID:' . StpUtil::getLoginId();
}

// 获取登录类型
$loginType = StpUtil::getLoginType();

// ========== 完整 Token 信息 ==========
$tokenInfo = StpUtil::getTokenInfo();
// [
//     'tokenValue' => 'xxx',
//     'loginId' => 10001,
//     'loginType' => 'login',
//     'device' => 'web',
//     'timeout' => 86400,
//     'createTime' => 1700000000,
//     'lastActivity' => 1700003600,
//     'extra' => ['nickname' => '张三'],
// ]

4.4 Token 操作

php
// ========== Token 刷新 ==========
// 刷新当前 Token(延长有效期)
$newToken = StpUtil::refreshToken();

// 刷新指定用户的 Token
$newToken = StpUtil::refreshToken(10001);

// ========== Token 检查 ==========
// 检查 Token 是否有效
$valid = StpUtil::checkToken($tokenValue);

// 获取 Token 对应的 LoginId
$loginId = StpUtil::getLoginIdByToken($tokenValue);

// ========== Token 替换 ==========
// 替换 Token(生成新 Token,旧 Token 失效)
$newToken = StpUtil::replaceToken();

// ========== Token 黑名单 ==========
// 加入黑名单
StpUtil::addTokenToBlacklist($tokenValue);

// 移除黑名单
StpUtil::removeTokenFromBlacklist($tokenValue);

// 判断是否在黑名单
$isBlacklisted = StpUtil::isTokenBlacklisted($tokenValue);

4.5 踢人下线

php
// ========== 踢人操作 ==========
// 踢指定用户下线(所有设备)
StpUtil::kickout(10001);

// 踢指定用户的指定设备下线
StpUtil::kickout(10001, 'mobile');

// 踢当前用户下线(退出登录)
StpUtil::kickout();

// ========== 获取被踢信息 ==========
$info = StpUtil::getKickoutInfo();
// [
//     'isKickout' => true,
//     'reason' => '管理员强制下线',
//     'time' => 1700000000,
// ]

// ========== 批量踢人 ==========
$userIds = [10001, 10002, 10003];
foreach ($userIds as $userId) {
    StpUtil::kickout($userId);
}

4.6 账号封禁

php
// ========== 封禁操作 ==========
// 封禁账号(禁止登录),默认永久
StpUtil::disable(10001);

// 封禁指定时长(秒)
StpUtil::disable(10001, 3600);

// 封禁并指定原因
StpUtil::disable(10001, 86400, '违规操作');

// 封禁特定服务(如:评论、发帖)
StpUtil::disableService(10001, 'comment', 3600, '违规评论');

// ========== 检查封禁 ==========
// 判断账号是否被封禁
if (StpUtil::isDisable(10001)) {
    echo '账号已被封禁';
}

// 判断账号是否被封禁特定服务
if (StpUtil::isDisableService(10001, 'comment')) {
    echo '评论功能被封禁';
}

// ========== 获取封禁信息 ==========
// 获取封禁剩余时间
$remaining = StpUtil::getDisableTime(10001);

// 获取封禁原因
$reason = StpUtil::getDisableReason(10001);

// 获取服务封禁剩余时间
$remaining = StpUtil::getDisableServiceTime(10001, 'comment');

// ========== 解封 ==========
// 解封账号
StpUtil::untieDisable(10001);

// 解封特定服务
StpUtil::untieDisableService(10001, 'comment');

4.7 多账号体系

php
use SaToken\StpLogic;

// ========== 创建不同账号体系 ==========
$adminStp = new StpLogic('admin');
$userStp = new StpLogic('user');
$guestStp = new StpLogic('guest');

// ========== 管理员登录 ==========
$adminStp->login(10001);
$adminToken = $adminStp->getTokenValue();
$adminStp->checkRole('super_admin');

// ========== 用户登录 ==========
$userStp->login(20002);
$userStp->checkPermission('order:view');

// ========== 获取各自信息 ==========
$adminId = $adminStp->getLoginId();
$userId = $userStp->getLoginId();

// ========== 各体系独立配置 ==========
// 管理员体系:有效期 8 小时
$adminStp->setConfig('timeout', 28800);

// 用户体系:有效期 24 小时
$userStp->setConfig('timeout', 86400);

4.8 身份切换

php
// ========== 切换到指定用户 ==========
// 切换到用户 10002 的身份
StpUtil::switchTo(10002);

// 执行操作(此时所有校验都基于用户 10002)
$currentId = StpUtil::getLoginId();  // 返回 10002

// ========== 切回原身份 ==========
StpUtil::switchBack();

// ========== 切换状态检查 ==========
if (StpUtil::isSwitch()) {
    $originalId = StpUtil::getSwitchOriginalId();
    $currentId = StpUtil::getLoginId();
    echo "当前以 {$originalId} 的身份切换到 {$currentId}";
}

// ========== 临时切换并执行 ==========
StpUtil::switchTo(10002, function() {
    // 在切换状态下执行操作
    $id = StpUtil::getLoginId();  // 10002
    // 操作完成后自动切回
});

五、权限认证

5.1 权限校验方法

php
use SaToken\StpUtil;

// ========== 校验(不通过抛异常 NotPermissionException) ==========
// 必须有此权限
StpUtil::checkPermission('user:add');

// 必须同时拥有多个权限
StpUtil::checkPermissionAnd(['user:add', 'user:edit']);

// 拥有任意一个权限即可
StpUtil::checkPermissionOr(['user:add', 'user:delete']);

// ========== 判断(返回 bool) ==========
if (StpUtil::hasPermission('user:delete')) {
    // 拥有删除用户权限
}

if (StpUtil::hasPermissionAnd(['user:add', 'user:edit'])) {
    // 同时拥有两个权限
}

if (StpUtil::hasPermissionOr(['user:add', 'user:delete'])) {
    // 拥有任意一个权限
}

// ========== 获取所有权限 ==========
$permissions = StpUtil::getPermissionList();

// ========== 检查是否有任意权限 ==========
$has = StpUtil::hasAnyPermission(['user:add', 'user:edit']);

5.2 角色校验

php
// ========== 校验(不通过抛异常 NotRoleException) ==========
// 必须有此角色
StpUtil::checkRole('admin');

// 必须同时拥有多个角色
StpUtil::checkRoleAnd(['admin', 'editor']);

// 拥有任意一个角色
StpUtil::checkRoleOr(['admin', 'super']);

// ========== 判断(返回 bool) ==========
if (StpUtil::hasRole('admin')) {
    // 是管理员
}

if (StpUtil::hasRoleOr(['admin', 'editor'])) {
    // 有管理或编辑角色
}

// ========== 获取所有角色 ==========
$roles = StpUtil::getRoleList();

5.3 二级认证

php
// ========== 二级认证概述 ==========
// 二级认证用于敏感操作前的二次身份确认
// 如:修改密码、删除数据、转账等

// ========== 开启二级认证 ==========
// 在敏感操作前开启
StpUtil::openSafe();

// ========== 校验二级认证 ==========
// 不通过抛异常 NotSafeException
StpUtil::checkSafe();

// ========== 判断二级认证状态 ==========
if (StpUtil::isSafe()) {
    // 已通过二级认证,执行敏感操作
}

// ========== 二级认证 + 权限校验 ==========
// 需要二级认证且拥有权限
StpUtil::openSafeAndCheck('user:delete');

// ========== 关闭二级认证 ==========
StpUtil::closeSafe();

// ========== 获取二级认证信息 ==========
// 获取剩余时间
$timeout = StpUtil::getSafeTimeout();

// 获取二级认证状态
$status = StpUtil::getSafeStatus();

// ========== 带自定义过期时间的二级认证 ==========
StpUtil::openSafe(300);  // 5分钟有效期
StpUtil::checkSafe();

5.4 权限数据注入

php
// ========== 在服务提供者中注入权限获取逻辑 ==========
// 在 SaTokenProvider 中配置

StpUtil::setPermissionGetter(function($loginId) {
    // 从数据库获取权限
    $service = new PermissionService();
    return $service->getAllPermissions((int) $loginId);
});

StpUtil::setRoleGetter(function($loginId) {
    // 从数据库获取角色
    $service = new PermissionService();
    return $service->getRoleList((int) $loginId);
});

// ========== 权限缓存优化 ==========
// 使用 Session 缓存权限数据
StpUtil::setPermissionGetter(function($loginId) {
    $cacheKey = 'permissions_' . $loginId;
    $permissions = StpUtil::getSession($cacheKey);
    if (empty($permissions)) {
        $service = new PermissionService();
        $permissions = $service->getAllPermissions((int) $loginId);
        StpUtil::setSession($cacheKey, $permissions);
    }
    return $permissions;
});

六、Cookie 与 Session 管理

php
// config/sa_token.php

return [
    'cookieName' => 'satoken',        // Cookie 名称
    'cookieDomain' => '',              // Cookie 域名,空表示当前域名
    'cookiePath' => '/',               // Cookie 路径
    'cookieSecure' => false,           // 是否仅 HTTPS 传输
    'cookieHttpOnly' => true,          // 是否禁止 JS 读取
    'cookieSameSite' => 'Strict',      // SameSite 策略
    'cookieMaxAge' => 86400,           // Cookie 最大生命周期(秒)
    'cookiePrefix' => '',              // Cookie 前缀
];
配置项类型默认值详细说明
cookieNamestringsatokenCookie 的名称,用于存储 Token
cookieDomainstring''Cookie 的作用域,设置如 .example.com 可在子域名间共享
cookiePathstring'/'Cookie 的路径,'/' 表示整个网站都有效
cookieSecureboolfalse是否仅通过 HTTPS 传输,生产环境建议 true
cookieHttpOnlybooltrue是否禁止 JavaScript 读取 Cookie,建议 true 防止 XSS
cookieSameSitestring'Strict'CSRF 防护策略:Strict/Lax/None
cookieMaxAgeint86400Cookie 在客户端的最大存活时间(秒)
cookiePrefixstring''Cookie 前缀,可设置如 __Secure- 增强安全性
php
// ========== Sa-Token 自动 Cookie 操作 ==========
// 登录时自动写入 Cookie(当 isWriteCookie = true)
StpUtil::login(10001);

// 登出时自动清除 Cookie
StpUtil::logout();

// 每次请求自动从 Cookie 读取 Token(当 isReadCookie = true)
// 框架自动完成,无需手动操作

// ========== 手动 Cookie 操作 ==========
// 获取 Cookie 中的 Token
$tokenFromCookie = cookie('satoken');

// 手动设置 Cookie(不推荐,建议由 Sa-Token 自动管理)
cookie('satoken', $tokenValue, 86400);

// 手动删除 Cookie
cookie('satoken', null);

// ========== 获取所有 Cookie ==========
$allCookies = request()->cookie();

// ========== Cookie 安全操作 ==========
// 设置安全 Cookie(HTTPS + HttpOnly + Secure)
cookie('satoken', $tokenValue, [
    'expire' => 86400,
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict',
]);

// 跨域 Cookie 设置(前后端分离)
cookie('satoken', $tokenValue, [
    'expire' => 86400,
    'secure' => true,
    'samesite' => 'None',
]);
php
// ========== 场景1:前后端分离(API 模式) ==========
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,

// ========== 场景2:传统 Web 应用(Session 模式) ==========
'isReadCookie' => true,
'isWriteCookie' => true,
'cookieHttpOnly' => true,
'cookieSecure' => false,
'cookieSameSite' => 'Lax',

// ========== 场景3:多应用共享域名 ==========
'cookieDomain' => '.example.com',
'cookiePath' => '/',
'cookieSameSite' => 'Lax',

// ========== 场景4:后台管理系统(高安全) ==========
'cookieSecure' => true,
'cookiePath' => '/admin',
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',

// ========== 场景5:HTTPS 环境 ==========
'cookieSecure' => true,
'cookieSameSite' => 'Strict',
'cookieHttpOnly' => true,
php
// 使用 Cookie 前缀可以增强安全性
'cookiePrefix' => '__Secure-',

// 实际写入的 Cookie 名称为:__Secure-satoken
// 这样浏览器会强制要求 HTTPS 传输

// 常用的 Cookie 前缀:
// __Secure-    : 强制 HTTPS
// __Host-      : 强制 HTTPS + 仅主机
php
// ========== 后端配置 ==========
'cookieSameSite' => 'None',
'cookieSecure' => true,  // SameSite=None 时必须为 true

// ========== 前端请求配置 ==========
fetch('/api/user/info', {
    credentials: 'include',
    headers: {
        'Content-Type': 'application/json',
    }
});

// Axios 配置
axios.defaults.withCredentials = true;

6.2 Session 管理详解

6.2.1 Session 类型

Session 类型作用范围说明
账号 Session当前账号的所有 Token登录后,同一账号的所有设备共享此 Session
Token Session当前 Token只对当前 Token 有效,不同设备相互隔离
自定义 Session自定义 Key开发者任意指定的 Session 对象
php
// ========== 账号 Session(所有设备共享) ==========
StpUtil::setSession('user_info', ['name' => '张三']);
$userInfo = StpUtil::getSession('user_info');
StpUtil::deleteSession('user_info');
if (StpUtil::hasSession('user_info')) {}
$session = StpUtil::getSession();

// ========== Token Session(仅当前 Token 独享) ==========
StpUtil::setTokenSession('temp_data', ['key' => 'value']);
$tempData = StpUtil::getTokenSession('temp_data');
StpUtil::deleteTokenSession('temp_data');

// ========== 自定义 Session(任意 Key) ==========
$customSession = StpUtil::getCustomSession('my_custom_key');
$customSession->set('data', 'value');
StpUtil::setCustomSession('my_key', 'my_value');
$value = StpUtil::getCustomSession('my_key');
StpUtil::deleteCustomSession('my_key');

6.2.2 Session 操作详解

php
// ========== Session 存储 ==========
StpUtil::setSession('user_id', 10001);
StpUtil::setSession('username', 'admin');
StpUtil::setSession('user_info', [
    'id' => 10001,
    'name' => '张三',
    'email' => 'zhangsan@example.com',
    'roles' => ['admin', 'editor'],
    'settings' => [
        'theme' => 'dark',
        'language' => 'zh-CN',
    ],
]);

// ========== Session 读取 ==========
$userId = StpUtil::getSession('user_id');
$username = StpUtil::getSession('username', 'guest');
$session = StpUtil::getSession();
$allData = $session->getAll();
$keys = StpUtil::getSessionKeys();

// ========== Session 判断 ==========
if (StpUtil::hasSession('user_info')) {}
if (StpUtil::hasSessionKey('user_info', 'name')) {}

// ========== Session 删除 ==========
StpUtil::deleteSession('temp_data');
StpUtil::deleteSession(['key1', 'key2', 'key3']);
StpUtil::clearSession();

// ========== Session 有效期 ==========
StpUtil::setSessionTimeout('user_info', 3600);
$timeout = StpUtil::getSessionTimeout('user_info');
StpUtil::refreshSession('user_info');
StpUtil::refreshSession(['user_info', 'permissions']);

6.2.3 Session 配置

php
// config/sa_token.php

'session' => [
    'timeout' => 86400,                 // Session 默认有效期(秒)
    'activityTimeout' => 1800,          // Session 活跃超时(秒)
    'maxSize' => 1024,                  // Session 最大存储大小(KB)
    'prefix' => 'session_',             // Session key 前缀
    'cleanInterval' => 3600,            // 清理间隔(秒)
],

6.2.4 Session 清理机制

php
// ========== 自动清理 ==========
// 配置了 activityTimeout 后,框架会自动清理过期 Session

// ========== 手动清理 ==========
SaToken::getDao()->cleanExpiredSession();
StpUtil::clearUserSession(10001);
StpUtil::clearDeviceSession('mobile');

// ========== 定时任务清理 ==========
// 创建 app/command/CleanSession.php
namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use SaToken\SaToken;

class CleanSession extends Command
{
    protected function configure()
    {
        $this->setName('clean:session')
            ->setDescription('清理过期 Session');
    }

    protected function execute(Input $input, Output $output)
    {
        $startTime = microtime(true);
        $count = SaToken::getDao()->cleanExpiredSession();
        $cost = microtime(true) - $startTime;
        $output->writeln("清理了 {$count} 个过期 Session,耗时 {$cost} 秒");
    }
}

// 注册命令 config/console.php
return [
    'commands' => [
        'clean:session' => \app\command\CleanSession::class,
    ],
];

// Cron: 0 2 * * * cd /path/to/project && php think clean:session
text
┌─────────────────────────────────────────────────────────────────────────────┐
│                           用户登录完整流程                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. 用户提交用户名/密码                                                      │
│     ↓                                                                       │
│  2. 验证用户凭证                                                            │
│     ↓                                                                       │
│  3. StpUtil::login(10001)                                                   │
│     │                                                                       │
│     ├── 生成 Token                                                          │
│     │   ├── uuid: 550e8400-e29b-41d4-a716-446655440000                     │
│     │   ├── simple-uuid: 550e8400e29b41d4a716446655440000                  │
│     │   ├── random-32: abc123def456...                                     │
│     │   └── tik: 随机24位字符串                                             │
│     │                                                                       │
│     ├── Token 与 LoginId 绑定 → Redis/内存                                  │
│     │   ├── key: sa_token_session_{token}                                  │
│     │   └── value: {loginId, device, timeout, extra, ...}                  │
│     │                                                                       │
│     ├── 创建 Session 对象 (账号 Session)                                    │
│     │   └── key: sa_token_session_{loginId}                                │
│     │                                                                       │
│     ├── 写入 Cookie                                                         │
│     │   └── Cookie: satoken=550e8400-e29b-41d4-a716-446655440000          │
│     │       ├── Domain: .example.com                                       │
│     │       ├── Path: /                                                    │
│     │       ├── Secure: true/false                                         │
│     │       ├── HttpOnly: true                                             │
│     │       └── SameSite: Strict/Lax/None                                  │
│     │                                                                       │
│     ├── 写入响应头                                                          │
│     │   └── Header: Authorization: Bearer 550e8400...                      │
│     │                                                                       │
│     ├── 触发 onLogin 事件                                                   │
│     │   └── 执行自定义监听器逻辑                                            │
│     │                                                                       │
│     └── 记录审计日志                                                        │
│         └── 写入 audit_log 表                                               │
│     ↓                                                                       │
│  4. 返回 Token 给客户端                                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

七、注解式鉴权

7.1 注解概览

注解说明示例
@SaCheckLogin登录校验#[SaCheckLogin]
@SaCheckRole角色校验#[SaCheckRole('admin')]
@SaCheckPermission权限校验#[SaCheckPermission('user:add')]
@SaCheckSafe二级认证#[SaCheckSafe]
@SaCheckDisable服务封禁#[SaCheckDisable('comment')]
@SaIgnore忽略校验#[SaIgnore]

7.2 基本用法

php
<?php

namespace app\controller;

use SaToken\Annotation\SaCheckLogin;
use SaToken\Annotation\SaCheckRole;
use SaToken\Annotation\SaCheckPermission;
use SaToken\Annotation\SaCheckSafe;
use SaToken\Annotation\SaCheckDisable;
use SaToken\Annotation\SaIgnore;

class UserController
{
    #[SaCheckLogin]
    public function info()
    {
        return '查询用户信息';
    }

    #[SaCheckRole('super-admin')]
    public function add()
    {
        return '用户增加';
    }

    #[SaCheckPermission('user-add')]
    public function delete()
    {
        return '删除用户';
    }

    #[SaCheckSafe]
    public function updatePwd()
    {
        return '修改密码';
    }

    #[SaCheckDisable('comment')]
    public function comment()
    {
        return '发表评论';
    }

    #[SaIgnore]
    public function getList()
    {
        return '公开数据';
    }
}

7.3 多权限场景

php
use SaToken\Annotation\SaCheckPermission;
use SaToken\SaMode;

#[SaCheckPermission(value: ['user-add', 'user-all'], mode: SaMode::OR)]
public function userManage()
{
    return '用户管理';
}

#[SaCheckPermission(value: ['user-add', 'user-edit'], mode: SaMode::AND)]
public function userEdit()
{
    return '编辑用户';
}

八、路由拦截鉴权

8.1 基础匹配

php
use SaToken\SaRouter;
use SaToken\StpUtil;

SaRouter::match('/api/user/info')->check(fn() => StpUtil::checkLogin());

SaRouter::match(['/api/user/**', '/api/order/**'])->check(fn() => StpUtil::checkLogin());

SaRouter::match('/api/user/**', 'POST')->check(fn() => StpUtil::checkPermission('user:add'));

SaRouter::match('/api/user/**', ['POST', 'PUT', 'DELETE'])->check(fn() => StpUtil::checkPermission('user:edit'));

SaRouter::match(
    ['/api/user/**', '/api/admin/**'],
    ['POST', 'PUT', 'DELETE']
)->check(fn() => StpUtil::checkPermission('user:edit'));

8.2 排除路径

php
SaRouter::match('/api/**')
    ->exclude('/api/login', '/api/register')
    ->check(fn() => StpUtil::checkLogin());

SaRouter::match('/api/**')
    ->exclude('/api/public/**')
    ->exclude('/api/health')
    ->check(fn() => StpUtil::checkLogin());

SaRouter::match('/api/**')
    ->exclude(['/api/login', '/api/register', '/api/refresh'])
    ->check(fn() => StpUtil::checkLogin());

8.3 复杂鉴权

php
SaRouter::match('/admin/**')->check(function() {
    StpUtil::checkLogin();
    StpUtil::checkRole('admin');
});

SaRouter::match('/api/payment/**')->check(function() {
    StpUtil::checkLogin();
    StpUtil::checkPermission('payment:operate');
    StpUtil::checkSafe();
});

SaRouter::match('/api/**')->check(function() {
    if (request()->isGet()) {
        StpUtil::checkPermission('read');
    } else {
        StpUtil::checkPermission('write');
    }
});

SaRouter::match('/api/**')->check(function() {
    if (StpUtil::isLogin()) {
        $userId = StpUtil::getLoginId();
        if ($userId > 10000) {
            StpUtil::checkPermission('vip');
        }
    }
});

8.4 链式匹配

php
SaRouter::match('/api/**')
    ->exclude('/api/login', '/api/register')
    ->check(fn() => StpUtil::checkLogin());

SaRouter::match('/api/admin/**')
    ->check(fn() => StpUtil::checkRole('admin'));

SaRouter::match('/api/super/**')
    ->check(fn() => StpUtil::checkRole('super'));

九、SSO 单点登录

9.1 SSO 配置 config/sa_token_sso.php

php
<?php

return [
    'mode' => 'cross-domain',
    'serverUrl' => 'https://sso.example.com',
    'clientId' => 'your-client-id',
    'clientSecret' => 'your-client-secret',
    'loginUrl' => 'https://sso.example.com/login',
    'authUrl' => 'https://sso.example.com/oauth/authorize',
    'tokenUrl' => 'https://sso.example.com/oauth/token',
    'userInfoUrl' => 'https://sso.example.com/oauth/userinfo',
    'logoutUrl' => 'https://sso.example.com/logout',
    'backUrl' => 'https://app.example.com/sso/callback',
    'allowDomains' => ['example.com', '*.example.com'],
    'ssoTimeout' => 86400,
    'ssoTokenName' => 'ssotoken',
    'jwtSecretKey' => 'your-jwt-secret',
    'jwtExpire' => 3600,
    'autoLogin' => true,
    'autoRegister' => false,
];

9.2 SSO 控制器 app/controller/SsoController.php

php
<?php

namespace app\controller;

use SaToken\StpUtil;
use SaToken\Sso\SaSsoManager;
use think\facade\Db;

class SsoController
{
    public function login()
    {
        $sso = SaSsoManager::getInstance();

        if ($sso->checkSsoSession()) {
            $userInfo = $sso->getUserInfo();
            $user = Db::name('user')
                ->where('sso_id', $userInfo['id'])
                ->find();

            if (!$user && config('sa_token_sso.autoRegister')) {
                $userId = Db::name('user')->insertGetId([
                    'sso_id' => $userInfo['id'],
                    'username' => $userInfo['username'],
                    'nickname' => $userInfo['nickname'],
                    'email' => $userInfo['email'] ?? '',
                    'status' => 1,
                    'create_time' => time(),
                ]);
                $user = ['id' => $userId];
            }

            if ($user) {
                StpUtil::login((string) $user['id']);
                return redirect('/dashboard');
            }
            return redirect('/login?error=user_not_found');
        }

        return redirect($sso->buildLoginUrl());
    }

    public function callback()
    {
        $sso = SaSsoManager::getInstance();

        try {
            $result = $sso->handleCallback();

            if ($result['success']) {
                $ssoUser = $result['user'];
                $user = Db::name('user')
                    ->where('sso_id', $ssoUser['id'])
                    ->find();

                if (!$user) {
                    $userId = Db::name('user')->insertGetId([
                        'sso_id' => $ssoUser['id'],
                        'username' => $ssoUser['username'] ?? '',
                        'nickname' => $ssoUser['nickname'] ?? '',
                        'email' => $ssoUser['email'] ?? '',
                        'status' => 1,
                        'create_time' => time(),
                    ]);
                } else {
                    $userId = $user['id'];
                    Db::name('user')
                        ->where('id', $userId)
                        ->update([
                            'nickname' => $ssoUser['nickname'] ?? $user['nickname'],
                            'email' => $ssoUser['email'] ?? $user['email'],
                            'last_login' => time(),
                        ]);
                }

                StpUtil::login((string) $userId);

                return json([
                    'code' => 200,
                    'msg' => '登录成功',
                    'data' => [
                        'token' => StpUtil::getTokenValue(),
                        'userId' => $userId,
                    ]
                ]);
            }

            return json(['code' => 401, 'msg' => $result['message'] ?? '登录失败']);

        } catch (\Exception $e) {
            return json(['code' => 500, 'msg' => 'SSO 登录失败:' . $e->getMessage()]);
        }
    }

    public function logout()
    {
        StpUtil::logout();
        $sso = SaSsoManager::getInstance();
        $logoutUrl = $sso->buildLogoutUrl();

        return json([
            'code' => 200,
            'msg' => '已登出',
            'data' => ['logoutUrl' => $logoutUrl]
        ]);
    }
}

十、OAuth2.0 认证

10.1 OAuth2 配置 config/sa_token_oauth2.php

php
<?php

return [
    'clients' => [
        'web-app' => [
            'clientId' => 'web-app',
            'clientSecret' => 'web-app-secret',
            'redirectUris' => [
                'https://app.example.com/callback',
                'http://localhost:8080/callback',
            ],
            'grantTypes' => ['authorization_code', 'refresh_token'],
            'scopes' => ['openid', 'profile', 'email', 'user:read'],
        ],
        'mobile-app' => [
            'clientId' => 'mobile-app',
            'clientSecret' => 'mobile-app-secret',
            'redirectUris' => ['com.example.app://oauth/callback'],
            'grantTypes' => ['authorization_code', 'password'],
            'scopes' => ['openid', 'profile'],
        ],
    ],
    'accessTokenTimeout' => 3600,
    'refreshTokenTimeout' => 2592000,
    'codeTimeout' => 300,
    'openIdMode' => true,
    'idTokenTimeout' => 3600,
    'jwtSecretKey' => 'your-jwt-secret',
    'issuer' => 'https://auth.example.com',
];

10.2 OAuth2 控制器 app/controller/OAuth2Controller.php

php
<?php

namespace app\controller;

use SaToken\StpUtil;
use SaToken\OAuth2\SaOAuth2Manager;
use think\facade\Db;

class OAuth2Controller
{
    public function authorize()
    {
        $oauth2 = SaOAuth2Manager::getInstance();

        $clientId = input('get.client_id', '');
        $redirectUri = input('get.redirect_uri', '');
        $scope = input('get.scope', '');
        $state = input('get.state', '');
        $responseType = input('get.response_type', 'code');

        $client = $oauth2->validateClient($clientId, $redirectUri);
        if (!$client) {
            return json(['error' => 'invalid_client'], 400);
        }

        if (!StpUtil::isLogin()) {
            return redirect('/login?redirect=' . urlencode(request()->url()));
        }

        if ($scope && !$oauth2->validateScope($scope, $client['scopes'])) {
            return json(['error' => 'invalid_scope'], 400);
        }

        if ($responseType === 'token') {
            $accessToken = $oauth2->createAccessToken(
                $clientId,
                StpUtil::getLoginId(),
                $redirectUri,
                $scope
            );

            $redirectUrl = $redirectUri . '#access_token=' . $accessToken;
            if ($state) {
                $redirectUrl .= '&state=' . $state;
            }
            return redirect($redirectUrl);
        }

        return view('oauth2/authorize', [
            'client' => $client,
            'scope' => $scope,
            'state' => $state,
            'user' => Db::name('user')->find(StpUtil::getLoginId()),
        ]);
    }

    public function approve()
    {
        $oauth2 = SaOAuth2Manager::getInstance();

        $clientId = input('post.client_id', '');
        $redirectUri = input('post.redirect_uri', '');
        $scope = input('post.scope', '');
        $state = input('post.state', '');
        $approved = input('post.approved', false);

        if (!$approved) {
            $redirectUrl = $redirectUri . '?error=access_denied';
            if ($state) {
                $redirectUrl .= '&state=' . $state;
            }
            return redirect($redirectUrl);
        }

        try {
            $code = $oauth2->createAuthorizationCode(
                $clientId,
                StpUtil::getLoginId(),
                $redirectUri,
                $scope
            );

            $redirectUrl = $redirectUri . '?code=' . $code;
            if ($state) {
                $redirectUrl .= '&state=' . $state;
            }

            return json(['code' => 200, 'data' => ['redirectUrl' => $redirectUrl]]);

        } catch (\Exception $e) {
            return json(['code' => 400, 'msg' => $e->getMessage()]);
        }
    }

    public function token()
    {
        $oauth2 = SaOAuth2Manager::getInstance();

        $grantType = input('post.grant_type', '');
        $code = input('post.code', '');
        $clientId = input('post.client_id', '');
        $clientSecret = input('post.client_secret', '');
        $redirectUri = input('post.redirect_uri', '');
        $refreshToken = input('post.refresh_token', '');
        $username = input('post.username', '');
        $password = input('post.password', '');
        $scope = input('post.scope', '');

        try {
            switch ($grantType) {
                case 'authorization_code':
                    $result = $oauth2->exchangeTokenByCode(
                        $code,
                        $clientId,
                        $clientSecret,
                        $redirectUri
                    );
                    break;

                case 'refresh_token':
                    $result = $oauth2->refreshToken(
                        $refreshToken,
                        $clientId,
                        $clientSecret
                    );
                    break;

                case 'password':
                    $user = Db::name('user')
                        ->where('username', $username)
                        ->where('status', 1)
                        ->find();

                    if (!$user || !password_verify($password, $user['password'])) {
                        return json(['error' => 'invalid_credentials'], 401);
                    }

                    $result = $oauth2->createTokenByPassword(
                        $user['id'],
                        $clientId,
                        $clientSecret,
                        $scope
                    );
                    break;

                case 'client_credentials':
                    $result = $oauth2->createTokenByClientCredentials(
                        $clientId,
                        $clientSecret,
                        $scope
                    );
                    break;

                default:
                    throw new \Exception('unsupported_grant_type');
            }

            return json($result);

        } catch (\Exception $e) {
            return json(['error' => $e->getMessage()], 400);
        }
    }

    public function userinfo()
    {
        $oauth2 = SaOAuth2Manager::getInstance();

        $accessToken = input('get.access_token', '');
        if (!$accessToken) {
            $authHeader = request()->header('Authorization');
            if ($authHeader && preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
                $accessToken = $matches[1];
            }
        }

        if (!$accessToken) {
            return json(['error' => 'invalid_token'], 401);
        }

        try {
            $userInfo = $oauth2->getUserInfo($accessToken);
            return json($userInfo);
        } catch (\Exception $e) {
            return json(['error' => $e->getMessage()], 401);
        }
    }
}

十一、安全防护

11.1 防暴力破解

php
public function login(): Json
{
    $username = input('post.username', '');
    $password = input('post.password', '');

    StpUtil::checkAntiBrute($username);

    $user = Db::name('user')->where('username', $username)->find();

    if (!$user || !password_verify($password, $user['password'])) {
        StpUtil::recordAntiBruteFailure($username);
        $info = StpUtil::getAntiBruteInfo($username);
        return json([
            'code' => 401,
            'msg' => "密码错误,剩余尝试次数:{$info['remainingAttempts']}",
            'data' => [
                'failCount' => $info['failCount'],
                'isLocked' => $info['isLocked'],
                'remainingAttempts' => $info['remainingAttempts'],
                'maxFailures' => $info['maxFailures'],
            ]
        ]);
    }

    StpUtil::clearAntiBruteFailure($username);
    StpUtil::login((string) $user['id']);
    return json(['code' => 200, 'msg' => '登录成功']);
}

// ========== 防暴力破解配置 ==========
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,

// ========== 防暴力破解 API ==========
$isLocked = StpUtil::isAccountLocked($username);
$remaining = StpUtil::getRemainingLockTime($username);
StpUtil::unlockAccount($username);
$info = StpUtil::getAntiBruteInfo($username);

11.2 Token 黑名单

php
StpUtil::addTokenToBlacklist($tokenValue);
StpUtil::addTokenToBlacklist($tokenValue, '异常登录');
StpUtil::removeTokenFromBlacklist($tokenValue);
$isBlacklisted = StpUtil::isTokenBlacklisted($tokenValue);
$blacklist = StpUtil::getBlacklist();

// 批量操作
$tokens = ['token1', 'token2', 'token3'];
foreach ($tokens as $token) {
    StpUtil::addTokenToBlacklist($token);
}

11.3 IP 异常检测

php
use SaToken\StpUtil;

$info = StpUtil::getLoginInfo(10001);
$count = StpUtil::getAnomalyCount(10001);
$history = StpUtil::getIpHistory(10001);
StpUtil::clearLoginHistory(10001);

if (StpUtil::isIpAnomalous()) {
    Log::warning('异地登录:用户 ' . StpUtil::getLoginId() . ' 从 ' . request()->ip() . ' 登录');
    $this->sendAlert(StpUtil::getLoginId(), request()->ip());
}

// ========== IP 异常检测配置 ==========
'ipAnomalyDetection' => true,
'ipAnomalySensitivity' => 3,

11.4 设备管理

php
use SaToken\StpUtil;

$devices = StpUtil::getDeviceList(10001);
$count = StpUtil::getDeviceCount(10001);
$device = StpUtil::findDevice(10001, 'device-id-xxx');
StpUtil::kickoutDevice(10001, 'device-id-xxx');
$kickedCount = StpUtil::kickoutAllDevices(10001, StpUtil::getTokenValue());
StpUtil::renameDevice(10001, 'device-id-xxx', '我的手机');

// ========== 设备管理配置 ==========
'deviceManagement' => true,

11.5 敏感操作验证

php
use SaToken\StpUtil;

// ========== OTP 验证码 ==========
$code = StpUtil::generateOtpCode('payment');
$code = StpUtil::sendOtpCode('payment');
StpUtil::verifyOtpCode('payment', input('post.code'));
$isVerified = StpUtil::isSensitiveVerified('payment');
$remaining = StpUtil::getSensitiveVerifyRemainingAttempts('payment');
StpUtil::clearSensitiveVerify('payment');

// ========== 安全令牌 ==========
$token = StpUtil::openSensitiveVerify('payment', 600);
StpUtil::checkSensitiveVerify('payment', input('post.stoken'));
$status = StpUtil::getSensitiveVerifyStatus('payment');

// ========== 敏感操作示例 ==========
public function transfer()
{
    StpUtil::checkLogin();
    StpUtil::checkPermission('account:transfer');

    if (!StpUtil::isSensitiveVerified('transfer')) {
        $code = StpUtil::sendOtpCode('transfer');
        return json([
            'code' => 403,
            'msg' => '需要验证身份',
            'data' => ['need_verify' => true]
        ]);
    }

    // 执行转账操作
    StpUtil::clearSensitiveVerify('transfer');

    return json(['code' => 200, 'msg' => '转账成功']);
}

11.6 Token 指纹绑定

php
// ========== 启用 Token 指纹绑定 ==========
'tokenFingerprint' => true,

// ========== 手动操作 ==========
$fingerprint = StpUtil::getCurrentFingerprint();
StpUtil::checkFingerprint();
StpUtil::updateFingerprint();

// ========== 指纹异常处理 ==========
try {
    StpUtil::checkFingerprint();
} catch (\Exception $e) {
    Log::alert('Token 指纹校验失败:' . StpUtil::getTokenValue());
    StpUtil::kickout();
    return json(['code' => 403, 'msg' => '设备异常,请重新登录']);
}

十二、高级功能

12.1 HTTP Basic/Digest 认证

php
use SaToken\SaToken;

$auth = SaToken::getHttpAuth();

$auth->setBasicValidator(function (string $username, string $password): mixed {
    $user = Db::name('user')
        ->where('username', $username)
        ->where('status', 1)
        ->find();

    if ($user && password_verify($password, $user['password'])) {
        return $user['id'];
    }
    return null;
});

$auth->checkBasic('My API');

// ========== HTTP Digest 认证 ==========
$auth->setDigestValidator(function (string $username): ?string {
    $user = Db::name('user')
        ->where('username', $username)
        ->find();

    if ($user) {
        return md5($username . ':My API:' . $user['password']);
    }
    return null;
});

$auth->checkDigest('My API');

12.2 参数签名校验(SaSign)

php
use SaToken\SaToken;

$sign = SaToken::getSign();

$sign->setSignKey(config('sa_token.signKey'));
$sign->setSignAlg('sha256');
$sign->setTimestampGap(600);
$sign->setNonceValidator(function (string $nonce): bool {
    $key = 'nonce_' . $nonce;
    if (StpUtil::getCustomSession($key)) {
        return false;
    }
    StpUtil::setCustomSession($key, time(), 3600);
    return true;
});

$params = [
    'userId' => '10001',
    'action' => 'query',
    'timestamp' => time(),
    'nonce' => uniqid(),
    'data' => ['key' => 'value'],
];

$signed = $sign->signParams($params);

$params = input('get.');
if (!$sign->verifySign($params)) {
    return json(['code' => 403, 'msg' => '签名无效']);
}

12.3 API Key 授权

php
use SaToken\SaToken;

$apiKey = SaToken::getApiKey();

$apiKey->registerKey('ak-123456', 'sk-abcdef', 10001);
$apiKey->registerKey('ak-789012', 'sk-ghijkl', 10002);

$apiKey->setValidator(function (string $apiKey, string $apiSecret): mixed {
    $key = Db::name('api_keys')
        ->where('api_key', $apiKey)
        ->where('status', 1)
        ->find();

    if ($key && hash_equals($key->api_secret, $apiSecret)) {
        return $key->user_id;
    }
    return null;
});

$apiKey->checkApiKey();
$userId = StpUtil::getLoginId();

12.4 全局过滤器

php
use SaToken\SaToken;
use SaToken\SaRouter;
use SaToken\StpUtil;

$filter = SaToken::getGlobalFilter();

$filter->setCors([
    'allowOrigin' => '*',
    'allowMethods' => 'GET, POST, PUT, DELETE, OPTIONS',
    'allowHeaders' => 'Content-Type, Authorization, X-Requested-With',
    'allowCredentials' => 'true',
    'maxAge' => '3600',
]);

$filter->setCorsOrigin(function($origin) {
    $allowOrigins = ['https://example.com', 'https://app.example.com'];
    if (in_array($origin, $allowOrigins)) {
        return $origin;
    }
    return '';
});

$filter->addBeforeFilter(function () {
    Log::info('Request: ' . request()->method() . ' ' . request()->url());
    SaRouter::match('/api/**')->check(fn() => StpUtil::checkLogin());
});

$filter->addAfterFilter(function ($response) {
    $response->header('X-Content-Type-Options', 'nosniff');
    $response->header('X-Frame-Options', 'DENY');
    $response->header('X-XSS-Protection', '1; mode=block');
    Log::info('Response: ' . $response->getCode());
});

$filter->execute();

if ($filter->isCorsRequest()) {
    $filter->handlePreflight();
    return;
}

12.5 自定义存储

php
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoMemory;
use SaToken\Dao\SaTokenDaoRedis;
use SaToken\Dao\SaTokenDaoPsr16;

SaToken::setDao(new SaTokenDaoMemory());
SaToken::setDao(new SaTokenDaoRedis($redisConfig));

$dao = SaTokenDaoRedis::createWithSeparateRedis(
    ['host' => '127.0.0.1', 'port' => 6379, 'db' => 0],
    ['host' => '127.0.0.1', 'port' => 6379, 'db' => 1],
);
SaToken::setDao($dao);

$psr16Cache = new SomePsr16Cache();
SaToken::setDao(new SaTokenDaoPsr16($psr16Cache));

// 自定义 DAO
class MyCustomDao implements SaTokenDaoInterface
{
    public function get(string $key): ?string
    {
        return MyStorage::get($key);
    }

    public function set(string $key, string $value, int $ttl): void
    {
        MyStorage::set($key, $value, $ttl);
    }

    public function delete(string $key): void
    {
        MyStorage::delete($key);
    }

    public function exists(string $key): bool
    {
        return MyStorage::exists($key);
    }
}

12.6 JWT 集成

php
use SaToken\Plugin\SaTokenJwt;

$payload = [
    'userId' => 10001,
    'username' => 'admin',
    'role' => 'admin',
    'exp' => time() + 3600,
    'iat' => time(),
    'iss' => 'https://example.com',
    'aud' => 'https://api.example.com',
];

$jwt = SaTokenJwt::generate($payload);

try {
    $payload = SaTokenJwt::verify($jwt);
    $userId = $payload['userId'];
    $username = $payload['username'];
} catch (\Exception $e) {
    return json(['code' => 401, 'msg' => 'JWT 无效']);
}

// 配置
'jwtSecretKey' => 'your-jwt-secret',
'jwtStateless' => false,

// 使用 JWT 作为 Token 载体
StpUtil::setTokenGenerator(function($loginId, $extra) {
    $payload = [
        'loginId' => $loginId,
        'extra' => $extra,
        'exp' => time() + config('sa_token.timeout'),
    ];
    return SaTokenJwt::generate($payload);
});

12.7 密码加密工具

php
use SaToken\Plugin\SaTokenCrypto;

$md5 = SaTokenCrypto::md5('password');
$sha1 = SaTokenCrypto::sha1('password');
$sha256 = SaTokenCrypto::sha256('password');
$sha512 = SaTokenCrypto::sha512('password');

$hmacSha256 = SaTokenCrypto::hmacSha256('data', 'key');
$hmacSha1 = SaTokenCrypto::hmacSha1('data', 'key');

$hash = SaTokenCrypto::bcryptHash('password', 12);
$valid = SaTokenCrypto::bcryptVerify('password', $hash);

$encrypted = SaTokenCrypto::aesEncrypt('data', '16-byte-key');
$decrypted = SaTokenCrypto::aesDecrypt($encrypted, '16-byte-key');

$encrypted = SaTokenCrypto::aes256Encrypt('data', '32-byte-key-32-byte-key-');
$decrypted = SaTokenCrypto::aes256Decrypt($encrypted, '32-byte-key-32-byte-key-');

$encrypted = SaTokenCrypto::rsaEncrypt('data', $publicKey);
$decrypted = SaTokenCrypto::rsaDecrypt($encrypted, $privateKey);

$signature = SaTokenCrypto::rsaSign('data', $privateKey);
$valid = SaTokenCrypto::rsaVerify('data', $signature, $publicKey);

$sm3 = SaTokenCrypto::sm3('data');
$encrypted = SaTokenCrypto::sm4Encrypt('data', '16-byte-key-16-byte');
$decrypted = SaTokenCrypto::sm4Decrypt($encrypted, '16-byte-key-16-byte');
$signature = SaTokenCrypto::sm2Sign('data', $privateKey);
$valid = SaTokenCrypto::sm2Verify('data', $signature, $publicKey);

12.8 审计日志

php
use SaToken\StpUtil;
use SaToken\Security\SaAuditLog;

SaAuditLog::logLogin(10001, ['ip' => request()->ip(), 'device' => 'web']);
SaAuditLog::logLogout(10001);
SaAuditLog::logKickout(10001, 'admin', '违规操作');
SaAuditLog::logDisable(10001, 'login', '账号异常');
SaAuditLog::logSwitchTo(10001, 20002);
SaAuditLog::logCustom('user_action', 10001, '导出数据', ['count' => 100]);

$logs = StpUtil::getAuditLogs(50);
$log = StpUtil::getAuditLog('log-id-xxx');
$recentLogs = SaAuditLog::getRecentLogs('login', 100);
$logsByIp = SaAuditLog::getLogsByIp('192.168.1.1');
$logsByEvent = SaAuditLog::getLogsByEvent('login');
$userLogs = SaAuditLog::getLogsByUserId(10001);
$timeRangeLogs = SaAuditLog::getLogsByTimeRange(strtotime('-7 days'), time());

SaAuditLog::clearLogs();
SaAuditLog::clearLogsByUser(10001);
SaAuditLog::clearLogsByEvent('login');

12.9 事件监听

php
use SaToken\Listener\SaTokenListenerInterface;
use SaToken\SaToken;
use think\facade\Log;

class MySaTokenListener implements SaTokenListenerInterface
{
    public function onLogin(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::info("用户 {$loginId} 登录成功,IP: " . request()->ip());
        $this->sendWelcomeEmail($loginId);
        $this->updateOnlineStatus($loginId, true);
    }

    public function onLogout(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::info("用户 {$loginId} 已登出");
        $this->updateOnlineStatus($loginId, false);
    }

    public function onKickout(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::warning("用户 {$loginId} 被踢下线,原因:" . ($extra['reason'] ?? '未知'));
        $this->sendKickoutNotification($loginId, $extra['reason'] ?? '');
    }

    public function onReplaced(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::warning("用户 {$loginId} 被顶下线,设备:" . ($extra['device'] ?? '未知'));
        $this->sendReplacedNotification($loginId);
    }

    public function onDisable(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::alert("用户 {$loginId} 被封禁,时长:" . ($extra['duration'] ?? '永久'));
        $this->sendDisableNotification($loginId, $extra['duration'] ?? 0);
    }

    public function onUntieDisable(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
    {
        Log::info("用户 {$loginId} 已解封");
    }

    public function onSwitchTo(string $loginType, mixed $loginId, mixed $targetId, array $extra = []): void
    {
        Log::info("用户 {$loginId} 切换到身份 {$targetId}");
        $this->logSwitchOperation($loginId, $targetId);
    }

    public function onRefresh(string $loginType, mixed $loginId, string $oldToken, string $newToken): void
    {
        Log::info("用户 {$loginId} 刷新了 Token");
    }

    private function sendWelcomeEmail($userId): void {}
    private function updateOnlineStatus($userId, bool $online): void {}
    private function sendKickoutNotification($userId, string $reason): void {}
    private function sendReplacedNotification($userId): void {}
    private function sendDisableNotification($userId, int $duration): void {}
    private function logSwitchOperation($userId, $targetId): void {}
}

SaToken::addListener(new MySaTokenListener());
SaToken::removeListener(MySaTokenListener::class);
SaToken::clearListeners();

十三、异常处理

13.1 异常类列表

异常类触发场景关联方法
NotLoginException未登录/Token 无效/过期/被踢StpUtil::checkLogin()
NotPermissionException权限校验不通过StpUtil::checkPermission()
NotRoleException角色校验不通过StpUtil::checkRole()
DisableServiceException账号被封禁StpUtil::checkLogin()
NotSafeException二级认证校验不通过StpUtil::checkSafe()
SaTokenException其他异常所有方法

13.2 异常类型详解

php
use SaToken\Exception\NotLoginException;

// NotLoginException 类型常量
const NOT_LOGIN = 1;          // 未登录
const INVALID_TOKEN = 2;      // Token 无效
const TOKEN_TIMEOUT = 3;      // Token 已过期
const BE_KICKOUT = 4;         // 已被踢下线
const BE_REPLACED = 5;        // 已被顶下线
const TOKEN_BLACKLIST = 6;    // Token 在黑名单

13.3 异常处理器 app/exception/Handler.php

php
<?php

declare(strict_types=1);

namespace app\exception;

use think\exception\Handle;
use think\Response;
use Throwable;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use SaToken\Exception\DisableServiceException;
use SaToken\Exception\NotSafeException;
use think\facade\Log;

class Handler extends Handle
{
    protected array $ignoreReport = [
        NotLoginException::class,
        NotPermissionException::class,
        NotRoleException::class,
        DisableServiceException::class,
        NotSafeException::class,
    ];

    public function render($request, Throwable $e): Response
    {
        if ($e instanceof NotLoginException) {
            $messages = [
                NotLoginException::NOT_LOGIN => '请先登录',
                NotLoginException::INVALID_TOKEN => 'Token 无效',
                NotLoginException::TOKEN_TIMEOUT => 'Token 已过期,请重新登录',
                NotLoginException::BE_KICKOUT => '已被踢下线',
                NotLoginException::BE_REPLACED => '已被顶下线',
                NotLoginException::TOKEN_BLACKLIST => 'Token 已被拉黑',
            ];

            return json([
                'code' => 401,
                'msg' => $messages[$e->getType()] ?? '未登录',
                'type' => $e->getType(),
            ], 401);
        }

        if ($e instanceof NotPermissionException) {
            return json([
                'code' => 403,
                'msg' => '权限不足:' . $e->getPermission(),
                'need' => $e->getPermission(),
            ], 403);
        }

        if ($e instanceof NotRoleException) {
            return json([
                'code' => 403,
                'msg' => '角色不足:' . $e->getRole(),
                'need' => $e->getRole(),
            ], 403);
        }

        if ($e instanceof DisableServiceException) {
            $remaining = StpUtil::getDisableTime();
            return json([
                'code' => 403,
                'msg' => "账号已被封禁,剩余 {$remaining} 秒",
                'remaining' => $remaining,
            ], 403);
        }

        if ($e instanceof NotSafeException) {
            return json([
                'code' => 403,
                'msg' => '需要二级认证',
                'need_safe' => true,
            ], 403);
        }

        if (!$this->isDebug()) {
            Log::error('系统异常:' . $e->getMessage() . "\n" . $e->getTraceAsString());
            return json(['code' => 500, 'msg' => '服务器内部错误'], 500);
        }

        return parent::render($request, $e);
    }

    protected function isDebug(): bool
    {
        return (bool) config('app.debug');
    }
}

十四、完整配置参考

php
<?php

return [
    // ==================== Token 基础配置 ====================
    'tokenName' => 'satoken',
    'tokenPrefix' => 'Bearer',
    'tokenStyle' => 'uuid',

    // ==================== 有效期配置 ====================
    'timeout' => 86400,
    'activityTimeout' => -1,

    // ==================== Cookie 配置 ====================
    'cookieName' => 'satoken',
    'cookieDomain' => '',
    'cookiePath' => '/',
    'cookieSecure' => false,
    'cookieHttpOnly' => true,
    'cookieSameSite' => 'Strict',
    'cookieMaxAge' => 86400,
    'cookiePrefix' => '',

    // ==================== 读取/写入配置 ====================
    'isReadHeader' => true,
    'isReadCookie' => true,
    'isReadBody' => false,
    'isWriteCookie' => true,
    'isWriteHeader' => false,

    // ==================== 登录策略 ====================
    'concurrent' => true,
    'isShare' => true,
    'maxLoginCount' => 12,
    'maxTryTimes' => 12,

    // ==================== 存储配置 ====================
    'storage' => 'memory',
    'redis' => [
        'host' => '127.0.0.1',
        'port' => 6379,
        'password' => '',
        'db' => 0,
        'timeout' => 3,
        'prefix' => 'sa_token_',
    ],

    // ==================== Session 配置 ====================
    'session' => [
        'timeout' => 86400,
        'activityTimeout' => 1800,
        'maxSize' => 1024,
        'prefix' => 'session_',
        'cleanInterval' => 3600,
    ],

    // ==================== 加密配置 ====================
    'cryptoType' => 'intl',
    'tokenEncrypt' => false,
    'tokenEncryptKey' => '',
    'tokenFingerprint' => false,

    // ==================== Refresh Token ====================
    'refreshToken' => false,
    'refreshTokenTimeout' => 2592000,
    'refreshTokenRotation' => true,

    // ==================== 安全配置 ====================
    'antiBruteMaxFailures' => 5,
    'antiBruteLockDuration' => 600,
    'ipAnomalyDetection' => true,
    'ipAnomalySensitivity' => 3,
    'deviceManagement' => true,
    'auditLog' => true,
    'auditLogMaxEntries' => 1000,
    'auditLogTtlDays' => 30,

    // ==================== JWT 配置 ====================
    'jwtSecretKey' => '',
    'jwtStateless' => false,

    // ==================== 签名配置 ====================
    'signKey' => '',
    'signTimestampGap' => 600,
    'signAlg' => 'sha256',

    // ==================== 日志配置 ====================
    'isLog' => false,
];

十五、项目结构

15.1 单应用项目结构

text
tp8-sa-token/
├── app/
│   ├── controller/
│   │   ├── AuthController.php
│   │   ├── UserController.php
│   │   └── ...
│   ├── middleware/
│   │   └── SaTokenMiddleware.php
│   ├── exception/
│   │   └── Handler.php
│   ├── service/
│   │   └── PermissionService.php
│   └── provider/
│       └── SaTokenProvider.php
├── config/
│   ├── sa_token.php
│   ├── sa_token_sso.php
│   ├── sa_token_oauth2.php
│   ├── middleware.php
│   └── service.php
├── route/
│   └── app.php
├── .env
└── composer.json

15.2 多应用项目结构

text
tp8-sa-token/
├── app/
│   ├── common/
│   │   ├── middleware/
│   │   │   └── SaTokenMiddleware.php
│   │   ├── service/
│   │   │   └── PermissionService.php
│   │   ├── exception/
│   │   │   └── Handler.php
│   │   └── provider/
│   │       └── SaTokenProvider.php
│   ├── api/
│   │   ├── controller/
│   │   │   └── AuthController.php
│   │   ├── config/
│   │   │   └── sa_token.php
│   │   └── route/
│   │       └── app.php
│   ├── admin/
│   │   ├── controller/
│   │   │   └── AuthController.php
│   │   ├── config/
│   │   │   └── sa_token.php
│   │   └── route/
│   │       └── app.php
│   └── provider.php
├── config/
│   └── sa_token.php
└── .env

十六、API 快速参考

16.1 StpUtil 核心方法

方法说明
login($loginId, $extra)登录
logout()登出
isLogin()判断是否登录
getLoginId()获取登录 ID
getTokenValue()获取 Token
getTokenTimeout()获取 Token 剩余有效期
refreshToken()刷新 Token
kickout($loginId)踢人下线
disable($loginId, $time)封禁账号
untieDisable($loginId)解封账号
checkPermission($perm)校验权限
checkRole($role)校验角色
getPermissionList()获取权限列表
getRoleList()获取角色列表
switchTo($loginId)身份切换
switchBack()切回原身份
setSession($key, $value)设置 Session
getSession($key)获取 Session
getExtra($key)获取扩展信息
updateExtra($key, $value)更新扩展信息

16.2 SaRouter 核心方法

方法说明
match($path, $method)匹配路由
exclude($path)排除路径
check($callback)执行校验

16.3 异常类

异常类说明
NotLoginException未登录异常
NotPermissionException无权限异常
NotRoleException无角色异常
DisableServiceException账号封禁异常
NotSafeException二级认证异常
SaTokenException基础异常

十七、常见问题

Q1: 四种模式如何选择?

场景推荐模式
生产环境、高并发单应用+Redis
开发测试、快速验证单应用+内存
微服务、分布式多应用+Redis
小型多模块项目多应用+内存

Q2: 为什么 Sa-Token 不生效?

检查:

  1. app/provider/SaTokenProvider.php 是否正确注册
  2. config/service.php 是否注册了服务提供者
  3. config/sa_token.php 是否正确加载
  4. config/middleware.php 是否注册了中间件

检查配置:

php
'isWriteCookie' => true,
'cookieSecure' => false,
php
'cookieDomain' => '.example.com',
'cookieSameSite' => 'Lax',

Q5: 前后端分离如何使用?

php
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,

Q6: 生产环境推荐哪种模式?

强烈推荐 单应用+Redis多应用+Redis

Q7: Session 数据会丢失吗?

内存模式下应用重启会丢失,Redis 模式下不会。

Q8: 如何实现 Token 自动续签?

配置 activityTimeout > 0,每次请求自动刷新 Token 有效期。


项目地址: https://github.com/pohoc/sa-token ThinkPHP 8 文档: https://www.thinkphp.cn/doc 问题反馈: https://github.com/pohoc/sa-token/issues