CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
认证与授权
这一课你将能做到
区分认证(Authentication)与授权(Authorization),说清楚 Session 登录与 Token 登录的适用边界
手写一套邮箱密码的登录、退出逻辑,并能用 Laravel Sanctum 给 API 签发与校验令牌
用 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 表恢复用户
概念对照:
Laravel Spring 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
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);
}
}
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]);
}
@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');。
讲清楚要点
认证与授权分层 :中间件解决「是否登录」(401),Policy/Gate 解决「能否操作」(403)。两者不要混为一谈,状态码也不能乱用。
Sanctum 不是 JWT :令牌是不透明随机串,服务端查库校验,因此可即时吊销(删记录即失效);JWT 无状态但签发后难以撤回。SPA 还可用 Sanctum 的 Cookie 模式。
Policy 自动发现 :Order → OrderPolicy 的约定免注册;自定义命名可在 Gate::policy() 显式绑定。
访客用户的授权 :Policy 方法把 User 参数设为 nullable(?User $user),可表达「未登录也允许查看公开商品」。
速率防爆破 :登录接口必须挂 throttle(如 throttle:6,1),防止撞库;可结合按邮箱 + IP 的自定义限流。
不要在前端校验权限当安全措施 :按钮隐藏只是体验,真正的判断必须在服务端 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 + 必要时角色表
卡点急救
登录成功但下一个请求又变游客 :检查路由是否在 web 中间件组(Session 才可用)、Cookie 是否被浏览器拦截(域名/ SameSite 设置)、config/session.php 的驱动与过期时间。
Sanctum 返回 401 但 token 正确 :确认请求头格式 Authorization: Bearer xxx 无多余空格;路由确实挂了 auth:sanctum;令牌未过期、未被删除。
授权永远 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」的用例。
自测三问
一个未登录请求访问受保护 API,与一个已登录但操作他人订单的请求,分别应该返回什么状态码、被哪一层拦住?
Sanctum 令牌为什么在数据库里看不到明文?用户换手机后如何让旧设备令牌失效?
「取消订单」的规则涉及归属与订单状态,你会把规则写在哪里?控制器、Blade、测试三处分别怎么调用?
小结一句话 认证确认身份(Session 或 Sanctum 令牌),授权约束行为(Gate 与 Policy),两层各司其职并守住 401/403 语义,系统就不会在身份问题上出洞。
延伸