Đăng nhập không mật khẩu với magic link trong Next.js 15 & Scalekit
Mật khẩu thường gây rắc rối. Người dùng thường quên mật khẩu, quy trình đặt lại mật khẩu thường gặp sự cố, và các đội ngũ bảo mật liên tục yêu cầu chúng ta thêm nhiều quy tắc (chữ hoa, ký tự đặc biệt, không tái sử dụng). Đối với các nhà phát triển, điều này có nghĩa là phức tạp. Đối với người dùng, điều này đồng nghĩa với sự thất vọng.
Có một cách đơn giản hơn: magic links.
Với Scalekit, bạn có thể triển khai đăng nhập không mật khẩu trong Next.js 15 chỉ với các route API và middleware. Hãy cùng xem cách thực hiện.
Tại sao lại là magic links?
Tại Scalekit, chúng tôi đã thấy nhiều đội ngũ gặp phải những vấn đề tương tự:
- Quy trình đặt lại mật khẩu khiến bộ phận hỗ trợ khách hàng bận rộn.
- Các mã xác thực qua SMS thường bị chậm hoặc không hoạt động khi quy mô lớn.
- Trạng thái phiên phân tán trên nhiều dịch vụ, khiến việc xử lý sự cố trở nên khó khăn.
Magic links giải quyết vấn đề này bằng cách giản lược quy trình đăng nhập thành ba bước đơn giản ở phía server:
- Phát hành một liên kết
- Xác thực nó
- Tạo một phiên
Chỉ vậy thôi. Không có mật khẩu, không có SMS, không có token kém an toàn trong trình duyệt.
Bước 1: Gửi một liên kết
Trong Next.js, bạn cần tạo một route API /api/send-magic-link để nhận email và gọi Scalekit để tạo một yêu cầu không mật khẩu:
typescript
// /api/send-magic-link/route.ts
export async function POST(req: NextRequest) {
const { email } = await req.json()
const resp = await client.passwordless.createAuthRequest({
email,
passwordlessType: 'MAGIC_LINK',
expiresIn: 600,
})
const res = NextResponse.json({ ok: true })
res.cookies.set('sk_auth_request_id', resp.authRequestId, { httpOnly: true, secure: true })
return res
}
Cookie (sk_auth_request_id) kết nối liên kết trở lại nguồn gốc này để các khách hàng không thể giả mạo nó.
Bước 2: Xác thực
Khi người dùng nhấp vào liên kết, route /api/verify-magic-link của bạn sẽ kiểm tra token:
typescript
export async function POST(req: NextRequest) {
const { link_token, auth_request_id } = await req.json()
const result = await client.passwordless.verifyAuthRequest({ linkToken: link_token, authRequestId: auth_request_id })
return NextResponse.json({ email: result.email })
}
Bước 3: Tạo một phiên
Cuối cùng, phát hành một JWT có thời gian sống ngắn được lưu trữ trong cookie HttpOnly:
typescript
const token = jwt.sign({ email }, process.env.SESSION_JWT_SECRET, { expiresIn: '30m' })
res.cookies.set('sk_session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
})
Middleware giờ đây có thể đảm bảo rằng bất kỳ route bảo vệ nào cũng yêu cầu một JWT hợp lệ trước khi chạy.
Tại sao cách này có hiệu quả
Hướng server: Tất cả logic nhạy cảm (tạo liên kết, xác thực, tạo phiên) đều diễn ra ở backend.
Độc lập với client: cùng một API hoạt động cho ứng dụng web, ứng dụng di động, thậm chí cả công cụ CLI.
Có thể quan sát: nhật ký cho bạn biết ai đã yêu cầu, ai đã xác thực và khi nào phiên được phát hành.
Thiết kế này cắt giảm các thành phần di chuyển và biến quy trình đăng nhập thành thứ bạn có thể tin tưởng và giám sát.
Hướng dẫn đầy đủ
Bài viết này chỉ chạm đến bề mặt. Hướng dẫn đầy đủ sẽ bao gồm:
- Giới hạn tần suất (ngăn chặn bot spam gửi/xác thực).
- Các header bảo mật (CSP, HSTS, v.v.).
- Nhật ký có cấu trúc với ID tương quan.
- Lưu trữ Redis/SQL cho sản xuất.
👉 Đọc hướng dẫn chi tiết từng bước tại đây.
Đến lượt bạn
Bạn đã triển khai đăng nhập không mật khẩu trong các dự án của mình chưa? Magic links, OTP, hay một cái gì khác?
Hãy chia sẻ thiết lập của bạn, bài học hoặc những khó khăn trong phần bình luận: Các nhà phát triển khác (và tương lai của bạn) sẽ cảm ơn bạn.