В Части 1 мы создали модуль офлайн-аутентификации с использованием Web Crypto API.
Теперь мы рассмотрим подключение этого к пользовательскому интерфейсу с помощью Vue 3 и API композиции.
Код закончился в Github, если вы хотите пропустить его и сразу перейти к нему.
Сфера
Чтобы сделать это приложение простым, мы будем использовать только 2 компонента. Наше основное приложение и компонент с проверкой подлинности, которые будут обрабатывать аутентификацию и логику отображения с проверкой подлинности.
Мы не будем создавать пользовательский интерфейс для создания новой пары ключей. Вместо этого я установил токен localStorage вверху index.html, который будет представлять ключ пользователя.
index.html
<script> localStorage.setItem('key', '{"crv": "P-384","ext": true,"kty": "EC","x": "rg9BzHryeKvuHln38btrKjxDpOaDVQz8Eczxwy2xnuL1deWCTthEbplser7NW3n_","y": "suob7sbqEuo285FhTDz4FE_hjhgu7ZyXToCzHkBTPegiodmzISXaFx1t9bcaHsyq"}'); </script>
В реальном потоке аутентификации мы позволим пользователю загружать или извлекать этот ключ из своего собственного хранилища, как мы это делаем в альфа-версии Concords Boards.
App.vue
Есть 3 состояния, которые нам нужно учитывать для контента: аутентифицированный контент; неаутентифицированный контент; и контент, который должен отображаться независимо от состояния аутентификации. Мы будем использовать именованные слоты для обработки контента для аутентифицированных пользователей и слот по умолчанию для контента, который будет отображаться в обоих состояниях.
<template> <authenticated> <h1>Offline Authenticaion</h1> <template #logged-in> <p> 🎉 You found the Key </p> </template> <template #not-logged-in> Sorry, you must know the key. </template> </authenticated> </template> <script> import { defineComponent} from 'vue'; import Authenticated from '@/components/Authenticated.vue'; export default defineComponent({ name: 'App', components: { Authenticated } }); </script>
Аутентифицированный.vue
В нашем компоненте Authenticated нам нужно предоставить наши 3 слота, а также обработать поток аутентификации, а также отобразить пользовательский интерфейс для входа в систему.
<template> <slot/> <slot name="logged-in" v-if="isAuthenticated"/> <div v-else> <slot name="not-logged-in"/> <p> <input type="password" v-model="jwk"> <button @click="login">Login</button> </p> </div> </template>
Мы абстрагируем нашу логику аутентификации в составные функции многократного использования, которые затем просто нужно настроить в нашем компоненте Authenticated.
import { defineComponent, ref, unref, watch } from 'vue'; import useAuthentication from '@/composables/useAuthentication'; export default defineComponent({ name: 'Authenticated', setup() { const { isAuthenticated, sessionLogin, login, storageCredentials } = useAuthentication(); const jwk = ref(null); function userLogin() { const { key } = storageCredentials; login(key, unref(jwk)) } sessionLogin(); return { isAuthenticated, login: userLogin, jwk }; }, });
Составляемые функции Vue 3
Мы будем использовать локальное хранилище браузера для хранения нашего ключа и токена JWK. Для входа в систему у нас есть компонуемая функция useStorageCredentials, которая хранит ссылку на реактивное представление наших учетных данных для входа.
использоватьStorageCredentials.js
import { reactive, watch } from 'vue'; const storageCredentials = reactive({ key: null, jwk: null, }); watch(storageCredentials, ({ key, jwk }) => { localStorage.setItem('key', JSON.stringify(key)); localStorage.setItem('jwk', JSON.stringify(jwk)); }); function setStorageCredentials(key, jwk) { storageCredentials.key = key; storageCredentials.jwk = jwk; } function getStorageCredentials() { storageCredentials.key = JSON.parse(localStorage.getItem('key')); storageCredentials.jwk = JSON.parse(localStorage.getItem('jwk')); } export default () => ({ getStorageCredentials, setStorageCredentials, storageCredentials });
Чтобы войти в систему, мы можем использовать методы setStorageCredentials для обновления состояния нашего локального хранилища. У нас есть наблюдатель для синхронизации этих значений с состоянием localStorage в браузере.
const login = async (key, jwk) => setStorageCredentials(key, jwk); const logout = async () => setStorageCredentials(storageCredentials.key, null);
Вход в систему
Хранения ключа и JWK недостаточно для аутентификации пользователя, теперь нам нужно использовать модуль аутентификации, который мы создали в Части 1.
useSigningKey.js
Затем мы передаем наш ключ и JWK методу getSigningKey
, который затем вызывает нашу функцию входа в систему аутентификации, используя API WebCrypto для создания ключа подписи из комбинации открытого ключа и JWK.
import { ref } from 'vue'; import { login } from '../platform/authentication'; const signingKey = ref(null); async function getSigningKey(key, jwk) { if (key && jwk) { signingKey.value = await login(jwk, key); } else { signingKey.value = false; } } export default () => ({ signingKey, getSigningKey });
Этот ключ подписи является нашим идентификатором аутентификации. Только совпадающий ключ и комбинация JWK создадут ключ подписи, поэтому теперь мы можем предположить, что наш пользователь аутентифицирован.
useAuthentication.js
Теперь мы собираем все это вместе в единую компонуемую функцию аутентификации, экспортируя необходимое состояние и методы для входа и выхода из аутентифицированного контента. Наблюдая за нашим signingKey
, чтобы обновить нашу реактивную ссылку isAuthenticated
, которую мы будем использовать в нашем приложении.
import { watch, ref } from 'vue'; import useStorageCredentials from './useStorageCredentials'; import useSigningKey from './useSigningKey'; import { create } from '../platform/authentication'; const { storageCredentials, setStorageCredentials, getStorageCredentials } = useStorageCredentials(); const { signingKey, getSigningKey } = useSigningKey(); const isAuthenticated = ref(false); watch(storageCredentials, ({ key, jwk }) => getSigningKey(key, jwk)); watch(signingKey, (newSigningKey = {}) => { isAuthenticated.value = newSigningKey.type === 'private'; }); const login = async (key, jwk) => setStorageCredentials(key, jwk); const logout = async () => setStorageCredentials(storageCredentials.key, null); export default () => ({ login, logout, sessionLogin: getStorageCredentials, storageCredentials, isAuthenticated, signingKey })
Демо: https://offline-authentication.concords.app/
Источник: https://github.com/teamconcords/offline-authentication