Sa-Token PHP 使用手册
12501 字
63 分钟
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+。登录认证只需一行代码:
StpUtil::login(10001);权限校验也只需一行:
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 环境要求
PHP >= 8.1ThinkPHP >= 8.0ext-openssl(必需)ext-json(必需)ext-redis(Redis 模式必需)2.2 创建项目
# 创建单应用项目composer create-project topthink/think tp8-sa-tokencd tp8-sa-token
# 或创建多应用项目composer create-project topthink/think tp8-sa-tokencd tp8-sa-tokencomposer require topthink/think-multi-app2.3 安装 Sa-Token
composer require pohoc/sa-token2.4 安装 Redis 扩展(Redis 模式必需)
composer require predis/predis2.5 文件清单(使用前需创建)
| 序号 | 文件路径 | 说明 | 优先级 |
|---|---|---|---|
| 1 | config/sa_token.php | 主配置文件 | ✅ 必须 |
| 2 | config/sa_token_sso.php | SSO 配置 | ⚠️ 按需 |
| 3 | config/sa_token_oauth2.php | OAuth2.0 配置 | ⚠️ 按需 |
| 4 | app/service/PermissionService.php | 权限数据提供者 | ✅ 必须 |
| 5 | app/middleware/SaTokenMiddleware.php | 鉴权中间件 | ✅ 推荐 |
| 6 | app/exception/Handler.php | 异常处理器 | ✅ 推荐 |
| 7 | app/provider/SaTokenProvider.php | 服务提供者 | ✅ 推荐 |
| 8 | config/middleware.php | 中间件注册配置 | ✅ 必须 |
| 9 | config/service.php | 服务提供者注册 | ✅ 必须 |
| 10 | route/app.php | 路由配置 | ✅ 必须 |
三、四种部署模式详解
3.1 单应用 + Redis 模式
3.1.1 场景说明
适用于生产环境的单体应用,使用 Redis 作为存储介质,支持数据持久化和分布式部署。
3.1.2 目录结构
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.json3.1.3 第一步:创建配置文件 config/sa_token.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
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
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
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
return [ 'providers' => [ \app\provider\SaTokenProvider::class, ],];3.1.8 第六步:注册中间件 config/middleware.php
<?php
return [ // 全局中间件 'global' => [ // ... ],
// 别名中间件(用于路由注册) 'alias' => [ 'sa-token' => \app\middleware\SaTokenMiddleware::class, ],];3.1.9 第七步:配置路由 route/app.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
# Redis 配置REDIS_HOST=127.0.0.1REDIS_PORT=6379REDIS_PASSWORD=REDIS_DB=0
# Sa-Token 配置SA_TOKEN_TIMEOUT=86400SA_TOKEN_REFRESH_TIMEOUT=2592000
# JWT 配置JWT_SECRET_KEY=your-jwt-secret
# 签名配置SIGN_KEY=your-sign-key
# 调试模式(生产环境设为 false)APP_DEBUG=false3.1.11 第九步:创建数据库表结构
-- 用户表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
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
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 内存模式注意事项
// ========== 内存模式特点 ==========// 1. 应用重启后所有会话丢失,用户需重新登录// 2. 不支持分布式部署// 3. 适合开发测试环境// 4. 数据存储在 SaTokenDaoMemory 类的静态属性中// 5. 进程内共享,进程重启丢失
// ========== 内存模式适用场景 ==========// - 本地开发调试// - 单元测试// - 演示/原型项目// - 低并发内部工具其余文件(app/service/PermissionService.php、app/middleware/SaTokenMiddleware.php、config/service.php、config/middleware.php、route/app.php)与 3.1 节相同。
3.3 多应用 + Redis 模式
3.3.1 场景说明
适用于微服务架构或模块化中大型项目,多个应用共享 Redis 存储。
3.3.2 启用多应用模式
# 安装多应用扩展composer require topthink/think-multi-app
# 生成应用目录php think build3.3.3 目录结构
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└── .env3.3.4 全局配置 config/sa_token.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
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
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
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
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
return [ 'providers' => [ \app\common\provider\SaTokenProvider::class, ],];3.3.10 API 应用路由 app/api/route/app.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
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
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
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
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
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/A | N/A | ✅ | ❌ |
| 部署复杂度 | 中 | 低 | 中 | 中 |
| 性能 | 高 | 极高 | 高 | 极高 |
| 推荐环境 | 生产 | 开发/测试 | 生产/微服务 | 开发/小型项目 |
四、登录认证
4.1 创建认证控制器 app/controller/AuthController.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 登录参数详解
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 获取当前登录信息
// ========== 基础信息 ==========// 获取当前登录用户 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 操作
// ========== 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 踢人下线
// ========== 踢人操作 ==========// 踢指定用户下线(所有设备)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 账号封禁
// ========== 封禁操作 ==========// 封禁账号(禁止登录),默认永久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 多账号体系
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 身份切换
// ========== 切换到指定用户 ==========// 切换到用户 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 权限校验方法
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 角色校验
// ========== 校验(不通过抛异常 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 二级认证
// ========== 二级认证概述 ==========// 二级认证用于敏感操作前的二次身份确认// 如:修改密码、删除数据、转账等
// ========== 开启二级认证 ==========// 在敏感操作前开启StpUtil::openSafe();
// ========== 校验二级认证 ==========// 不通过抛异常 NotSafeExceptionStpUtil::checkSafe();
// ========== 判断二级认证状态 ==========if (StpUtil::isSafe()) { // 已通过二级认证,执行敏感操作}
// ========== 二级认证 + 权限校验 ==========// 需要二级认证且拥有权限StpUtil::openSafeAndCheck('user:delete');
// ========== 关闭二级认证 ==========StpUtil::closeSafe();
// ========== 获取二级认证信息 ==========// 获取剩余时间$timeout = StpUtil::getSafeTimeout();
// 获取二级认证状态$status = StpUtil::getSafeStatus();
// ========== 带自定义过期时间的二级认证 ==========StpUtil::openSafe(300); // 5分钟有效期StpUtil::checkSafe();5.4 权限数据注入
// ========== 在服务提供者中注入权限获取逻辑 ==========// 在 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 管理
6.1 Cookie 管理详解
6.1.1 Cookie 配置项详解
return [ 'cookieName' => 'satoken', // Cookie 名称 'cookieDomain' => '', // Cookie 域名,空表示当前域名 'cookiePath' => '/', // Cookie 路径 'cookieSecure' => false, // 是否仅 HTTPS 传输 'cookieHttpOnly' => true, // 是否禁止 JS 读取 'cookieSameSite' => 'Strict', // SameSite 策略 'cookieMaxAge' => 86400, // Cookie 最大生命周期(秒) 'cookiePrefix' => '', // Cookie 前缀];6.1.2 Cookie 各配置项详细说明
| 配置项 | 类型 | 默认值 | 详细说明 |
|---|---|---|---|
cookieName | string | satoken | Cookie 的名称,用于存储 Token |
cookieDomain | string | '' | Cookie 的作用域,设置如 .example.com 可在子域名间共享 |
cookiePath | string | '/' | Cookie 的路径,'/' 表示整个网站都有效 |
cookieSecure | bool | false | 是否仅通过 HTTPS 传输,生产环境建议 true |
cookieHttpOnly | bool | true | 是否禁止 JavaScript 读取 Cookie,建议 true 防止 XSS |
cookieSameSite | string | 'Strict' | CSRF 防护策略:Strict/Lax/None |
cookieMaxAge | int | 86400 | Cookie 在客户端的最大存活时间(秒) |
cookiePrefix | string | '' | Cookie 前缀,可设置如 __Secure- 增强安全性 |
6.1.3 Cookie 读写操作
// ========== Sa-Token 自动 Cookie 操作 ==========// 登录时自动写入 Cookie(当 isWriteCookie = true)StpUtil::login(10001);
// 登出时自动清除 CookieStpUtil::logout();
// 每次请求自动从 Cookie 读取 Token(当 isReadCookie = true)// 框架自动完成,无需手动操作
// ========== 手动 Cookie 操作 ==========// 获取 Cookie 中的 Token$tokenFromCookie = cookie('satoken');
// 手动设置 Cookie(不推荐,建议由 Sa-Token 自动管理)cookie('satoken', $tokenValue, 86400);
// 手动删除 Cookiecookie('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',]);6.1.4 Cookie 常见场景配置
// ========== 场景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,6.1.5 Cookie 前缀说明
// 使用 Cookie 前缀可以增强安全性'cookiePrefix' => '__Secure-',
// 实际写入的 Cookie 名称为:__Secure-satoken// 这样浏览器会强制要求 HTTPS 传输
// 常用的 Cookie 前缀:// __Secure- : 强制 HTTPS// __Host- : 强制 HTTPS + 仅主机6.1.6 跨域 Cookie 配置
// ========== 后端配置 =========='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 对象 |
// ========== 账号 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 操作详解
// ========== Session 存储 ==========StpUtil::setSession('user_id', 10001);StpUtil::setSession('username', 'admin');StpUtil::setSession('user_info', [ 'id' => 10001, 'name' => '张三', '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 配置
'session' => [ 'timeout' => 86400, // Session 默认有效期(秒) 'activityTimeout' => 1800, // Session 活跃超时(秒) 'maxSize' => 1024, // Session 最大存储大小(KB) 'prefix' => 'session_', // Session key 前缀 'cleanInterval' => 3600, // 清理间隔(秒)],6.2.4 Session 清理机制
// ========== 自动清理 ==========// 配置了 activityTimeout 后,框架会自动清理过期 Session
// ========== 手动清理 ==========SaToken::getDao()->cleanExpiredSession();StpUtil::clearUserSession(10001);StpUtil::clearDeviceSession('mobile');
// ========== 定时任务清理 ==========// 创建 app/command/CleanSession.phpnamespace 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.phpreturn [ 'commands' => [ 'clean:session' => \app\command\CleanSession::class, ],];
// Cron: 0 2 * * * cd /path/to/project && php think clean:session6.3 Cookie 与 Session 交互流程图
┌─────────────────────────────────────────────────────────────────────────────┐│ 用户登录完整流程 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 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
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 多权限场景
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 基础匹配
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 排除路径
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 复杂鉴权
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 链式匹配
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
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
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
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
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 防暴力破解
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 黑名单
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 异常检测
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 设备管理
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 敏感操作验证
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 指纹绑定
// ========== 启用 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 认证
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)
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 授权
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 全局过滤器
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 自定义存储
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));
// 自定义 DAOclass 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 集成
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 密码加密工具
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 审计日志
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 事件监听
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 异常类型详解
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
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
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 单应用项目结构
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.json15.2 多应用项目结构
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 不生效?
检查:
app/provider/SaTokenProvider.php是否正确注册config/service.php是否注册了服务提供者config/sa_token.php是否正确加载config/middleware.php是否注册了中间件
Q3: Token 没有自动写入 Cookie?
检查配置:
'isWriteCookie' => true,'cookieSecure' => false,Q4: 如何跨域共享 Cookie?
'cookieDomain' => '.example.com','cookieSameSite' => 'Lax',Q5: 前后端分离如何使用?
'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
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
Sa-Token PHP 使用手册
https://fanrich.eu.org/posts/程技/php/sa-token/相关文章智能推荐
1
ThinkPHP8微服务架构实战:构建高可用API网关系统 | PHP微服务教程
程技2026-06-18
2
Apache Shiro 教程
程技2026-06-26
3
Veitool V2.3.5 源码深度解读
程技2026-06-20
4
勾股OA 源码解读 — ThinkPHP 8 + Layui 企业OA系统
程技2026-06-20
5
AI
程技2026-03-31
随机文章随机推荐
樊笼










