برنامه نویسی

Sveltekit + Lucia auth (Google OAuth) + MongoDB

ما بر اساس آموزش لوسیا خلاصه و ترجمه آن به اسپانیایی خواهیم بود.
شما می توانید کد کامل را در GitHub من مشاهده کنید

جلسات اساسی با MongoDB

این جلسات به دولت اجازه می دهد تا روی سرور حفظ شود ، به خصوص برای تأیید اعتبار و شناسایی کاربر مفید است. به هر جلسه یک شناسه منحصر به فرد اختصاص داده می شود که در سرور ذخیره می شود و به عنوان نشانه استفاده می شود. مشتری آن را در برنامه های بعدی ارسال می کند تا آنها را با جلسه و کاربر خود مرتبط کند.

شناسه جلسه را می توان در کوکی ها یا ذخیره محلی مرورگر ذخیره کرد ، اما توصیه می شود از کوکی ها استفاده کنید زیرا در برابر XSS ایمن تر هستند و کنترل آنها آسان تر است.

اتصال MongoDB

ما می توانیم از MongoDB در ابر با اطلس استفاده کنیم
پس از ثبت نام ، می توانید زنجیره اتصال خود را با کلیک روی “اتصال” دریافت کنید

اطلس mongodb
ما می توانیم گزینه VS Code را انتخاب کنیم
گزینه های زنجیره اتصال MongoDB
ما آن را در یک پرونده نگه می داریم .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 مرحله):

  1. آیا جلسه پایگاه داده وجود دارد؟
  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>

حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا