我有一点腌渍。
我正在使用Route guard(实现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)。 / p>
问题:
如何向用户显示此特殊错误视图,而不将其重定向到错误路由哪个接缝是我找到的众多来源的首选选项?
如何在不实际加载禁止路由的情况下仍然使用我的RouteGuard
,因为如果我检查FooViewComponent
内的访问权限并显示不同的视图,那就有点失败了{首先是{1}}。
理想情况下,我希望我的RouteGuard
不仅可以在RouteGuard
方法中返回false,还可以使用canActivate()
完全替换组件。但我不知道该怎么做,或者事件是否可能。任何替代方案?
这就是我的护卫队现在的样子:
ErrorForbiddenViewComponent
所以我只是阻止路由加载,但我没有重定向。我只将未登录的访问者重定向到登录路由。
推理:
有没有人为此提供某种解决方案?我也想知道,在Angular 2+出现这么长时间后,没有人遇到这种情况怎么样?每个人都可以重定向吗?
另请注意,虽然我目前正在同步使用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;
}
}
,但未来可能会有所改变!
答案 0 :(得分:4)
我曾经研究过类似的问题。
分享我创建的stackblitz poc -
/auth
路由随PermissionGuardService
后卫提供)警卫正在评估用户类型并相应地处理重定向/错误。
用例是 -
shows a toast with log in message
)shows a toast with unauthorised message
)show a toast with success messaage
)我已将用户存储在本地存储中。
如果您需要特殊处理,请告诉我,我将更新代码库。
干杯!
答案 1 :(得分:2)
您的RouteGuard可以为模态窗口注入您正在使用的任何服务,.canActivate()
可以弹出模态而无需重定向,以通知用户而不会干扰应用程序的当前状态。
我们使用toastr及其角度包装器,因为它创建了一个无模式弹出窗口,在这么多秒后自动解除,不需要OK / Cancel按钮。
答案 2 :(得分:1)
我最近遇到了同样的问题。最后,我无法使用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>
。 / p>
答案 3 :(得分:0)
在查看Tarun Lalwani提供的angular2 example后,在对问题的评论进行深入研究后{I} {}我设法将其应用于我的代码:
指定路线时我不再使用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文档应用了动态组件示例):
@Directive({
selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
这不是我自己的问题的完整答案(但它是一个开始),所以如果有人提供更好的东西(即仍然使用canActivate
的方式和延迟加载的能力)我将确保考虑到这一点。