Next.js Резюме практики - Проверка авторизации входа в систему Лучшая программа

React.js
Next.js Резюме практики - Проверка авторизации входа в систему Лучшая программа

В последнее время несколько проектов были использованы для использования строительных лесовnext-antd-scaffoldДля этого в процессе разработки системы авторизация входа и аутентификация маршрутизации обдумывались и совершенствовались, надеясь найти оптимальное решение.Сегодня я буду следовать нескольким схемам проверки авторизации Next.js, которые я обобщил. поделиться~

Вот почему это решение Next.js.Из-за разницы в структуре SSR первый рендеринг экрана будет выполняться на стороне сервера, поэтому некоторые обработки или запросы несколько отличаются от обычного SPA.Обычный рендеринг на стороне клиента, вход только проверка Все, что вам нужно, это набор логики.В Next.js сервер и клиент фактически должны обрабатываться отдельно.Поэтому абстрагирование единого и удобного решения поможет развитию нашего бизнеса~

Поскольку Nuxt и Next в основном одинаковы, а проверка авторизации не имеет ничего общего с кодом, это вопрос логического дизайна, поэтому я думаю, что это должна быть общая схема авторизации входа в систему SSR ~

Авторизоваться

Логика входа очень проста, тут и говорить нечего.Независимо от того, какая это система, модуль входа должен быть незаменим.Поэтому позвольте мне рассказать здесь о некоторых проблемах и выводах, с которыми я столкнулся в процессе разработки. Вообще говоря, для коммерческих систем или систем блогов существует два сценария входа в систему.

Первый: пользователь вступил в систему в первый раз, затем предоставлен пользователю, является страница входа в систему, пользователь входит в систему, чтобы войти в систему;

Во-вторых, пользователь входит в систему, система сохраняет информацию об авторизации пользователя.В определенный период времени пользователь не будет входить в систему, пользователь переходит непосредственно на домашнюю страницу системы или страницу URL, когда сбой авторизованной информации пользователя или пользователь очистил браузер, пользователю будет предложено войти в систему.

После успешного входа в систему мы записываем информацию о пользователе в указанное место (общая информация о пользователе помещается в состояние, а информация об авторизации пользователя помещается в файл cookie), что удобно для следующей операции проверки авторизации.

Небольшой вопрос по входу

По поводу повторного входа в систему после того, как данные авторизации пользователя недействительны, на самом деле есть два случая: первый - пользователь отправляет запрос на страницу после входа на системную страницу (это не обязательно домашняя страница, это может быть любая страница ), и обнаруживается, что срок действия авторизации пользователя истек.При появлении сообщения о том, что логин недействителен, авторизоваться заново; во-вторых, до того, как пользователь войдет в систему и отобразит страницу в браузере (т. е. когда сервер ), считается, что логин пользователя недействителен, и перенаправляет на страницу входа в это время (независимо от того, какая страница открыта пользователем).

Первый сценарий я называю флеш-логин (как следует из названия, пользователь будет перенаправлен на страницу входа после входа в систему, еще один процесс переключения мигающих страниц), второй я называю схемой входа без флэш-памяти. Это не значит, что никакая прошивка не обязательно лучше, чем перепрошивка. Оба решения должны использоваться в коммерческих системах. Это зависит от вашего бизнес-сценария. Я не планирую говорить об этом здесь, просто напишите здесь и кратко расскажите об этом~

Разрешить

Авторизация - это определение разрешений фронтенда и бекенда на интерфейсе, доступен ли доступ к интерфейсу всем, если нет, то какие поля проверяются фронтендом и бекендом, где они размещены , и так далее. У каждого и каждой команды свои привычки в коде. Я не заставляю всех писать в соответствии со своими привычками. Следующие решения являются просто абстракцией их собственных бизнес-сценариев. Вы можете улучшать и использовать их в соответствии со своими привычками~

cookie + token

Моя привычка здесь заключается в том, чтобы использовать токены для аутентификации внешних и внутренних пользователей, генерировать токены в фоновом режиме и сохранять их во внешнем файле cookie, а затем внешний интерфейс запроса извлекает токен из файла cookie. и перенесите его в заголовок запроса (здесь я использую выборку). Возможны два следующих сценария:

Пользователь никогда не входил в систему -> логика входа

Когда пользователь входит в систему в первый раз и входит в систему, после прохождения проверки пароля пользователя соответствующая информация и токен получаются и сохраняются в файле cookie.

Пользователь вошел в систему -> логика авторизации переднего плана

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

Почему два сценария?

Поскольку информация о пользователе хранится в состоянии, и когда система обновляется, состояние будет начальным состоянием. На самом деле, в большинстве случаев не нужно проходить запрос на получение данных с фона. Соответствующий пользователь Необходимая информация (имя пользователя, идентификатор пользователя и т. Д.) Информация) Мы можем хранить его в файле cookie, а затем хранить информацию о пользователе из файла cookie в состояние в логике авторизации, сохраняя ненужные сетевые запросы.

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

Схема авторизации

После успешного входа в систему соответствующая информация будет сохранена в файле cookie. Здесь я используюUSER_TOKEN,USER_NAME和USER_IDУказывает информацию о том, что пользователь успешно входит в систему и записывает файл cookie.

  • Добавлена ​​функция логики авторизации в getinitialProps файла _app.js.initialize(ctx)
 static async getInitialProps ({ Component, ctx }) {
    let pageProps = {};
    /** 应用初始化, 一定要在Component.getInitiialProps前面
     *  因为里面是授权,系统最优先的逻辑
     *  传入的参数是ctx,里面包含store和req等
     **/
    initialize(ctx);
    
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps({ ctx });
    }
    return { pageProps };
  }
  • Инициализировать логику
/**
 * 进入系统初始化函数,用于用户授权相关
 * @param {Object} ctx
 */
export default function(ctx) {
  const { req, store } = ctx;
  const userToken = getCookie('USER_TOKEN', req);
  if (userToken && !store.getState().user.auth.user) {
    // cookie存在token并且auth.user不存在为null,直接走auth流程即可,判断user是否为空是为了避免每次一路由跳转都走auth流程
    const payload = {
      username: getCookie('USER_NAME', req),
      userId: getCookie('USER_ID', req),
    } // 获取相关用户信息存入state
    store.dispatch(
      authUserSuccess(payload)
    );
  }
}
  • Инкапсуляция файлов cookie

Зачем инкапсулировать файлы cookie? Позвольте мне объяснить здесь. Мы используем внешние файлы cookie.js-cookieДля выполнения операций и обработки, но в структуре SSR есть процесс для сервера для получения данных.Когда сервер используется, мы не можем передатьjs-cookieЧтобы получить его, но получить его через req, который мы передали, чтобы наконец реализовать инкапсуляцию, которую могут получить как сервер, так и клиент.

import cookie from 'js-cookie';
/**
 * 基于js-cookie插件进行封装
 * Client-Side -> 直接使用js-cookie API进行获取
 * Server-Side -> 使用ctx.req进行获取(req.headers.cookie)
 */
export const getCookie = (key, req) => {
  return process.browser
    ? getCookieFromBrowser(key)
    : getCookieFromServer(key, req);
};

const getCookieFromBrowser = key => {
  return cookie.get(key);
};

const getCookieFromServer = (key, req) => {
  if (!req.headers.cookie) {
    return undefined;
  }
  const rawCookie = req.headers.cookie
    .split(';')
    .find(c => c.trim().startsWith(`${key}=`));
  if (!rawCookie) {
    return undefined;
  }
  return rawCookie.split('=')[1];
};

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

Наконец, я также организовал логику авторизации входа в систему, нарисовал блок-схему:

Вышеупомянутая подготовительная работа, эм, да, это только подготовительная работа, потому что это всего лишь набор логика входа + авторизации, а то, о чем я хочу рассказать, это лучшая практика проверки авторизации, и логика проверки интерфейса с фоном это несколько Система, которая доставляет мне больше всего головной боли, следующая часть - часть проверки.

проверять

Это очень просто.В бизнес-системе все интерфейсы, кроме входа в систему и регистрации, должны быть обязательными для проверки личности пользователя.Как правило, внешний и внутренний интерфейс на моей стороне используют токены. Различные интерфейсные и серверные соглашения также различаются.

