TaskFlow chưa biết ai đang dùng nó, ai mở cũng xóa được file của mọi task (bài trước). Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về xác thực người dùng trong Laravel 13: tự viết đăng ký, đăng nhập, đăng xuất và bảo vệ route.
1. Xác thực và starter kit.
Xác thực (authentication) là kiểm tra người dùng là ai. Đăng nhập đúng thì guard web (config/auth.php) lưu id của user vào session, các request sau đọc lại để biết ai đang đăng nhập.
Các starter kit (React, Svelte, Vue, Livewire) chọn lúc chạy laravel new sinh sẵn các trang này nhưng rất nhiều file (Bài 2), nên series tự viết.
2. Middleware auth và guest.
Hai middleware alias có sẵn (Bài 9): auth chỉ cho người đã đăng nhập, khách bị chuyển tới route tên login (request JSON nhận lỗi 401); guest chỉ cho khách, người đã đăng nhập bị chuyển tới route dashboard hoặc home.
- routes/web.php (trích, thêm use hai controller của phần sau)
// chỉ khách (chưa đăng nhập) mới vào được trang đăng ký, đăng nhập
Route::middleware('guest')->group(function () {
Route::get('/register', [RegisterController::class, 'create'])->name('register');
Route::post('/register', [RegisterController::class, 'store']);
Route::get('/login', [LoginController::class, 'create'])->name('login');
Route::post('/login', [LoginController::class, 'store']);
});
Route::post('/logout', [LoginController::class, 'destroy'])
->middleware('auth')
->name('logout');
Nhóm route projects thêm 'auth' vào đầu mảng middleware, 4 route file của bài trước bọc trong Route::middleware('auth')->group(). Trong withMiddleware() của bootstrap/app.php, $middleware->redirectUsersTo('/projects'); cho người đã đăng nhập về trang project. Lúc này khách mở /projects nhận 302 tới /login.
3. Đăng ký tài khoản.
Mật khẩu luôn lưu dạng băm (hash) một chiều bằng facade Hash. Thử trong php artisan tinker:
$hash = Hash::make('password'); // "$2y$12$3Hl1oK49hUe0np..." 60 ký tự, mỗi lần chạy ra chuỗi khác
Hash::check('password', $hash); // true
Hash::check('Password', $hash); // false
Quy tắc mật khẩu khai báo một lần trong boot() của AppServiceProvider: Password::defaults(fn () => Password::min(8)->letters()->numbers()); (ít nhất 8 ký tự, có chữ và số, nhớ use Illuminate\Validation\Rules\Password;).
- app/Http/Controllers/RegisterController.php (tạo bằng make:controller, LoginController cũng vậy)
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Illuminate\View\View;
class RegisterController extends Controller
{
public function create(): View
{
return view('auth.register');
}
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users'],
// confirmed: phải khớp ô password_confirmation
'password' => ['required', 'confirmed', Password::defaults()],
]);
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
// đăng nhập luôn user vừa tạo
Auth::login($user);
$request->session()->regenerate();
return to_route('projects.index')->with('status', 'Chào mừng '.$user->name.'!');
}
}
Cast 'password' => 'hashed' có sẵn của User không băm lại chuỗi đã băm. View auth/register.blade.php giống form tạo project (Bài 10), gồm name, email, password, password_confirmation. Thông báo tiếng Việt của các rule mới:
- lang/vi/validation.php (trích, các key thêm vào mảng)
'email' => 'Trường :attribute phải là địa chỉ email hợp lệ.',
'lowercase' => 'Trường :attribute phải viết chữ thường.',
'unique' => 'Trường :attribute đã có người sử dụng.',
'confirmed' => 'Trường :attribute nhập lại không khớp.',
'min' => [
'string' => 'Trường :attribute phải có ít nhất :min ký tự.',
],
'password' => [
'letters' => 'Trường :attribute phải có ít nhất một chữ cái.',
'numbers' => 'Trường :attribute phải có ít nhất một chữ số.',
],
'attributes' => [
'name' => 'họ tên',
'password' => 'mật khẩu',
],
4. Đăng nhập và đăng xuất.
- app/Http/Controllers/LoginController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\View\View;
class LoginController extends Controller
{
public function create(): View
{
return view('auth.login');
}
public function store(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
// đếm số lần đăng nhập sai theo email + IP
$key = Str::lower($request->input('email')).'|'.$request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
return back()->withErrors([
'email' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]),
])->onlyInput('email');
}
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
RateLimiter::hit($key); // tự hết sau 60 giây
return back()->withErrors(['email' => __('auth.failed')])->onlyInput('email');
}
RateLimiter::clear($key);
// đổi session ID để chống session fixation
$request->session()->regenerate();
// về trang định vào trước khi bị chuyển tới /login
return redirect()->intended(route('projects.index'));
}
public function destroy(Request $request): RedirectResponse
{
Auth::logout();
// xóa dữ liệu session cũ và tạo CSRF token mới
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('login')->with('status', 'Đã đăng xuất.');
}
}
Trong đó:
Auth::attempt($credentials, $remember): đúng email và mật khẩu thì đăng nhập, trả vềtrue.$rememberthêm cookieremember_web_...giữ đăng nhập khi hết session.RateLimiter(Bài 4 dùng quathrottle):hit()đếm lần sai,tooManyAttempts()kiểm tra quá 5 lần,availableIn()trả về số giây còn khóa.regenerate(): đổi session ID để chống session fixation.intended(): khách vào/projects/2bị chuyển tới/login, đăng nhập xong quay lại đó.
- resources/views/auth/login.blade.php
<x-layout title="Đăng nhập">
<h1 class="mb-4 text-2xl font-bold">Đăng nhập</h1>
<form method="POST" action="{{ route('login') }}" class="space-y-4">
@csrf
<input type="email" name="email" value="{{ old('email') }}" placeholder="Email" class="w-full rounded border p-2">
@error('email')
<p class="text-sm text-red-600">{{ $message }}</p>
@enderror
<input type="password" name="password" placeholder="Mật khẩu" class="w-full rounded border p-2">
<label class="block">
<input type="checkbox" name="remember"> Ghi nhớ đăng nhập
</label>
<button type="submit" class="rounded bg-blue-600 px-4 py-2 text-white">Đăng nhập</button>
</form>
</x-layout>
Thông báo auth.failed, auth.throttle dịch trong file mới, :seconds là số giây còn khóa (sai lần thứ 6 báo "...thử lại sau 58 giây."):
- lang/vi/auth.php
<?php
return [
'failed' => 'Email hoặc mật khẩu không đúng.',
'password' => 'Mật khẩu không đúng.',
'throttle' => 'Bạn đăng nhập sai quá nhiều lần, thử lại sau :seconds giây.',
];
5. Lấy user đang đăng nhập.
Facade Auth, helper auth() hay $request->user() đều cho biết user hiện tại:
Auth::check(); // false, chưa đăng nhập
Auth::loginUsingId(1); // đăng nhập bằng id, tiện để thử
Auth::id(); // 1
auth()->user()->name; // "Nguyễn Văn An"
Blade có @auth, @guest để hiện nội dung theo trạng thái đăng nhập:
- resources/views/partials/nav.blade.php (trích, sau link "Thống kê")
@auth
<span class="ml-auto">{{ auth()->user()->name }}</span>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-blue-600">Đăng xuất</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}" class="ml-auto">Đăng nhập</a>
<a href="{{ route('register') }}">Đăng ký</a>
@endguest
ProjectController::index() đổi thành $request->user()->projects()->withCount('tasks')->paginate(5) và store() tạo qua relationship (Bài 17): $request->user()->projects()->create($request->validated()) tự gán user_id. LimitProjects chỉ đếm project của user: mỗi người tối đa 20.
DatabaseSeeder đổi user thứ hai thành "Trần Thị Bình" ([email protected]). Chạy php artisan migrate:fresh --seed rồi đăng nhập (mật khẩu đều là password): [email protected] thấy nav "Nguyễn Văn An Đăng xuất" và hai project "Website toidicode.com", "TaskFlow API", [email protected] chỉ thấy "Du lịch Đà Lạt".
Chú ý: Ai cũng tự đăng ký được tài khoản, nên mọi tài khoản vẫn mở được /projects/1 của An khi gõ thẳng URL, xem, tải lên, xóa file của mọi task, và /stats vẫn công khai project của mọi người. Bài sau sẽ kiểm tra quyền, đừng deploy TaskFlow trước đó.
6. Lời kết.
Như vậy chúng ta đã tìm hiểu xong xác thực người dùng trong Laravel 13: Hash, Auth::attempt, middleware auth, guest và giới hạn đăng nhập sai. Các bạn xem thêm tại tài liệu chính thức. Bài sau mình sẽ giới thiệu về phân quyền với Gate và Policy.
Đăng ký nhận tin.
Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!





0 Comments