Что мне нужно знать для этого урока?

Начиная

На старт, внимание, марш! SvelteKit предоставляет приложение командной строки, которое мы можем использовать для запуска нового проекта, интерфейс командной строки задаст нам кучу вопросов, давайте пройдемся по ним. В вашем терминале создайте новую папку для этого проекта. Давайте назовем проект authy или любым другим именем, которое вы предпочитаете:

mkdir authy
cd authy

Используйте функцию npm init для создания проекта SvelteKit.

npm init svelte@next

Пройдемся по вопросам:

create-svelte version 2.0.0-next.73

Welcome to SvelteKit!

This is beta software; expect bugs and missing features.

If you encounter a problem, open an issue on https://github.com/sveltejs/kit/issues if none exists already.

? Directory not empty. Continue? › (y/N) y
? Which Svelte app template? › - Use arrow-keys. Return to submit.
[Choose Skeleton project]
? Use TypeScript? › No / Yes -> No
? Add ESLint for code linting? › No / Yes -> No
? Add Prettier for code formatting? › No / Yes -> No

✨ Yay! We just setup SvelteKit

Create Github OAuth Application

Go to https://github.com/settings/applications/new in your browser and create a new application called authy with a homepage of http://localhost:3000 and a callback url of http://localhost:3000/callback

Нажмите Register application

Вы будете перенаправлены на страницу, которая выглядит примерно так:

В каталоге вашего проекта создайте файл .env и в этом файле возьмите идентификатор клиента со страницы github и добавьте в файл .env как VITE_CLIENT_ID, затем щелкните Generate a new client secret, затем скопируйте секрет и добавьте его в файл .env как VITE_CLIENT_SECRET

VITE_CLIENT_ID=XXXXXXX
VITE_CLIENT_SECRET=XXXXXXXXXX

Сохраните и закройте файл .env.

🎉 вы создали приложение Github OAuth! Теперь мы можем подключить приложение OAuth к нашему проекту, чтобы создать безопасный рабочий процесс.

Настройте кнопку входа

Настроив логин, нам нужно будет добавить кнопку в src/routes/index.svelte, а затем создать конечную точку Sveltekit, эта конечная точка будет выполнять перенаправление на Github для аутентификации.

источник/маршруты/index.svelte

<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<a href="/login">
  <button>Login using Github</button>
</a>

Создайте конечную точку /login

SvelteKit использует файловую систему не только для определения маршрутов страниц, SvelteKit также использует файловую систему для определения конечных точек. В папке маршрутов или любой дочерней папке в папке маршрутов, если файл заканчивается расширением .svelte, это страница, если файл заканчивается расширением .js, это конечная точка. Используя функцию экспорта esm, вы можете сопоставить http глаголов с обработчиками javascript. В нашем случае мы хотим создать файл src/routes/login.js и сопоставить HTTP-глагол GET с экспортированной функцией get.

export async function get(req) {
  return {
    body: 'Hello'
  }
}

Когда обработчик get определен для src/routes/login.js, он будет принимать объект Request в качестве входных данных и возвращать объект Response в качестве выходных данных. Каждый из этих типов объектов определен как часть спецификации fetch:





В документации SvelteKit вы можете увидеть, что они определены как типы typescript:

Документы SvelteKit

Полная документация для SvelteKit

type Headers = Record<string, string>;

type Request<Locals = Record<string, any>, Body = unknown> = {
	method: string;
	host: string;
	headers: Headers;
	path: string;
	params: Record<string, string>;
	query: URLSearchParams;
	rawBody: string | Uint8Array;
	body: ParameterizedBody<Body>;
	locals: Locals; // populated by hooks handle
};

type EndpointOutput = {
	status?: number;
	headers?: Headers;
	body?: string | Uint8Array | JSONValue;
};

type RequestHandler<Locals = Record<string, any>> = (
	request: Request<Locals>
) => void | EndpointOutput | Promise<EndpointOutput>;

Итак, чего мы хотим добиться здесь?

Мы хотим перенаправить запрос на конечную точку аутентификации github с нашим CLIENT_ID.

Чтобы сервер ответил клиенту директивой перенаправления, нам нужно вернуть код состояния 3xx, давайте использовать 302, и нам нужно указать location в заголовке. Это место должно быть местом авторизации github oauth. https://github.com/login/oauth/authorize

источник/маршруты/login.js

const ghAuthURL = 'https://github.com/login/oauth/authorize'
const clientId = import.meta.env.VITE_CLIENT_ID

export async function get(req) {
  const sessionId = '1234'
  return {
    status: 302,
    headers: {
      location: `${ghAuthURL}?client_id=${clientId}&state=${sessionId}`
    }
  }
}

Обработка обратного вызова

Когда Github разрешает или не разрешает, Github нужен способ сообщить об этом нашему приложению. Вот почему мы дали Github URL-адрес callback. Этот URL-адрес является конечной точкой, которую нам нужно создать следующей. Создайте новый файл src/routes/callback.js и укажите в нем обработчик get.

источник/маршруты/callback.js

export async function get(req) {
  return {
    body: 'callback'
  }
}

Когда мы перенаправляем пользователя на Github, Github попросит его войти в систему, а затем авторизует наше приложение. Если пользователь решит авторизовать приложение, Github перенаправит браузер на нашу конечную точку обратного вызова, передав ей параметр запроса code. Мы хотим использовать этот параметр запроса code, чтобы получить access_token для авторизованного пользователя. Затем мы будем использовать access_token для получения информации о пользователе из Github.

Мы можем использовать метод query.get для объекта request, чтобы получить значение code. Мы можем использовать функцию fetch из библиотеки node-fetch, чтобы сделать наш запрос.

yarn add node-fetch

Получить токен доступа

источник/маршруты/callback.js

import fetch from 'node-fetch'
const tokenURL = 'https://github.com/login/oauth/access_token'

const clientId = import.meta.env.VITE_CLIENT_ID
const secret = import.meta.env.VITE_CLIENT_SECRET


export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)


  return {
    body: JSON.stringify(accessToken)
  }
}

function getAccessToken(code) {
  return fetch(tokenURL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({
      client_id: clientId,
      client_secret: secret,
      code
    })
  }).then(r => r.json())
    .then(r => r.access_token)
}

Получить информацию о пользователе

const userURL = 'https://api.github.com/user'

function getUser(accessToken) {
  return fetch(userURL, {
    headers: {
      Accept: 'application/json',
      Authorization: `Bearer ${accessToken}`
    }
  })
    .then(r => r.json())

}

изменить функцию получения

export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)
  const user = await getUser(accessToken)

  return {
    body: JSON.stringify(user)
  }
}

В нашем обработчике callback теперь мы должны видеть пользовательский объект! Отличная работа, у вас есть счастливый путь Github OAuth, работающий в SvelteKit. Но мы еще не закончили.

Установка файла cookie для сеанса пользователя

Нам нужно поручить SvelteKit писать cookie только для http. Этот файл cookie сохранит нашу пользовательскую сессию.

крючки

Нам нужно создать файл src/hooks.js, этот файл будет содержать функцию handle, которая позволит нам читать файлы cookie и записывать файлы cookie, так как он оборачивает входящий запрос для каждого запроса.

import cookie from 'cookie'

export async function handle({request, resolve}) {
  const cookies = cookie.parse(request.headers.cookie || '')
  
  // code here happends before the endpoint or page is called
  
  const response = await resolve(request)
  
  // code here happens after the endpoint or page is called
  
  return response
}

После функции разрешения мы хотим проверить и посмотреть, был ли объект locals запроса изменен с помощью ключа user. Если это так, мы хотим установить cookie со значением.

import cookie from 'cookie'

export async function handle({ request, resolve }) {
  const cookies = cookie.parse(request.headers.cookie || '')

  // code here happends before the endpoint or page is called

  const response = await resolve(request)

  // code here happens after the endpoint or page is called
  
  response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
  
  return response
}

Установив куки с HttpOnly, это гарантирует, что он может быть записан только сервером. Файл cookie будет храниться в браузере и оставаться там до тех пор, пока мы его не очистим. Поэтому, если мы хотим получить доступ к информации о файлах cookie в любой из наших обработчиков страниц или конечных точек, нам нужно проанализировать файл cookie и установить значение для объекта request.locals.

import cookie from 'cookie'