Для личных проектов я обычно использую JWT, поэтому токен должен быть помещен в поле Authorization шапки, и тогда токен будет добавлен перед ним.Bearer ${token}.

Для корпоративных проектов я обычно соглашаюсь на фронтенд и бэкенд: бэкенду дается поле заголовка, а затем фронтэнд помещает токен в поле заголовка.

Проверьте первый шаг, пакет Fetch

Конкретный метод пакета, я написал перед соответствующими статьями, и вы будете переданы напрямую.

Соглашение здесь заключается в том, что поле заголовка интерфейсных и внутренних токеновUser-Token

import fetch from 'isomorphic-unfetch';
import qs from 'query-string';
import { getCookie } from './cookie';

// initial fetch
const nextFetch = Object.create(null);
// browser support methods
// ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PATCH', 'PUT']
const HTTP_METHOD = ['get', 'post', 'put', 'patch', 'delete'];
// can send data method
const CAN_SEND_METHOD = ['post', 'put', 'delete', 'patch'];

HTTP_METHOD.forEach(method => {
  // is can send data in opt.body
  const canSend = CAN_SEND_METHOD.includes(method);
  nextFetch[method] = (path, { data, query, timeout = 5000 } = {}) => {
    let url = path;
    const opts = {
      method,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Accept: 'application/json'
        /* 将token放入header字段 */
        'User-Token': getCookie('USER_TOKEN')
      },
      credentials: 'include',
      timeout,
      mode: 'cors',
      cache: 'no-cache'
    };

    // 构造query
    if (query) {
      url += `${url.includes('?') ? '&' : '?'}${qs.stringify(query)}`;
    }
  
    if (canSend && data) {
      opts.body = JSON.stringify(data);
    }

    console.log('Request Url:', url);

    return fetch(url, opts)
      .then(res => res.json());
  };
});

export default nextFetch;

Ну а логика проверки написана выше, каждый запрос на выборку мы вытаскиваем токен из куки и пихаем его в заголовок.Так вот вопрос, можно ли его ставить каждый раз?

ответ отрицательный! Как упоминалось ранее, основной особенностью фреймворка рендеринга на стороне сервера является то, что сервер получает данные. Во многих случаях мы получаем данные с сервера (если вы используетеgetInitialProps, то данные получены сервером при инициализации системы или обновлении страницы), сервер не может пройтиjs-cookieДоступ к данным, некоторые могут сказать, выше не пакеты cookie могут быть получены с сервера делать? Пакет Ah Shi, но подробнее рассмотрим необходимость передачи второго параметраctx.req, мы передаем req при каждой выборке? Непрактично и несовместимо со здравым смыслом разработки ~ так что продолжайте углубляться ~

Проверьте второй шаг, правильно и элегантно получите cookie

Здесь я тщательно все обдумал.На самом деле есть два решения.Хотя одно из решений более хлопотное,но есть и подходящие сценарии.Чтобы выразить трудности моего исследовательского процесса,я расскажу о них здесь:

  • Первый: сервер передает req на выборку отдельно, а клиент получает напрямую из куки

Здесь говорится о том, что сервер переходит в fetch один, вместо передачи req в fetch для каждого запроса, то есть способ записи запроса между сервером и клиентом изменится.

// nextFetch.js
HTTP_METHOD.forEach(method => {
  ...
  /* 新增一个req属性,客户端不传就是undefined */
  nextFetch[method] = (path, { data, query, timeout = 5000, req } = {}) => {
    let url = path;
    const opts = {
      method,
      headers: {
        ...
        /* 将token放入header字段 */
        'User-Token': getCookie('USER_TOKEN', req) 
      },
     ...
    };
   ...
  };
});

export default nextFetch;

В это время нашgetInitialPropsМетоды могут нуждаться в изменении.

/pages/home.js

==========> 之前的写法

import Home from '../../containers/home';
import { fetchHomeData } from '../../redux/actions/home';

Home.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  store.dispatch(fetchHomeData());
  return { isServer };
};

export default Home;

