CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Laravel 实战入门/课时/认证与授权
RawBlame
课时目录12 节

Laravel 实战入门

  1. 01Laravel 概览
  2. 02目录结构
  3. 03路由
  4. 04控制器
  5. 05Blade 模板
  6. 06Eloquent ORM
  7. 07认证与授权
  8. 08验证与异常
  9. 09队列与事件
  10. 10运维与工具
  11. 11测试
  12. 12综合练习

认证与授权

Sanctum、Passport 与 Policy。

30 分钟

认证与授权

这一课你将能做到

  1. 区分认证(Authentication)与授权(Authorization),说清楚 Session 登录与 Token 登录的适用边界
  2. 手写一套邮箱密码的登录、退出逻辑,并能用 Laravel Sanctum 给 API 签发与校验令牌
  3. 用 Gate 与 Policy 表达「这个用户能不能操作这个资源」,避免把权限判断散落在控制器里

为什么要学

「你是谁」与「你能干什么」是所有后端系统绕不开的两件事。Spring 生态里你可能学过 Spring Security:过滤器链、UserDetailsService、@PreAuthorize。Laravel 同样有完整的对应物——auth 守卫、Hash 加密、Sanctum 令牌、Gate/Policy 授权层。

权限逻辑最怕的是失控:每个方法里写一串 if ($order->user_id != auth()->id()),漏一个就是越权漏洞(IDOR)。Policy 的价值就是把授权规则收敛到一个地方,控制器、Blade、测试中统一引用。本课我们同时实现网页登录和 API 令牌两种形态。

先建立图像

两种认证形态:

网页(有状态):
  登录成功 → 写 Session(含 user_id)→ 浏览器保存会话 Cookie
  后续请求自动带 Cookie → auth 中间件从 Session 恢复用户

API(无状态):
  POST /api/login 校验账密 → Sanctum 生成 token(明文仅返回一次)
  客户端存令牌,请求带 Authorization: Bearer <token>
  auth:sanctum 中间件每次查 personal_access_tokens 表恢复用户

概念对照:

LaravelSpring Security
Guard(web / sanctum)Authentication 机制(Session/JWT)
User Provider(Eloquent)UserDetailsService
Hash::check()PasswordEncoder.matches()
Gate / Policy@PreAuthorize / AccessDecisionManager
@can Blade 指令标签库 / 方法安全
Sanctum token自实现 bearer token / OAuth2 简化版

跟着做

步骤 1:手写网页登录

创建控制器:

php artisan make:controller Auth/LoginController

app/Http/Controllers/Auth/LoginController.php:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    public function create()
    {
        return view('auth.login');
    }

    public function store(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required', 'string'],
        ]);

        // attempt:校验密码并写入登录 Session;第二个参数控制「记住我」
        if (! Auth::attempt($credentials, $request->boolean('remember'))) {
            return back()
                ->withInput($request->only('email'))
                ->withErrors(['email' => '邮箱或密码不正确']);
        }

        // 防会话固定攻击:重新生成 Session ID
        $request->session()->regenerate();

        return redirect()->intended(route('dashboard'))->with('success', '欢迎回来');
    }

    public function destroy(Request $request): RedirectResponse
    {
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/');
    }
}

resources/views/auth/login.blade.php:

@extends('layouts.app')
@section('content')
<form method="POST" action="{{ route('login') }}">
    @csrf
    <input type="email" name="email" value="{{ old('email') }}" required autofocus>
    @error('email') <p>{{ $message }}</p> @enderror

    <input type="password" name="password" required>

    <label><input type="checkbox" name="remember"> 记住我</label>
    <button type="submit">登录</button>
</form>
@endsection

路由(routes/web.php):

use App\Http\Controllers\Auth\LoginController;

