Я использую Nuxt 3 с рендерингом на стороне сервера. Для запросов REST я использую Axios.
Когда пользователь входит в систему, в браузере сохраняется файл cookie токена обновления. Затем для всех запросов к любой из конечных точек REST в заголовок включается токен доступа. Если токен доступа не найден, он запрашивается с использованием токена обновления. Если срок действия токена доступа истек, запрашивается новый, и неудачный запрос отправляется снова. Всем этим занимаются перехватчики Axios.
Теперь проблема, с которой я столкнулся, заключается в том, что если токен обновления больше не действителен, создать новый токен доступа вообще невозможно. Таким образом, конечная точка токена доступа возвращает код 401. Но когда я пытаюсь перехватить этот код 401 в своих перехватчиках Axios и перейти на страницу входа в систему, это произойдет только на стороне клиента.
Чтобы устранить проблему, я создал эту простую страницу. Он по-прежнему использует Axios, но перехватчиков нет. Основная проблема, похоже, та же самая: навигация на стороне сервера не может осуществляться из цепочки обещаний.
<template>
<div>
{{ data }}
{{ error }}
</div>
</template>
<script setup lang = "ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
return userService.getSelf().catch(() => {
try {
console.info('handling error');
navigateTo('/login');
console.info('navigated to /login');
} catch (ex) {
console.info(ex);
}
})
});
</script>
В журнале на стороне сервера написано следующее:
handling error
WARN [nuxt] useAsyncData should return a value that is not null or undefined or the request may be duplicated on the client side.
[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables.
Таким образом, очевидно, что NavigationTo терпит неудачу с исключением, поскольку он никогда не регистрирует «переход в / вход». В результате этого страница отображается. Страница, к которой у вас больше нет доступа.
На стороне клиента журналы говорят следующее:
GET http://127.0.0.1:8080/users/self 401 (Unauthorized)
restricted.vue:13 handling error
restricted.vue:15 navigated to /login
Таким образом, после рендеринга страницы /restricted и повторной отправки запроса к конечной точке REST на стороне клиента переход к /login успешен, и вы будете перенаправлены на /login.
Теперь, если я просто проверю, установлен ли объект данных, и, если нет, перейду к /login, все будет работать нормально. Он вернет статус 302 по запросу /restricted без предварительного отображения этой страницы. Именно так, как мне хотелось бы. Этот код выглядит следующим образом:
<template>
<div>
{{ data }}
{{ error }}
</div>
</template>
<script setup lang = "ts">
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
return userService.getSelf()
});
if (!data.value) {
navigateTo('/login')
}
</script>
Конечно, это не лучший способ справиться с этим, поскольку токен обновления может стать недействительным в любой момент, на любой из сотен страниц и во время любого из сотен различных вызовов конечной точки REST. Мне нужно какое-то централизованное управление всем этим.
Что мне не хватает? Есть ли лучшие способы справиться с чем-то подобным?
Я предлагаю создать собственный компонуемый объект, который можно будет использовать вместо встроенного в Nuxt useFetch или useAsyncData (или Axios, если уж на то пошло, поскольку он дает вам гораздо меньше возможностей по сравнению с useFetch в Nuxt). Внутри этого компонуемого объекта вы просто оборачиваете встроенный useFetch своей бизнес-логикой. Примерно так (кредит https://github.com/nuxt/nuxt/discussions/16715#discussioncomment-6564177):
// create useCustomFetch to replace useFetch
import { withQuery } from 'ufo'
import { defu } from 'defu'
import type { FetchError } from 'ofetch'
import type { AvailableRouterMethod, NitroFetchRequest } from 'nitropack'
import type { AsyncData, UseFetchOptions, FetchResult } from 'nuxt/app'
type PickFrom<T, K extends Array<string>> = T extends Array<any> ? T : T extends Record<string, any> ? keyof T extends K[number] ? T : K[number] extends never ? T : Pick<T, K[number]> : T;
type KeysOf<T> = Array<T extends T ? (keyof T extends string ? keyof T : never) : never>;
export function useCustomFetch<
ResT = void,
ErrorT = FetchError,
ReqT extends NitroFetchRequest = NitroFetchRequest,
Method extends AvailableRouterMethod<ReqT> = ResT extends void
? 'get' extends AvailableRouterMethod<ReqT>
? 'get'
: AvailableRouterMethod<ReqT>
: AvailableRouterMethod<ReqT>,
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
DataT = _ResT,
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
DefaultT = null,
>(
request: Ref<ReqT> | ReqT | (() => ReqT),
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null> {
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const { token, logout } = useUserSession()
return useFetch(
request,
defu(
<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>>{
baseURL: config.api.baseUrl,
headers: {
authorization: `bearer ${token.value}`,
},
onRequestError: (error) => {
if (error.response?.status === 401) {
logout()
router.push(withQuery('/auth', { redirect: route.fullPath }))
}
},
},
opts,
),
)
}
Вы можете изменить это в соответствии с вашей собственной бизнес-логикой. Просто помните, что во всем вашем приложении вы должны использовать этот собственный компонуемый объект вместо метода useFetch по умолчанию.
Вместо этого я сделал что-то вроде этого:
const userService = useUserService();
const { data, error } = await useAsyncData('data', () => {
return userService.getSelf().catch(() => {
if (import.meta.server) {
const response = requestEvent.node.res;
if (!response.headersSent) {
response.writeHead(302, {Location: '/login'});
response.end();
} else {
router.push('/login');
}
}
})
});
При этом я получаю правильный ответ 302 и перенаправляюсь на страницу входа, если первоначальный запрос, обработанный на стороне сервера, не проходит аутентификацию.
В Nuxt3 вы можете использовать useFetch придумать Nuxt3 вместо axios. Чтобы перенаправить на страницу входа, используйте завернутый
useFetch
вместо необработанногоuseFetch
, вот ответ и пример, который может помочь.