ピクルスが少しあります。ルートガード(CanActivate
インターフェイスを実装)を使用して、ユーザーに特定のルートへのアクセスが許可されているかどうかを確認しています。
_const routes: Routes = [
{
path: '',
component: DashboardViewComponent
},
{
path: 'login',
component: LoginViewComponent
},
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']},
canActivate: [RouteGuard]
},
{
path: '**',
component: ErrorNotFoundViewComponent
}
];
_
これで、 '/ protected/foo'ルートがアクティブにならないように保護できますが、ユーザーにアクセスしようとしているルートが禁止されていることを伝えたいと思います(403 Forbiddenに似ています)。
問題:エラールートにリダイレクトせずに、この特別なエラービューをユーザーに表示するにはどうすればよいですか私が見つけた多くのソースが推奨するオプションはどれですか? そして、どうすればRouteGuard
を使用できますか?実際に禁止されたルートをロードせずに、FooViewComponent
内のアクセスを確認し、異なるビューを表示すると、最初にRouteGuard
を持ちます。
理想的には、RouteGuard
がcanActivate()
メソッドでfalseを返すだけでなく、コンポーネントをErrorForbiddenViewComponent
に完全に置き換えたいと思います。しかし、私はそれを行う方法がわからない、またはそれがイベント可能です。代替案はありますか?
これが私のルートガードの外観です。
_import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {AuthService} from '../services/auth.service';
@Injectable()
export class RouteGuard implements CanActivate {
constructor(
private router: Router,
private auth: AuthService
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const { auth, router } = this;
const { allowAccessTo } = next.data;
const identity = auth.getIdentity();
if (
identity &&
allowAccessTo.indexOf(identity.role)
) {
// all good, proceed with activating route
return true;
}
if (identity) {
// TODO show ErrorForbiddenViewComponent instead of redirecting
console.log('403 Forbidden >>', next);
}
else {
// not logged in: redirect to login page with the return url
const [returnUrl, returnQueryParams] = state.url.split('?');
console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
}
return false;
}
}
_
そのため、ルートの読み込みを防止しているだけですが、リダイレクトしていません。ログに記録されていない訪問者のみをログインルートにリダイレクトします。
推論:
誰かがこれに対して何らかの解決策を持っていますか?また、Angular 2+が長い間存在していなかったので、これまで誰もこのような状況はなかったのでしょうか?リダイレクトで大丈夫ですか?
また、現在FooViewComponent
を同期的に使用していますが、今後変更される可能性があることに注意してください。
私はかつて同様の問題に取り組んでいました。
stackblitz poc 私が作成した場所の共有-
/auth
ルートにはPermissionGuardService
ガードが提供されます)ガードはユーザータイプを評価し、それに応じてリダイレクト/エラーを処理しています。
ユースケースは次のとおりです-
shows a toast with log in message
)shows a toast with unauthorised message
)show a toast with success messaage
)ユーザーをローカルストレージに保存しました。
特別な処理が必要な場合はお知らせください。コードベースを更新します。
乾杯!
RouteGuardは、モーダルウィンドウに使用しているサービスをインジェクトでき、.canActivate()
はリダイレクトなしでモーダルをポップして、アプリの現在の状態を乱すことなくユーザーに通知できます。
Toastrとそのangular=ラッパーを使用します。これはmodelessのポップアップを作成するためです。 。
angular2の例 を質問のコメントでTarun Lalwaniが提供した後、および Angular docs 私のコードにそれを適用することができました:
ルートを指定するときにRouteGuard
を使用しなくなりました。
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']}, // admin only
canActivate: [RouteGuard]
},
代わりに、特別なRouteGuardComponent
を作成しました。これを使用する方法を次に示します。
{
path: 'protected/foo',
component: RouteGuardComponent,
data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},
これはRouteGuardComponent
のコードです:
@Component({
selector: 'app-route-guard',
template: '<ng-template route-guard-bind-component></ng-template>
// note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {
@ViewChild(RouteGuardBindComponentDirective)
bindComponent: RouteGuardBindComponentDirective;
// ^^ and here we bind to that directive instance in template
constructor(
private auth: AuthService,
private route: ActivatedRoute,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit() {
const {auth, route, componentFactoryResolver, bindComponent} = this;
const {component, allowAccessTo} = route.snapshot.data;
const identity = auth.getIdentity();
const hasAccess = identity && allowAccessTo.indexOf(identity.role);
const componentFactory = componentFactoryResolver.resolveComponentFactory(
hasAccess ?
component : // render component
ErrorForbiddenViewComponent // render Forbidden view
);
// finally use factory to create proper component
routeGuardBindComponentDirective
.viewContainerRef
.createComponent(componentFactory);
}
}
また、これには特別なディレクティブを定義する必要があります(これは他の方法でも実行できると確信していますが、Angular docs)の動的コンポーネントの例を適用したところです:
@Directive({
selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
それは私自身の質問に対する完全な答えではありませんが(開始点です)、誰かがより良い何かを提供する場合(つまり、canActivate
を使用する方法と遅延ロードする能力)、私はそれを取ることを確認しますアカウントに。
最近、同じ問題に遭遇しました。最終的に、CanActivate
ガードを使用してこれを行うことができなかったため、<router-outlet>
を保持するコンポーネントに認証ロジックを実装しました。
そのテンプレートは次のとおりです。
<div class="content">
<router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
<div class="message">
<mat-icon>lock</mat-icon>
<span>Access denied.</span>
</div>
</ng-template>
そしてそのソースコード:
import { ActivatedRoute, ActivationStart, Router } from '@angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '@angular/core';
@Component({
selector: 'app-panel-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {
/**
* A stream of flags whether access to current route is permitted.
*/
accessAllowed$: Observable<boolean>;
constructor(
permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
route: ActivatedRoute,
router: Router,
) {
const streams: Observable<boolean>[] = [];
/*
The main purpose of this component is to replace `<router-outlet>` with "Access denied"
message, if necessary. Such logic will be universal for all possible route components, and
doesn't require any additional components - you will always have at least one component with
`<router-outlet>`.
This component contains `<router-outlet>`, which by definition means that all possible authorisable
routes are beneath it in the hierarchy.
This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route
itself in this component will always be the parent route of the one we need to process.
So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
router events.
However, by the time an instance of this component is constructed, all routing events will have been
triggered. This is especially important in case user loads the page on this route.
To solve that, we can merge two streams, the first one of which will be a single access flag
for **activated route**, and the second will be a stream of flags, emitted from router
events (e.g. caused by user navigating through app).
This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the
last value emitted from the stream created from router events will be `true`.
*/
const deepestChild = this.findDeepestTreeNode(route);
const currentData = deepestChild.routeConfig.data;
// `data.authActions` is just an array of strings in my case
if (currentData &&
currentData.authActions &&
Array.isArray(currentData.authActions) &&
currentData.authActions.length > 0) {
streams.Push(
// `hasPermissions(actions: strings[]): Observable<boolean>`
permissions.hasPermissions(currentData.authActions).pipe(take(1))
);
} else {
// If the route in question doesn't have any authorisation logic, simply allow access
streams.Push(of(true));
}
streams.Push(router.events
.pipe(
filter(e => e instanceof ActivationStart),
switchMap((event: ActivationStart) => {
const data = event.snapshot.data;
if (data.authActions &&
Array.isArray(currentData.authActions) &&
data.authActions.length > 0) {
return permissions.hasPermissions(data.authActions);
}
return of(true);
}),
));
this.accessAllowed$ = merge(...streams);
}
/**
* Returns the deepest node in a tree with specified root node, or the first
* encountered node if there are several on the lowest level.
*
* @param root The root node.
*/
findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
const findDeepest = (node: T, level = 1): [number, T] => {
if (node.children && node.children.length > 0) {
const found = node.children.map(child => findDeepest(child as T, level + 1));
found.sort((a, b) => a[0] - b[0]);
return found[0];
} else {
return [level, node];
}
};
return findDeepest(root)[1];
}
}
interface TreeNodeLike {
children?: TreeNodeLike[];
}
ソースコードのコメントでアプローチを説明しましたが、要するに、ルーターイベントを使用してroute.data
の認証データにアクセスし、アクセスが拒否された場合は<router-outlet>
をエラーメッセージに置き換えます。