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.1
ThinkPHP >= 8.0
ext-openssl(必需)
ext-json(必需)
ext-redis(Redis 模式必需)

2.2 创建项目#

Terminal window
# 创建单应用项目
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#

Terminal window
composer require pohoc/sa-token

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

Terminal window
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 目录结构#

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
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#

Terminal window
# 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 第九步:创建数据库表结构#

-- 用户表
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.phpapp/middleware/SaTokenMiddleware.phpconfig/service.phpconfig/middleware.phproute/app.php)与 3.1 节相同。

3.3 多应用 + Redis 模式#

3.3.1 场景说明#

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

3.3.2 启用多应用模式#

Terminal window
# 安装多应用扩展
composer require topthink/think-multi-app
# 生成应用目录
php think build

3.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
└── .env

3.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/AN/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();
// ========== 校验二级认证 ==========
// 不通过抛异常 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 权限数据注入#

// ========== 在服务提供者中注入权限获取逻辑 ==========
// 在 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 管理#

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- 增强安全性
// ========== 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',
]);
// ========== 场景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,
// 使用 Cookie 前缀可以增强安全性
'cookiePrefix' => '__Secure-',
// 实际写入的 Cookie 名称为:__Secure-satoken
// 这样浏览器会强制要求 HTTPS 传输
// 常用的 Cookie 前缀:
// __Secure- : 强制 HTTPS
// __Host- : 强制 HTTPS + 仅主机
// ========== 后端配置 ==========
'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' => '张三',
'email' => '[email protected]',
'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 配置#

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 清理机制#

// ========== 自动清理 ==========
// 配置了 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
┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户登录完整流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 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));
// 自定义 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 集成#

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.json

15.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 不生效?#

检查:

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

检查配置:

'isWriteCookie' => true,
'cookieSecure' => false,
'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/
作者
richfan
发布于
2026-06-18
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
richfan
Hello, I'm richfan.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
779
分类
8
标签
138
总字数
3,267,679
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录