Защищенные маршруты и авторизация

Маршрутизация и авторизация во фронтенд-приложениях на Knockout.js обычно реализуются без «из коробки» решений. Фреймворк предоставляет реактивную модель данных и биндинги, а архитектура защищённых маршрутов строится поверх него — с использованием сторонних библиотек или собственной логики.


Knockout.js не включает встроенный роутер, поэтому на практике применяются:

  • Sammy.js
  • Director
  • Navigo
  • собственная маршрутизация на основе location.hash или history.pushState

Типовой маршрут связывает URL с состоянием приложения и активной моделью представления.

function AppViewModel() {
    this.currentPage = ko.observable('login');
}

var app = new AppViewModel();
ko.applyBindings(app);
function router() {
    var hash = location.hash.replace('#', '');
    app.currentPage(hash || 'login');
}

window.addEventListener('hashchange', router);
router();

Понятие защищённого маршрута

Защищённый маршрут — это состояние приложения, доступ к которому разрешён только аутентифицированному пользователю. Проверка выполняется до активации представления или во время смены маршрута.

Ключевые требования:

  • наличие глобального состояния авторизации
  • централизованная проверка прав доступа
  • перенаправление при отсутствии прав

Модель авторизации

Состояние авторизации обычно хранится в корневой модели приложения.

function AuthService() {
    this.isAuthenticated = ko.observable(false);
    this.user = ko.observable(null);
}

var auth = new AuthService();

После успешного входа:

auth.isAuthenticated(true);
auth.user({ id: 1, role: 'admin' });

Это состояние реактивно и автоматически обновляет интерфейс.


Связывание маршрутов с правами доступа

Каждому маршруту сопоставляется метаинформация:

var routes = {
    login: { secure: false },
    dashboard: { secure: true },
    profile: { secure: true },
    admin: { secure: true, role: 'admin' }
};

При изменении маршрута выполняется проверка:

function router() {
    var hash = location.hash.replace('#', '') || 'login';
    var route = routes[hash];

    if (route.secure && !auth.isAuthenticated()) {
        location.hash = 'login';
        return;
    }

    if (route.role && auth.user()?.role !== route.role) {
        location.hash = 'dashboard';
        return;
    }

    app.currentPage(hash);
}

Управление отображением через биндинги

Представления можно переключать с помощью if или visible.

<div data-bind="if: currentPage() === 'dashboard'">
    <!-- ko component: 'dashboard-page' --><!-- /ko -->
</div>

<div data-bind="if: currentPage() === 'login'">
    <!-- ko component: 'login-page' --><!-- /ko -->
</div>

Реактивность Knockout.js гарантирует, что неавторизованный пользователь физически не увидит защищённый контент.


Авторизация на основе ролей

Для сложных сценариев используется ролевая модель:

this.hasRole = function(role) {
    return auth.isAuthenticated() && auth.user()?.role === role;
};

В шаблонах:

<button data-bind="visible: hasRole('admin')">
    Панель администратора
</button>

Это исключает дублирование проверок и повышает читаемость.


Хранение состояния между перезагрузками

Чтобы пользователь не терял сессию при обновлении страницы, данные авторизации сохраняются:

  • localStorage
  • sessionStorage
  • cookie с HttpOnly (через сервер)
auth.isAuthenticated.subscribe(function(value) {
    localStorage.setItem('auth', value);
});

При инициализации:

auth.isAuthenticated(
    localStorage.getItem('auth') === 'true'
);

Проверка токена при старте приложения

При использовании JWT или аналогичных токенов необходимо валидировать их при загрузке:

var token = localStorage.getItem('token');

if (token) {
    fetch('/api/validate', {
        headers: { Authorization: 'Bearer ' + token }
    })
    .then(r => r.ok ? r.json() : Promise.reject())
    .then(user => {
        auth.isAuthenticated(true);
        auth.user(user);
    })
    .catch(() => {
        auth.isAuthenticated(false);
        auth.user(null);
    });
}

Маршрутизация запускается после этой проверки.


Защита компонентов

Компоненты Knockout.js также могут проверять авторизацию при инициализации:

ko.components.register('dashboard-page', {
    viewModel: function() {
        if (!auth.isAuthenticated()) {
            location.hash = 'login';
        }
    },
    template: '...'
});

Это дополнительный уровень защиты при динамической загрузке.


Централизованный guard маршрутов

Для масштабных приложений логика проверки выносится в единый guard:

function canActivate(route) {
    if (route.secure && !auth.isAuthenticated()) {
        return false;
    }
    if (route.role && auth.user()?.role !== route.role) {
        return false;
    }
    return true;
}

Использование:

if (!canActivate(route)) {
    location.hash = 'login';
    return;
}

Ограничения клиентской защиты

Клиентская авторизация:

  • предотвращает доступ к интерфейсу
  • улучшает UX
  • не является механизмом безопасности

Все защищённые данные и действия должны проверяться на сервере. Knockout.js отвечает только за управление состоянием и отображением.


Архитектурные принципы

  • единый источник прав доступа
  • минимизация логики в шаблонах
  • изоляция авторизации от бизнес-логики
  • реактивное управление состоянием

Knockout.js хорошо подходит для построения прозрачной и управляемой модели защищённых маршрутов за счёт наблюдаемых значений и предсказуемого потока данных.