Ý tưởng: interceptor bắt 401, gọi refresh một lần duy nhất, các request còn lại xếp hàng chờ token mới rồi retry.
ts
let refreshing = false
const token$ = new BehaviorSubject<string | null>(null)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService)
const withToken = (t: string | null) =>
t ? req.clone({ setHeaders: { Authorization: `Bearer ${t}` } }) : req
return next(withToken(auth.token)).pipe(
catchError(err => {
if (err.status !== 401 || req.url.includes('/auth/refresh')) throw err
if (!refreshing) {
refreshing = true
token$.next(null)
return auth.refresh().pipe(
switchMap(t => {
refreshing = false
token$.next(t) // release the queue
return next(withToken(t))
}),
catchError(e => { refreshing = false; auth.logout(); throw e }),
)
}
// queued requests: wait for the first non-null token, then retry once
return token$.pipe(
filter((t): t is string => t !== null),
take(1),
switchMap(t => next(withToken(t))),
)
}),
)
}Chi tiết dễ bị hỏi thêm:
- Loại trừ chính endpoint refresh khỏi interceptor, nếu không sẽ đệ quy vô hạn khi refresh token cũng hết hạn.
- take(1) ở nhánh chờ để mỗi request chỉ retry đúng một lần.
- Refresh thất bại → xoá phiên và điều hướng về login, không im lặng nuốt lỗi.
- Refresh token nên nằm ở cookie httpOnly thay vì localStorage; khi đó cần withCredentials và CSRF token.
- Nhiều tab: đồng bộ qua storage event hoặc BroadcastChannel để tránh mỗi tab tự refresh và làm token xoay vòng loạn nhau.