export async function handle({ request, resolve }) {
  const cookies = cookie.parse(request.headers.cookie || '')

  // code here happends before the endpoint or page is called
  request.locals.user = cookies.user
  console.log({ user: request.locals.user })

  const response = await resolve(request)

  // code here happens after the endpoint or page is called
  response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
  
  return response
}

установите значение request.locals.user в callback.js

В src/routes/callback.js нам нужно установить значение request.locals.user с идентификатором user.login, который гарантированно будет уникальным и отлично работает для этой демонстрации.

export async function get(req) {
  const code = req.query.get('code')
  const accessToken = await getAccessToken(code)
  const user = await getUser(accessToken)
  
  // this mutates the locals object on the request
  // and will be read by the hooks/handle function
  // after the resolve
  req.locals.user = user.login

  return {
    status: 302,
    headers: {
      location: '/'
    }
  }
}

Отправить информацию о сеансе в SvelteKit Load

В файле src/hooks.js мы можем настроить другую функцию с именем getSession. Эта функция позволит нам установить объект сеанса, который будет получен каждой функцией load на компоненте страницы SvelteKit.

export async function getSession(request) {
  return {
    user: request.locals.user
  }
}

Получить сеанс в теге модуля скрипта

В наш компонент страницы src/routes/index.js мы добавим два тега скрипта, первый тег скрипта будет иметь контекст module и будет работать на сервере, второй тег скрипта будет содержать нашу логику на стороне клиента для нашего компонента Svelte.

<script context="module">
  export async function load({ session }) {
    
    return {
      props: {
        user: session.user,
      },
    };
  }
</script>
<script>
  export let user
</script>

<h1>Welcome to SvelteKit</h1>
<p>
  Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>
{#if user}
<h2>Welcome {user}</h2>
<a href="/logout">
  <button>Logout</button>
</a>
{:else}
<a href="/login">
  <button>Login using Github</button>
</a>
{/if}

Мы используем оба тега script для передачи значения сеанса из функции load в клиентский скрипт. Это позволяет нам изменять представление в зависимости от того, присутствует ли пользователь в сеансе. Мы можем показать имя пользователя на экране.

Сладкий! ⚡️

Выйти

Создайте новый файл с именем src/routes/logout.js, в этом файле мы создадим функцию обработчика конечной точки get. В этой функции мы хотим установить пользователя равным нулю и перенаправить запрос обратно на домашнюю страницу.

export async function get(req) {
  req.locals.user = null
  console.log(req.locals.user)
  return {
    status: 302,
    headers: {
      location: '/'
    }
  }
}

Теперь, когда вы нажимаете кнопку выхода, для пользователя устанавливается пустая строка, а не user.login.

Защита страниц и конечных точек

Теперь, когда у вас есть аутентификация, работающая с Github OAuth, вы можете захотеть защитить некоторые страницы и конечные точки. Вы можете выполнить тест на каждой странице, которую вы хотите защитить, или вы можете использовать компонент __layout.svelte и создать принятый список путей, которые вы хотели бы защитить.

источник/маршруты/__layout.js

<script context="module">
export async function load({page, session}) {
  if (/^\/admin\/(.*)/.test(page.path) && session.user === '') {
    return { redirect: '/', status: 302 }
  }
  return { props: {} }
}
</script>

<slot />

В этом примере мы защищаем все страницы, путь которых начинается с /admin/*.

Резюме

Это конец этого маленького путешествия, мой друг, это было приятное путешествие, надеюсь, вы больше смеялись, чем плакали, и узнали что-то о SvelteKit. Биты маршрутизации SvelteKit просты, когда вы можете понять, как они работают, без особого волшебства, и, устанавливая файлы cookie только для http, вы можете создавать простые долгоживущие сеансы для своих приложений. Помните, что информация, хранящаяся в файле cookie, не зашифрована, поэтому не храните никаких секретов, используйте кеш или базу данных, если вам нужно собрать вместе еще некоторые данные, относящиеся к сеансу/пользователю.

Демонстрационный репозиторий: https://github.com/hyper63/tutorial-sveltekit-authentication

При поддержке гипер

Если вы создаете приложение и хотите, чтобы ваше приложение было:

  • Легко поддерживать!
  • Легко проверить!
  • Без непреднамеренного технического долга

Вы должны проверить гипер! https://hyper.io