Route::middleware('guest')->group(function () {
    Route::get('/login', [LoginController::class, 'create'])->name('login');
    Route::post('/login', [LoginController::class, 'store']);
});
Route::post('/logout', [LoginController::class, 'destroy')->name('logout')->middleware('auth');

先注册一个用户验证:php artisan tinker 中执行 \App\Models\User::create(['name'=>'测试','email'=>'a@b.com','password'=>bcrypt('123456')])。

步骤 2:密码哈希与「记住我」

use Illuminate\Support\Facades\Hash;

// 注册时:bcrypt(自动加盐,cost 默认 12)
$user->password = Hash::make($request->password);

// 登录校验由 attempt 内部完成;手动比对:
Hash::check($plain, $user->password);

// 判断是否需要升级哈希强度(如调大 cost 后平滑重哈希)
if (Hash::needsRehash($user->password)) {
    $user->update(['password' => Hash::make($plain)]);
}

「记住我」勾选后框架发放长效 remember cookie,用户模型需包含 remember_token 字段(默认迁移已含)。

步骤 3:用 Sanctum 保护 API

安装(Laravel 11 中已预装,未装时执行):

composer require laravel/sanctum
php artisan migrate

登录发令牌的接口写在 routes/api.php:

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::post('/login', function (Request $request) {
    $data = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
        'device_name' => ['required', 'string'],
    ]);

    $user = User::where('email', $data['email'])->first();

    if (! $user || ! Hash::check($data['password'], $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['凭证不正确'],
        ]); // 自动渲染为 422
    }

    // abilities 声明令牌权限范围
    $token = $user->createToken($data['device_name'], ['orders:write'])->plainTextToken;

    return response()->json(['token' => $token], 201);
});

保护路由:

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $request) => $request->user());

    Route::post('/orders', function (Request $request) {
        abort_unless($request->user()->tokenCan('orders:write'), 403);
        // ... 下单逻辑
    });
});

验证:

TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"a@b.com","password":"123456","device_name":"iphone"}' | python -c "import sys,json;print(json.load(sys.stdin)['token'])")

curl http://127.0.0.1:8000/api/user -H "Authorization: Bearer $TOKEN"

令牌明文只在创建时返回;库里存的是 SHA-256 哈希,泄露数据库无法冒用。

步骤 4:用 Policy 收敛授权

生成策略(Laravel 会自动按命名约定关联 Order 模型):

php artisan make:policy OrderPolicy --model=Order

app/Policies/OrderPolicy.php:

<?php

namespace App\Policies;

use App\Models\Order;
use App\Models\User;

class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $user->id === $order->user_id;
    }

    public function cancel(User $user, Order $order): bool
    {
        return $user->id === $order->user_id
            && in_array($order->status, ['pending', 'paid'], true);
    }
}

控制器中授权(不需要再手写 if):

public function show(Order $order)
{
    $this->authorize('view', $order); // 失败抛 403
    return new \App\Http\Resources\OrderResource($order);
}

public function cancel(Order $order)
{
    Gate::authorize('cancel', $order);
    $order->update(['status' => 'cancelled']);
    return response()->json(['ok' => true]);
}

Blade 中:

@can('cancel', $order)
    <form method="POST" action="{{ route('orders.cancel', $order) }}">
        @csrf @method('PATCH')
        <button>取消订单</button>
    </form>
@endcan

步骤 5:Gate 处理非模型权限

简单角色判断不必为每个动作建 Policy,在 AppServiceProvider::boot() 定义 Gate:

use Illuminate\Support\Facades\Gate;

Gate::define('access-admin', function (User $user) {
    return $user->is_admin;
});

// 超管可通过 before 短路所有检查(等价 Spring Security 的 ROLE 层级)
Gate::before(function (User $user, string $ability) {
    if ($user->is_admin) {
        return true;
    }
});

路由或控制器中:Route::get('/admin/...')->middleware('can:access-admin');。

讲清楚要点

  1. 认证与授权分层:中间件解决「是否登录」(401),Policy/Gate 解决「能否操作」(403)。两者不要混为一谈,状态码也不能乱用。
  2. Sanctum 不是 JWT:令牌是不透明随机串,服务端查库校验,因此可即时吊销(删记录即失效);JWT 无状态但签发后难以撤回。SPA 还可用 Sanctum 的 Cookie 模式。
  3. Policy 自动发现:Order → OrderPolicy 的约定免注册;自定义命名可在 Gate::policy() 显式绑定。
  4. 访客用户的授权:Policy 方法把 User 参数设为 nullable(?User $user),可表达「未登录也允许查看公开商品」。
  5. 速率防爆破:登录接口必须挂 throttle(如 throttle:6,1),防止撞库;可结合按邮箱 + IP 的自定义限流。
  6. 不要在前端校验权限当安全措施:按钮隐藏只是体验,真正的判断必须在服务端 Policy。

