Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: fix interceptor #1710

Merged
merged 6 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 2 additions & 31 deletions packages/auth/src/provide.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand All @@ -26,38 +23,12 @@ function makeAuthFeature<KindT extends AuthFeatureKind>(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<AuthFeatureKind.Token>,
store?: AuthFeature<AuthFeatureKind.Store>
): EnvironmentProviders {
return makeEnvironmentProviders([type.ɵproviders, (store ?? withLocalStorage()).ɵproviders]);
}

/** Use simple auth type, */
export function withSimple(): AuthFeature<AuthFeatureKind.Token> {
return makeAuthFeature(AuthFeatureKind.Token, [
{
provide: HTTP_INTERCEPTORS,
useClass: SimpleInterceptor,
multi: true
}
]);
}

export function withJWT(): AuthFeature<AuthFeatureKind.Token> {
return makeAuthFeature(AuthFeatureKind.Token, [
{
provide: HTTP_INTERCEPTORS,
useClass: JWTInterceptor,
multi: true
}
]);
export function provideAuth(store?: AuthFeature<AuthFeatureKind.Store>): EnvironmentProviders {
return makeEnvironmentProviders([(store ?? withLocalStorage()).ɵproviders]);
}

/** `cookie` storage */
Expand Down
18 changes: 14 additions & 4 deletions packages/auth/src/token/base.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<T extends ITokenModel>(modelType: new () => T, token: string | null = `123`): any {
Expand Down Expand Up @@ -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)
});
Expand Down
97 changes: 24 additions & 73 deletions packages/auth/src/token/base.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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<any>): Observable<HttpEvent<any>> {
return this.interceptor.intercept(req, this.next);
export function isAnonymous(req: HttpRequest<unknown>, 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<any>, options: AlainAuthConfig): HttpRequest<any>;
export function throwErr(req: HttpRequest<unknown>, options: AlainAuthConfig): Observable<HttpEvent<unknown>> {
ToLogin(options);

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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<HttpEvent<any>>) => {
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<any>) => err$
}
);
return chain.handle(req);
}
}
return err$;
// Interrupt Http request, so need to generate a new Observable
return new Observable((observer: Observer<HttpEvent<any>>) => {
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);
});
}
12 changes: 7 additions & 5 deletions packages/auth/src/token/helper.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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>(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]);
}
Expand Down
9 changes: 3 additions & 6 deletions packages/auth/src/token/jwt/jwt.guard.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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>(JWTTokenModel), cog.token_exp_offset!);
if (!res) {
ToLogin(cog, this.injector, url);
ToLogin(cog, url);
}
return res;
}
Expand Down
15 changes: 10 additions & 5 deletions packages/auth/src/token/jwt/jwt.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
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(
Expand All @@ -30,15 +31,19 @@ describe('auth: jwt.interceptor', () => {
TestBed.configureTestingModule({
declarations: [MockComponent],
imports: [
HttpClientTestingModule,
RouterTestingModule.withRoutes([
{
path: 'login',
component: MockComponent
}
])
],
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>(HttpClient);
Expand Down
42 changes: 23 additions & 19 deletions packages/auth/src/token/jwt/jwt.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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>(JWTTokenModel);
return CheckJwt(this.model as JWTTokenModel, options.token_exp_offset!);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
setReq(req: HttpRequest<any>, _options: AlainAuthConfig): HttpRequest<any> {
return req.clone({
setHeaders: {
Authorization: `Bearer ${this.model.token}`
}
});
}
function newReq(req: HttpRequest<unknown>, model: JWTTokenModel): HttpRequest<unknown> {
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>(JWTTokenModel);
if (CheckJwt(model, options.token_exp_offset!)) return next(newReq(req, model));

return throwErr(req, options);
};
9 changes: 3 additions & 6 deletions packages/auth/src/token/simple/simple.guard.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand Down
Loading