Sa-Token PHP 完全开发手册(ThinkPHP 8 版)
框架版本: ThinkPHP 8.0+ 项目版本: 基于 pohoc/sa-token 最新源码 PHP 版本: PHP >= 8.1
目录
- 一、框架概述
- 二、环境要求与安装
- 三、四种部署模式详解
- 四、登录认证
- 五、权限认证
- 六、Cookie 与 Session 管理
- 七、注解式鉴权
- 八、路由拦截鉴权
- 九、SSO 单点登录
- 十、OAuth2.0 认证
- 十一、安全防护
- 十二、高级功能
- 十三、异常处理
- 十四、完整配置参考
- 十五、项目结构
- 十六、API 快速参考
- 十七、常见问题
一、框架概述
1.1 什么是 Sa-Token?
Sa-Token 是一个轻量级权限认证框架,专为 PHP 8.1+ 设计,完美适配 ThinkPHP 8.0+。登录认证只需一行代码:
php
StpUtil::login(10001);权限校验也只需一行:
php
StpUtil::checkPermission('user:add');1.2 核心功能矩阵
| 类别 | 功能 | 说明 |
|---|---|---|
| 登录认证 | 单端登录 | 一个账号只能在一个设备登录 |
| 多端登录 | 一个账号可在多个设备同时登录 | |
| 同端互斥登录 | 同一设备类型只能登录一个 | |
| 记住我 | 延长登录有效期 | |
| 踢人下线 | 强制指定用户下线 | |
| 账号封禁 | 禁止账号登录指定时长 | |
| 服务封禁 | 封禁特定服务如评论、发帖 | |
| 权限认证 | 权限校验 | 判断用户是否拥有某权限 |
| 角色校验 | 判断用户是否拥有某角色 | |
| 二级认证 | 敏感操作二次身份验证 | |
| 身份切换 | 临时切换到其他用户身份 | |
| 注解式鉴权 | 通过注解声明鉴权规则 | |
| Cookie 管理 | 自动写入 | 登录后自动写入 Cookie |
| 自动读取 | 请求时自动读取 Cookie | |
| 安全配置 | Secure/HttpOnly/SameSite | |
| 跨域共享 | 多域名间共享 Cookie | |
| Session 会话 | 账号 Session | 同一账号所有设备共享 |
| Token Session | 仅当前 Token 独享 | |
| 自定义 Session | 任意 Key 的 Session | |
| 自动清理 | 过期 Session 自动清理 | |
| Token 安全 | 多种风格 | uuid/simple-uuid/random-32/random-64/random-128/tik |
| Token 前缀 | 如 Bearer 前缀 | |
| Token 加密 | AES-256/SM4 加密 | |
| 指纹绑定 | IP+UA 防止 Token 盗用 | |
| 黑名单 | 手动拉黑 Token | |
| Refresh Token | 双 Token 机制 | AccessToken + RefreshToken |
| Token 轮换 | 刷新时自动轮换 | |
| 无感刷新 | 客户端无感知刷新 Token | |
| SSO 单点登录 | 四种模式 | 同域/跨域/前后端分离/无 SDK |
| OAuth2.0 | 四种授权模式 | 授权码/隐藏式/密码/客户端凭证 |
| OpenID Connect | 支持 OIDC 协议 | |
| 安全防护 | 防暴力破解 | 登录失败次数限制 |
| IP 异常检测 | 异地登录检测 | |
| 设备管理 | 登录设备记录与管理 | |
| 敏感操作验证 | OTP/安全令牌 |
1.3 核心术语
| 术语 | 说明 |
|---|---|
StpUtil | 静态工具类,提供所有认证 API |
StpLogic | 逻辑类,支持多账号体系隔离 |
SaRouter | 路由匹配器,用于 URL 拦截鉴权 |
Token | 身份凭证,由框架生成和管理 |
LoginId | 登录标识,通常为用户 ID |
SaTokenDao | 数据持久层接口,负责所有会话数据的底层写入和读取 |
Session | 会话对象,存储当前用户的状态数据 |
Cookie | 客户端存储 Token 的机制 |
二、环境要求与安装
2.1 环境要求
text
PHP >= 8.1
ThinkPHP >= 8.0
ext-openssl(必需)
ext-json(必需)
ext-redis(Redis 模式必需)2.2 创建项目
bash
# 创建单应用项目
composer create-project topthink/think tp8-sa-token
cd tp8-sa-token
# 或创建多应用项目
composer create-project topthink/think tp8-sa-token
cd tp8-sa-token
composer require topthink/think-multi-app2.3 安装 Sa-Token
bash
composer require pohoc/sa-token2.4 安装 Redis 扩展(Redis 模式必需)
bash
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 目录结构
text
tp8-sa-token/
├── app/
│ ├── controller/
│ │ ├── AuthController.php
│ │ └── UserController.php
│ ├── middleware/
│ │ └── SaTokenMiddleware.php
│ ├── exception/
│ │ └── Handler.php
│ ├── service/
│ │ └── PermissionService.php
│ └── provider/
│ └── SaTokenProvider.php
├── config/
│ ├── sa_token.php
│ ├── sa_token_sso.php
│ ├── sa_token_oauth2.php
│ ├── middleware.php
│ └── service.php
├── route/
│ └── app.php
├── .env
└── composer.json3.1.3 第一步:创建配置文件 config/sa_token.php
php
<?php
return [
// ==================== Token 基础配置 ====================
// tokenName: Token 名称,同时也是 Cookie 名称
'tokenName' => 'satoken',
// tokenPrefix: Token 前缀,如 Bearer
'tokenPrefix' => 'Bearer',
// tokenStyle: Token 风格
// uuid: UUID 格式 (550e8400-e29b-41d4-a716-446655440000)
// simple-uuid: 简化 UUID (550e8400e29b41d4a716446655440000)
// random-32: 32位随机字符串
// random-64: 64位随机字符串
// random-128: 128位随机字符串
// tik: 24位随机字符串
'tokenStyle' => 'uuid',
// ==================== 有效期配置 ====================
// timeout: Token 有效期(秒),默认30天,-1代表永不过期
'timeout' => 86400,
// activityTimeout: 最低活跃频率(秒),-1不限制
// 设置为正数时,每次请求会刷新 Token 有效期
'activityTimeout' => -1,
// ==================== Cookie 详细配置 ====================
// cookieName: Cookie 名称(默认与 tokenName 一致)
'cookieName' => 'satoken',
// cookieDomain: Cookie 域名,空表示当前域名
// 设置为 .example.com 可在子域名间共享
'cookieDomain' => '',
// cookiePath: Cookie 路径,'/' 表示整个网站都有效
'cookiePath' => '/',
// cookieSecure: 是否仅通过 HTTPS 传输,生产环境建议 true
'cookieSecure' => false,
// cookieHttpOnly: 是否禁止 JavaScript 读取 Cookie,建议 true 防止 XSS
'cookieHttpOnly' => true,
// cookieSameSite: CSRF 防护策略
// Strict: 最安全,Lax: 兼容性更好,None: 需要配合 Secure
'cookieSameSite' => 'Strict',
// cookieMaxAge: Cookie 在客户端的最大存活时间(秒)
'cookieMaxAge' => 86400,
// cookiePrefix: Cookie 前缀,如 __Secure- 增强安全性
'cookiePrefix' => '',
// ==================== 读取/写入配置 ====================
// isReadHeader: 是否从 Header 读取 Token
'isReadHeader' => true,
// isReadCookie: 是否从 Cookie 读取 Token
'isReadCookie' => true,
// isReadBody: 是否从请求体读取 Token
'isReadBody' => false,
// isWriteCookie: 登录后是否写入 Cookie
'isWriteCookie' => true,
// isWriteHeader: 登录后是否写入响应头
'isWriteHeader' => false,
// ==================== 登录策略 ====================
// concurrent: 是否允许多端同时登录
'concurrent' => true,
// isShare: 同端是否复用 Token
'isShare' => true,
// maxLoginCount: 同账号最大登录数,-1不限
'maxLoginCount' => 12,
// maxTryTimes: 创建 Token 最高循环次数
'maxTryTimes' => 12,
// ==================== 存储配置(Redis 模式) ====================
'storage' => 'redis',
'redis' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', ''),
'db' => env('REDIS_DB', 0),
'timeout' => 3,
'prefix' => 'sa_token_',
],
// ==================== Session 配置 ====================
'session' => [
'timeout' => 86400,
'activityTimeout' => 1800,
'maxSize' => 1024,
'prefix' => 'session_',
],
// ==================== 加密配置 ====================
// cryptoType: 加密类型,intl(国际)/ sm(国密)
'cryptoType' => 'intl',
// tokenEncrypt: 是否启用 Token 内容加密
'tokenEncrypt' => false,
// tokenEncryptKey: Token 加密密钥(16/24/32 字节)
'tokenEncryptKey' => '',
// tokenFingerprint: 是否启用 Token 指纹绑定(IP + User-Agent)
'tokenFingerprint' => false,
// ==================== Refresh Token ====================
// refreshToken: 是否启用 RefreshToken
'refreshToken' => false,
// refreshTokenTimeout: RefreshToken 有效期(秒),默认30天
'refreshTokenTimeout' => 2592000,
// refreshTokenRotation: 刷新时是否轮换 RefreshToken
'refreshTokenRotation' => true,
// ==================== 安全配置 ====================
// antiBruteMaxFailures: 防暴力破解最大失败次数,0不限制
'antiBruteMaxFailures' => 5,
// antiBruteLockDuration: 锁定持续时间(秒)
'antiBruteLockDuration' => 600,
// ipAnomalyDetection: IP 异常检测
'ipAnomalyDetection' => true,
// ipAnomalySensitivity: IP 异常灵敏度(1-5)
'ipAnomalySensitivity' => 3,
// deviceManagement: 设备管理
'deviceManagement' => true,
// auditLog: 审计日志
'auditLog' => true,
// auditLogMaxEntries: 每种事件最大日志条数
'auditLogMaxEntries' => 1000,
// auditLogTtlDays: 日志保留天数
'auditLogTtlDays' => 30,
// ==================== JWT 配置 ====================
'jwtSecretKey' => env('JWT_SECRET_KEY', ''),
'jwtStateless' => false,
// ==================== 签名配置 ====================
'signKey' => env('SIGN_KEY', ''),
'signTimestampGap' => 600,
'signAlg' => 'sha256',
// ==================== 日志配置 ====================
'isLog' => env('APP_DEBUG', false),
];3.1.4 第二步:创建权限数据提供者 app/service/PermissionService.php
php
<?php
declare(strict_types=1);
namespace app\service;
use think\facade\Db;
/**
* 权限数据提供者
* 负责从数据库获取用户的权限和角色信息
*/
class PermissionService
{
/**
* 获取用户权限码集合
*
* @param int $userId 用户ID
* @return array 权限码列表
*/
public function getPermissionList(int $userId): array
{
$perms = Db::name('user_permission')
->where('user_id', $userId)
->column('permission_code');
return $perms ?: [];
}
/**
* 获取用户角色标识集合
*
* @param int $userId 用户ID
* @return array 角色标识列表
*/
public function getRoleList(int $userId): array
{
$roles = Db::name('user_role')
->alias('ur')
->join('role r', 'ur.role_id = r.id')
->where('ur.user_id', $userId)
->column('r.role_code');
return $roles ?: [];
}
/**
* 获取用户所有权限(含角色继承)
*
* @param int $userId 用户ID
* @return array 所有权限码列表
*/
public function getAllPermissions(int $userId): array
{
$roles = $this->getRoleList($userId);
$perms = [];
// 从角色继承权限
if (!empty($roles)) {
$rolePerms = Db::name('role_permission')
->whereIn('role_code', $roles)
->column('permission_code');
$perms = array_merge($perms, $rolePerms);
}
// 合并用户直接权限
$userPerms = $this->getPermissionList($userId);
$perms = array_merge($perms, $userPerms);
return array_unique($perms);
}
/**
* 验证用户密码
*
* @param string $username 用户名
* @param string $password 密码
* @return array|null 用户信息或 null
*/
public function verifyPassword(string $username, string $password): ?array
{
$user = Db::name('user')
->where('username', $username)
->where('status', 1)
->find();
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return null;
}
/**
* 根据用户ID获取用户信息
*
* @param int $userId 用户ID
* @return array|null 用户信息
*/
public function getUserById(int $userId): ?array
{
return Db::name('user')->find($userId);
}
/**
* 检查用户是否有指定权限
*
* @param int $userId 用户ID
* @param string $permission 权限码
* @return bool
*/
public function hasPermission(int $userId, string $permission): bool
{
$permissions = $this->getAllPermissions($userId);
return in_array($permission, $permissions);
}
/**
* 检查用户是否有指定角色
*
* @param int $userId 用户ID
* @param string $role 角色码
* @return bool
*/
public function hasRole(int $userId, string $role): bool
{
$roles = $this->getRoleList($userId);
return in_array($role, $roles);
}
}3.1.5 第三步:创建服务提供者 app/provider/SaTokenProvider.php
php
<?php
declare(strict_types=1);
namespace app\provider;
use think\Service;
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoRedis;
use SaToken\StpUtil;
use app\service\PermissionService;
class SaTokenProvider extends Service
{
public function register(): void
{
// 1. 加载配置
$config = config('sa_token');
// 2. 初始化 Sa-Token
SaToken::init($config);
// 3. 设置 Redis 存储驱动
if ($config['storage'] === 'redis') {
SaToken::setDao(new SaTokenDaoRedis($config['redis']));
}
// 4. 注入权限提供者
$this->injectPermissionGetter();
// 5. 注册事件监听器(可选)
$this->registerListeners();
}
protected function injectPermissionGetter(): void
{
$service = new PermissionService();
// 设置权限获取回调
StpUtil::setPermissionGetter(function($loginId) use ($service) {
return $service->getAllPermissions((int) $loginId);
});
// 设置角色获取回调
StpUtil::setRoleGetter(function($loginId) use ($service) {
return $service->getRoleList((int) $loginId);
});
}
protected function registerListeners(): void
{
// 注册事件监听器
// SaToken::addListener(new MySaTokenListener());
}
}3.1.6 第四步:创建中间件 app/middleware/SaTokenMiddleware.php
php
<?php
declare(strict_types=1);
namespace app\middleware;
use SaToken\SaRouter;
use SaToken\StpUtil;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use SaToken\Exception\DisableServiceException;
use think\Response;
use think\facade\Log;
class SaTokenMiddleware
{
public function handle($request, \Closure $next)
{
try {
// ========== 定义路由拦截规则 ==========
// 1. 登录校验:所有 /api/** 接口需要登录
// 排除登录、注册、刷新接口
SaRouter::match('/api/**')
->exclude('/api/login', '/api/register', '/api/refresh')
->check(function() {
StpUtil::checkLogin();
});
// 2. 角色校验:/admin/** 需要 admin 角色
SaRouter::match('/admin/**')
->check(function() {
StpUtil::checkLogin();
StpUtil::checkRole('admin');
});
// 3. 权限校验:/api/user/** 需要 user:read 权限
SaRouter::match('/api/user/**')
->check(function() {
StpUtil::checkPermission('user:read');
});
// 4. 多权限校验:/api/order/** 需要 order:manage 或 order:view
SaRouter::match('/api/order/**')
->check(function() {
StpUtil::checkPermissionOr(['order:manage', 'order:view']);
});
// 5. GET 请求特殊校验:/api/search/** 需要登录
SaRouter::match('/api/search/**', 'GET')
->check(function() {
StpUtil::checkLogin();
});
// 6. POST 请求特殊校验:/api/upload/** 需要 upload 权限
SaRouter::match('/api/upload/**', 'POST')
->check(function() {
StpUtil::checkPermission('upload');
});
} catch (NotLoginException $e) {
return $this->error(401, '未登录或登录已过期', ['type' => $e->getType()]);
} catch (NotPermissionException $e) {
return $this->error(403, '权限不足:' . $e->getPermission());
} catch (NotRoleException $e) {
return $this->error(403, '角色不足:' . $e->getRole());
} catch (DisableServiceException $e) {
$remaining = StpUtil::getDisableTime();
return $this->error(403, "账号已被封禁,剩余 {$remaining} 秒");
} catch (\Exception $e) {
Log::error('SaToken 鉴权异常:' . $e->getMessage());
return $this->error(500, '服务器内部错误');
}
return $next($request);
}
protected function error(int $code, string $msg, array $extra = []): Response
{
return json([
'code' => $code,
'msg' => $msg,
'data' => null,
'extra' => $extra
], $code);
}
}3.1.7 第五步:注册服务提供者 config/service.php
php
<?php
return [
'providers' => [
\app\provider\SaTokenProvider::class,
],
];3.1.8 第六步:注册中间件 config/middleware.php
php
<?php
return [
// 全局中间件
'global' => [
// ...
],
// 别名中间件(用于路由注册)
'alias' => [
'sa-token' => \app\middleware\SaTokenMiddleware::class,
],
];3.1.9 第七步:配置路由 route/app.php
php
<?php
use think\facade\Route;
// ========== 公开路由(无需鉴权) ==========
Route::post('api/login', 'Auth/login');
Route::post('api/register', 'Auth/register');
Route::post('api/refresh', 'Auth/refresh');
// ========== 需要鉴权的路由(使用中间件) ==========
Route::group('api', function () {
Route::get('user/info', 'Auth/info');
Route::post('user/logout', 'Auth/logout');
Route::resource('users', 'User');
})->middleware('sa-token');
// ========== 后台管理路由 ==========
Route::group('admin', function () {
Route::get('/', 'Admin/index');
Route::get('/users', 'Admin/users');
Route::get('/settings', 'Admin/settings');
})->middleware('sa-token');
// ========== 公开API ==========
Route::get('api/public', 'Public/index');3.1.10 第八步:环境变量配置 .env
bash
# Redis 配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Sa-Token 配置
SA_TOKEN_TIMEOUT=86400
SA_TOKEN_REFRESH_TIMEOUT=2592000
# JWT 配置
JWT_SECRET_KEY=your-jwt-secret
# 签名配置
SIGN_KEY=your-sign-key
# 调试模式(生产环境设为 false)
APP_DEBUG=false3.1.11 第九步:创建数据库表结构
sql
-- 用户表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`nickname` VARCHAR(100),
`email` VARCHAR(100),
`avatar` VARCHAR(255),
`status` TINYINT DEFAULT 1,
`last_login` INT,
`last_ip` VARCHAR(45),
`create_time` INT,
`update_time` INT,
INDEX `idx_username` (`username`),
INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 用户权限表
CREATE TABLE `user_permission` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`permission_code` VARCHAR(50) NOT NULL,
`create_time` INT,
UNIQUE KEY `uk_user_perm` (`user_id`, `permission_code`),
INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户权限表';
-- 角色表
CREATE TABLE `role` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`role_code` VARCHAR(50) NOT NULL UNIQUE,
`role_name` VARCHAR(100) NOT NULL,
`description` VARCHAR(255),
`create_time` INT,
INDEX `idx_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
-- 用户角色关联表
CREATE TABLE `user_role` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL,
`role_id` INT NOT NULL,
`create_time` INT,
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
-- 角色权限表
CREATE TABLE `role_permission` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`role_code` VARCHAR(50) NOT NULL,
`permission_code` VARCHAR(50) NOT NULL,
`create_time` INT,
UNIQUE KEY `uk_role_perm` (`role_code`, `permission_code`),
INDEX `idx_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限表';
-- 审计日志表
CREATE TABLE `audit_log` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT,
`event_type` VARCHAR(50) NOT NULL,
`ip` VARCHAR(45),
`user_agent` VARCHAR(255),
`details` JSON,
`create_time` INT,
INDEX `idx_user_id` (`user_id`),
INDEX `idx_event_type` (`event_type`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志表';3.2 单应用 + 内存模式
3.2.1 场景说明
适用于开发测试环境,使用内存作为存储介质,无需 Redis。
3.2.2 配置文件 config/sa_token.php
php
<?php
return [
// ==================== Token 基础配置 ====================
'tokenName' => 'satoken',
'tokenPrefix' => 'Bearer',
'tokenStyle' => 'uuid',
// ==================== 有效期配置 ====================
'timeout' => 86400,
'activityTimeout' => -1,
// ==================== Cookie 配置 ====================
'cookieName' => 'satoken',
'cookieDomain' => '',
'cookiePath' => '/',
'cookieSecure' => false,
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',
'cookieMaxAge' => 86400,
// ==================== 读取/写入配置 ====================
'isReadHeader' => true,
'isReadCookie' => true,
'isReadBody' => false,
'isWriteCookie' => true,
'isWriteHeader' => false,
// ==================== 登录策略 ====================
'concurrent' => true,
'isShare' => true,
'maxLoginCount' => 12,
// ==================== 存储配置(内存模式) ====================
'storage' => 'memory',
// ==================== Session 配置 ====================
'session' => [
'timeout' => 86400,
'activityTimeout' => 1800,
'maxSize' => 1024,
],
// ==================== 加密配置 ====================
'cryptoType' => 'intl',
'tokenEncrypt' => false,
'tokenFingerprint' => false,
// ==================== Refresh Token ====================
'refreshToken' => false,
'refreshTokenTimeout' => 2592000,
// ==================== 安全配置 ====================
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,
'ipAnomalyDetection' => true,
'deviceManagement' => true,
'auditLog' => true,
// ==================== JWT 配置 ====================
'jwtSecretKey' => '',
'jwtStateless' => false,
// ==================== 签名配置 ====================
'signKey' => '',
'signTimestampGap' => 600,
'signAlg' => 'sha256',
'isLog' => true,
];3.2.3 服务提供者 app/provider/SaTokenProvider.php(内存模式)
php
<?php
declare(strict_types=1);
namespace app\provider;
use think\Service;
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoMemory;
use SaToken\StpUtil;
use app\service\PermissionService;
class SaTokenProvider extends Service
{
public function register(): void
{
$config = config('sa_token');
SaToken::init($config);
// ========== 使用内存存储 ==========
// 所有会话数据存储在 PHP 内存中
// 应用重启后数据全部丢失
SaToken::setDao(new SaTokenDaoMemory());
$service = new PermissionService();
StpUtil::setPermissionGetter(function($loginId) use ($service) {
return $service->getAllPermissions((int) $loginId);
});
StpUtil::setRoleGetter(function($loginId) use ($service) {
return $service->getRoleList((int) $loginId);
});
}
}3.2.4 内存模式注意事项
php
// ========== 内存模式特点 ==========
// 1. 应用重启后所有会话丢失,用户需重新登录
// 2. 不支持分布式部署
// 3. 适合开发测试环境
// 4. 数据存储在 SaTokenDaoMemory 类的静态属性中
// 5. 进程内共享,进程重启丢失
// ========== 内存模式适用场景 ==========
// - 本地开发调试
// - 单元测试
// - 演示/原型项目
// - 低并发内部工具
其余文件(app/service/PermissionService.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 启用多应用模式
bash
# 安装多应用扩展
composer require topthink/think-multi-app
# 生成应用目录
php think build3.3.3 目录结构
text
tp8-sa-token/
├── app/
│ ├── common/
│ │ ├── middleware/
│ │ │ └── SaTokenMiddleware.php
│ │ ├── service/
│ │ │ └── PermissionService.php
│ │ ├── exception/
│ │ │ └── Handler.php
│ │ └── provider/
│ │ └── SaTokenProvider.php
│ ├── api/
│ │ ├── controller/
│ │ │ └── AuthController.php
│ │ ├── config/
│ │ │ └── sa_token.php
│ │ └── route/
│ │ └── app.php
│ ├── admin/
│ │ ├── controller/
│ │ │ └── AuthController.php
│ │ ├── config/
│ │ │ └── sa_token.php
│ │ └── route/
│ │ └── app.php
│ └── provider.php
├── config/
│ └── sa_token.php
└── .env3.3.4 全局配置 config/sa_token.php
php
<?php
return [
// ==================== 通用配置 ====================
'tokenName' => 'satoken',
'tokenPrefix' => 'Bearer',
'tokenStyle' => 'uuid',
// ==================== Cookie 通用配置 ====================
'cookieDomain' => '',
'cookiePath' => '/',
'cookieSecure' => false,
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',
// ==================== 存储配置(共享 Redis) ====================
'storage' => 'redis',
'redis' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', ''),
'db' => env('REDIS_DB', 0),
'timeout' => 3,
// 全局前缀由各应用配置覆盖
],
// ==================== Session 配置(共享) ====================
'session' => [
'timeout' => 86400,
'activityTimeout' => 1800,
'maxSize' => 1024,
],
// ==================== 安全配置(全局通用) ====================
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,
'ipAnomalyDetection' => true,
'deviceManagement' => true,
'auditLog' => true,
// ==================== 各应用差异化配置 ====================
'apps' => [
'api' => [
// API Token 有效期 2 小时
'timeout' => 7200,
'refreshToken' => true,
'refreshTokenTimeout' => 2592000,
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,
'tokenFingerprint' => true,
'redis' => ['prefix' => 'sa_token_api_'],
],
'admin' => [
// Admin Token 有效期 8 小时
'timeout' => 28800,
'refreshToken' => true,
'concurrent' => false,
'isShare' => false,
'isReadCookie' => true,
'isWriteCookie' => true,
'cookiePath' => '/admin',
'cookieSecure' => true,
'tokenFingerprint' => true,
'redis' => ['prefix' => 'sa_token_admin_'],
],
],
];3.3.5 API 应用专属配置 app/api/config/sa_token.php
php
<?php
return [
'timeout' => 7200,
'refreshToken' => true,
'refreshTokenTimeout' => 2592000,
'refreshTokenRotation' => true,
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,
'concurrent' => true,
'tokenFingerprint' => true,
'redis' => [
'prefix' => 'sa_token_api_',
],
];3.3.6 Admin 应用专属配置 app/admin/config/sa_token.php
php
<?php
return [
'timeout' => 28800,
'refreshToken' => true,
'concurrent' => false,
'isShare' => false,
'isReadCookie' => true,
'isWriteCookie' => true,
'cookiePath' => '/admin',
'cookieSecure' => true,
'tokenFingerprint' => true,
'redis' => [
'prefix' => 'sa_token_admin_',
],
];3.3.7 公共服务提供者 app/common/provider/SaTokenProvider.php
php
<?php
declare(strict_types=1);
namespace app\common\provider;
use think\Service;
use SaToken\SaToken;
use SaToken\StpUtil;
use SaToken\Dao\SaTokenDaoRedis;
use app\common\service\PermissionService;
class SaTokenProvider extends Service
{
public function register(): void
{
$baseConfig = config('sa_token');
$appName = app()->http->getName() ?: 'api';
// 获取应用专属配置
$appConfig = $this->getAppConfig($appName, $baseConfig);
SaToken::init($appConfig);
// ========== 使用 Redis 存储 ==========
$redisConfig = $appConfig['redis'] ?? $baseConfig['redis'] ?? [];
SaToken::setDao(new SaTokenDaoRedis($redisConfig));
$this->injectPermission($appName);
}
protected function getAppConfig(string $appName, array $baseConfig): array
{
$appConfigFile = app()->getAppPath() . $appName . '/config/sa_token.php';
if (is_file($appConfigFile)) {
$appConfig = include $appConfigFile;
return array_merge($baseConfig, $appConfig);
}
if (isset($baseConfig['apps'][$appName])) {
return array_merge($baseConfig, $baseConfig['apps'][$appName]);
}
return $baseConfig;
}
protected function injectPermission(string $appName): void
{
// 不同应用使用不同的权限服务
$service = match ($appName) {
'admin' => new \app\admin\service\AdminPermissionService(),
'api' => new \app\api\service\ApiPermissionService(),
default => new PermissionService(),
};
StpUtil::setPermissionGetter(function($loginId) use ($service) {
return $service->getAllPermissions((int) $loginId);
});
StpUtil::setRoleGetter(function($loginId) use ($service) {
return $service->getRoleList((int) $loginId);
});
}
}3.3.8 公共中间件 app/common/middleware/SaTokenMiddleware.php
php
<?php
declare(strict_types=1);
namespace app\common\middleware;
use SaToken\SaRouter;
use SaToken\StpUtil;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use think\Response;
use think\facade\Log;
class SaTokenMiddleware
{
protected array $appRoutes = [
'api' => [
'match' => '/api/**',
'exclude' => ['/api/login', '/api/register', '/api/refresh'],
'check' => 'login',
],
'admin' => [
'match' => '/admin/**',
'exclude' => ['/admin/login'],
'check' => ['login', 'role:admin'],
],
];
public function handle($request, \Closure $next)
{
try {
$appName = app()->http->getName() ?: 'api';
$rules = $this->appRoutes[$appName] ?? $this->appRoutes['api'];
$router = SaRouter::match($rules['match']);
if (!empty($rules['exclude'])) {
$router->exclude(...$rules['exclude']);
}
$router->check(function() use ($rules) {
$checks = is_array($rules['check']) ? $rules['check'] : [$rules['check']];
foreach ($checks as $check) {
if ($check === 'login') {
StpUtil::checkLogin();
} elseif (str_starts_with($check, 'role:')) {
StpUtil::checkRole(substr($check, 5));
} elseif (str_starts_with($check, 'perm:')) {
StpUtil::checkPermission(substr($check, 5));
}
}
});
} catch (NotLoginException $e) {
return json(['code' => 401, 'msg' => '请先登录'], 401);
} catch (NotPermissionException $e) {
return json(['code' => 403, 'msg' => '权限不足:' . $e->getPermission()], 403);
} catch (NotRoleException $e) {
return json(['code' => 403, 'msg' => '角色不足:' . $e->getRole()], 403);
} catch (\Exception $e) {
Log::error('SaToken 鉴权异常:' . $e->getMessage());
return json(['code' => 500, 'msg' => '服务器内部错误'], 500);
}
return $next($request);
}
}3.3.9 注册服务提供者 app/provider.php
php
<?php
return [
'providers' => [
\app\common\provider\SaTokenProvider::class,
],
];3.3.10 API 应用路由 app/api/route/app.php
php
<?php
use think\facade\Route;
Route::post('login', 'Auth/login');
Route::post('register', 'Auth/register');
Route::post('refresh', 'Auth/refresh');
Route::group(function () {
Route::get('user/info', 'Auth/info');
Route::post('user/logout', 'Auth/logout');
Route::resource('users', 'User');
})->middleware('sa-token');3.3.11 Admin 应用路由 app/admin/route/app.php
php
<?php
use think\facade\Route;
Route::post('login', 'Auth/login');
Route::group(function () {
Route::get('dashboard', 'Admin/dashboard');
Route::get('users', 'Admin/users');
Route::get('settings', 'Admin/settings');
})->middleware('sa-token');3.3.12 API 权限服务 app/api/service/ApiPermissionService.php
php
<?php
declare(strict_types=1);
namespace app\api\service;
use think\facade\Db;
class ApiPermissionService
{
public function getPermissionList(int $userId): array
{
return Db::name('user_permission')
->where('user_id', $userId)
->column('permission_code') ?: [];
}
public function getRoleList(int $userId): array
{
return Db::name('user_role')
->alias('ur')
->join('role r', 'ur.role_id = r.id')
->where('ur.user_id', $userId)
->column('r.role_code') ?: [];
}
public function getAllPermissions(int $userId): array
{
return $this->getPermissionList($userId);
}
public function verifyPassword(string $username, string $password): ?array
{
$user = Db::name('user')
->where('username', $username)
->where('status', 1)
->find();
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return null;
}
}3.3.13 Admin 权限服务 app/admin/service/AdminPermissionService.php
php
<?php
declare(strict_types=1);
namespace app\admin\service;
use think\facade\Db;
class AdminPermissionService
{
public function getPermissionList(int $adminId): array
{
$perms = Db::name('admin_permission')
->where('admin_id', $adminId)
->column('permission_code');
return $perms ?: [];
}
public function getRoleList(int $adminId): array
{
$roles = Db::name('admin_role')
->alias('ar')
->join('role r', 'ar.role_id = r.id')
->where('ar.admin_id', $adminId)
->column('r.role_code');
return $roles ?: [];
}
public function getAllPermissions(int $adminId): array
{
$roles = $this->getRoleList($adminId);
$perms = [];
if (!empty($roles)) {
$rolePerms = Db::name('role_permission')
->whereIn('role_code', $roles)
->column('permission_code');
$perms = array_merge($perms, $rolePerms);
}
$userPerms = $this->getPermissionList($adminId);
$perms = array_merge($perms, $userPerms);
return array_unique($perms);
}
public function verifyPassword(string $username, string $password): ?array
{
$user = Db::name('admin')
->where('username', $username)
->where('status', 1)
->find();
if ($user && password_verify($password, $user['password'])) {
return $user;
}
return null;
}
}3.4 多应用 + 内存模式
3.4.1 全局配置 config/sa_token.php
php
<?php
return [
'tokenName' => 'satoken',
'tokenPrefix' => 'Bearer',
'tokenStyle' => 'uuid',
'cookieDomain' => '',
'cookiePath' => '/',
'cookieSecure' => false,
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',
// ========== 存储配置(内存模式) ==========
'storage' => 'memory',
'session' => [
'timeout' => 86400,
'activityTimeout' => 1800,
'maxSize' => 1024,
],
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,
'ipAnomalyDetection' => true,
'deviceManagement' => true,
'auditLog' => true,
'apps' => [
'api' => [
'timeout' => 7200,
'refreshToken' => true,
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,
'tokenFingerprint' => true,
],
'admin' => [
'timeout' => 28800,
'refreshToken' => true,
'concurrent' => false,
'isShare' => false,
'cookiePath' => '/admin',
'tokenFingerprint' => true,
],
],
];3.4.2 公共服务提供者 app/common/provider/SaTokenProvider.php(内存模式)
php
<?php
declare(strict_types=1);
namespace app\common\provider;
use think\Service;
use SaToken\SaToken;
use SaToken\StpUtil;
use SaToken\Dao\SaTokenDaoMemory;
use app\common\service\PermissionService;
class SaTokenProvider extends Service
{
public function register(): void
{
$baseConfig = config('sa_token');
$appName = app()->http->getName() ?: 'api';
$appConfig = $this->getAppConfig($appName, $baseConfig);
SaToken::init($appConfig);
// ========== 使用内存存储 ==========
SaToken::setDao(new SaTokenDaoMemory());
$this->injectPermission($appName);
}
protected function getAppConfig(string $appName, array $baseConfig): array
{
$appConfigFile = app()->getAppPath() . $appName . '/config/sa_token.php';
if (is_file($appConfigFile)) {
return array_merge($baseConfig, include $appConfigFile);
}
if (isset($baseConfig['apps'][$appName])) {
return array_merge($baseConfig, $baseConfig['apps'][$appName]);
}
return $baseConfig;
}
protected function injectPermission(string $appName): void
{
$service = match ($appName) {
'admin' => new \app\admin\service\AdminPermissionService(),
'api' => new \app\api\service\ApiPermissionService(),
default => new PermissionService(),
};
StpUtil::setPermissionGetter(function($loginId) use ($service) {
return $service->getAllPermissions((int) $loginId);
});
StpUtil::setRoleGetter(function($loginId) use ($service) {
return $service->getRoleList((int) $loginId);
});
}
}3.5 四种模式完整对比
| 对比项 | 单应用+Redis | 单应用+内存 | 多应用+Redis | 多应用+内存 |
|---|---|---|---|---|
| Redis 依赖 | ✅ 需要 | ❌ 不需要 | ✅ 需要 | ❌ 不需要 |
| 数据持久化 | ✅ | ❌ | ✅ | ❌ |
| 分布式支持 | ✅ | ❌ | ✅ | ❌ |
| 多应用支持 | ❌ | ❌ | ✅ | ✅ |
| 应用间共享会话 | N/A | N/A | ✅ | ❌ |
| 部署复杂度 | 中 | 低 | 中 | 中 |
| 性能 | 高 | 极高 | 高 | 极高 |
| 推荐环境 | 生产 | 开发/测试 | 生产/微服务 | 开发/小型项目 |
四、登录认证
4.1 创建认证控制器 app/controller/AuthController.php
php
<?php
declare(strict_types=1);
namespace app\controller;
use think\facade\Db;
use SaToken\StpUtil;
use app\service\PermissionService;
use think\response\Json;
use SaToken\Security\SaAuditLog;
class AuthController
{
protected PermissionService $permissionService;
public function __construct(PermissionService $permissionService)
{
$this->permissionService = $permissionService;
}
/**
* 用户登录 - 完整流程演示
*/
public function login(): Json
{
$username = input('post.username', '');
$password = input('post.password', '');
$device = input('post.device', 'web');
$remember = input('post.remember', false);
if (empty($username) || empty($password)) {
return json(['code' => 400, 'msg' => '用户名和密码不能为空']);
}
// ========== 防暴力破解检查 ==========
StpUtil::checkAntiBrute($username);
// ========== 验证用户凭证 ==========
$user = $this->permissionService->verifyPassword($username, $password);
if (!$user) {
StpUtil::recordAntiBruteFailure($username);
$info = StpUtil::getAntiBruteInfo($username);
return json([
'code' => 401,
'msg' => '用户名或密码错误',
'remaining' => $info['remainingAttempts'] ?? 0
]);
}
// 登录成功,清除失败记录
StpUtil::clearAntiBruteFailure($username);
// ========== 执行登录 ==========
// Sa-Token 登录,一行代码搞定
// 内部自动完成:生成Token、绑定LoginId、创建Session、写入Cookie、触发事件
StpUtil::login((string) $user['id'], [
'device' => $device,
'isLasting' => $remember,
'extra' => [
'nickname' => $user['nickname'],
'ip' => request()->ip(),
'login_time' => time(),
]
]);
// ========== 登录后操作 ==========
// 1. 存储用户信息到 Session
StpUtil::setSession('user_info', [
'id' => $user['id'],
'username' => $user['username'],
'nickname' => $user['nickname'],
'email' => $user['email'] ?? '',
]);
// 2. 存储权限信息到 Session(避免每次查询数据库)
$permissions = $this->permissionService->getAllPermissions((int) $user['id']);
$roles = $this->permissionService->getRoleList((int) $user['id']);
StpUtil::setSession('permissions', $permissions);
StpUtil::setSession('roles', $roles);
// 3. 更新数据库登录信息
Db::name('user')
->where('id', $user['id'])
->update([
'last_login' => time(),
'last_ip' => request()->ip(),
]);
// 4. 记录审计日志
SaAuditLog::logLogin($user['id'], [
'ip' => request()->ip(),
'device' => $device,
]);
// ========== 返回结果 ==========
return json([
'code' => 200,
'msg' => '登录成功',
'data' => [
'token' => StpUtil::getTokenValue(),
'tokenPrefix' => config('sa_token.tokenPrefix'),
'userId' => $user['id'],
'nickname' => $user['nickname'],
'expires' => StpUtil::getTokenTimeout(),
'permissions' => $permissions,
'roles' => $roles,
]
]);
}
/**
* 退出登录
*/
public function logout(): Json
{
// 获取用户信息(用于审计)
$userId = StpUtil::getLoginId(false);
// 执行登出
StpUtil::logout();
// 记录审计日志
if ($userId) {
SaAuditLog::logLogout($userId);
}
return json([
'code' => 200,
'msg' => '退出成功'
]);
}
/**
* 获取当前用户信息
*/
public function info(): Json
{
StpUtil::checkLogin();
$userId = StpUtil::getLoginId();
// 从 Session 读取用户信息
$userInfo = StpUtil::getSession('user_info');
if (empty($userInfo)) {
// Session 中没有,从数据库读取
$user = Db::name('user')->find($userId);
if (!$user) {
return json(['code' => 404, 'msg' => '用户不存在']);
}
$userInfo = $user;
}
// 获取权限和角色
$permissions = StpUtil::getPermissionList();
$roles = StpUtil::getRoleList();
return json([
'code' => 200,
'data' => [
'id' => $userId,
'username' => $userInfo['username'] ?? '',
'nickname' => $userInfo['nickname'] ?? '',
'email' => $userInfo['email'] ?? '',
'permissions' => $permissions,
'roles' => $roles,
'token' => StpUtil::getTokenValue(),
'tokenTimeout' => StpUtil::getTokenTimeout(),
'loginDevice' => StpUtil::getLoginDevice(),
'extra' => StpUtil::getExtra(),
]
]);
}
/**
* 刷新 Token(启用 RefreshToken 时使用)
*/
public function refresh(): Json
{
if (!config('sa_token.refreshToken')) {
return json(['code' => 400, 'msg' => 'RefreshToken 未启用']);
}
try {
$newToken = StpUtil::refreshToken();
return json([
'code' => 200,
'msg' => 'Token 刷新成功',
'data' => [
'token' => $newToken,
'expires' => StpUtil::getTokenTimeout(),
]
]);
} catch (\Exception $e) {
return json([
'code' => 401,
'msg' => '刷新失败:' . $e->getMessage()
]);
}
}
/**
* 注册账号
*/
public function register(): Json
{
$username = input('post.username', '');
$password = input('post.password', '');
$nickname = input('post.nickname', '');
if (empty($username) || empty($password)) {
return json(['code' => 400, 'msg' => '用户名和密码不能为空']);
}
if (strlen($password) < 6) {
return json(['code' => 400, 'msg' => '密码长度不能少于6位']);
}
$exist = Db::name('user')->where('username', $username)->find();
if ($exist) {
return json(['code' => 400, 'msg' => '用户名已存在']);
}
$userId = Db::name('user')->insertGetId([
'username' => $username,
'password' => password_hash($password, PASSWORD_BCRYPT),
'nickname' => $nickname ?: $username,
'status' => 1,
'create_time' => time(),
'update_time' => time(),
]);
StpUtil::login((string) $userId);
$token = StpUtil::getTokenValue();
return json([
'code' => 200,
'msg' => '注册成功',
'data' => [
'userId' => $userId,
'token' => $token,
]
]);
}
}4.2 登录参数详解
php
use SaToken\SaLoginParameter;
// ========== 方式一:数组参数 ==========
StpUtil::login(10001, [
'device' => 'web', // 设备标识,用于同端互斥
'isLasting' => true, // 记住我,延长有效期
'timeout' => 3600, // 自定义有效期(秒),覆盖全局配置
'extra' => [ // 扩展数据,可通过 StpUtil::getExtra() 获取
'nickname' => '张三',
'avatar' => 'https://...',
'role' => 'admin',
]
]);
// ========== 方式二:登录参数对象 ==========
$param = SaLoginParameter::create()
->setDevice('mobile')
->setIsLasting(true)
->setTimeout(7200)
->setExtra([
'nickname' => '张三',
'app_version' => '2.3.1',
]);
StpUtil::login(10001, $param);
// ========== 方式三:简单登录(使用默认参数) ==========
StpUtil::login(10001);4.3 获取当前登录信息
php
// ========== 基础信息 ==========
// 获取当前登录用户 ID
$userId = StpUtil::getLoginId();
// 获取当前 Token 值
$token = StpUtil::getTokenValue();
// 获取 Token 剩余有效期(秒)
$timeout = StpUtil::getTokenTimeout();
// 获取 Token 创建时间
$createTime = StpUtil::getTokenCreateTime();
// 获取 Token 上次活动时间
$lastActivity = StpUtil::getTokenLastActivityTime();
// ========== 扩展信息 ==========
// 获取 Token 扩展信息
$extra = StpUtil::getExtra();
$nickname = StpUtil::getExtra('nickname');
// 更新扩展信息
StpUtil::updateExtra(['nickname' => '李四']);
StpUtil::updateExtra('level', 5);
// ========== 设备信息 ==========
// 获取当前登录设备
$device = StpUtil::getLoginDevice();
// ========== 登录状态 ==========
// 判断是否登录
if (StpUtil::isLogin()) {
echo '已登录,用户ID:' . StpUtil::getLoginId();
}
// 获取登录类型
$loginType = StpUtil::getLoginType();
// ========== 完整 Token 信息 ==========
$tokenInfo = StpUtil::getTokenInfo();
// [
// 'tokenValue' => 'xxx',
// 'loginId' => 10001,
// 'loginType' => 'login',
// 'device' => 'web',
// 'timeout' => 86400,
// 'createTime' => 1700000000,
// 'lastActivity' => 1700003600,
// 'extra' => ['nickname' => '张三'],
// ]
4.4 Token 操作
php
// ========== Token 刷新 ==========
// 刷新当前 Token(延长有效期)
$newToken = StpUtil::refreshToken();
// 刷新指定用户的 Token
$newToken = StpUtil::refreshToken(10001);
// ========== Token 检查 ==========
// 检查 Token 是否有效
$valid = StpUtil::checkToken($tokenValue);
// 获取 Token 对应的 LoginId
$loginId = StpUtil::getLoginIdByToken($tokenValue);
// ========== Token 替换 ==========
// 替换 Token(生成新 Token,旧 Token 失效)
$newToken = StpUtil::replaceToken();
// ========== Token 黑名单 ==========
// 加入黑名单
StpUtil::addTokenToBlacklist($tokenValue);
// 移除黑名单
StpUtil::removeTokenFromBlacklist($tokenValue);
// 判断是否在黑名单
$isBlacklisted = StpUtil::isTokenBlacklisted($tokenValue);4.5 踢人下线
php
// ========== 踢人操作 ==========
// 踢指定用户下线(所有设备)
StpUtil::kickout(10001);
// 踢指定用户的指定设备下线
StpUtil::kickout(10001, 'mobile');
// 踢当前用户下线(退出登录)
StpUtil::kickout();
// ========== 获取被踢信息 ==========
$info = StpUtil::getKickoutInfo();
// [
// 'isKickout' => true,
// 'reason' => '管理员强制下线',
// 'time' => 1700000000,
// ]
// ========== 批量踢人 ==========
$userIds = [10001, 10002, 10003];
foreach ($userIds as $userId) {
StpUtil::kickout($userId);
}4.6 账号封禁
php
// ========== 封禁操作 ==========
// 封禁账号(禁止登录),默认永久
StpUtil::disable(10001);
// 封禁指定时长(秒)
StpUtil::disable(10001, 3600);
// 封禁并指定原因
StpUtil::disable(10001, 86400, '违规操作');
// 封禁特定服务(如:评论、发帖)
StpUtil::disableService(10001, 'comment', 3600, '违规评论');
// ========== 检查封禁 ==========
// 判断账号是否被封禁
if (StpUtil::isDisable(10001)) {
echo '账号已被封禁';
}
// 判断账号是否被封禁特定服务
if (StpUtil::isDisableService(10001, 'comment')) {
echo '评论功能被封禁';
}
// ========== 获取封禁信息 ==========
// 获取封禁剩余时间
$remaining = StpUtil::getDisableTime(10001);
// 获取封禁原因
$reason = StpUtil::getDisableReason(10001);
// 获取服务封禁剩余时间
$remaining = StpUtil::getDisableServiceTime(10001, 'comment');
// ========== 解封 ==========
// 解封账号
StpUtil::untieDisable(10001);
// 解封特定服务
StpUtil::untieDisableService(10001, 'comment');4.7 多账号体系
php
use SaToken\StpLogic;
// ========== 创建不同账号体系 ==========
$adminStp = new StpLogic('admin');
$userStp = new StpLogic('user');
$guestStp = new StpLogic('guest');
// ========== 管理员登录 ==========
$adminStp->login(10001);
$adminToken = $adminStp->getTokenValue();
$adminStp->checkRole('super_admin');
// ========== 用户登录 ==========
$userStp->login(20002);
$userStp->checkPermission('order:view');
// ========== 获取各自信息 ==========
$adminId = $adminStp->getLoginId();
$userId = $userStp->getLoginId();
// ========== 各体系独立配置 ==========
// 管理员体系:有效期 8 小时
$adminStp->setConfig('timeout', 28800);
// 用户体系:有效期 24 小时
$userStp->setConfig('timeout', 86400);4.8 身份切换
php
// ========== 切换到指定用户 ==========
// 切换到用户 10002 的身份
StpUtil::switchTo(10002);
// 执行操作(此时所有校验都基于用户 10002)
$currentId = StpUtil::getLoginId(); // 返回 10002
// ========== 切回原身份 ==========
StpUtil::switchBack();
// ========== 切换状态检查 ==========
if (StpUtil::isSwitch()) {
$originalId = StpUtil::getSwitchOriginalId();
$currentId = StpUtil::getLoginId();
echo "当前以 {$originalId} 的身份切换到 {$currentId}";
}
// ========== 临时切换并执行 ==========
StpUtil::switchTo(10002, function() {
// 在切换状态下执行操作
$id = StpUtil::getLoginId(); // 10002
// 操作完成后自动切回
});五、权限认证
5.1 权限校验方法
php
use SaToken\StpUtil;
// ========== 校验(不通过抛异常 NotPermissionException) ==========
// 必须有此权限
StpUtil::checkPermission('user:add');
// 必须同时拥有多个权限
StpUtil::checkPermissionAnd(['user:add', 'user:edit']);
// 拥有任意一个权限即可
StpUtil::checkPermissionOr(['user:add', 'user:delete']);
// ========== 判断(返回 bool) ==========
if (StpUtil::hasPermission('user:delete')) {
// 拥有删除用户权限
}
if (StpUtil::hasPermissionAnd(['user:add', 'user:edit'])) {
// 同时拥有两个权限
}
if (StpUtil::hasPermissionOr(['user:add', 'user:delete'])) {
// 拥有任意一个权限
}
// ========== 获取所有权限 ==========
$permissions = StpUtil::getPermissionList();
// ========== 检查是否有任意权限 ==========
$has = StpUtil::hasAnyPermission(['user:add', 'user:edit']);5.2 角色校验
php
// ========== 校验(不通过抛异常 NotRoleException) ==========
// 必须有此角色
StpUtil::checkRole('admin');
// 必须同时拥有多个角色
StpUtil::checkRoleAnd(['admin', 'editor']);
// 拥有任意一个角色
StpUtil::checkRoleOr(['admin', 'super']);
// ========== 判断(返回 bool) ==========
if (StpUtil::hasRole('admin')) {
// 是管理员
}
if (StpUtil::hasRoleOr(['admin', 'editor'])) {
// 有管理或编辑角色
}
// ========== 获取所有角色 ==========
$roles = StpUtil::getRoleList();5.3 二级认证
php
// ========== 二级认证概述 ==========
// 二级认证用于敏感操作前的二次身份确认
// 如:修改密码、删除数据、转账等
// ========== 开启二级认证 ==========
// 在敏感操作前开启
StpUtil::openSafe();
// ========== 校验二级认证 ==========
// 不通过抛异常 NotSafeException
StpUtil::checkSafe();
// ========== 判断二级认证状态 ==========
if (StpUtil::isSafe()) {
// 已通过二级认证,执行敏感操作
}
// ========== 二级认证 + 权限校验 ==========
// 需要二级认证且拥有权限
StpUtil::openSafeAndCheck('user:delete');
// ========== 关闭二级认证 ==========
StpUtil::closeSafe();
// ========== 获取二级认证信息 ==========
// 获取剩余时间
$timeout = StpUtil::getSafeTimeout();
// 获取二级认证状态
$status = StpUtil::getSafeStatus();
// ========== 带自定义过期时间的二级认证 ==========
StpUtil::openSafe(300); // 5分钟有效期
StpUtil::checkSafe();5.4 权限数据注入
php
// ========== 在服务提供者中注入权限获取逻辑 ==========
// 在 SaTokenProvider 中配置
StpUtil::setPermissionGetter(function($loginId) {
// 从数据库获取权限
$service = new PermissionService();
return $service->getAllPermissions((int) $loginId);
});
StpUtil::setRoleGetter(function($loginId) {
// 从数据库获取角色
$service = new PermissionService();
return $service->getRoleList((int) $loginId);
});
// ========== 权限缓存优化 ==========
// 使用 Session 缓存权限数据
StpUtil::setPermissionGetter(function($loginId) {
$cacheKey = 'permissions_' . $loginId;
$permissions = StpUtil::getSession($cacheKey);
if (empty($permissions)) {
$service = new PermissionService();
$permissions = $service->getAllPermissions((int) $loginId);
StpUtil::setSession($cacheKey, $permissions);
}
return $permissions;
});六、Cookie 与 Session 管理
6.1 Cookie 管理详解
6.1.1 Cookie 配置项详解
php
// config/sa_token.php
return [
'cookieName' => 'satoken', // Cookie 名称
'cookieDomain' => '', // Cookie 域名,空表示当前域名
'cookiePath' => '/', // Cookie 路径
'cookieSecure' => false, // 是否仅 HTTPS 传输
'cookieHttpOnly' => true, // 是否禁止 JS 读取
'cookieSameSite' => 'Strict', // SameSite 策略
'cookieMaxAge' => 86400, // Cookie 最大生命周期(秒)
'cookiePrefix' => '', // Cookie 前缀
];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 读写操作
php
// ========== Sa-Token 自动 Cookie 操作 ==========
// 登录时自动写入 Cookie(当 isWriteCookie = true)
StpUtil::login(10001);
// 登出时自动清除 Cookie
StpUtil::logout();
// 每次请求自动从 Cookie 读取 Token(当 isReadCookie = true)
// 框架自动完成,无需手动操作
// ========== 手动 Cookie 操作 ==========
// 获取 Cookie 中的 Token
$tokenFromCookie = cookie('satoken');
// 手动设置 Cookie(不推荐,建议由 Sa-Token 自动管理)
cookie('satoken', $tokenValue, 86400);
// 手动删除 Cookie
cookie('satoken', null);
// ========== 获取所有 Cookie ==========
$allCookies = request()->cookie();
// ========== Cookie 安全操作 ==========
// 设置安全 Cookie(HTTPS + HttpOnly + Secure)
cookie('satoken', $tokenValue, [
'expire' => 86400,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
// 跨域 Cookie 设置(前后端分离)
cookie('satoken', $tokenValue, [
'expire' => 86400,
'secure' => true,
'samesite' => 'None',
]);6.1.4 Cookie 常见场景配置
php
// ========== 场景1:前后端分离(API 模式) ==========
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,
// ========== 场景2:传统 Web 应用(Session 模式) ==========
'isReadCookie' => true,
'isWriteCookie' => true,
'cookieHttpOnly' => true,
'cookieSecure' => false,
'cookieSameSite' => 'Lax',
// ========== 场景3:多应用共享域名 ==========
'cookieDomain' => '.example.com',
'cookiePath' => '/',
'cookieSameSite' => 'Lax',
// ========== 场景4:后台管理系统(高安全) ==========
'cookieSecure' => true,
'cookiePath' => '/admin',
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',
// ========== 场景5:HTTPS 环境 ==========
'cookieSecure' => true,
'cookieSameSite' => 'Strict',
'cookieHttpOnly' => true,6.1.5 Cookie 前缀说明
php
// 使用 Cookie 前缀可以增强安全性
'cookiePrefix' => '__Secure-',
// 实际写入的 Cookie 名称为:__Secure-satoken
// 这样浏览器会强制要求 HTTPS 传输
// 常用的 Cookie 前缀:
// __Secure- : 强制 HTTPS
// __Host- : 强制 HTTPS + 仅主机
6.1.6 跨域 Cookie 配置
php
// ========== 后端配置 ==========
'cookieSameSite' => 'None',
'cookieSecure' => true, // SameSite=None 时必须为 true
// ========== 前端请求配置 ==========
fetch('/api/user/info', {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
}
});
// Axios 配置
axios.defaults.withCredentials = true;6.2 Session 管理详解
6.2.1 Session 类型
| Session 类型 | 作用范围 | 说明 |
|---|---|---|
| 账号 Session | 当前账号的所有 Token | 登录后,同一账号的所有设备共享此 Session |
| Token Session | 当前 Token | 只对当前 Token 有效,不同设备相互隔离 |
| 自定义 Session | 自定义 Key | 开发者任意指定的 Session 对象 |
php
// ========== 账号 Session(所有设备共享) ==========
StpUtil::setSession('user_info', ['name' => '张三']);
$userInfo = StpUtil::getSession('user_info');
StpUtil::deleteSession('user_info');
if (StpUtil::hasSession('user_info')) {}
$session = StpUtil::getSession();
// ========== Token Session(仅当前 Token 独享) ==========
StpUtil::setTokenSession('temp_data', ['key' => 'value']);
$tempData = StpUtil::getTokenSession('temp_data');
StpUtil::deleteTokenSession('temp_data');
// ========== 自定义 Session(任意 Key) ==========
$customSession = StpUtil::getCustomSession('my_custom_key');
$customSession->set('data', 'value');
StpUtil::setCustomSession('my_key', 'my_value');
$value = StpUtil::getCustomSession('my_key');
StpUtil::deleteCustomSession('my_key');6.2.2 Session 操作详解
php
// ========== Session 存储 ==========
StpUtil::setSession('user_id', 10001);
StpUtil::setSession('username', 'admin');
StpUtil::setSession('user_info', [
'id' => 10001,
'name' => '张三',
'email' => 'zhangsan@example.com',
'roles' => ['admin', 'editor'],
'settings' => [
'theme' => 'dark',
'language' => 'zh-CN',
],
]);
// ========== Session 读取 ==========
$userId = StpUtil::getSession('user_id');
$username = StpUtil::getSession('username', 'guest');
$session = StpUtil::getSession();
$allData = $session->getAll();
$keys = StpUtil::getSessionKeys();
// ========== Session 判断 ==========
if (StpUtil::hasSession('user_info')) {}
if (StpUtil::hasSessionKey('user_info', 'name')) {}
// ========== Session 删除 ==========
StpUtil::deleteSession('temp_data');
StpUtil::deleteSession(['key1', 'key2', 'key3']);
StpUtil::clearSession();
// ========== Session 有效期 ==========
StpUtil::setSessionTimeout('user_info', 3600);
$timeout = StpUtil::getSessionTimeout('user_info');
StpUtil::refreshSession('user_info');
StpUtil::refreshSession(['user_info', 'permissions']);6.2.3 Session 配置
php
// config/sa_token.php
'session' => [
'timeout' => 86400, // Session 默认有效期(秒)
'activityTimeout' => 1800, // Session 活跃超时(秒)
'maxSize' => 1024, // Session 最大存储大小(KB)
'prefix' => 'session_', // Session key 前缀
'cleanInterval' => 3600, // 清理间隔(秒)
],6.2.4 Session 清理机制
php
// ========== 自动清理 ==========
// 配置了 activityTimeout 后,框架会自动清理过期 Session
// ========== 手动清理 ==========
SaToken::getDao()->cleanExpiredSession();
StpUtil::clearUserSession(10001);
StpUtil::clearDeviceSession('mobile');
// ========== 定时任务清理 ==========
// 创建 app/command/CleanSession.php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use SaToken\SaToken;
class CleanSession extends Command
{
protected function configure()
{
$this->setName('clean:session')
->setDescription('清理过期 Session');
}
protected function execute(Input $input, Output $output)
{
$startTime = microtime(true);
$count = SaToken::getDao()->cleanExpiredSession();
$cost = microtime(true) - $startTime;
$output->writeln("清理了 {$count} 个过期 Session,耗时 {$cost} 秒");
}
}
// 注册命令 config/console.php
return [
'commands' => [
'clean:session' => \app\command\CleanSession::class,
],
];
// Cron: 0 2 * * * cd /path/to/project && php think clean:session
6.3 Cookie 与 Session 交互流程图
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ 用户登录完整流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户提交用户名/密码 │
│ ↓ │
│ 2. 验证用户凭证 │
│ ↓ │
│ 3. StpUtil::login(10001) │
│ │ │
│ ├── 生成 Token │
│ │ ├── uuid: 550e8400-e29b-41d4-a716-446655440000 │
│ │ ├── simple-uuid: 550e8400e29b41d4a716446655440000 │
│ │ ├── random-32: abc123def456... │
│ │ └── tik: 随机24位字符串 │
│ │ │
│ ├── Token 与 LoginId 绑定 → Redis/内存 │
│ │ ├── key: sa_token_session_{token} │
│ │ └── value: {loginId, device, timeout, extra, ...} │
│ │ │
│ ├── 创建 Session 对象 (账号 Session) │
│ │ └── key: sa_token_session_{loginId} │
│ │ │
│ ├── 写入 Cookie │
│ │ └── Cookie: satoken=550e8400-e29b-41d4-a716-446655440000 │
│ │ ├── Domain: .example.com │
│ │ ├── Path: / │
│ │ ├── Secure: true/false │
│ │ ├── HttpOnly: true │
│ │ └── SameSite: Strict/Lax/None │
│ │ │
│ ├── 写入响应头 │
│ │ └── Header: Authorization: Bearer 550e8400... │
│ │ │
│ ├── 触发 onLogin 事件 │
│ │ └── 执行自定义监听器逻辑 │
│ │ │
│ └── 记录审计日志 │
│ └── 写入 audit_log 表 │
│ ↓ │
│ 4. 返回 Token 给客户端 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘七、注解式鉴权
7.1 注解概览
| 注解 | 说明 | 示例 |
|---|---|---|
@SaCheckLogin | 登录校验 | #[SaCheckLogin] |
@SaCheckRole | 角色校验 | #[SaCheckRole('admin')] |
@SaCheckPermission | 权限校验 | #[SaCheckPermission('user:add')] |
@SaCheckSafe | 二级认证 | #[SaCheckSafe] |
@SaCheckDisable | 服务封禁 | #[SaCheckDisable('comment')] |
@SaIgnore | 忽略校验 | #[SaIgnore] |
7.2 基本用法
php
<?php
namespace app\controller;
use SaToken\Annotation\SaCheckLogin;
use SaToken\Annotation\SaCheckRole;
use SaToken\Annotation\SaCheckPermission;
use SaToken\Annotation\SaCheckSafe;
use SaToken\Annotation\SaCheckDisable;
use SaToken\Annotation\SaIgnore;
class UserController
{
#[SaCheckLogin]
public function info()
{
return '查询用户信息';
}
#[SaCheckRole('super-admin')]
public function add()
{
return '用户增加';
}
#[SaCheckPermission('user-add')]
public function delete()
{
return '删除用户';
}
#[SaCheckSafe]
public function updatePwd()
{
return '修改密码';
}
#[SaCheckDisable('comment')]
public function comment()
{
return '发表评论';
}
#[SaIgnore]
public function getList()
{
return '公开数据';
}
}7.3 多权限场景
php
use SaToken\Annotation\SaCheckPermission;
use SaToken\SaMode;
#[SaCheckPermission(value: ['user-add', 'user-all'], mode: SaMode::OR)]
public function userManage()
{
return '用户管理';
}
#[SaCheckPermission(value: ['user-add', 'user-edit'], mode: SaMode::AND)]
public function userEdit()
{
return '编辑用户';
}八、路由拦截鉴权
8.1 基础匹配
php
use SaToken\SaRouter;
use SaToken\StpUtil;
SaRouter::match('/api/user/info')->check(fn() => StpUtil::checkLogin());
SaRouter::match(['/api/user/**', '/api/order/**'])->check(fn() => StpUtil::checkLogin());
SaRouter::match('/api/user/**', 'POST')->check(fn() => StpUtil::checkPermission('user:add'));
SaRouter::match('/api/user/**', ['POST', 'PUT', 'DELETE'])->check(fn() => StpUtil::checkPermission('user:edit'));
SaRouter::match(
['/api/user/**', '/api/admin/**'],
['POST', 'PUT', 'DELETE']
)->check(fn() => StpUtil::checkPermission('user:edit'));8.2 排除路径
php
SaRouter::match('/api/**')
->exclude('/api/login', '/api/register')
->check(fn() => StpUtil::checkLogin());
SaRouter::match('/api/**')
->exclude('/api/public/**')
->exclude('/api/health')
->check(fn() => StpUtil::checkLogin());
SaRouter::match('/api/**')
->exclude(['/api/login', '/api/register', '/api/refresh'])
->check(fn() => StpUtil::checkLogin());8.3 复杂鉴权
php
SaRouter::match('/admin/**')->check(function() {
StpUtil::checkLogin();
StpUtil::checkRole('admin');
});
SaRouter::match('/api/payment/**')->check(function() {
StpUtil::checkLogin();
StpUtil::checkPermission('payment:operate');
StpUtil::checkSafe();
});
SaRouter::match('/api/**')->check(function() {
if (request()->isGet()) {
StpUtil::checkPermission('read');
} else {
StpUtil::checkPermission('write');
}
});
SaRouter::match('/api/**')->check(function() {
if (StpUtil::isLogin()) {
$userId = StpUtil::getLoginId();
if ($userId > 10000) {
StpUtil::checkPermission('vip');
}
}
});8.4 链式匹配
php
SaRouter::match('/api/**')
->exclude('/api/login', '/api/register')
->check(fn() => StpUtil::checkLogin());
SaRouter::match('/api/admin/**')
->check(fn() => StpUtil::checkRole('admin'));
SaRouter::match('/api/super/**')
->check(fn() => StpUtil::checkRole('super'));九、SSO 单点登录
9.1 SSO 配置 config/sa_token_sso.php
php
<?php
return [
'mode' => 'cross-domain',
'serverUrl' => 'https://sso.example.com',
'clientId' => 'your-client-id',
'clientSecret' => 'your-client-secret',
'loginUrl' => 'https://sso.example.com/login',
'authUrl' => 'https://sso.example.com/oauth/authorize',
'tokenUrl' => 'https://sso.example.com/oauth/token',
'userInfoUrl' => 'https://sso.example.com/oauth/userinfo',
'logoutUrl' => 'https://sso.example.com/logout',
'backUrl' => 'https://app.example.com/sso/callback',
'allowDomains' => ['example.com', '*.example.com'],
'ssoTimeout' => 86400,
'ssoTokenName' => 'ssotoken',
'jwtSecretKey' => 'your-jwt-secret',
'jwtExpire' => 3600,
'autoLogin' => true,
'autoRegister' => false,
];9.2 SSO 控制器 app/controller/SsoController.php
php
<?php
namespace app\controller;
use SaToken\StpUtil;
use SaToken\Sso\SaSsoManager;
use think\facade\Db;
class SsoController
{
public function login()
{
$sso = SaSsoManager::getInstance();
if ($sso->checkSsoSession()) {
$userInfo = $sso->getUserInfo();
$user = Db::name('user')
->where('sso_id', $userInfo['id'])
->find();
if (!$user && config('sa_token_sso.autoRegister')) {
$userId = Db::name('user')->insertGetId([
'sso_id' => $userInfo['id'],
'username' => $userInfo['username'],
'nickname' => $userInfo['nickname'],
'email' => $userInfo['email'] ?? '',
'status' => 1,
'create_time' => time(),
]);
$user = ['id' => $userId];
}
if ($user) {
StpUtil::login((string) $user['id']);
return redirect('/dashboard');
}
return redirect('/login?error=user_not_found');
}
return redirect($sso->buildLoginUrl());
}
public function callback()
{
$sso = SaSsoManager::getInstance();
try {
$result = $sso->handleCallback();
if ($result['success']) {
$ssoUser = $result['user'];
$user = Db::name('user')
->where('sso_id', $ssoUser['id'])
->find();
if (!$user) {
$userId = Db::name('user')->insertGetId([
'sso_id' => $ssoUser['id'],
'username' => $ssoUser['username'] ?? '',
'nickname' => $ssoUser['nickname'] ?? '',
'email' => $ssoUser['email'] ?? '',
'status' => 1,
'create_time' => time(),
]);
} else {
$userId = $user['id'];
Db::name('user')
->where('id', $userId)
->update([
'nickname' => $ssoUser['nickname'] ?? $user['nickname'],
'email' => $ssoUser['email'] ?? $user['email'],
'last_login' => time(),
]);
}
StpUtil::login((string) $userId);
return json([
'code' => 200,
'msg' => '登录成功',
'data' => [
'token' => StpUtil::getTokenValue(),
'userId' => $userId,
]
]);
}
return json(['code' => 401, 'msg' => $result['message'] ?? '登录失败']);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => 'SSO 登录失败:' . $e->getMessage()]);
}
}
public function logout()
{
StpUtil::logout();
$sso = SaSsoManager::getInstance();
$logoutUrl = $sso->buildLogoutUrl();
return json([
'code' => 200,
'msg' => '已登出',
'data' => ['logoutUrl' => $logoutUrl]
]);
}
}十、OAuth2.0 认证
10.1 OAuth2 配置 config/sa_token_oauth2.php
php
<?php
return [
'clients' => [
'web-app' => [
'clientId' => 'web-app',
'clientSecret' => 'web-app-secret',
'redirectUris' => [
'https://app.example.com/callback',
'http://localhost:8080/callback',
],
'grantTypes' => ['authorization_code', 'refresh_token'],
'scopes' => ['openid', 'profile', 'email', 'user:read'],
],
'mobile-app' => [
'clientId' => 'mobile-app',
'clientSecret' => 'mobile-app-secret',
'redirectUris' => ['com.example.app://oauth/callback'],
'grantTypes' => ['authorization_code', 'password'],
'scopes' => ['openid', 'profile'],
],
],
'accessTokenTimeout' => 3600,
'refreshTokenTimeout' => 2592000,
'codeTimeout' => 300,
'openIdMode' => true,
'idTokenTimeout' => 3600,
'jwtSecretKey' => 'your-jwt-secret',
'issuer' => 'https://auth.example.com',
];10.2 OAuth2 控制器 app/controller/OAuth2Controller.php
php
<?php
namespace app\controller;
use SaToken\StpUtil;
use SaToken\OAuth2\SaOAuth2Manager;
use think\facade\Db;
class OAuth2Controller
{
public function authorize()
{
$oauth2 = SaOAuth2Manager::getInstance();
$clientId = input('get.client_id', '');
$redirectUri = input('get.redirect_uri', '');
$scope = input('get.scope', '');
$state = input('get.state', '');
$responseType = input('get.response_type', 'code');
$client = $oauth2->validateClient($clientId, $redirectUri);
if (!$client) {
return json(['error' => 'invalid_client'], 400);
}
if (!StpUtil::isLogin()) {
return redirect('/login?redirect=' . urlencode(request()->url()));
}
if ($scope && !$oauth2->validateScope($scope, $client['scopes'])) {
return json(['error' => 'invalid_scope'], 400);
}
if ($responseType === 'token') {
$accessToken = $oauth2->createAccessToken(
$clientId,
StpUtil::getLoginId(),
$redirectUri,
$scope
);
$redirectUrl = $redirectUri . '#access_token=' . $accessToken;
if ($state) {
$redirectUrl .= '&state=' . $state;
}
return redirect($redirectUrl);
}
return view('oauth2/authorize', [
'client' => $client,
'scope' => $scope,
'state' => $state,
'user' => Db::name('user')->find(StpUtil::getLoginId()),
]);
}
public function approve()
{
$oauth2 = SaOAuth2Manager::getInstance();
$clientId = input('post.client_id', '');
$redirectUri = input('post.redirect_uri', '');
$scope = input('post.scope', '');
$state = input('post.state', '');
$approved = input('post.approved', false);
if (!$approved) {
$redirectUrl = $redirectUri . '?error=access_denied';
if ($state) {
$redirectUrl .= '&state=' . $state;
}
return redirect($redirectUrl);
}
try {
$code = $oauth2->createAuthorizationCode(
$clientId,
StpUtil::getLoginId(),
$redirectUri,
$scope
);
$redirectUrl = $redirectUri . '?code=' . $code;
if ($state) {
$redirectUrl .= '&state=' . $state;
}
return json(['code' => 200, 'data' => ['redirectUrl' => $redirectUrl]]);
} catch (\Exception $e) {
return json(['code' => 400, 'msg' => $e->getMessage()]);
}
}
public function token()
{
$oauth2 = SaOAuth2Manager::getInstance();
$grantType = input('post.grant_type', '');
$code = input('post.code', '');
$clientId = input('post.client_id', '');
$clientSecret = input('post.client_secret', '');
$redirectUri = input('post.redirect_uri', '');
$refreshToken = input('post.refresh_token', '');
$username = input('post.username', '');
$password = input('post.password', '');
$scope = input('post.scope', '');
try {
switch ($grantType) {
case 'authorization_code':
$result = $oauth2->exchangeTokenByCode(
$code,
$clientId,
$clientSecret,
$redirectUri
);
break;
case 'refresh_token':
$result = $oauth2->refreshToken(
$refreshToken,
$clientId,
$clientSecret
);
break;
case 'password':
$user = Db::name('user')
->where('username', $username)
->where('status', 1)
->find();
if (!$user || !password_verify($password, $user['password'])) {
return json(['error' => 'invalid_credentials'], 401);
}
$result = $oauth2->createTokenByPassword(
$user['id'],
$clientId,
$clientSecret,
$scope
);
break;
case 'client_credentials':
$result = $oauth2->createTokenByClientCredentials(
$clientId,
$clientSecret,
$scope
);
break;
default:
throw new \Exception('unsupported_grant_type');
}
return json($result);
} catch (\Exception $e) {
return json(['error' => $e->getMessage()], 400);
}
}
public function userinfo()
{
$oauth2 = SaOAuth2Manager::getInstance();
$accessToken = input('get.access_token', '');
if (!$accessToken) {
$authHeader = request()->header('Authorization');
if ($authHeader && preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
$accessToken = $matches[1];
}
}
if (!$accessToken) {
return json(['error' => 'invalid_token'], 401);
}
try {
$userInfo = $oauth2->getUserInfo($accessToken);
return json($userInfo);
} catch (\Exception $e) {
return json(['error' => $e->getMessage()], 401);
}
}
}十一、安全防护
11.1 防暴力破解
php
public function login(): Json
{
$username = input('post.username', '');
$password = input('post.password', '');
StpUtil::checkAntiBrute($username);
$user = Db::name('user')->where('username', $username)->find();
if (!$user || !password_verify($password, $user['password'])) {
StpUtil::recordAntiBruteFailure($username);
$info = StpUtil::getAntiBruteInfo($username);
return json([
'code' => 401,
'msg' => "密码错误,剩余尝试次数:{$info['remainingAttempts']}",
'data' => [
'failCount' => $info['failCount'],
'isLocked' => $info['isLocked'],
'remainingAttempts' => $info['remainingAttempts'],
'maxFailures' => $info['maxFailures'],
]
]);
}
StpUtil::clearAntiBruteFailure($username);
StpUtil::login((string) $user['id']);
return json(['code' => 200, 'msg' => '登录成功']);
}
// ========== 防暴力破解配置 ==========
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,
// ========== 防暴力破解 API ==========
$isLocked = StpUtil::isAccountLocked($username);
$remaining = StpUtil::getRemainingLockTime($username);
StpUtil::unlockAccount($username);
$info = StpUtil::getAntiBruteInfo($username);11.2 Token 黑名单
php
StpUtil::addTokenToBlacklist($tokenValue);
StpUtil::addTokenToBlacklist($tokenValue, '异常登录');
StpUtil::removeTokenFromBlacklist($tokenValue);
$isBlacklisted = StpUtil::isTokenBlacklisted($tokenValue);
$blacklist = StpUtil::getBlacklist();
// 批量操作
$tokens = ['token1', 'token2', 'token3'];
foreach ($tokens as $token) {
StpUtil::addTokenToBlacklist($token);
}11.3 IP 异常检测
php
use SaToken\StpUtil;
$info = StpUtil::getLoginInfo(10001);
$count = StpUtil::getAnomalyCount(10001);
$history = StpUtil::getIpHistory(10001);
StpUtil::clearLoginHistory(10001);
if (StpUtil::isIpAnomalous()) {
Log::warning('异地登录:用户 ' . StpUtil::getLoginId() . ' 从 ' . request()->ip() . ' 登录');
$this->sendAlert(StpUtil::getLoginId(), request()->ip());
}
// ========== IP 异常检测配置 ==========
'ipAnomalyDetection' => true,
'ipAnomalySensitivity' => 3,11.4 设备管理
php
use SaToken\StpUtil;
$devices = StpUtil::getDeviceList(10001);
$count = StpUtil::getDeviceCount(10001);
$device = StpUtil::findDevice(10001, 'device-id-xxx');
StpUtil::kickoutDevice(10001, 'device-id-xxx');
$kickedCount = StpUtil::kickoutAllDevices(10001, StpUtil::getTokenValue());
StpUtil::renameDevice(10001, 'device-id-xxx', '我的手机');
// ========== 设备管理配置 ==========
'deviceManagement' => true,11.5 敏感操作验证
php
use SaToken\StpUtil;
// ========== OTP 验证码 ==========
$code = StpUtil::generateOtpCode('payment');
$code = StpUtil::sendOtpCode('payment');
StpUtil::verifyOtpCode('payment', input('post.code'));
$isVerified = StpUtil::isSensitiveVerified('payment');
$remaining = StpUtil::getSensitiveVerifyRemainingAttempts('payment');
StpUtil::clearSensitiveVerify('payment');
// ========== 安全令牌 ==========
$token = StpUtil::openSensitiveVerify('payment', 600);
StpUtil::checkSensitiveVerify('payment', input('post.stoken'));
$status = StpUtil::getSensitiveVerifyStatus('payment');
// ========== 敏感操作示例 ==========
public function transfer()
{
StpUtil::checkLogin();
StpUtil::checkPermission('account:transfer');
if (!StpUtil::isSensitiveVerified('transfer')) {
$code = StpUtil::sendOtpCode('transfer');
return json([
'code' => 403,
'msg' => '需要验证身份',
'data' => ['need_verify' => true]
]);
}
// 执行转账操作
StpUtil::clearSensitiveVerify('transfer');
return json(['code' => 200, 'msg' => '转账成功']);
}11.6 Token 指纹绑定
php
// ========== 启用 Token 指纹绑定 ==========
'tokenFingerprint' => true,
// ========== 手动操作 ==========
$fingerprint = StpUtil::getCurrentFingerprint();
StpUtil::checkFingerprint();
StpUtil::updateFingerprint();
// ========== 指纹异常处理 ==========
try {
StpUtil::checkFingerprint();
} catch (\Exception $e) {
Log::alert('Token 指纹校验失败:' . StpUtil::getTokenValue());
StpUtil::kickout();
return json(['code' => 403, 'msg' => '设备异常,请重新登录']);
}十二、高级功能
12.1 HTTP Basic/Digest 认证
php
use SaToken\SaToken;
$auth = SaToken::getHttpAuth();
$auth->setBasicValidator(function (string $username, string $password): mixed {
$user = Db::name('user')
->where('username', $username)
->where('status', 1)
->find();
if ($user && password_verify($password, $user['password'])) {
return $user['id'];
}
return null;
});
$auth->checkBasic('My API');
// ========== HTTP Digest 认证 ==========
$auth->setDigestValidator(function (string $username): ?string {
$user = Db::name('user')
->where('username', $username)
->find();
if ($user) {
return md5($username . ':My API:' . $user['password']);
}
return null;
});
$auth->checkDigest('My API');12.2 参数签名校验(SaSign)
php
use SaToken\SaToken;
$sign = SaToken::getSign();
$sign->setSignKey(config('sa_token.signKey'));
$sign->setSignAlg('sha256');
$sign->setTimestampGap(600);
$sign->setNonceValidator(function (string $nonce): bool {
$key = 'nonce_' . $nonce;
if (StpUtil::getCustomSession($key)) {
return false;
}
StpUtil::setCustomSession($key, time(), 3600);
return true;
});
$params = [
'userId' => '10001',
'action' => 'query',
'timestamp' => time(),
'nonce' => uniqid(),
'data' => ['key' => 'value'],
];
$signed = $sign->signParams($params);
$params = input('get.');
if (!$sign->verifySign($params)) {
return json(['code' => 403, 'msg' => '签名无效']);
}12.3 API Key 授权
php
use SaToken\SaToken;
$apiKey = SaToken::getApiKey();
$apiKey->registerKey('ak-123456', 'sk-abcdef', 10001);
$apiKey->registerKey('ak-789012', 'sk-ghijkl', 10002);
$apiKey->setValidator(function (string $apiKey, string $apiSecret): mixed {
$key = Db::name('api_keys')
->where('api_key', $apiKey)
->where('status', 1)
->find();
if ($key && hash_equals($key->api_secret, $apiSecret)) {
return $key->user_id;
}
return null;
});
$apiKey->checkApiKey();
$userId = StpUtil::getLoginId();12.4 全局过滤器
php
use SaToken\SaToken;
use SaToken\SaRouter;
use SaToken\StpUtil;
$filter = SaToken::getGlobalFilter();
$filter->setCors([
'allowOrigin' => '*',
'allowMethods' => 'GET, POST, PUT, DELETE, OPTIONS',
'allowHeaders' => 'Content-Type, Authorization, X-Requested-With',
'allowCredentials' => 'true',
'maxAge' => '3600',
]);
$filter->setCorsOrigin(function($origin) {
$allowOrigins = ['https://example.com', 'https://app.example.com'];
if (in_array($origin, $allowOrigins)) {
return $origin;
}
return '';
});
$filter->addBeforeFilter(function () {
Log::info('Request: ' . request()->method() . ' ' . request()->url());
SaRouter::match('/api/**')->check(fn() => StpUtil::checkLogin());
});
$filter->addAfterFilter(function ($response) {
$response->header('X-Content-Type-Options', 'nosniff');
$response->header('X-Frame-Options', 'DENY');
$response->header('X-XSS-Protection', '1; mode=block');
Log::info('Response: ' . $response->getCode());
});
$filter->execute();
if ($filter->isCorsRequest()) {
$filter->handlePreflight();
return;
}12.5 自定义存储
php
use SaToken\SaToken;
use SaToken\Dao\SaTokenDaoMemory;
use SaToken\Dao\SaTokenDaoRedis;
use SaToken\Dao\SaTokenDaoPsr16;
SaToken::setDao(new SaTokenDaoMemory());
SaToken::setDao(new SaTokenDaoRedis($redisConfig));
$dao = SaTokenDaoRedis::createWithSeparateRedis(
['host' => '127.0.0.1', 'port' => 6379, 'db' => 0],
['host' => '127.0.0.1', 'port' => 6379, 'db' => 1],
);
SaToken::setDao($dao);
$psr16Cache = new SomePsr16Cache();
SaToken::setDao(new SaTokenDaoPsr16($psr16Cache));
// 自定义 DAO
class MyCustomDao implements SaTokenDaoInterface
{
public function get(string $key): ?string
{
return MyStorage::get($key);
}
public function set(string $key, string $value, int $ttl): void
{
MyStorage::set($key, $value, $ttl);
}
public function delete(string $key): void
{
MyStorage::delete($key);
}
public function exists(string $key): bool
{
return MyStorage::exists($key);
}
}12.6 JWT 集成
php
use SaToken\Plugin\SaTokenJwt;
$payload = [
'userId' => 10001,
'username' => 'admin',
'role' => 'admin',
'exp' => time() + 3600,
'iat' => time(),
'iss' => 'https://example.com',
'aud' => 'https://api.example.com',
];
$jwt = SaTokenJwt::generate($payload);
try {
$payload = SaTokenJwt::verify($jwt);
$userId = $payload['userId'];
$username = $payload['username'];
} catch (\Exception $e) {
return json(['code' => 401, 'msg' => 'JWT 无效']);
}
// 配置
'jwtSecretKey' => 'your-jwt-secret',
'jwtStateless' => false,
// 使用 JWT 作为 Token 载体
StpUtil::setTokenGenerator(function($loginId, $extra) {
$payload = [
'loginId' => $loginId,
'extra' => $extra,
'exp' => time() + config('sa_token.timeout'),
];
return SaTokenJwt::generate($payload);
});12.7 密码加密工具
php
use SaToken\Plugin\SaTokenCrypto;
$md5 = SaTokenCrypto::md5('password');
$sha1 = SaTokenCrypto::sha1('password');
$sha256 = SaTokenCrypto::sha256('password');
$sha512 = SaTokenCrypto::sha512('password');
$hmacSha256 = SaTokenCrypto::hmacSha256('data', 'key');
$hmacSha1 = SaTokenCrypto::hmacSha1('data', 'key');
$hash = SaTokenCrypto::bcryptHash('password', 12);
$valid = SaTokenCrypto::bcryptVerify('password', $hash);
$encrypted = SaTokenCrypto::aesEncrypt('data', '16-byte-key');
$decrypted = SaTokenCrypto::aesDecrypt($encrypted, '16-byte-key');
$encrypted = SaTokenCrypto::aes256Encrypt('data', '32-byte-key-32-byte-key-');
$decrypted = SaTokenCrypto::aes256Decrypt($encrypted, '32-byte-key-32-byte-key-');
$encrypted = SaTokenCrypto::rsaEncrypt('data', $publicKey);
$decrypted = SaTokenCrypto::rsaDecrypt($encrypted, $privateKey);
$signature = SaTokenCrypto::rsaSign('data', $privateKey);
$valid = SaTokenCrypto::rsaVerify('data', $signature, $publicKey);
$sm3 = SaTokenCrypto::sm3('data');
$encrypted = SaTokenCrypto::sm4Encrypt('data', '16-byte-key-16-byte');
$decrypted = SaTokenCrypto::sm4Decrypt($encrypted, '16-byte-key-16-byte');
$signature = SaTokenCrypto::sm2Sign('data', $privateKey);
$valid = SaTokenCrypto::sm2Verify('data', $signature, $publicKey);12.8 审计日志
php
use SaToken\StpUtil;
use SaToken\Security\SaAuditLog;
SaAuditLog::logLogin(10001, ['ip' => request()->ip(), 'device' => 'web']);
SaAuditLog::logLogout(10001);
SaAuditLog::logKickout(10001, 'admin', '违规操作');
SaAuditLog::logDisable(10001, 'login', '账号异常');
SaAuditLog::logSwitchTo(10001, 20002);
SaAuditLog::logCustom('user_action', 10001, '导出数据', ['count' => 100]);
$logs = StpUtil::getAuditLogs(50);
$log = StpUtil::getAuditLog('log-id-xxx');
$recentLogs = SaAuditLog::getRecentLogs('login', 100);
$logsByIp = SaAuditLog::getLogsByIp('192.168.1.1');
$logsByEvent = SaAuditLog::getLogsByEvent('login');
$userLogs = SaAuditLog::getLogsByUserId(10001);
$timeRangeLogs = SaAuditLog::getLogsByTimeRange(strtotime('-7 days'), time());
SaAuditLog::clearLogs();
SaAuditLog::clearLogsByUser(10001);
SaAuditLog::clearLogsByEvent('login');12.9 事件监听
php
use SaToken\Listener\SaTokenListenerInterface;
use SaToken\SaToken;
use think\facade\Log;
class MySaTokenListener implements SaTokenListenerInterface
{
public function onLogin(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::info("用户 {$loginId} 登录成功,IP: " . request()->ip());
$this->sendWelcomeEmail($loginId);
$this->updateOnlineStatus($loginId, true);
}
public function onLogout(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::info("用户 {$loginId} 已登出");
$this->updateOnlineStatus($loginId, false);
}
public function onKickout(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::warning("用户 {$loginId} 被踢下线,原因:" . ($extra['reason'] ?? '未知'));
$this->sendKickoutNotification($loginId, $extra['reason'] ?? '');
}
public function onReplaced(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::warning("用户 {$loginId} 被顶下线,设备:" . ($extra['device'] ?? '未知'));
$this->sendReplacedNotification($loginId);
}
public function onDisable(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::alert("用户 {$loginId} 被封禁,时长:" . ($extra['duration'] ?? '永久'));
$this->sendDisableNotification($loginId, $extra['duration'] ?? 0);
}
public function onUntieDisable(string $loginType, mixed $loginId, string $tokenValue, array $extra = []): void
{
Log::info("用户 {$loginId} 已解封");
}
public function onSwitchTo(string $loginType, mixed $loginId, mixed $targetId, array $extra = []): void
{
Log::info("用户 {$loginId} 切换到身份 {$targetId}");
$this->logSwitchOperation($loginId, $targetId);
}
public function onRefresh(string $loginType, mixed $loginId, string $oldToken, string $newToken): void
{
Log::info("用户 {$loginId} 刷新了 Token");
}
private function sendWelcomeEmail($userId): void {}
private function updateOnlineStatus($userId, bool $online): void {}
private function sendKickoutNotification($userId, string $reason): void {}
private function sendReplacedNotification($userId): void {}
private function sendDisableNotification($userId, int $duration): void {}
private function logSwitchOperation($userId, $targetId): void {}
}
SaToken::addListener(new MySaTokenListener());
SaToken::removeListener(MySaTokenListener::class);
SaToken::clearListeners();十三、异常处理
13.1 异常类列表
| 异常类 | 触发场景 | 关联方法 |
|---|---|---|
NotLoginException | 未登录/Token 无效/过期/被踢 | StpUtil::checkLogin() |
NotPermissionException | 权限校验不通过 | StpUtil::checkPermission() |
NotRoleException | 角色校验不通过 | StpUtil::checkRole() |
DisableServiceException | 账号被封禁 | StpUtil::checkLogin() |
NotSafeException | 二级认证校验不通过 | StpUtil::checkSafe() |
SaTokenException | 其他异常 | 所有方法 |
13.2 异常类型详解
php
use SaToken\Exception\NotLoginException;
// NotLoginException 类型常量
const NOT_LOGIN = 1; // 未登录
const INVALID_TOKEN = 2; // Token 无效
const TOKEN_TIMEOUT = 3; // Token 已过期
const BE_KICKOUT = 4; // 已被踢下线
const BE_REPLACED = 5; // 已被顶下线
const TOKEN_BLACKLIST = 6; // Token 在黑名单
13.3 异常处理器 app/exception/Handler.php
php
<?php
declare(strict_types=1);
namespace app\exception;
use think\exception\Handle;
use think\Response;
use Throwable;
use SaToken\Exception\NotLoginException;
use SaToken\Exception\NotPermissionException;
use SaToken\Exception\NotRoleException;
use SaToken\Exception\DisableServiceException;
use SaToken\Exception\NotSafeException;
use think\facade\Log;
class Handler extends Handle
{
protected array $ignoreReport = [
NotLoginException::class,
NotPermissionException::class,
NotRoleException::class,
DisableServiceException::class,
NotSafeException::class,
];
public function render($request, Throwable $e): Response
{
if ($e instanceof NotLoginException) {
$messages = [
NotLoginException::NOT_LOGIN => '请先登录',
NotLoginException::INVALID_TOKEN => 'Token 无效',
NotLoginException::TOKEN_TIMEOUT => 'Token 已过期,请重新登录',
NotLoginException::BE_KICKOUT => '已被踢下线',
NotLoginException::BE_REPLACED => '已被顶下线',
NotLoginException::TOKEN_BLACKLIST => 'Token 已被拉黑',
];
return json([
'code' => 401,
'msg' => $messages[$e->getType()] ?? '未登录',
'type' => $e->getType(),
], 401);
}
if ($e instanceof NotPermissionException) {
return json([
'code' => 403,
'msg' => '权限不足:' . $e->getPermission(),
'need' => $e->getPermission(),
], 403);
}
if ($e instanceof NotRoleException) {
return json([
'code' => 403,
'msg' => '角色不足:' . $e->getRole(),
'need' => $e->getRole(),
], 403);
}
if ($e instanceof DisableServiceException) {
$remaining = StpUtil::getDisableTime();
return json([
'code' => 403,
'msg' => "账号已被封禁,剩余 {$remaining} 秒",
'remaining' => $remaining,
], 403);
}
if ($e instanceof NotSafeException) {
return json([
'code' => 403,
'msg' => '需要二级认证',
'need_safe' => true,
], 403);
}
if (!$this->isDebug()) {
Log::error('系统异常:' . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 500, 'msg' => '服务器内部错误'], 500);
}
return parent::render($request, $e);
}
protected function isDebug(): bool
{
return (bool) config('app.debug');
}
}十四、完整配置参考
php
<?php
return [
// ==================== Token 基础配置 ====================
'tokenName' => 'satoken',
'tokenPrefix' => 'Bearer',
'tokenStyle' => 'uuid',
// ==================== 有效期配置 ====================
'timeout' => 86400,
'activityTimeout' => -1,
// ==================== Cookie 配置 ====================
'cookieName' => 'satoken',
'cookieDomain' => '',
'cookiePath' => '/',
'cookieSecure' => false,
'cookieHttpOnly' => true,
'cookieSameSite' => 'Strict',
'cookieMaxAge' => 86400,
'cookiePrefix' => '',
// ==================== 读取/写入配置 ====================
'isReadHeader' => true,
'isReadCookie' => true,
'isReadBody' => false,
'isWriteCookie' => true,
'isWriteHeader' => false,
// ==================== 登录策略 ====================
'concurrent' => true,
'isShare' => true,
'maxLoginCount' => 12,
'maxTryTimes' => 12,
// ==================== 存储配置 ====================
'storage' => 'memory',
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'db' => 0,
'timeout' => 3,
'prefix' => 'sa_token_',
],
// ==================== Session 配置 ====================
'session' => [
'timeout' => 86400,
'activityTimeout' => 1800,
'maxSize' => 1024,
'prefix' => 'session_',
'cleanInterval' => 3600,
],
// ==================== 加密配置 ====================
'cryptoType' => 'intl',
'tokenEncrypt' => false,
'tokenEncryptKey' => '',
'tokenFingerprint' => false,
// ==================== Refresh Token ====================
'refreshToken' => false,
'refreshTokenTimeout' => 2592000,
'refreshTokenRotation' => true,
// ==================== 安全配置 ====================
'antiBruteMaxFailures' => 5,
'antiBruteLockDuration' => 600,
'ipAnomalyDetection' => true,
'ipAnomalySensitivity' => 3,
'deviceManagement' => true,
'auditLog' => true,
'auditLogMaxEntries' => 1000,
'auditLogTtlDays' => 30,
// ==================== JWT 配置 ====================
'jwtSecretKey' => '',
'jwtStateless' => false,
// ==================== 签名配置 ====================
'signKey' => '',
'signTimestampGap' => 600,
'signAlg' => 'sha256',
// ==================== 日志配置 ====================
'isLog' => false,
];十五、项目结构
15.1 单应用项目结构
text
tp8-sa-token/
├── app/
│ ├── controller/
│ │ ├── AuthController.php
│ │ ├── UserController.php
│ │ └── ...
│ ├── middleware/
│ │ └── SaTokenMiddleware.php
│ ├── exception/
│ │ └── Handler.php
│ ├── service/
│ │ └── PermissionService.php
│ └── provider/
│ └── SaTokenProvider.php
├── config/
│ ├── sa_token.php
│ ├── sa_token_sso.php
│ ├── sa_token_oauth2.php
│ ├── middleware.php
│ └── service.php
├── route/
│ └── app.php
├── .env
└── composer.json15.2 多应用项目结构
text
tp8-sa-token/
├── app/
│ ├── common/
│ │ ├── middleware/
│ │ │ └── SaTokenMiddleware.php
│ │ ├── service/
│ │ │ └── PermissionService.php
│ │ ├── exception/
│ │ │ └── Handler.php
│ │ └── provider/
│ │ └── SaTokenProvider.php
│ ├── api/
│ │ ├── controller/
│ │ │ └── AuthController.php
│ │ ├── config/
│ │ │ └── sa_token.php
│ │ └── route/
│ │ └── app.php
│ ├── admin/
│ │ ├── controller/
│ │ │ └── AuthController.php
│ │ ├── config/
│ │ │ └── sa_token.php
│ │ └── route/
│ │ └── app.php
│ └── provider.php
├── config/
│ └── sa_token.php
└── .env十六、API 快速参考
16.1 StpUtil 核心方法
| 方法 | 说明 |
|---|---|
login($loginId, $extra) | 登录 |
logout() | 登出 |
isLogin() | 判断是否登录 |
getLoginId() | 获取登录 ID |
getTokenValue() | 获取 Token |
getTokenTimeout() | 获取 Token 剩余有效期 |
refreshToken() | 刷新 Token |
kickout($loginId) | 踢人下线 |
disable($loginId, $time) | 封禁账号 |
untieDisable($loginId) | 解封账号 |
checkPermission($perm) | 校验权限 |
checkRole($role) | 校验角色 |
getPermissionList() | 获取权限列表 |
getRoleList() | 获取角色列表 |
switchTo($loginId) | 身份切换 |
switchBack() | 切回原身份 |
setSession($key, $value) | 设置 Session |
getSession($key) | 获取 Session |
getExtra($key) | 获取扩展信息 |
updateExtra($key, $value) | 更新扩展信息 |
16.2 SaRouter 核心方法
| 方法 | 说明 |
|---|---|
match($path, $method) | 匹配路由 |
exclude($path) | 排除路径 |
check($callback) | 执行校验 |
16.3 异常类
| 异常类 | 说明 |
|---|---|
NotLoginException | 未登录异常 |
NotPermissionException | 无权限异常 |
NotRoleException | 无角色异常 |
DisableServiceException | 账号封禁异常 |
NotSafeException | 二级认证异常 |
SaTokenException | 基础异常 |
十七、常见问题
Q1: 四种模式如何选择?
| 场景 | 推荐模式 |
|---|---|
| 生产环境、高并发 | 单应用+Redis |
| 开发测试、快速验证 | 单应用+内存 |
| 微服务、分布式 | 多应用+Redis |
| 小型多模块项目 | 多应用+内存 |
Q2: 为什么 Sa-Token 不生效?
检查:
app/provider/SaTokenProvider.php是否正确注册config/service.php是否注册了服务提供者config/sa_token.php是否正确加载config/middleware.php是否注册了中间件
Q3: Token 没有自动写入 Cookie?
检查配置:
php
'isWriteCookie' => true,
'cookieSecure' => false,Q4: 如何跨域共享 Cookie?
php
'cookieDomain' => '.example.com',
'cookieSameSite' => 'Lax',Q5: 前后端分离如何使用?
php
'isReadCookie' => false,
'isWriteCookie' => false,
'isReadHeader' => true,
'isWriteHeader' => true,Q6: 生产环境推荐哪种模式?
强烈推荐 单应用+Redis 或 多应用+Redis。
Q7: Session 数据会丢失吗?
内存模式下应用重启会丢失,Redis 模式下不会。
Q8: 如何实现 Token 自动续签?
配置 activityTimeout > 0,每次请求自动刷新 Token 有效期。
项目地址: https://github.com/pohoc/sa-token ThinkPHP 8 文档: https://www.thinkphp.cn/doc 问题反馈: https://github.com/pohoc/sa-token/issues
- 本文链接: https://richfan.eu.org/posts/%E7%A8%8B%E6%8A%80/php/sa-token/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。