你可能会懵的地方

  • Auth::attempt 和 Hash::check 我该用哪个? 网页登录用 attempt(校验 + 建立 Session 一步到位);纯 API 发 token 前用 Hash::check 手动校验。
  • API 用 Sanctum 还是 Passport? 自有 App、SPA、小程序用 Sanctum(简单、可吊销);需要完整 OAuth2(第三方应用代用户授权、authorization code 流程)才用 Passport。
  • Policy 不生效/总是 403? 检查命名与位置(app/Policies/OrderPolicy.php)、方法名与 authorize() 第一个参数一致、控制器基类是否使用了 AuthorizesRequests(Laravel 11 默认含 authorize 方法)。
  • 多端登录、互踢怎么实现? 登录成功后删除该用户其他 Session(Auth::logoutOtherDevices($password),需带校验),或清理其 personal_access_tokens 中其他设备的记录。
  • 邮箱验证、密码找回要自己写吗? 框架内置:User 实现 MustVerifyEmail 即自动接入验证邮件;php artisan make:controller Auth/ForgotPasswordController 配合内置的 Password Broker 即可,Breeze 已提供完整范本。

错法对照

错法问题正解
明文 / MD5 存密码拖库即泄露全部密码Hash::make()(bcrypt/argon2)
每个方法手写 user_id == 判断漏一处就是越权漏洞Policy 统一规则
把 JWT/token 存 localStorage 又不做 XSS 防护令牌可被脚本窃取评估 httpOnly Cookie + CSRF 或严格 CSP
登录接口不限流可暴力撞库throttle + 失败锁定
改密码后旧令牌仍然有效安全风险敏感操作后删除旧 token / 登出其他设备
用 is_admin 字段替代细粒度授权角色膨胀、规则僵化资源级 Policy + 必要时角色表

卡点急救

  1. 登录成功但下一个请求又变游客:检查路由是否在 web 中间件组(Session 才可用)、Cookie 是否被浏览器拦截(域名/ SameSite 设置)、config/session.php 的驱动与过期时间。
  2. Sanctum 返回 401 但 token 正确:确认请求头格式 Authorization: Bearer xxx 无多余空格;路由确实挂了 auth:sanctum;令牌未过期、未被删除。
  3. 授权永远 403:用 Gate::allows('view', $order) 在 tinker 里对具体用户与记录单测;检查 Policy 中比较的字段与类型(字符串 id 可用 (string) 转换对比)。

面试常问

  • 认证和授权有什么区别?Laravel 分别用什么组件实现? 认证回答「你是谁」:Guard + User Provider,web 走 Session、sanctum 走令牌表;授权回答「你能不能做」:Gate(闭包规则)与 Policy(按模型组织的类),控制器用 authorize()、Blade 用 @can、路由用 can: 中间件。对应 Spring Security 的认证入口点与方法级安全。

  • Sanctum 的令牌原理是什么?和 JWT 如何取舍? 创建时生成随机明文令牌返回客户端,服务端存其 SHA-256 哈希与 abilities;请求时按不透明令牌查库校验,可随时删除吊销。优点是可撤回、可按设备管理;代价是每请求一次查库(可缓存)。JWT 无状态、适合分布式但难撤回且续期/登出麻烦。自有产品选 Sanctum,跨第三方授权选 OAuth2/Passport。

  • 如何防止水平越权(IDOR)? 所有按 ID 取资源的写操作必须过资源级 Policy,校验资源归属($order->user_id === $user->id);路由模型绑定只解决「存在性」,不解决「归属」,二者要叠加;测试中专门构造「用户 A 尝试操作用户 B 资源应 403」的用例。

自测三问

  1. 一个未登录请求访问受保护 API,与一个已登录但操作他人订单的请求,分别应该返回什么状态码、被哪一层拦住?
  2. Sanctum 令牌为什么在数据库里看不到明文?用户换手机后如何让旧设备令牌失效?
  3. 「取消订单」的规则涉及归属与订单状态,你会把规则写在哪里?控制器、Blade、测试三处分别怎么调用?

小结一句话

认证确认身份(Session 或 Sanctum 令牌),授权约束行为(Gate 与 Policy),两层各司其职并守住 401/403 语义,系统就不会在身份问题上出洞。

延伸

  • Laravel 认证文档
  • Laravel Sanctum
  • 授权 Gate / Policy
  • 密码哈希
  • 下一课:表单验证与错误处理——系统学习规则体系、表单请求类与自定义验证规则。
加载本节练习…
← 上一课Eloquent ORM下一课 →验证与异常