Избегайте атак XSS и CSRF в JWT (React + Golang): руководство
21 марта 2022 г.Межсайтовый скриптинг (XSS) и подделка межсайтовых запросов (CSRF), скорее всего, произойдут, если JSON Web Token (JWT) неправильно хранится в браузере.
В этой статье я расскажу, как мы можем избежать этих двух атак при использовании JWT в нашем веб-приложении.
Я ценю ваше время, поэтому я начну с того, как это сделать.
- Пользователь заполняет форму входа и нажимает кнопку отправки во внешнем интерфейсе.
- После того, как пользователь будет аутентифицирован из серверной части, будет отправлен JWT «access_token», а «refresh_token» будет установлен в файле cookie только для HTTP.
- Фронтенд хранит
access_token
в памяти
```машинопись
var access_token = data.access_token;
- Когда пользователь обновляет страницу,
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.
```машинопись
вернуть (
Добро пожаловать {имя пользователя}.
<Форма
имя = "основной"
labelCol={{ интервал: 8 }}
wrapperCol={{диапазон: 16}}
initialValues={{ помните: true }}
onFinish={конец}
автозаполнение = "выключено"
<Форма.Элемент
метка = "Имя пользователя"
имя = "имя пользователя"
rules={[{ обязательно: true, сообщение: 'Пожалуйста, введите ваше имя пользователя!' }]}
<Ввод />
Авторизоваться
Заключение
Как видите, чтобы избежать проблем с безопасностью, упомянутых в заголовке, самый безопасный способ — хранить токен JWT в памяти. Не о чем беспокоиться в отношении «refresh_token» в HTTP-файле cookie, поскольку злоумышленник не может использовать «refresh_token» для выполнения каких-либо действий на своем веб-сайте.
Вы можете скачать полный исходный код [здесь] (https://github.com/lengzuo/jwt-http-only).
Вот и все, чем я делюсь, надеюсь, вам понравится.
Оригинал