==========> 之后的写法

import Home from '../../containers/home';
import { fetchHomeDataSuccess } from '../../redux/actions/home';
import nextFetch from '../../core/nextFetch';

Home.getInitialProps = async (props) => {
  const { store, isServer, req } = props.ctx;
  // 不通过action,而是直接传入req到fetch获取数据,最后触发success的action来更改state
  const homeData = await nextFetch.get(url, { query, req });
  store.dispatch(fetchHomeDataSuccess(homeData));
  return { isServer };
};

export default Home;

На самом деле эта программа также не большая проблема, единственная проблема заключается в том, что весь процесс становится несколько неразъемным, в принципе мы должны получить доступ к данным, распространяемым по действию, в соответствии с этим сценарием становится прямым запросом для получения успеха успеха РЕЧЕНИЕ ДЕЙСТВИЯ. Это не рекомендуется, потому что регион состоит в том, чтобы сделать формулировку запроса на два, сервер, и клиент оказывается не то же самое, чувствуя себя немного запутанным какой-то логикой, ничего личного развития также не является большой проблемой, но если вы разрабатываете Если всем нам нужна хорошая дискуссия заранее, но не без применимой сцены, когда система относительно проста и не Redux и другой механизм управления государством, когда вы используете что-то вроде ~

Таким образом, это решение на самом деле не бесполезно, оно очень подходит для сценариев управления без состояния, идеально, когда не нужен редукс, чтобы мы могли напрямую передавать полученные данные компоненту в качестве реквизита.

  • Второй: логика кода остается неизменной, токен не получается в fetch, а входящий fetch получается на уровне redux

Эта схема - одна из схем, которые я использую в системе. Идея в том, что я не хочу менять логику получения данных, и я не хочу, чтобы req передавался в действие или выборка. Идея в том, что клиент проходитjs-cookieЧтобы получить токен, сервер не может пройтиjs-cookieЧтобы получить его, получите его из других мест, а сервер получает токен через состояние (как было сказано выше, я сохранил оба из них один раз, когда я залогинился и авторизовался, и эхо начало и конец). общая логика проверки следующая.

// nextFetch.js

import fetch from 'isomorphic-unfetch';
...

HTTP_METHOD.forEach(method => {
  // is can send data in opt.body
  const canSend = CAN_SEND_METHOD.includes(method);
  /* 新增token属性,接收上一层传过来的token */
  nextFetch[method] = (path, { data, query, timeout = 5000, token } = {}) => {
    let url = path;
    const opts = {
      ...
      headers: {
        ...
        'User-Token': token
      },
      ...
    };
    ...
  };
});

export default nextFetch;

Как видите, в nextFetch добавляется параметр token, так откуда взялся этот токен? Ну, как было сказано выше, он передается из редукс-слоя, я использую асинхронную логику.redux-saga, остальные такие же.

import { take, put, fork, select } from 'redux-saga/effects';
import { FETCH_HOME_DATA } from '../../../constants/ActionTypes';
import {
  fetchHomeDataFail,
  fetchHomeDataSuccess,
} from '../../actions/home';
import api from '../../../constants/ApiUrlForBE';
import nextFetch from '../../../core/nextFetch';
import { getToken } from '../../../core/util';
/**
 * 获取首页数据
 */
export function* fetchHomeData() {
  while (true) {
    yield take(FETCH_HOME_DATA);
    /* 获取token */
    const token = yield getToken(select);
    const query = {...queryProps}
    try {
      const data = yield nextFetch.get(api.home, { token, query });
      yield put(fetchHomeDataSuccess(data));
    } catch (error) {
      yield put(fetchHomeDataFail());
    }
  }
}

export default [
  fork(fetchHomeData)
];

Когда saga использует nextFetch для получения данных, мы используем метод для предварительного получения токена и передачи его в nextFetch.

/**
 * 获取token,如果是客户端通过cookie获取,如果是服务端通过state获取
 * @param {Function} select 
 */
export function getToken (select) {
  return process.browser
    ? getCookie('USER_TOKEN')
    : select(state => state.user.auth.token);
}

