Angular中的身份验证DI中的循环依赖问题突然冒出来
从我们离开的地方开始,我们的 Auth 服务的第一个用例是标头令牌。 添加标头令牌的最佳方法是通过 Http 拦截器。 开始吧。
由于我们无论如何都要注入 AuthService,并使用 AppModule,因此使用 HttpInterceptorFunction 而不是 goold ol" HttpClientModule 并没有太大区别。 最终会更明显地表明它确实是一个更好的选择。
您可以阅读有关 Angular 15 独立 HTTPClient 提供程序的信息。
在我们的 App Module provider 数组中,我们为拦截器添加了另一个条目: // app.module @NgModule({ // ... providers: [ { provide: HTTP_INTERCEPTORS, multi: true, useClass: AppInterceptor, }, // ... ] }) export class AppModule {}
拦截器立即注入 AuthService 以使用它。 让我在第一行添加一个控制台日志。 // services/http HttpInterceptor @Injectable() export class AppInterceptor implements HttpInterceptor { constructor(private authState: AuthState) { console.log("interceptor injected"); } intercept(req: HttpRequest, next: HttpHandler): Observable { // prefixing the api with proper value, mostly from config // remote config url are expected to be filtered out, it would not make sense const url = "https://saphire.sekrab.com/api" + req.url; const adjustedReq = req.clone({ url: url, setHeaders: this.getHeaders(), }); return next.handle(adjustedReq); } private getHeaders(): any { // TODO authorization here let headers: any = {}; return headers; } }
在我们添加我们的头之前,让我们记住我们的事件顺序:假设我们使用 Http 调用来获取远程配置,它通常有正确的 API URL,很明显我们需要过滤掉配置 URL。 在这个例子中,我没有为配置调用远程 URL,但很高兴知道拦截器应该检查 req.url 并过滤掉它不想处理的那些。 // simple check to exclude local data or config url if (req.url.indexOf("config") > -1) { // pass through return next(req); } DI 问题中的循环依赖
错误:NG0200:为 InjectionToken HTTP_INTERCEPTORS 检测到 DI 中的循环依赖。
你见过这个吗? 当您在另一个服务中注入一个服务时,它会发生,该服务将其注入自身。 在我们的例子中,AuthService 和 HttpClient 相互注入。
除了这两个服务之外,AuthService中还注入了使用Http的配置服务。 怎么看都是乱七八糟的。
但在你步入中年之前,这件事会杀死你。 由于我们没有在 AuthService 构造函数中使用 HttpClient,因此这个肿瘤是良性的。 然而,如果我们确实在构造函数中发起了一个 Http 调用,那就是它在我们面前爆炸的时候。
有很多修复,其中大部分都是围绕延迟 Http 调用来确保 AuthService 已构建。 就像等待远程配置准备就绪一样。 但这不是一个干净的解决方案。
那么,作为一般规则,那就解决了:避免在服务构造函数中进行 Http 请求。 特别是那些早期注射的。
如果您确实需要注入一个在其构造函数中调用 Http 的服务(下周我们将有一个用例),请将您的服务分开,并将它们分散在您的应用程序中。AuthState 服务
为了在我们前进的过程中进行清理并变得更加系统化,让我们将所有与 Http 无关的方法移至它们自己的服务中。 AuthState 是将保存 Observable 状态的服务,并且不包含对 HttpClient 的引用。 构造函数负责读取LocalStorage信息,它有GetToken新方法返回token。// services/auth.state @Injectable({ providedIn: "root" }) export class AuthState { // create an internal subject and an observable to keep track private stateItem: BehaviorSubject = new BehaviorSubject( null ); stateItem$: Observable = this.stateItem.asObservable(); constructor() { // simpler to initiate state here // check item validity console.log("authState in"); const _localuser: IAuthInfo = JSON.parse(localStorage.getItem("user")); if (this.CheckAuth(_localuser)) { this.SetState(_localuser); } else { this.Logout(); } } // also move here: SetState RemoveState CheckAuth Logout }
现在AuthService就简单多了,只有Login,用AuthState存入localStorage。 我们稍后会在使用适当的 localStorage 包装器时增强它。
所以现在我们需要创建一个 GetToken 方法来检索访问令牌,然后在 HttpInterceptor 中使用它 // services/auth.state // add this new method GetToken() { const _auth = this.stateItem.getValue(); // check if auth is still valid first before you return return this.CheckAuth(_auth) ? _auth.accessToken : null;; }
稍后我们将添加检查令牌的逻辑。 然后我们在拦截器中使用它 // update http file to fill out get headers private getHeaders(): any { // authorization here let headers: any = {}; const _auth = this.authState.GetToken(); if (_auth && _auth !== "") { headers["authorization"] = `Bearer ${_auth}`; } return headers; }
在全模块解决方案中,一切都是我们类的私有成员。 在独立的情况下,一切都是松散变量。 你更喜欢哪种方式? 401刷新
当我们收到 401 时会发生什么? 我们可以将用户显示出来,或者使用我们的刷新令牌来获取新的访问令牌。 在 Angular 中,这可能是让我伤痕累累的任务之一。 这是事件的顺序:
赶上 401(哪个 401)
使用刷新令牌创建新的 Http 调用,并请求新的访问令牌
等待回应
更新本地存储
重新提交原始请求(重试)
回去好好生活
否则注销
捕获另一个并发 401,排队等待
所以我们先修改Http函数来捕获401,并为其调用一个函数。 // services/http intercept(req: HttpRequest, next: HttpHandler): Observable { // ... return next.handle(adjustedReq).pipe( catchError(error => { // if this is really an http error if (error instanceof HttpErrorResponse // and of 401 status && error.status === 401 ){ // handle 401 error, return an observable to continue the pipe return this.handle401Error(); } // rethrow error, to be caught elsewhere return throwError(() => error); }) ); } private handle401Error(): Observable { // let"s first try to submit a refresh access token request // return authService.RefreshToken() // switchMap when done to resubmit the req passed, using next.handler // catchError means it is not working, rethrow and logout }
我们现在要做的是填写 handle401Error 函数。 首先,看起来我们需要 AuthService(而不是 AuthState)中的 RefreshToken 方法。 这意味着我们也需要注入它。 请记住:AuthService 在构造函数中没有 Http 调用。 // services/auth.service // add RefreshToken method RefreshToken(): Observable { return this.http .post(this._refreshUrl, { token: this.authState.GetToken() }) .pipe( map((response) => { // this response has the new refresh token and access token if (!response) { // something terrible happened throw(new Error("Oh oh")); } // update session const retUser: IAuthInfo = (response).data; // we"ll be more selective later... localStorage.setItem("user", JSON.stringify(retUser)); this.authState.SetState(retUser); return true; }) ); }
回到我们的 handle401Error 函数 // services/http // update handle401Error function, also, inject AuthService in the constructor private handle401Error( // pass in orginalReq and handler originalReq: HttpRequest, next: HttpHandler ): Observable { return this.authService.RefreshToken().pipe( switchMap((result: boolean) => { if (result) { // token saved (in RefreshToken), now recall the original req after adjustment // so we need to pass "next" handler, and "originalReq" return next.handle(originalReq.clone({setHeaders: this.getHeaders()})); } }), catchError(error => { // else refresh token did not work, its bigger than both of us // log out and throw error this.authState.Logout(); return throwError(() => error); }) ); }
我们调整签名以传入 originalReq 和下一个处理程序: // services/http // adjust call return next.handle(adjustedReq).pipe( catchError((error) => { // ... return this.handle401Error(adjustedReq, next); } // ... }) );
对此进行测试,第一个问题是 /login 点。 如果是 401,则无需重试,这仅表示凭据错误。 所以处理程序必须过滤掉 /login 点 // services/http filter out login from handler401Error return next.handle(adjustedReq).pipe( catchError((error) => { // if this is really an http error if ( error instanceof HttpErrorResponse && // and of 401 status error.status === 401 && // filter out login calls req.url.indexOf("login") < 0 ) { return this.handle401Error(adjustedReq, next); } // rethrow error return throwError(() => error); })
通过在某个页面中进行调用并在我的测试服务器上硬编码一些东西来测试它,这就是我注销序列的结果:
因此,您可以看到使用正确的访问令牌撤回了原始请求。
你想生产类似的彩色原木吗? 阅读驯服控制台 锁定和解锁
我们还没有完成。 让我们创建一个示例用法来查看由此产生的问题。 我们将同时发出两个请求。 这意味着当第一个请求试图刷新令牌时,第二个请求进来了,它也可能请求一个新令牌,从而搞砸了原始令牌。 这是虚拟日志,它不会破坏系统,因为它很笨:
请注意以下事项:
抛出两个 401 错误,这是预期的
两次调用刷新令牌,使用相同的访问令牌,一个应该工作,另一个不应该
响应带有新令牌,在我的示例中它是相同的,因为它很笨。 在现实生活中会有两种不同的访问令牌,一种必须失败(如果它还没有失败的话)
要解决这个问题,我们需要锁定、排队,然后解锁。
使用私有布尔成员可以直接锁定和解锁: // services/http // add lock boolean @Injectable() export class AppInterceptor implements HttpInterceptor { // if refreshing token, it is busy, lock isBusy: boolean; private handle401Error( originalReq: HttpRequest, next: HttpHandler ): Observable { if (!this.isBusy) { // lock this.isBusy = true; return this.authService.RefreshToken().pipe( // ... finalize(() => { // unlock this.isBusy = false; }) ); } else { // return unadjusted, for now return next.handle(originalReq); } } }
现在有了这个,一个调用会重试,而所有其他调用都会失败。 在我们调整和召回所有其他令牌之前,我们需要稍等片刻,直到令牌准备就绪。 为此,我们可以有一个私有成员来跟踪成功的令牌。 准备好后,冲洗干净。
对此最广泛接受的解决方案是使用 Boolean 的 Subject 并在其上使用管道。 它在锁定时和令牌准备就绪时更新。 // services/http update to allow subject queuing @Injectable() export class AppInterceptor implements HttpInterceptor { // create a subject to queue outstanding refresh calls recall: Subject = new Subject(); // ... private handle401Error(...): Observable { if (!this.isBusy) { // ... // progress subject to false this.recall.next(false); return this.authService.RefreshToken().pipe( switchMap((result: boolean) => { if (result) { // progress subject to true this.recall.next(true); // ... return next.handle } }), // ... ); } else { // return the subject, watch when it"s ready, switch to recall original request return this.recall.pipe( filter(ready => ready === true), switchMap(ready => { // try again with adjusted header return next.handle(originalReq.clone({ setHeaders: this.getHeaders() })); }) ); } }
我试图打破它,但我做不到。 如果您遇到它出现问题的情况,请告诉我。 侧点
如果访问令牌无效(已过期),您可能会想要停止传出请求。 不。 这是一个 API 决定。 有些点不需要访问令牌(如 /login),如果令牌无效,有些点可以灵活地返回较少的数据。 提高
我们可以添加的一项增强功能是,如果刷新令牌失败,则将用户重定向到登录页面。
另一个增强功能是登录解析。 我们现在可以将导致重定向的 URL 保存在 auth 状态,并在登录后尝试重定向到它。 下周将出现关于用户帐户详细信息的那件事和另一件事。
感谢您阅读到这里,您是否破坏了 401 处理程序?
南京玄奘寺住持南大历史系毕业,有6家公司,寺内供奉日本战犯近日,南京玄奘寺殿内供奉着南京大屠杀主犯田中军吉南京大屠杀的甲级战犯松井石根南京大屠杀的乙级战犯谷寿夫南京大屠杀的丙级战犯野田毅一事引起了社会各界的愤怒。供奉4个日本战犯的人名叫吴
玄奘寺主持身份不简单,开公司拍电影,曾自嘲是渣男7月21日,一则消息震惊了全国乃至全世界。那就是位于南京并且距离南京大屠杀纪念馆不远的玄奘寺,居然供奉着四位日本战犯的长生牌位!长生牌顾名思义是为活人而立,往生牌是为已故的人而立。
绿媒炒作9架次解放军军机昨日进入台西南空域环球网综合报道记者赵友平岛内绿媒自由时报声称,台防务部门8日晚消息,解放军空军战机8日上午越过海峡中线,进入台空域。台空军司令部8日晚间又声称,9架次解放军军机8日进入台西南空域。
7。9签约交易流言汇总奥兰多魔术官方宣布,正式和球队中锋穆罕默德班巴签下一份续约合同。根据球队政策,魔术没有公布班巴的合同细节。不过此前名记称,班巴的合同是2年2100万美元。202122赛季,班巴为魔
总台记者看世界难舍冰墩墩和雪容融,难忘我在瑞士看北京冬奥会的那些点点滴滴总台记者看世界!大家好,我是总台驻日内瓦记者张婧昊。位于瑞士洛桑的奥林匹克博物馆入口处的冰墩墩和雪容融(张婧昊摄)前几天看到一则消息,根据北京冬奥组委与特许生产商的特许经营协议,7
人世间中残酷的三个现实这是一部有关亲情友情爱情的百姓生活史。这是一部中国五十年的变迁史。作家梁晓声的呕心力作人世间一经问世,就好评如潮。除了能引起一代人满满的回忆以外,我觉得好多人喜欢它的原因还在于,作
天津卷高分作文钟南山的脸,文章层层递进,网友竖起大拇指文萌妈教育日记语文这个学科,之所以是三大主科之首,是因为它看似通俗易懂,实则博大精深,想要学好学精,并不容易。每年高考结束,各地区的高考状元,数学英语学科成绩考到高分满分并不少。但
安倍晋三之死日本前首相安倍晋三遇刺身亡,事件一出就有同事兴奋不已,惊呼肯定是国人干的。我不以为然,只是把它看作一起政治事件,没有过多的想法,因为这本身与国人没有多大关系,平静地看着部分网友的各
爱乐之都综艺不抢戏,专业不自卑2022年7月9日刊总第2928期7月7日下午,东方卫视打造的全国首档音乐剧全产业链文化推广节目爱乐之都专家研讨会在沪举行。上海广播电视台上海文化广播影视集团有限公司党委副书记台长
结合江歌案判决以及日本法律,枪击安倍晋三的刺客会判死刑吗?日本前首相安倍晋三被枪击身亡还在持续发酵,此事件震惊世界,美国政要表示这是一个灾难!印度总理表示要为他默哀一天,而他们也纷纷表达了对暴力的谴责!关于开枪的嫌疑人,据日本媒体报道,他
网剧我叫刘金凤被下架,之前因剧倭风太重被吐槽,网友活该7月9日,之前备受争议的网剧我叫刘金凤又上热搜,有媒体发现由辣目洋子李宏毅一起主演的电视剧,在平台已经搜索不到了,已被下架。我叫刘金凤这部剧在6月24日上架,之前就上过好几次热搜,