如何实现cookie认证| SvelteKit & MongoDB

How to implement cookie authentication | SvelteKit & MongoDB

问题保持原样 - 如何在 SvelteKit 和 MongoDB 应用程序中实现 cookie 身份验证?意思是如何正确使用挂钩、端点、建立数据库连接并将其显示在样板项目中。

SvelteKit 项目初始化后


#1 安装额外的依赖项

npm install config cookie uuid string-hash mongodb
  • 我更喜欢 config 而不是 vite 的 .env 变量,因为它存在所有泄漏和问题
  • cookie用于正确设置cookie
  • uuid用于生成复杂的cookie ID
  • string-hash 是一种简单而安全的哈希算法,用于存储在您的数据库中的密码
  • mongodb 用于与您的数据库建立连接

#2 设置 config

在根目录中,创建一个名为 config 的文件夹。在其中,创建一个名为 default.json.

的文件

config/default.json

{
    "mongoURI": "<yourMongoURI>",
    "mongoDB": "<yourDatabaseName>"
}

#3 设置基础数据库连接代码

src 中创建 lib 文件夹。在其中,创建 db.js 文件。

src/lib/db.js

import { MongoClient } from 'mongodb';
import config from 'config';

export const MONGODB_URI = config.get('mongoURI');
export const MONGODB_DB = config.get('mongoDB');

if (!MONGODB_URI) {
    throw new Error('Please define the mongoURI property inside config/default.json');
}

if (!MONGODB_DB) {
    throw new Error('Please define the mongoDB property inside config/default.json');
}

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let cached = global.mongo;

if (!cached) {
    cached = global.mongo = { conn: null, promise: null };
}

export const connectToDatabase = async () => {
    if (cached.conn) {
        return cached.conn;
    }

    if (!cached.promise) {
        const opts = {
            useNewUrlParser: true,
            useUnifiedTopology: true
        };

        cached.promise = MongoClient.connect(MONGODB_URI, opts).then((client) => {
            return {
                client,
                db: client.db(MONGODB_DB)
            };
        });
    }
    cached.conn = await cached.promise;
    return cached.conn;
}

代码取自 next.js 实现 MongoDB 连接建立并修改为使用 config .env.

#4 在 src

中创建 hooks.js 文件

src/hooks.js

import * as cookie from 'cookie';
import { connectToDatabase } from '$lib/db';

// Sets context in endpoints
// Try console logging context in your endpoints' HTTP methods to understand the structure
export const handle = async ({ request, resolve }) => {
    // Connecting to DB
    // All database code can only run inside async functions as it uses await
    const dbConnection = await connectToDatabase();
    const db = dbConnection.db;

    // Getting cookies from request headers - all requests have cookies on them
    const cookies = cookie.parse(request.headers.cookie || '');
    request.locals.user = cookies;

    // If there are no cookies, the user is not authenticated
    if (!cookies.session_id) {
        request.locals.user.authenticated = false;
    }

    // Searching DB for the user with the right cookie
    // All database code can only run inside async functions as it uses await
    const userSession = await db.collection('cookies').findOne({ cookieId: cookies.session_id });

    // If there is that user, authenticate him and pass his email to context
    if (userSession) {
        request.locals.user.authenticated = true;
        request.locals.user.email = userSession.email;
    } else {
        request.locals.user.authenticated = false;
    }

    const response = await resolve(request);

    return {
        ...response,
        headers: {
            ...response.headers
            // You can add custom headers here
            // 'x-custom-header': 'potato'
        }
    };
};

// Sets session on client-side
// try console logging session in routes' load({ session }) functions
export const getSession = async (request) => {
    // Pass cookie with authenticated & email properties to session
    return request.locals.user
        ? {
                user: {
                    authenticated: true,
                    email: request.locals.user.email
                }
          }
        : {};
};

Hooks 根据 cookie 对用户进行身份验证,并将所需的变量(在本例中是用户的电子邮件等)传递给上下文和会话。

#5 在 auth 文件夹中创建 register.js & login.js 端点

src/routes/auth/register.js

import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';

export const post = async ({ body }) => {
    // Connecting to DB
    // All database code can only run inside async functions as it uses await
    const dbConnection = await connectToDatabase();
    const db = dbConnection.db;

    // Is there a user with such an email?
    const user = await db.collection('testUsers').findOne({ email: body.email });

    // If there is, either send status 409 Conflict and inform the user that their email is already taken
    // or send status 202 or 204 and tell them to double-check on their credentials and try again - it is considered more secure
    if (user) {
        return {
            status: 409,
            body: {
                message: 'User with that email already exists'
            }
        };
    }

    // Add user to DB
    // All database code can only run inside async functions as it uses await
    await db.collection('testUsers').insertOne({
        name: body.name,
        email: body.email,
        password: stringHash(body.password)
    });

    // Add cookie with user's email to DB
    // All database code can only run inside async functions as it uses await
    const cookieId = uuidv4();
    await db.collection('cookies').insertOne({
        cookieId,
        email: body.email
    });

    // Set cookie
    // If you want cookies to be passed alongside user when they redirect to another website using a link, change sameSite to 'lax'
    // If you don't want cookies to be valid everywhere in your app, modify the path property accordingly
    const headers = {
        'Set-Cookie': cookie.serialize('session_id', cookieId, {
            httpOnly: true,
            maxAge: 60 * 60 * 24 * 7,
            sameSite: 'strict',
            path: '/'
        })
    };

    return {
        status: 200,
        headers,
        body: {
            message: 'Success'
        }
    };
};

如果您想更进一步,请不要忘记使用 Mongoose!

创建 Schemas

src/routes/auth/login.js

import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';

export const post = async ({ body }) => {
    const dbConnection = await connectToDatabase();
    const db = dbConnection.db;

    const user = await db.collection('testUsers').findOne({ email: body.email });

    if (!user) {
        return {
            status: 401,
            body: {
                message: 'Incorrect email or password'
            }
        };
    }

    if (user.password !== stringHash(body.password)) {
        return {
            status: 401,
            body: {
                message: 'Unauthorized'
            }
        };
    }

    const cookieId = uuidv4();

    // Look for existing email to avoid duplicate entries
    const duplicateUser = await db.collection('cookies').findOne({ email: body.email });

    // If there is user with cookie, update the cookie, otherwise create a new DB entry
    if (duplicateUser) {
        await db.collection('cookies').updateOne({ email: body.email }, { $set: { cookieId } });
    } else {
        await db.collection('cookies').insertOne({
            cookieId,
            email: body.email
        });
    }

    // Set cookie
    const headers = {
        'Set-Cookie': cookie.serialize('session_id', cookieId, {
            httpOnly: true,
            maxAge: 60 * 60 * 24 * 7,
            sameSite: 'strict',
            path: '/'
        })
    };

    return {
        status: 200,
        headers,
        body: {
            message: 'Success'
        }
    };
};

#6 创建 Register.svelteLogin.svelte 组件

src/lib/Register.svelte

<script>
    import { createEventDispatcher } from 'svelte';

    // Dispatcher for future usage in /index.svelte
    const dispatch = createEventDispatcher();

    // Variables bound to respective inputs via bind:value
    let email;
    let password;
    let name;
    let error;

    const register = async () => {
        // Reset error from previous failed attempts
        error = undefined;

        try {
            // POST method to src/routes/auth/register.js endpoint
            const res = await fetch('/auth/register', {
                method: 'POST',
                body: JSON.stringify({
                    email,
                    password,
                    name
                }),
                headers: {
                    'Content-Type': 'application/json'
                }
            });

            if (res.ok) {
                dispatch('success');
            } else {
                error = 'An error occured';
            }
        } catch (err) {
            console.log(err);
            error = 'An error occured';
        }
    };
</script>

<h1>Register</h1>
<input type="text" name="name" placeholder="Enter your name" bind:value={name} />
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
    <p>{error}</p>
{/if}
<button on:click={register}>Register</button>

src/lib/Login.svelte

<script>
    import { createEventDispatcher } from 'svelte';

    // Dispatcher for future usage in /index.svelte
    const dispatch = createEventDispatcher();

    // Variables bound to respective inputs via bind:value
    let email;
    let password;
    let error;

    const login = async () => {
        // Reset error from previous failed attempts
        error = undefined;

        // POST method to src/routes/auth/login.js endpoint
        try {
            const res = await fetch('/auth/login', {
                method: 'POST',
                body: JSON.stringify({
                    email,
                    password
                }),
                headers: {
                    'Content-Type': 'application/json'
                }
            });

            if (res.ok) {
                dispatch('success');
            } else {
                error = 'An error occured';
            }
        } catch (err) {
            console.log(err);
            error = 'An error occured';
        }
    };
</script>

<h1>Login</h1>
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
    <p>{error}</p>
{/if}
<button on:click={login}>Login</button>

#7 更新src/routes/index.svelte

src/routes/index.svelte

<script>
    import Login from '$lib/Login.svelte';
    import Register from '$lib/Register.svelte';
    import { goto } from '$app/navigation';

    // Redirection to /profile
    function redirectToProfile() {
        goto('/profile');
    }
</script>

<main>
    <h1>Auth with cookies</h1>

    <!-- on:success listens for dispatched 'success' events -->
    <Login on:success={redirectToProfile} />
    <Register on:success={redirectToProfile} />
</main>

#8 在 profile 文件夹中创建 index.svelte

src/routes/profile/index.svelte

<script context="module">
    export async function load({ session }) {
        if (!session.user.authenticated) {
            return {
                status: 302,
                redirect: '/auth/unauthorized'
            };
        }

        return {
            props: {
                email: session.user.email
            }
        };
    }
</script>

<script>
    import { onMount } from 'svelte';

    export let email;
    let name;

    onMount(async () => {
        const res = await fetch('/user');
        const user = await res.json();
        name = user.name;
    });
</script>

<h1>Profile</h1>
<p>Hello {name} you are logged in with the email {email}</p>

注意我们在hooks.js设置的sessionconsole.log() 以便更好地理解其结构。我不会实施 /auth/unauthorized 路线,所以请注意。

#9 在 user 文件夹中创建 index.js 端点

src/routes/user/index.js

import { connectToDatabase } from '$lib/db';

export const get = async (context) => {
    // Connecting to DB
    // All database code can only run inside async functions as it uses await
    const dbConnection = await connectToDatabase();
    const db = dbConnection.db;

    // Checking for auth coming from hooks' handle({ request, resolve })
    if (!context.locals.user.authenticated) {
        return {
            status: 401,
            body: {
                message: 'Unauthorized'
            }
        };
    }

    const user = await db.collection('testUsers').findOne({ email: context.locals.user.email });

    if (!user) {
        return {
            status: 404,
            body: {
                message: 'User not found'
            }
        };
    }

    // Find a proper way in findOne(), I've run out of gas ;)
    delete user.password;

    return {
        status: 200,
        body: user
    };
};

最后的想法

几乎有 none 个关于 SvelteKit 的教程,我肯定会发现本指南对我未来的项目很有用。如果您发现错误或发现改进,请随时告诉我,以便我改进本指南;)

非常感谢 Brayden Girard 为本指南开辟了先例!

https://www.youtube.com/channel/UCGl66MHcjMDJyIPZkuKULSQ

编码愉快!