Избегайте атак XSS и CSRF в JWT (React + Golang): руководство

Избегайте атак XSS и CSRF в JWT (React + Golang): руководство

21 марта 2022 г.

Межсайтовый скриптинг (XSS) и подделка межсайтовых запросов (CSRF), скорее всего, произойдут, если JSON Web Token (JWT) неправильно хранится в браузере.


В этой статье я расскажу, как мы можем избежать этих двух атак при использовании JWT в нашем веб-приложении.


Я ценю ваше время, поэтому я начну с того, как это сделать.


  1. Пользователь заполняет форму входа и нажимает кнопку отправки во внешнем интерфейсе.

  1. После того, как пользователь будет аутентифицирован из серверной части, будет отправлен JWT «access_token», а «refresh_token» будет установлен в файле cookie только для HTTP.

  1. Фронтенд хранит access_token в памяти

```машинопись


var access_token = data.access_token;


  1. Когда пользователь обновляет страницу, access_token, хранящийся в памяти, исчезнет. Но мы по-прежнему можем получить access_token, выполнив вызов API через refresh_token, хранящийся в файле cookie только для HTTP (шаг 2). Затем FE снова будет иметь «access_token», который используется для связи с Backend.

Я проведу вас через учебник от Backend, а затем от Frontend. Надеюсь, вам будет легко понять статью.


Бэкенд


Я буду использовать echo в качестве своей инфраструктуры маршрутизации HTTP во всем этом руководстве.


1. Создайте структуру маршрутизации


Запустите внутренний маршрутизатор с параметром AllowCredentials: true, который устанавливает Access-Control-Allow-Credentials в заголовке ответа. Эта конфигурация сообщает браузерам, следует ли отображать все ответы на внешний код JavaScript, когда режим учетных данных запроса (Request.credentials) включен.


```javascript


е := эхо.Новый()


e.Use(промежуточное ПО.CORSWithConfig(промежуточное ПО.CORSConfig{


Разрешить учетные данные: правда,


2. Создать API входа


В этом Login API вы должны выполнить аутентификацию на основе учетных данных, отправленных пользователем, прежде чем продолжить.


После того, как пользователь аутентифицирован, серверная часть должна сгенерировать «access_token» и «refresh_token» для отправки обратно во внешний интерфейс в теле ответа вместе с файлом cookie только для HTTP.


```javascript


тип Пользовательская структура {


Строка имени json:"имя"


ошибка входа в систему (c echo.Context) {


u := новый(пользователь)


если ошибка := c.Bind(u); ошибка !=ноль {


log.Errorf("ошибка привязки: %s", ошибка)


вернуть ошибку


accessToken, exp, err := generateAccessToken(u)


если ошибка != ноль {


вернуть ошибку


RefreshToken, exp, err := generateRefreshToken(u)


если ошибка != ноль {


вернуть ошибку


setTokenCookie(refreshTokenCookieName, refreshToken, exp, c)


вернуть c.JSON(http.StatusOK, echo.Map{


"message": "вход выполнен успешно",


"access_token": токен доступа,


3. Создать API обновления


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


```javascript


func RefreshToken (c echo.Context) ошибка {


cookie, ошибка: = c.Cookie (refreshTokenCookieName)


если ошибка != ноль {


если ошибка == http.ErrNoCookie {


вернуть c.NoContent(http.StatusNoContent)


log.Errorf("ошибка: %s", ошибка)


вернуть ошибку


токен, ошибка := jwt.ParseWithClaims(cookie.Value, &jwtCustomClaims{}, func(токен *jwt.Token) (интерфейс{}, ошибка) {


вернуть [] байт (GetRefreshJWTSecret()), ноль


если ошибка != ноль {


log.Errorf("ошибка: %s", ошибка)


вернуть ошибку


если !токен.Действителен {


возвращать ошибки.Новый("ошибка")


претензии := token.Claims.(*jwtCustomClaims)


log.Infof("утверждения.Имя: %+v", утверждения.Имя)


u := &User{Имя: претензия.Имя}


accessToken, exp, err := generateAccessToken(u)


если ошибка != ноль {


вернуть ошибку


RefreshToken, exp, err := generateRefreshToken(u)


если ошибка != ноль {


вернуть ошибку


setTokenCookie(refreshTokenCookieName, refreshToken, exp, c)


вернуть c.JSON(http.StatusOK, echo.Map{


"access_token": токен доступа,


4. Создайте API получения пользователя


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


```javascript


func UserAPI (ctx echo.Context) ошибка {


accessToken := ctx.Request().Header.Get("Авторизация")


token, err := jwt.ParseWithClaims(accessToken, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) {


вернуть [] байт (jwtSecretKey), ноль


если ошибка != ноль {


log.Errorf("ошибка: %s", ошибка)


вернуть ошибку


претензии := token.Claims.(*jwtCustomClaims)


log.Infof("утверждения.Имя: %+v", утверждения.Имя)


если !токен.Действителен {


вернуть ctx.JSON(http.StatusUnauthorized, echo.Map{


"message": "Неавторизованный",


вернуть ctx.JSON(http.StatusOK, echo.Map{


"имя": претензии.Имя,


Вышеупомянутые API (Login, Refresh и Get User API) являются единственными API, необходимыми для Backend.


Компоненты внешнего интерфейса будут представлены в следующих частях.


Внешний интерфейс


1. Обновить и получить пользовательский API


Во время загрузки страницы мы выполним API-вызов API обновления Backend с учетными данными: 'include'. Файл cookie только для HTTP будет отправлен на серверную часть из браузера. Если в cookie-файле HTTP-Only существует действительный refresh_token, бэкенд ответит новым access_token во внешний интерфейс, который используется для выполнения вызова API для получения информации о пользователе из бэкэнда.


```машинопись


// Используйте этот токен в качестве токена авторизации для связи с другим API.


const [токен, setToken] = React.useState('');


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


const [имя пользователя, setUsername] = React.useState('');


использоватьЭффект(() => {


асинхронная функция обновления () {


// Здесь можно ждать


const resp = await fetch('http://localhost:8080/refresh', {


метод: «ПОСТ»,


заголовки: {


«Тип контента»: «приложение/json»,


учетные данные: «включить»,


console.log("статус " + resp.status);


если (соответственно статус === 204) {


console.log("выход")


вернуть


const d = ожидание resp.json();


если(соотв.хорошо) {


console.log('Успешный вход в систему:', d);


setToken(d.access_token);


пользователь (d.access_token);


обновить();


const user = async (токен: строка) => {


const resp = await fetch('http://localhost:8080/user', {


метод: «ПОЛУЧИТЬ»,


заголовки: {


«Тип контента»: «приложение/json»,


'Авторизация': ${токен},


учетные данные: «включить»,


const d = ожидание resp.json();


если(соотв.хорошо) {


console.log('Успех пользователя:', d);


установить имя пользователя (d.name);


2. Функция OnFinish


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


```машинопись


const onFinish = async (значения: любые) => {


const resp = await fetch('http://localhost:8080/login', {


метод: «ПОСТ»,


заголовки: {


«Тип контента»: «приложение/json»,


тело: JSON.stringify({'имя': values.username}),


учетные данные: «включить»,


const {данные, ошибки} = await resp.json();


если (соответственно хорошо) {


setToken(data.access_token);


setUsername (значения.имя пользователя);


} еще {


console.error(ошибки);


3. Функция рендеринга


Этот пользовательский интерфейс содержит только один ввод Имя пользователя. Когда пользователь нажимает кнопку отправки, onFinish будет запущен с параметром значения в input.


```машинопись


вернуть (




logo



Добро пожаловать {имя пользователя}.



<Форма


имя = "основной"


labelCol={{ интервал: 8 }}


wrapperCol={{диапазон: 16}}


initialValues={{ помните: true }}


onFinish={конец}


автозаполнение = "выключено"


<Форма.Элемент


метка = "Имя пользователя"


имя = "имя пользователя"


rules={[{ обязательно: true, сообщение: 'Пожалуйста, введите ваше имя пользователя!' }]}


<Ввод />





Заключение


Как видите, чтобы избежать проблем с безопасностью, упомянутых в заголовке, самый безопасный способ — хранить токен JWT в памяти. Не о чем беспокоиться в отношении «refresh_token» в HTTP-файле cookie, поскольку злоумышленник не может использовать «refresh_token» для выполнения каких-либо действий на своем веб-сайте.


Вы можете скачать полный исходный код [здесь] (https://github.com/lengzuo/jwt-http-only).


Вот и все, чем я делюсь, надеюсь, вам понравится.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE