Laravel: Laravel认证系统—Sanctum
最后更新:2026-08-26
Sanctum 是 Laravel 的"门卫"——Web 页面用 Session 认证,API 用 Token 认证,两套门卫一套系统。
1. 你将学到
- Laravel Sanctum 安装与配置:SPA 认证 + Token 认证
- 注册/登录/登出流程:Web Session + API Token 双通道
- Token 能力与权限:abilities 字段精细化控制
- 多租户认证隔离:Tenant-aware middleware 与 scope
- 密码重置与邮箱验证流程
2. 一个安全工程师的真实故事
(1) 痛点:API 认证方案混乱导致安全事故
Bob 给 ShopMetrics API 用了 JWT,Web 用了 Session——两套认证系统,用户登录状态不同步。Alice 在浏览器登录后,API 调用还要单独获取 Token,经常过期导致操作中断。更严重的是,Charlie 发现一个 API Token 能访问所有租户的数据——没有租户隔离,跨租户数据泄露风险极大。
(2) Sanctum 的解法
Sanctum 统一了 Web 和 API 认证——SPA 模式下浏览器和 API 共享 Session,Token 模式为第三方客户端提供 API Token,abilities 字段精细控制权限。
PHP
// SPA auth — share session between web and API
// Just ensure Sanctum's middleware is on API routes
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
// Token auth — for third-party clients
$token = $user->createToken('api-token', ['read-orders', 'write-products']);
// Token can only do what its abilities allow
(3) 收益
Bob 统一认证后,Alice 的 SPA 体验丝滑无感,Charlie 的 Token 权限精确到"只能读取订单",跨租户数据泄露风险降为零。
3. Sanctum 安装与配置
(1) 安装
BASH
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
# Creates: personal_access_tokens table
(2) 配置
PHP
// config/sanctum.php
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : '',
))),
'expiration' => null, // Token never expires (or set minutes)
(3) SPA 前端配置
JAVASCRIPT
// resources/js/bootstrap.js
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.withCredentials = true; // Send cookies with API requests
| 配置项 | 作用 | 默认值 |
|---|---|---|
stateful |
SPA 认证域名 | localhost:3000 等 |
expiration |
Token 过期时间 | null(永不过期) |
guard |
认证守卫 | web |
▶ 示例:Sanctum SPA 认证配置
BASH
# .env — Configure SPA domains
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost:3000,localhost:5173
SESSION_DRIVER=redis
# Install Sanctum and configure
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
输出:
TEXT
📖 仅展示
# 命令执行成功
4. 注册/登录/登出流程
(1) 注册
PHP
// app/Http/Controllers/Auth/RegisterController.php
class RegisterController extends Controller
{
public function register(RegisterRequest $request): JsonResponse
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'tenant_id' => $request->tenant_id,
'role' => 'tenant_owner',
]);
event(new Registered($user));
Auth::login($user);
return response()->json([
'user' => $user,
'message' => 'Registration successful.',
], 201);
}
}
(2) 登录
PHP
// app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
if (!Auth::attempt($request->only('email', 'password'))) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$request->session()->regenerate();
return response()->json([
'user' => Auth::user(),
'message' => 'Login successful.',
]);
}
public function logout(Request $request): JsonResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json(['message' => 'Logged out.']);
}
}
(3) Token 认证登录
PHP
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Invalid credentials.'],
]);
}
$token = $user->createToken(
$request->device_name ?? 'api-token',
$request->abilities ?? ['*'],
);
return response()->json([
'user' => $user,
'token' => $token->plainTextToken,
]);
}
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Token revoked.']);
}
}
| 模式 | 登录方式 | 状态保持 | 适合场景 |
|---|---|---|---|
| SPA Session | 表单登录 | Cookie/Session | SPA 前端 |
| Token | 获取 plainTextToken | Token 字符串 | 移动端/第三方 |
▶ 示例:ShopMetrics 双模式认证路由
PHP
// routes/web.php — Session auth for SPA
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LogoutController::class, 'logout'])->middleware('auth');
Route::post('/register', [RegisterController::class, 'register']);
// routes/api.php — Token auth for API
Route::post('/tokens', [ApiTokenController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::delete('/tokens/current', [ApiTokenController::class, 'logout']);
Route::get('/user', fn (Request $request) => $request->user());
});
输出:
TEXT
📖 仅展示
// 执行成功
5. Token 能力与权限
(1) 创建带 abilities 的 Token
PHP
// Create token with specific abilities
$token = $user->createToken('analytics-read', [
'read-analytics',
'read-orders',
]);
// Full access token
$token = $user->createToken('admin-token', ['*']);
// Token with expiry
$token = $user->createToken('temp-token', ['read-orders'], now()->addDays(7));
(2) 检查 Token 能力
PHP
// In controller or middleware
if ($request->user()->tokenCan('read-analytics')) {
return AnalyticsResource::collection($analytics);
}
// In middleware
class CheckAbilities
{
public function handle($request, $next, ...$abilities)
{
foreach ($abilities as $ability) {
if (!$request->user()->tokenCan($ability)) {
abort(403, 'Insufficient permissions.');
}
}
return $next($request);
}
}
(3) Abilities 权限矩阵
| Role | Abilities | 说明 |
|---|---|---|
| Super Admin | * |
全部权限 |
| Tenant Owner | read-*, write-* |
租户内全权限 |
| Analyst | read-analytics, read-orders |
只读 |
| API Client | read-products, write-orders |
第三方限定 |
▶ 示例:ShopMetrics Token 能力控制
PHP
// app/Http/Controllers/Api/TokenController.php
class TokenController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'abilities' => 'sometimes|array',
'abilities.*' => 'in:read-analytics,read-orders,write-orders,read-products,write-products',
'expires_in_days' => 'sometimes|integer|min:1|max:365',
]);
$abilities = $validated['abilities'] ?? match ($request->user()->role) {
'super_admin' => ['*'],
'tenant_owner' => ['read-analytics', 'read-orders', 'write-orders', 'read-products', 'write-products'],
'analyst' => ['read-analytics', 'read-orders', 'read-products'],
default => [],
};
$expiresAt = isset($validated['expires_in_days'])
? now()->addDays($validated['expires_in_days'])
: null;
$token = $request->user()->createToken(
$validated['name'],
$abilities,
$expiresAt,
);
return response()->json([
'token' => $token->plainTextToken,
'abilities' => $abilities,
], 201);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
6. 多租户认证隔离
(1) Sanctum Token 认证时序图
sequenceDiagram
participant C as Client
participant S as Sanctum
participant DB as Database
participant M as Middleware
C->>S: POST /api/tokens (email + password)
S->>DB: Find user + verify password
DB-->>S: User found
S->>DB: Create personal_access_token
DB-->>S: Token created
S-->>C: Return plainTextToken
C->>M: GET /api/shops (Bearer token)
M->>DB: Find token in personal_access_tokens
DB-->>M: Token + user + abilities
M->>M: Check tokenCan() ability
M->>M: Check tenant scope
M-->>C: Return tenant-scoped data
(2) Tenant 认证中间件
PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();
if (!$user || !$user->tenant_id) {
abort(403, 'No tenant associated.');
}
$tenant = $user->tenant;
if ($tenant->status !== 'active') {
abort(403, 'Tenant account is suspended.');
}
// Set tenant context for all queries
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}
(3) 多租户 Scope
PHP
// app/Models/TenantScope.php
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if ($tenant = app()->make(Tenant::class)) {
$builder->where($model->getTable() . '.tenant_id', $tenant->id);
}
}
}
// Apply to models
class Shop extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new TenantScope);
}
}
▶ 示例:ShopMetrics 多租户认证路由
PHP
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
// All API routes require tenant resolution
Route::middleware('tenant.resolve')->prefix('v1')->group(function () {
Route::apiResource('shops', Api\ShopController::class);
Route::apiResource('products', Api\ProductController::class);
Route::apiResource('orders', Api\OrderController::class)
->only(['index', 'show', 'update']);
// Analytics — requires specific ability
Route::get('analytics', [Api\AnalyticsController::class, 'index'])
->middleware('ability:read-analytics');
// Admin-only routes
Route::middleware('ability:*')->prefix('admin')->group(function () {
Route::apiResource('plans', Api\Admin\PlanController::class);
Route::apiResource('tenants', Api\Admin\TenantController::class);
});
});
});
输出:
TEXT
📖 仅展示
// 执行成功
7. 密码重置与邮箱验证
(1) 密码重置
PHP
// routes/web.php
Route::post('/forgot-password', [PasswordResetController::class, 'sendResetLink']);
Route::post('/reset-password', [PasswordResetController::class, 'reset']);
// app/Http/Controllers/PasswordResetController.php
class PasswordResetController extends Controller
{
public function sendResetLink(Request $request): JsonResponse
{
$request->validate(['email' => 'required|email|exists:users']);
$status = Password::sendResetLink($request->only('email'));
return response()->json([
'message' => $status === Password::RESET_LINK_SENT
? 'Reset link sent to your email.'
: 'Unable to send reset link.',
]);
}
}
(2) 邮箱验证
PHP
// Model must implement MustVerifyEmail
class User extends Model implements MustVerifyEmail
{
use Notifiable, VerifiesEmails;
}
// Protected routes — only verified users
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
// API email verification
Route::middleware('auth:sanctum')->group(function () {
Route::post('/email/verification-notification', function (Request $request) {
$request->user()->sendEmailVerificationNotification();
return response()->json(['message' => 'Verification link sent.']);
});
});
▶ 示例:ShopMetrics 认证流程完整配置
PHP
// bootstrap/app.php — Configure auth middleware
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'tenant.resolve' => TenantResolve::class,
'ability' => CheckAbilities::class,
]);
})
// User model with verification
class User extends Authenticatable implements MustVerifyEmail
{
use HasApiTokens, Notifiable;
protected $fillable = [
'tenant_id', 'name', 'email', 'password', 'role', 'email_verified_at',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
8. 综合示例:ShopMetrics 完整认证系统
PHP
// ============================================
// Comprehensive: ShopMetrics Auth System
// Covers: SPA auth, token auth, abilities, tenant isolation
// ============================================
// routes/api.php — Complete API auth routes
Route::prefix('auth')->group(function () {
Route::post('/register', [Auth\RegisterController::class, 'register']);
Route::post('/login', [Auth\ApiTokenController::class, 'login']);
Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $r) => $r->user()->load('tenant'));
Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
Route::post('/email/verification', function (Request $request) {
$request->user()->sendEmailVerificationNotification();
return response()->json(['message' => 'Verification email sent.']);
});
Route::post('/tokens', [Auth\TokenController::class, 'store']);
Route::get('/tokens', fn (Request $r) => $r->user()->tokens);
Route::delete('/tokens/{id}', fn (Request $r, $id) => $r->user()->tokens()->where('id', $id)->delete());
});
});
// Protected API routes
Route::middleware(['auth:sanctum', 'verified', 'tenant.resolve'])
->prefix('v1')->group(function () {
Route::apiResource('shops', Api\ShopController::class);
Route::apiResource('products', Api\ProductController::class);
Route::apiResource('orders', Api\OrderController::class)->only(['index', 'show', 'update']);
Route::get('analytics/overview', [Api\AnalyticsController::class, 'overview'])
->middleware('ability:read-analytics');
Route::post('reports/generate', [Api\ReportController::class, 'generate'])
->middleware('ability:write-reports');
});
❓ 常见问题
Q Sanctum 和 Passport 有什么区别?
A Sanctum 轻量,支持 SPA Session + 简单 Token 认证;Passport 完整实现 OAuth2(授权码、隐式、客户端凭证等)。大部分应用用 Sanctum 就够了,需要 OAuth2 才用 Passport。
Q SPA 模式下 Token 存在哪?
A SPA 模式不使用 Token!它用 Cookie/Session 认证,就像传统 Web 应用一样。前端只需设置
withCredentials: true,Sanctum 自动处理 CSRF 和 Session。Q Token 应该存前端哪里?
A 移动端存 Keychain/Keystore;SPA 如果用 Token 模式存 localStorage(有 XSS 风险)或 HttpOnly Cookie。最佳实践:SPA 用 Session 模式避免存 Token。
Q 如何让 Token 过期?
A 在 config/sanctum.php 设置
expiration(分钟数),或在创建 Token 时传第三个参数:$user->createToken('name', ['*'], now()->addDays(30))。过期 Token 自动失效。Q 多租户如何防止 Token 跨租户使用?
A 在 TenantResolve 中间件中验证 Token 所属用户的 tenant_id 与请求的租户一致。也可以在 Token 的 abilities 中编码租户信息。
Q 如何撤销所有 Token?
A
$user->tokens()->delete() 撤销用户所有 Token;$user->currentAccessToken()->delete() 只撤销当前 Token。📖 小节
- Sanctum 统一 Web Session 和 API Token 两套认证机制
- SPA 模式用 Cookie/Session 认证,无需管理 Token
- Token 模式用 plainTextToken 为第三方客户端提供 API 访问
- Token abilities 精细控制权限,避免过度授权
- TenantResolve 中间件 + TenantScope 实现多租户认证隔离
- 邮箱验证和密码重置开箱即用
📝 作业
-
基础题(⭐):配置 Sanctum SPA 认证,实现注册、登录、登出三个 API 端点,用 Postman 测试 Session 认证流程。
-
进阶题(⭐⭐):实现 Token 认证模式,创建带 abilities 的 Token,编写中间件检查
tokenCan()权限,确保 analyst 角色只能读取数据。 -
挑战题(⭐⭐⭐):实现 TenantScope + TenantResolve 中间件的多租户隔离,确保 API Token 只能访问所属租户的数据,跨租户请求返回 403。