— 用 Directive 收斂權限判斷,讓模板乾淨俐落 —
延續 Day19 的設計思維,這篇把「角色(RBAC)→ 資源(Resource-Based)→ 屬性/策略(ABAC/PBAC)」三層權限,落到 Angular 17 + ng-zorro 的實作:
我們用兩個 Directive,把所有 UI 授權判斷收斂起來——
appPermit
(顯示/隱藏):適用「看不見就不能點」的場景(RBAC、Resource-Based、ABAC 都支援)。[appPermitDisable]
(自動 disabled):需要顯示但不可操作時,維持 UX 一致(常見於 ABAC)。提醒:前端只負責體驗與提示。真正的存取控制仍要由後端強制驗證。
這是最小可運作版本,實務上你可以把 can() 換成後端回傳的 policy、或接入 OPA/Casbin。
網站畫面參考:Day20
permission.service.ts
import { Injectable, signal } from '@angular/core';
export interface CurrentUser {
id: string;
roles: string[]; // 例:['Admin','Manager']
permissions: string[]; // 例:['order.view','order.edit']
}
@Injectable({ providedIn: 'root' })
export class PermissionService {
// 可改成從 JWT / 後端 Session 載入
currentUser = signal<CurrentUser | null>(null);
setUser(u: CurrentUser | null) { this.currentUser.set(u); }
hasRole(role: string): boolean {
const u = this.currentUser();
return !!u?.roles?.includes(role);
}
hasPermission(perm: string): boolean {
const u = this.currentUser();
return !!u?.permissions?.includes(perm);
}
/**
* 細粒度 ABAC:依 action + resource 判斷
* 預設策略(示意):只有 owner 能 view/edit 自己的 order
* 可替換為策略表、規則引擎,或後端回傳的政策。
*/
can(action: string, resource?: any): boolean {
const u = this.currentUser();
if (!u) return false;
// 先走中粒度資源權限(若具備就直接通過)
if (this.hasPermission(`order.${action}`)) return true;
// 再走細粒度:針對資料列/屬性檢查
if (resource && 'ownerId' in resource) {
if (action === 'view' || action === 'edit') {
return resource.ownerId === u.id;
}
}
return false;
}
}
語意化地描述「誰可以看到這個元素」。
支援三種輸入:roles(粗)、permissions(中)、action+resource(細)。
app-permit.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, effect, inject, OnDestroy } from '@angular/core';
import { PermissionService } from './permission.service';
type OneOrMany = string | string[];
@Directive({
selector: '[appPermit]',
standalone: true,
})
export class AppPermitDirective implements OnDestroy {
private tpl = inject(TemplateRef<any>);
private vcr = inject(ViewContainerRef);
private perm = inject(PermissionService);
// 三種權限輸入:擇一或混搭,任一條件通過即顯示
@Input('appPermitRoles') roles?: OneOrMany;
@Input('appPermitPermissions') permissions?: OneOrMany;
@Input('appPermitAction') action?: string;
@Input('appPermitResource') resource?: any;
private viewCreated = false;
private run = effect(() => this.render()); // 跟著 signal 更新
private normalize(v?: OneOrMany) { return Array.isArray(v) ? v : (v ? [v] : []); }
private allowed(): boolean {
// 粗:角色
for (const r of this.normalize(this.roles)) {
if (this.perm.hasRole(r)) return true;
}
// 中:資源權限
for (const p of this.normalize(this.permissions)) {
if (this.perm.hasPermission(p)) return true;
}
// 細:ABAC(action + resource)
if (this.action) return this.perm.can(this.action, this.resource);
// 未設定條件 → 預設不顯示,避免誤放權
return false;
}
private render() {
const ok = this.allowed();
if (ok && !this.viewCreated) {
this.vcr.createEmbeddedView(this.tpl);
this.viewCreated = true;
} else if (!ok && this.viewCreated) {
this.vcr.clear();
this.viewCreated = false;
}
}
ngOnDestroy() { this.vcr.clear(); }
}
<!-- 粗:角色 -->
<button nz-button *appPermit="let _; roles: 'Admin'">管理後台</button>
<!-- 中:資源權限 -->
<button nz-button *appPermit="let _; permissions: 'order.create'">新增訂單</button>
<!-- 細:依資料屬性(只能編輯自己的) -->
<button nz-button *appPermit="let _; action: 'edit'; resource: order">編輯</button>
提醒:*appPermit="let _; roles: 'Admin'; permissions: 'order.edit'; action: 'edit'; resource: order" 也可,Directive 會「條件 OR」通過即顯示。
當元素需要顯示,但要禁止操作時使用(例如「你能看見這列,但不能改」)。
app-permit-disable.directive.ts
import { Directive, Input, OnChanges, SimpleChanges, inject, ElementRef, effect } from '@angular/core';
import { PermissionService } from '../services/permission.service';
@Directive({
selector: '[appPermitDisable]',
standalone: true,
})
export class AppPermitDisableDirective implements OnChanges {
private el = inject(ElementRef<HTMLElement>);
private perm = inject(PermissionService);
@Input() appPermitDisable?: { roles?: string|string[], permissions?: string|string[], action?: string, resource?: any };
constructor() { effect(() => this.apply()); }
ngOnChanges(_: SimpleChanges) { this.apply(); }
private normalize(v?: string|string[]) { return Array.isArray(v) ? v : (v ? [v] : []); }
private isAllowed(): boolean {
const cfg = this.appPermitDisable ?? {};
for (const r of this.normalize(cfg.roles)) if (this.perm.hasRole(r)) return true;
for (const p of this.normalize(cfg.permissions)) if (this.perm.hasPermission(p)) return true;
if (cfg.action) return this.perm.can(cfg.action, cfg.resource);
return false;
}
private apply() {
const ok = this.isAllowed();
// disabled:適用原生 button / a[nz-button]
(this.el.nativeElement as any).disabled = !ok;
}
}
<!-- 細:Row 級 → 未授權時 disabled -->
<button nz-button nzType="link"
[appPermitDisable]="{ action: 'edit', resource: order }">
編輯
</button>
<!-- 中:需要刪除權限 -->
<button nz-button nzDanger [appPermitDisable]="{ permissions: 'order.delete' }">刪除</button>
<!-- 粗:只有 Admin 可以操作 -->
<a nz-button [appPermitDisable]="{ roles: 'Admin' }">後台設定</a>
orders.component.ts(standalone)
import { Component } from '@angular/core';
import { NzTableModule } from 'ng-zorro-antd/table';
import { AppPermitDirective } from './app-permit.directive';
import { AppPermitDisableDirective } from './app-permit-disable.directive';
import { PermissionService } from './permission.service';
type User = { id: string; roles: string[]; permissions: string[]; };
const mockUsers: User[] = [
// 案例:Admin — 看得到「管理後台」,也看得到「新增訂單」
{ id: 'u1', roles: ['Admin'], permissions: ['order.view','order.edit','order.create','order.delete'] },
// 案例:Manager — 看不到「管理後台」,但看得到「新增訂單」
{ id: 'u2', roles: ['Manager'], permissions: ['order.view','order.create'] },
// 案例:User — 兩個都看不到(只有查看權)
{ id: 'u3', roles: ['User'], permissions: ['order.view'] },
// 案例:Guest — 什麼都沒有
{ id: 'u4', roles: ['Guest'], permissions: [] },
];
@Component({
standalone: true,
imports: [CommonModule, NzTableModule, AppPermitDirective, AppPermitDisableDirective],
selector: 'app-orders',
template: `
<div class="mb-3 space-x-8">
<!-- 粗:角色 -->
<button nz-button nzType="primary" *appPermit="let _; roles: 'Admin'">管理後台</button>
<!-- 中:資源權限 -->
<button nz-button *appPermit="let _; permissions: 'order.create'">新增訂單</button>
</div>
<nz-table #tb [nzData]="orders" [nzShowPagination]="false" [nzFrontPagination]="false">
<thead>
<tr>
<th>ID</th>
<th>Owner</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of tb.data; trackBy: trackById">
<td>{{ order.id }}</td>
<td>{{ order.ownerId }}</td>
<td>
<!-- 細:只能編輯自己的訂單(沒權限就 disabled) -->
<button nz-button nzType="link" [appPermitDisable]="{ action: 'edit', resource: order }">
編輯
</button>
<!-- 中:需刪除權限才顯示 -->
<button nz-button nzDanger *appPermit="let _; permissions: 'order.delete'">
刪除
</button>
</td>
</tr>
</tbody>
</nz-table>
`,
})
export class OrdersComponent {
currentUser: User | null = null;
orders = [
{ id: 'A001', ownerId: 'u1' }, // Admin 擁有
{ id: 'A002', ownerId: 'u2' }, // Manager 擁有
{ id: 'A003', ownerId: 'u3' }, // User 擁有
];
constructor(private perms: PermissionService) {
this.use(2); // 預設用 Admin,可以手動切換0/1/2
}
use(idx: number) {
this.currentUser = mockUsers[idx];
this.perms.setUser(this.currentUser);
}
trackById = (_: number, x: { id: string }) => x.id;
}
appPermit
:避免「多層 if 判斷」,增加程式碼可讀性跟可維護性。[appPermitDisable]
:需要顯示但禁用,以 一致的 UX 告知使用者「看得到、但不能用」。permissions
、policies
從後端載入(登入後存到 signal/store),確保一致性。can(action, resource)
最好支援多資源(order|invoice|customer
),可把 key 做成 \${entity}.${action}
。signal + effect
,當使用者或權限更新,Directive 會自動重算並更新 DOM。