diff --git a/packages/auth/src/provide.ts b/packages/auth/src/provide.ts index 382beee8c..f2ffd35bf 100644 --- a/packages/auth/src/provide.ts +++ b/packages/auth/src/provide.ts @@ -1,13 +1,10 @@ -import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { EnvironmentProviders, Provider, makeEnvironmentProviders } from '@angular/core'; import { CookieService } from '@delon/util/browser'; import { CookieStorageStore, DA_STORE_TOKEN, LocalStorageStore, MemoryStore, SessionStorageStore } from './store'; -import { JWTInterceptor, SimpleInterceptor } from './token/index'; export enum AuthFeatureKind { - Token, Store } @@ -26,38 +23,12 @@ function makeAuthFeature(kind: KindT, providers: /** * Configures authentication process service to be available for injection. * - * @see {@link withSimple} - * @see {@link withJWT} * @see {@link withCookie} * @see {@link withLocalStorage} * @see {@link withSessionStorage} */ -export function provideAuth( - type: AuthFeature, - store?: AuthFeature -): EnvironmentProviders { - return makeEnvironmentProviders([type.ɵproviders, (store ?? withLocalStorage()).ɵproviders]); -} - -/** Use simple auth type, */ -export function withSimple(): AuthFeature { - return makeAuthFeature(AuthFeatureKind.Token, [ - { - provide: HTTP_INTERCEPTORS, - useClass: SimpleInterceptor, - multi: true - } - ]); -} - -export function withJWT(): AuthFeature { - return makeAuthFeature(AuthFeatureKind.Token, [ - { - provide: HTTP_INTERCEPTORS, - useClass: JWTInterceptor, - multi: true - } - ]); +export function provideAuth(store?: AuthFeature): EnvironmentProviders { + return makeEnvironmentProviders([(store ?? withLocalStorage()).ɵproviders]); } /** `cookie` storage */ diff --git a/packages/auth/src/token/base.interceptor.spec.ts b/packages/auth/src/token/base.interceptor.spec.ts index 4c0842451..bb99d1b01 100644 --- a/packages/auth/src/token/base.interceptor.spec.ts +++ b/packages/auth/src/token/base.interceptor.spec.ts @@ -8,9 +8,16 @@ import { HttpInterceptor, HttpRequest, HttpResponse, - HTTP_INTERCEPTORS + HTTP_INTERCEPTORS, + provideHttpClient, + withInterceptors } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { + HttpClientTestingModule, + HttpTestingController, + TestRequest, + provideHttpClientTesting +} from '@angular/common/http/testing'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -20,8 +27,9 @@ import { Observable, throwError, catchError } from 'rxjs'; import { AlainAuthConfig, provideAlainConfig } from '@delon/util/config'; import { AuthReferrer, DA_SERVICE_TOKEN, ITokenModel, ITokenService } from './interface'; +import { authSimpleInterceptor } from './simple'; import { SimpleTokenModel } from './simple/simple.model'; -import { provideAuth, withSimple } from '../provide'; +import { provideAuth } from '../provide'; import { ALLOW_ANONYMOUS } from '../token'; function genModel(modelType: new () => T, token: string | null = `123`): any { @@ -80,8 +88,10 @@ describe('auth: base.interceptor', () => { imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], providers: [ { provide: DOCUMENT, useValue: MockDoc }, + provideHttpClient(withInterceptors([authSimpleInterceptor])), + provideHttpClientTesting(), provideAlainConfig({ auth: options }), - provideAuth(withSimple()), + provideAuth(), { provide: DA_SERVICE_TOKEN, useClass: MockTokenService } ].concat(provider) }); diff --git a/packages/auth/src/token/base.interceptor.ts b/packages/auth/src/token/base.interceptor.ts index 9de285ffe..5595df6b6 100644 --- a/packages/auth/src/token/base.interceptor.ts +++ b/packages/auth/src/token/base.interceptor.ts @@ -1,86 +1,37 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { - HttpErrorResponse, - HttpEvent, - HttpHandler, - HttpInterceptor, - HttpRequest, - HTTP_INTERCEPTORS -} from '@angular/common/http'; -import { Injectable, Injector, Optional } from '@angular/core'; +import { HttpErrorResponse, HttpEvent, HttpRequest } from '@angular/common/http'; import { Observable, Observer } from 'rxjs'; -import { AlainAuthConfig, AlainConfigService } from '@delon/util/config'; +import { AlainAuthConfig } from '@delon/util/config'; import { ToLogin } from './helper'; -import { ITokenModel } from './interface'; -import { mergeConfig } from '../auth.config'; import { ALLOW_ANONYMOUS } from '../token'; -class HttpAuthInterceptorHandler implements HttpHandler { - constructor( - private next: HttpHandler, - private interceptor: HttpInterceptor - ) {} - - handle(req: HttpRequest): Observable> { - return this.interceptor.intercept(req, this.next); +export function isAnonymous(req: HttpRequest, options: AlainAuthConfig): boolean { + if (req.context.get(ALLOW_ANONYMOUS)) return true; + if (Array.isArray(options.ignores)) { + for (const item of options.ignores) { + if (item.test(req.url)) return true; + } } + return false; } -@Injectable() -export abstract class BaseInterceptor implements HttpInterceptor { - constructor(@Optional() protected injector: Injector) {} - - protected model!: ITokenModel; - - abstract isAuth(options: AlainAuthConfig): boolean; - - abstract setReq(req: HttpRequest, options: AlainAuthConfig): HttpRequest; +export function throwErr(req: HttpRequest, options: AlainAuthConfig): Observable> { + ToLogin(options); - intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (req.context.get(ALLOW_ANONYMOUS)) return next.handle(req); - - const options = mergeConfig(this.injector.get(AlainConfigService)); - if (Array.isArray(options.ignores)) { - for (const item of options.ignores) { - if (item.test(req.url)) return next.handle(req); - } - } - - if (this.isAuth(options)) { - req = this.setReq(req, options); - } else { - ToLogin(options, this.injector); - // Interrupt Http request, so need to generate a new Observable - const err$ = new Observable((observer: Observer>) => { - let statusText = ''; - if (typeof ngDevMode === 'undefined' || ngDevMode) { - statusText = `来自 @delon/auth 的拦截,所请求URL未授权,若是登录API可加入 [url?_allow_anonymous=true] 来表示忽略校验,更多方法请参考: https://ng-alain.com/auth/getting-started#AlainAuthConfig\nThe interception from @delon/auth, the requested URL is not authorized. If the login API can add [url?_allow_anonymous=true] to ignore the check, please refer to: https://ng-alain.com/auth/getting-started#AlainAuthConfig`; - } - const res = new HttpErrorResponse({ - url: req.url, - headers: req.headers, - status: 401, - statusText - }); - observer.error(res); - }); - if (options.executeOtherInterceptors) { - const interceptors = this.injector.get(HTTP_INTERCEPTORS, []); - const lastInterceptors = interceptors.slice(interceptors.indexOf(this) + 1); - if (lastInterceptors.length > 0) { - const chain = lastInterceptors.reduceRight( - (_next, _interceptor) => new HttpAuthInterceptorHandler(_next, _interceptor), - { - handle: (_: HttpRequest) => err$ - } - ); - return chain.handle(req); - } - } - return err$; + // Interrupt Http request, so need to generate a new Observable + return new Observable((observer: Observer>) => { + let statusText = ''; + if (typeof ngDevMode === 'undefined' || ngDevMode) { + statusText = `来自 @delon/auth 的拦截,所请求URL未授权,若是登录API可加入 new HttpContext().set(ALLOW_ANONYMOUS, true) 来表示忽略校验,更多方法请参考: https://ng-alain.com/auth/getting-started#AlainAuthConfig\nThe interception from @delon/auth, the requested URL is not authorized. If the login API can add new HttpContext().set(ALLOW_ANONYMOUS, true) to ignore the check, please refer to: https://ng-alain.com/auth/getting-started#AlainAuthConfig`; } - return next.handle(req); - } + const res = new HttpErrorResponse({ + url: req.url, + headers: req.headers, + status: 401, + statusText + }); + observer.error(res); + }); } diff --git a/packages/auth/src/token/helper.ts b/packages/auth/src/token/helper.ts index 176b0e41e..8bb499540 100644 --- a/packages/auth/src/token/helper.ts +++ b/packages/auth/src/token/helper.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from '@angular/common'; -import { Injector } from '@angular/core'; +import { inject } from '@angular/core'; import { Router } from '@angular/router'; import { AlainAuthConfig } from '@delon/util/config'; @@ -23,13 +23,15 @@ export function CheckJwt(model: JWTTokenModel, offset: number): boolean { } } -export function ToLogin(options: AlainAuthConfig, injector: Injector, url?: string): void { - const router = injector.get(Router); - (injector.get(DA_SERVICE_TOKEN) as ITokenService).referrer!.url = url || router.url; +export function ToLogin(options: AlainAuthConfig, url?: string): void { + const router = inject(Router); + const token = inject(DA_SERVICE_TOKEN) as ITokenService; + const doc = inject(DOCUMENT); + token.referrer!.url = url || router.url; if (options.token_invalid_redirect === true) { setTimeout(() => { if (/^https?:\/\//g.test(options.login_url!)) { - injector.get(DOCUMENT).location.href = options.login_url as string; + doc.location.href = options.login_url as string; } else { router.navigate([options.login_url]); } diff --git a/packages/auth/src/token/jwt/jwt.guard.ts b/packages/auth/src/token/jwt/jwt.guard.ts index cc48c4bea..963b69c4b 100644 --- a/packages/auth/src/token/jwt/jwt.guard.ts +++ b/packages/auth/src/token/jwt/jwt.guard.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Injector, inject } from '@angular/core'; +import { Inject, Injectable, inject } from '@angular/core'; import { CanActivateChildFn, CanActivateFn, CanMatchFn } from '@angular/router'; import { JWTTokenModel } from './jwt.model'; @@ -7,16 +7,13 @@ import { DA_SERVICE_TOKEN, ITokenService } from '../interface'; @Injectable({ providedIn: 'root' }) export class AuthJWTGuardService { - constructor( - @Inject(DA_SERVICE_TOKEN) private srv: ITokenService, - private injector: Injector - ) {} + constructor(@Inject(DA_SERVICE_TOKEN) private srv: ITokenService) {} process(url?: string): boolean { const cog = this.srv.options; const res = CheckJwt(this.srv.get(JWTTokenModel), cog.token_exp_offset!); if (!res) { - ToLogin(cog, this.injector, url); + ToLogin(cog, url); } return res; } diff --git a/packages/auth/src/token/jwt/jwt.interceptor.spec.ts b/packages/auth/src/token/jwt/jwt.interceptor.spec.ts index 44d374781..1d1c18547 100644 --- a/packages/auth/src/token/jwt/jwt.interceptor.spec.ts +++ b/packages/auth/src/token/jwt/jwt.interceptor.spec.ts @@ -1,5 +1,5 @@ -import { HttpClient } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { HttpTestingController, TestRequest, provideHttpClientTesting } from '@angular/common/http/testing'; import { Component, Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; @@ -7,8 +7,9 @@ import { of, catchError } from 'rxjs'; import { AlainAuthConfig, provideAlainConfig } from '@delon/util/config'; +import { authJWTInterceptor } from './jwt.interceptor'; import { JWTTokenModel } from './jwt.model'; -import { provideAuth, withJWT } from '../../provide'; +import { provideAuth } from '../../provide'; import { DA_SERVICE_TOKEN } from '../interface'; function genModel( @@ -30,7 +31,6 @@ describe('auth: jwt.interceptor', () => { TestBed.configureTestingModule({ declarations: [MockComponent], imports: [ - HttpClientTestingModule, RouterTestingModule.withRoutes([ { path: 'login', @@ -38,7 +38,12 @@ describe('auth: jwt.interceptor', () => { } ]) ], - providers: [provideAlainConfig({ auth: options }), provideAuth(withJWT())] + providers: [ + provideHttpClient(withInterceptors([authJWTInterceptor])), + provideHttpClientTesting(), + provideAlainConfig({ auth: options }), + provideAuth() + ] }); if (tokenData) TestBed.inject(DA_SERVICE_TOKEN).set(tokenData); http = TestBed.inject(HttpClient); diff --git a/packages/auth/src/token/jwt/jwt.interceptor.ts b/packages/auth/src/token/jwt/jwt.interceptor.ts index a12db51f1..2f2a2629a 100644 --- a/packages/auth/src/token/jwt/jwt.interceptor.ts +++ b/packages/auth/src/token/jwt/jwt.interceptor.ts @@ -1,26 +1,30 @@ -import { HttpRequest } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { HttpInterceptorFn, HttpRequest } from '@angular/common/http'; +import { inject } from '@angular/core'; -import { AlainAuthConfig } from '@delon/util/config'; +import { AlainConfigService } from '@delon/util/config'; import { JWTTokenModel } from './jwt.model'; -import { BaseInterceptor } from '../base.interceptor'; +import { mergeConfig } from '../../auth.config'; +import { isAnonymous, throwErr } from '../base.interceptor'; import { CheckJwt } from '../helper'; import { DA_SERVICE_TOKEN } from '../interface'; -@Injectable() -export class JWTInterceptor extends BaseInterceptor { - isAuth(options: AlainAuthConfig): boolean { - this.model = this.injector.get(DA_SERVICE_TOKEN).get(JWTTokenModel); - return CheckJwt(this.model as JWTTokenModel, options.token_exp_offset!); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - setReq(req: HttpRequest, _options: AlainAuthConfig): HttpRequest { - return req.clone({ - setHeaders: { - Authorization: `Bearer ${this.model.token}` - } - }); - } +function newReq(req: HttpRequest, model: JWTTokenModel): HttpRequest { + return req.clone({ + setHeaders: { + Authorization: `Bearer ${model.token}` + } + }); } + +export const authJWTInterceptor: HttpInterceptorFn = (req, next) => { + const options = mergeConfig(inject(AlainConfigService)); + + if (isAnonymous(req, options)) return next(req); + + const model = inject(DA_SERVICE_TOKEN).get(JWTTokenModel); + if (CheckJwt(model, options.token_exp_offset!)) return next(newReq(req, model)); + + return throwErr(req, options); +}; diff --git a/packages/auth/src/token/simple/simple.guard.ts b/packages/auth/src/token/simple/simple.guard.ts index 8059531fc..7786968d8 100644 --- a/packages/auth/src/token/simple/simple.guard.ts +++ b/packages/auth/src/token/simple/simple.guard.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Injector, inject } from '@angular/core'; +import { Inject, Injectable, inject } from '@angular/core'; import { CanActivateChildFn, CanActivateFn, CanMatchFn } from '@angular/router'; import { SimpleTokenModel } from './simple.model'; @@ -7,15 +7,12 @@ import { DA_SERVICE_TOKEN, ITokenService } from '../interface'; @Injectable({ providedIn: 'root' }) export class AuthSimpleGuardService { - constructor( - @Inject(DA_SERVICE_TOKEN) private srv: ITokenService, - private injector: Injector - ) {} + constructor(@Inject(DA_SERVICE_TOKEN) private srv: ITokenService) {} process(url?: string): boolean { const res = CheckSimple(this.srv.get() as SimpleTokenModel); if (!res) { - ToLogin(this.srv.options, this.injector, url); + ToLogin(this.srv.options, url); } return res; } diff --git a/packages/auth/src/token/simple/simple.interceptor.spec.ts b/packages/auth/src/token/simple/simple.interceptor.spec.ts index edef77505..7aa193320 100644 --- a/packages/auth/src/token/simple/simple.interceptor.spec.ts +++ b/packages/auth/src/token/simple/simple.interceptor.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { HttpClient } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { HttpTestingController, TestRequest, provideHttpClientTesting } from '@angular/common/http/testing'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { DefaultUrlSerializer, Router } from '@angular/router'; @@ -9,8 +9,9 @@ import { Observable } from 'rxjs'; import { AlainAuthConfig, provideAlainConfig } from '@delon/util/config'; +import { authSimpleInterceptor } from './simple.interceptor'; import { SimpleTokenModel } from './simple.model'; -import { provideAuth, withSimple } from '../../provide'; +import { provideAuth } from '../../provide'; import { DA_SERVICE_TOKEN, ITokenModel, ITokenService } from '../interface'; function genModel(token: string = `123`): SimpleTokenModel { @@ -55,11 +56,13 @@ describe('auth: simple.interceptor', () => { function genModule(options: AlainAuthConfig, tokenData?: SimpleTokenModel): void { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + imports: [RouterTestingModule.withRoutes([])], providers: [ + provideHttpClient(withInterceptors([authSimpleInterceptor])), + provideHttpClientTesting(), provideAlainConfig({ auth: options }), { provide: Router, useValue: mockRouter }, - provideAuth(withSimple()), + provideAuth(), { provide: DA_SERVICE_TOKEN, useClass: MockTokenService } ] }); diff --git a/packages/auth/src/token/simple/simple.interceptor.ts b/packages/auth/src/token/simple/simple.interceptor.ts index a49f1277e..754ba4838 100644 --- a/packages/auth/src/token/simple/simple.interceptor.ts +++ b/packages/auth/src/token/simple/simple.interceptor.ts @@ -1,45 +1,49 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { HttpRequest } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { HttpInterceptorFn, HttpRequest } from '@angular/common/http'; +import { inject } from '@angular/core'; -import { AlainAuthConfig } from '@delon/util/config'; +import { AlainAuthConfig, AlainConfigService } from '@delon/util/config'; import { SimpleTokenModel } from './simple.model'; -import { BaseInterceptor } from '../base.interceptor'; +import { mergeConfig } from '../../auth.config'; +import { isAnonymous, throwErr } from '../base.interceptor'; import { CheckSimple } from '../helper'; import { DA_SERVICE_TOKEN } from '../interface'; -@Injectable() -export class SimpleInterceptor extends BaseInterceptor { - isAuth(_options: AlainAuthConfig): boolean { - this.model = this.injector.get(DA_SERVICE_TOKEN).get() as SimpleTokenModel; - return CheckSimple(this.model as SimpleTokenModel); - } - - setReq(req: HttpRequest, options: AlainAuthConfig): HttpRequest { - const { token_send_template, token_send_key } = options; - const token = token_send_template!.replace(/\$\{([\w]+)\}/g, (_: string, g) => this.model[g]); - switch (options.token_send_place) { - case 'header': - const obj: any = {}; - obj[token_send_key!] = token; - req = req.clone({ - setHeaders: obj - }); - break; - case 'body': - const body = req.body || {}; - body[token_send_key!] = token; - req = req.clone({ - body - }); - break; - case 'url': - req = req.clone({ - params: req.params.append(token_send_key!, token) - }); - break; - } - return req; +function newReq(req: HttpRequest, model: SimpleTokenModel, options: AlainAuthConfig): HttpRequest { + const { token_send_template, token_send_key } = options; + const token = token_send_template!.replace(/\$\{([\w]+)\}/g, (_: string, g) => model[g]); + switch (options.token_send_place) { + case 'header': + const obj: any = {}; + obj[token_send_key!] = token; + req = req.clone({ + setHeaders: obj + }); + break; + case 'body': + const body: any = req.body || {}; + body[token_send_key!] = token; + req = req.clone({ + body + }); + break; + case 'url': + req = req.clone({ + params: req.params.append(token_send_key!, token) + }); + break; } + return req; } + +export const authSimpleInterceptor: HttpInterceptorFn = (req, next) => { + const options = mergeConfig(inject(AlainConfigService)); + + if (isAnonymous(req, options)) return next(req); + + const model = inject(DA_SERVICE_TOKEN).get() as SimpleTokenModel; + if (CheckSimple(model)) return next(newReq(req, model, options)); + + return throwErr(req, options); +}; diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 1fafecf63..ad30c6a49 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { provideHttpClient, withFetch } from '@angular/common/http'; +import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import ngLang from '@angular/common/locales/zh'; import { APP_ID, ApplicationConfig, ErrorHandler, importProvidersFrom } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; @@ -90,7 +90,7 @@ const ngZorroConfig: NzConfig = {}; export const appConfig: ApplicationConfig = { providers: [ { provide: APP_ID, useValue: 'ngAlainDoc' }, - provideHttpClient(withFetch()), + provideHttpClient(withFetch(), withInterceptors([])), provideAnimations(), provideRouter(routes, withComponentInputBinding()), // provideClientHydration(), // 暂时不开启水合,除了编译时间长,还有就是对DOM要求比较高 @@ -119,7 +119,7 @@ export const appConfig: ApplicationConfig = { withTinymceWidget() ] }), - // provideAuth(withJWT(), withLocalStorage()), + // provideAuth(withLocalStorage()), // Thirds provideNuMonacoEditorConfig({ defaultOptions: { scrollBeyondLastLine: false } }), provideTinymce({