Sveltekit + Lucia auth (Google OAuth) + MongoDB

ما بر اساس آموزش لوسیا خلاصه و ترجمه آن به اسپانیایی خواهیم بود.
شما می توانید کد کامل را در GitHub من مشاهده کنید
جلسات اساسی با MongoDB
این جلسات به دولت اجازه می دهد تا روی سرور حفظ شود ، به خصوص برای تأیید اعتبار و شناسایی کاربر مفید است. به هر جلسه یک شناسه منحصر به فرد اختصاص داده می شود که در سرور ذخیره می شود و به عنوان نشانه استفاده می شود. مشتری آن را در برنامه های بعدی ارسال می کند تا آنها را با جلسه و کاربر خود مرتبط کند.
شناسه جلسه را می توان در کوکی ها یا ذخیره محلی مرورگر ذخیره کرد ، اما توصیه می شود از کوکی ها استفاده کنید زیرا در برابر XSS ایمن تر هستند و کنترل آنها آسان تر است.
اتصال MongoDB
ما می توانیم از MongoDB در ابر با اطلس استفاده کنیم
پس از ثبت نام ، می توانید زنجیره اتصال خود را با کلیک روی “اتصال” دریافت کنید
ما می توانیم گزینه VS Code را انتخاب کنیم
ما آن را در یک پرونده نگه می داریم .env
به عنوان MONGODB_URI
و ما اتصال را به عنوان “مشتری” از آن صادر می کنیم src/lib/server/db.js
import { MongoClient } from 'mongodb';
import { MONGODB_URI } from '$env/static/private';
const uri = MONGODB_URI;
let client;
client = new MongoClient(uri);
export default client;
تولید شناسه جلسه
کاربران به جای شناسه مستقیم ، از یک نشست جلسه مرتبط با یک جلسه استفاده می کنند. شناسه جلسه یک Hash Sha-256 Token خواهد بود. از آنجا که SHA-256 یک عملکرد غیر قابل برگشت است ، حتی اگر یک فیلتراسیون پایگاه داده وجود داشته باشد ، یک مهاجم قادر به دستیابی به نشانه های معتبر نخواهد بود.
ما از OSLO برای عملیات مختلف استفاده خواهیم کرد.npm i @oslojs/encoding @oslojs/crypto
ایجاد API
نشانه جلسه زنجیره ای از حداقل 20 به طور تصادفی توسط Base32 کدگذاری خواهد شد
// src/lib/server/session.js
import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";
// ...
export function generateSessionToken(): string {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
const token = encodeBase32LowerCaseNoPadding(bytes);
return token;
}
شناسه جلسه یک Hash Sha-256 Token خواهد بود و 30 روز انقضا خواهد داشت.
import client from '$lib/server/db.js';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
// ...
export async function createSession(token, userId) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)
};
try {
const mongoClient = await client.connect();
const database = mongoClient.db('adminhood');
const sessions = database.collection('sessions');
await sessions.insertOne(session);
return session;
} catch (error) {
console.log(error);
}
}
اعتبار سنجی جلسه (2 مرحله):
- آیا جلسه پایگاه داده وجود دارد؟
- هنوز منقضی نشده است؟
اگر جلسه نزدیک به انقضا باشد ، اعتبار آن تمدید می شود. که جلسات را در حال استفاده نگه می دارد و موارد غیرفعال را از بین می برد.
ما هم جلسه و هم کاربر مرتبط با آن شناسه را برمی گردانیم.
import client from '$lib/server/db.js';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export async function validateSessionToken(token) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
try {
const mongoClient = await client.connect();
const db = mongoClient.db('adminhood');
const sessions = db.collection('sessions');
const session = await sessions.findOne({ id: sessionId });
if (session === null) {
return { session: null, user: null };
}
const user = await db.collection('users').findOne({ _id: session.userId });
if (Date.now() >= session.expiresAt.getTime()) {
await sessions.deleteOne({ id: sessionId });
return { session: null, user: null };
}
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
const filter = { id: session.id };
const updateDoc = {
$set: {
expiresAt: session.expiresAt
}
};
await sessions.updateOne(filter, updateDoc);
}
return { session, user };
} catch (error) {
console.log(error);
}
}
سرانجام ، ما جلسه را به سادگی با پاک کردن آن از MongoDB باطل می کنیم.
import client from '$lib/server/db.js';
export async function invalidateSession(sessionId) {
try {
const mongoClient = await client.connect();
const db = mongoClient.db('adminhood');
const sessions = db.collection('sessions');
await sessions.deleteOne({ id: sessionId });
} catch (error) {
console.log(error);
}
}
کوکی ها
محافظت در برابر جعل کاربرد در سایت های متقاطع (CSRF) با کوکی ها اجباری است.
Sveltekit شامل محافظت اولیه CSRF به طور پیش فرض با استفاده از مبدا است.
ویژگی های توصیه شده برای کوکی های جلسه:
- httponly: فقط از سرور قابل دسترسی است
- samesite = Lax: برای سایت های بحرانی از سخت استفاده کنید
- ایمن: آنها فقط توسط HTTPS ارسال می شوند (در LocalHost حذف می شوند)
- حداکثر سن یا منقضی می شود: باید تعریف کرد که کوکی همچنان ادامه دارد
- مسیر =/: کوکی ها از همه مسیرها قابل دسترسی هستند
Sveltekit به طور خودکار “ایمن” را در تولید اضافه می کند.
// src/lib/server/session.js
// ...
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void {
event.cookies.set("session", token, {
httpOnly: true,
sameSite: "lax",
expires: expiresAt,
path: "/"
});
}
export function deleteSessionTokenCookie(event: RequestEvent): void {
event.cookies.set("session", "", {
httpOnly: true,
sameSite: "lax",
maxAge: 0,
path: "/"
});
}
اعتبار سنجی جلسه
نشانه های جلسه را می توان با عملکرد معتبر () معتبر () تأیید کرد. اگر جلسه نامعتبر است ، کوکی را از جلسه حذف می کند.
مهم است که یک کوکی جلسه جدید پس از اعتبارسنجی ایجاد کنید تا زمان زندگی خود را افزایش دهید.
ما جلسه را در قلاب دسته تأیید می کنیم و زمینه تأیید اعتبار فعلی را به هر مسیر منتقل می کنیم.
import {
validateSessionToken,
setSessionTokenCookie,
deleteSessionTokenCookie
} from '$lib/server/session';
export const handle = async ({ event, resolve }) => {
const token = event.cookies.get('session') ?? null;
if (token === null) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await validateSessionToken(token);
if (session !== null) {
setSessionTokenCookie(event, token, session.expiresAt);
} else {
deleteSessionTokenCookie(event);
}
event.locals.session = session;
event.locals.user = user;
return resolve(event);
};
هم کاربر فعلی و هم جلسه در توابع بار ، اقدامات و نقاط پایانی در دسترس خواهد بود.
یک برنامه OAUTH ایجاد کنید
یک مشتری OAUTH را در Google Cloud Console ایجاد کنید. URI تغییر مسیر را به صورت پیکربندی کنید http://localhost:5173/api/oauth/google/callback
بشر شناسه مشتری و راز را در پرونده ذخیره کنید .env
بشر
# .env
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
قطب شمال را نصب کنید
npm install arctic
تأمین کننده Google را با شناسه مشتری ، مشتری راز و تغییر مسیر URI آغاز می کند.
// src/lib/server/google-oauth.js
import { Google } from 'arctic';
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } from '$env/static/private';
export const google = new Google(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
'http://localhost:5173/api/oauth/google/callback'
);
پاگینا وارد سیستم
کرای routes/login/+page.svelte
و دکمه ای را که پیوند دارد اضافه کنید /login/google
بشر
URL مجوز ایجاد کنید
یک مسیر API ایجاد کنید routes/api/oauth/google/+server.js
بشر این یک کد حالت و معتبر ایجاد می کند و URL مجوز را با Scopes ایجاد می کند openid
حرف profile
بشر تأیید کننده حالت و کد را ذخیره کرده و دوباره به صفحه ورود به سیستم Google تغییر دهید.
import { generateState, generateCodeVerifier } from 'arctic';
import { google } from '$lib/server/google-oauth';
export async function GET(event) {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile']);
event.cookies.set('google_oauth_state', state, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax'
});
event.cookies.set('google_code_verifier', codeVerifier, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax'
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}
پاسخ به تماس با اعتبار
یک مسیر API ایجاد کنید routes/api/oauth/google/callback/+server.js
برای رسیدگی به پاسخ به تماس تأیید کنید که دولت همزمان با ذخیره سازی است ، کد مجوز را تأیید کرده و رمزگذار کننده را نگه می دارد. اگر Scopes OpenID و Profile را درج کرده اید ، Google یک شناسه شناسه را با مشخصات کاربر باز می گرداند. بررسی کنید که آیا کاربر از قبل ثبت شده است یا خیر. اگر نه ، آن را باور کنید. در آخر ، یک جلسه ایجاد کنید و کوکی را برای تکمیل احراز هویت پیکربندی کنید
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/session';
import { google } from '$lib/server/google-oauth';
import { decodeIdToken } from 'arctic';
import { getUserFromGoogleId, createUser } from '$lib/server/user.js';
export async function GET(event) {
const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state');
const storedState = event.cookies.get('google_oauth_state') ?? null;
const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400
});
}
if (state !== storedState) {
return new Response(null, {
status: 400
});
}
let tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
console.log(e);
return new Response(null, {
status: 400
});
}
const claims = decodeIdToken(tokens.idToken());
const googleUserId = claims.sub;
const name = claims.name;
const existingUser = await getUserFromGoogleId(googleUserId);
if (existingUser !== null) {
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, existingUser._id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/profile'
}
});
}
const user = await createUser(googleUserId, name);
const sessionToken = generateSessionToken();
const session = await createSession(sessionToken, user._id);
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/profile'
}
});
}
کاربر فعلی را دریافت کنید
می توانید جلسه و کاربر فعلی را از آن دریافت کنید locals
// routes/profile/+page.server.js
import { redirect } from '@sveltejs/kit';
export const load = async ({ locals }) => {
if (!locals.user) {
return redirect(302, '/login');
}
const username = locals.user.name;
return { username };
};
از سیستم خارج شدن
Sing Out را بی اعتبار در جلسه فعلی اجرا کنید و کوکی های مرتبط را حذف کنید.
import { fail, redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/session';
export const actions = {
signOut: async (event) => {
if (event.locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, '/login');
}
};
نتیجه نهایی
سرانجام می توانیم نام کاربر را در صفحه پروفایل مشاهده کنیم.
// routes/profile/+page.svelte
<script>
let { data } = $props();
</script>
<p>hello {data.username}</p>
<form method="post" use:enhance action="/?/signOut">
<button>Logout</button>
</form>