Laravel: Laravel认证系统—Sanctum

最后更新:2026-08-26

Sanctum 是 Laravel 的"门卫"——Web 页面用 Session 认证,API 用 Token 认证,两套门卫一套系统。

1. 你将学到


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 认证时序图

100%
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。

📖 小节


📝 作业

  1. 基础题(⭐):配置 Sanctum SPA 认证,实现注册、登录、登出三个 API 端点,用 Postman 测试 Session 认证流程。

  2. 进阶题(⭐⭐):实现 Token 认证模式,创建带 abilities 的 Token,编写中间件检查 tokenCan() 权限,确保 analyst 角色只能读取数据。

  3. 挑战题(⭐⭐⭐):实现 TenantScope + TenantResolve 中间件的多租户隔离,确保 API Token 只能访问所属租户的数据,跨租户请求返回 403。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