Проблемы существуют?Я думаю, что это решение действительно хорошо.После завершения упаковки это не хлопотно и выглядит очень элегантно, и нет проблем с командной разработкой.Процесс приобретения каждого должен быть ограничен единообразно упакованным кодом. Однако все же есть небольшой недостаток, то есть каждой саге нужно получать процесс токена отдельно, а затем вставлять его в nextFetch.

Проверьте третий шаг, лучшее решение

Вышеприведенная схема это процесс постоянного улучшения и свержения в написании нескольких систем.Сущностная проблема в том что сервер и клиент не могут обмениваться куками.На самом деле вторая схема ок,хоть напишите Это не некрасиво,и это действительно не проблематично после упаковки, но я думаю,Действительно ли необходимо, чтобы каждая сага проходила процесс получения токена, а затем передала его в выборку? Было бы здорово, если бы это можно было сделать в fetch.

С вышеупомянутыми идеями происходит внезапная вспышка вдохновения.Вы можете не верить тому, о чем я думал во сне~ Ха-ха, но это действительно вспышка мудрости. Как было сказано выше, второй вариант говорит о том, чтоnextFetchДобавьте атрибут токена в параметр получения, и мы передаем токен для использования. Тогда я подумал, если мы получим токен, мы назначим егоnextFetchВы не можете сделать это? Теперь, когда у вас есть идея, попробуйте~

// 授权逻辑initialize
import { getCookie } from './cookie';
/* 引入nextFetch */
import nextFetch from './nextFetch';
import { authUserSuccess } from '../redux/actions/user';

/**
 * 进入系统初始化函数,用于用户授权相关
 * @param {Object} ctx
 */
export default function(ctx) {
  const { req, store } = ctx;
  const userToken = getCookie('USER_TOKEN', req);
  /** 增加下面一行,将获取到的token赋值到nextFetch的Authorization属性上 **/
  nextFetch.Authorization = userToken;
  if (userToken && !store.getState().user.auth.user) {
    ...
    store.dispatch(
      authUserSuccess(payload)
    );
  }
}

На данный момент мы получаем токен в запросе, и он становится следующим.

import fetch from 'isomorphic-unfetch';
...

HTTP_METHOD.forEach(method => {
  // is can send data in opt.body
  const canSend = CAN_SEND_METHOD.includes(method);
  nextFetch[method] = function (path, { data, query, timeout = 5000 } = {}) {
    let url = path;
    const opts = {
      ...
      headers: {
        ...
        /* 获取token,如果是浏览器环境拿cookie的,如果是node端,拿自身的Authorization属性 */
        'User-Token': process.browser ? getCookie('USER_TOKEN') : this.Authorization
      },
      ...
    };
    ...
  };
});

export default nextFetch;

Попробуем распечатать в браузере~

Видно, что после того, как мы успешно проверили, токен был успешно назначенnextFetch.Authorizationсвойств, поэтому данная схема реализуема.

Тогда окончательное решение заключается в том, что офис авторизации присваивает токен в качестве атрибута nextFetch, а затем nextFetch берет его из куки на стороне клиента, а на стороне сервера берет из своего атрибута, так что нам не нужно для выполнения дополнительных операций в других местах~

Фактически, первая версия плана заключалась в использованииglobalпеременная, но если вы хорошенько об этом подумаете, вы обнаружите проблему. Global используется совместно сервером, и назначения, сделанные разными пользователями, будут перезаписаны. Будут проблемы в сценариях с высоким параллелизмом, и его нельзя использовать.

Суммировать

Код может быть слишком абстрактным. Я использовал скаффолдинг, чтобы открыть здесь новую ветку аутентификации. Вы можете запустить код снова~, и пакет выборки в нем также очень полный~ используйте его по мере необходимости~

Кодовый адрес:next-antd-scaffold_auth

Эта статья может быть немного запутанной, или она может использовать слишком много собственных логических привычек, но основная идея состоит в том, чтобы помочь вам упростить логику проверки авторизации входа в систему.Некоторые из этих решений на самом деле осуществимы, и вы должны использовать их в ваши собственные проекты.У нас также есть свои планы, мы можем общаться друг с другом~

Группа обмена: