【问题标题】:How to implement cookie authentication | SvelteKit & MongoDB如何实现cookie认证| SvelteKit 和 MongoDB
【发布时间】:2021-09-06 02:42:29
【问题描述】:

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

【问题讨论】:

    标签: javascript mongodb authentication cookies sveltekit


    【解决方案1】:

    SvelteKit 项目初始化后


    #1 安装额外的依赖项

    npm install config cookie uuid string-hash mongodb
    
    • 我更喜欢 config 而不是 vite 的 .env 变量,因为它存在所有泄漏和问题
    • cookie 用于正确设置cookies
    • 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.jslogin.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
        };
    };
    

    最后的想法

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

    非常感谢 Brayden Girard 为本指南提供了先例!

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

    编码愉快!

    【讨论】:

    • 对于所有这些工作,公共 github 存储库不是对所有人都有帮助吗?
    • Public repo done. Click here... 请注意,您必须添加自己的 Mongodb Atlas 登录信息...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-30
    • 1970-01-01
    • 2020-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-13
    相关资源
    最近更新 更多