iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

Angular 進階實務 30天系列 第 20

Day20:實戰篇 – Angular Directive 實作權限控制

  • 分享至 

  • xImage
  •  

— 用 Directive 收斂權限判斷,讓模板乾淨俐落 —

延續 Day19 的設計思維,這篇把「角色(RBAC)→ 資源(Resource-Based)→ 屬性/策略(ABAC/PBAC)」三層權限,落到 Angular 17 + ng-zorro 的實作:

我們用兩個 Directive,把所有 UI 授權判斷收斂起來——

  • appPermit顯示/隱藏):適用「看不見就不能點」的場景(RBAC、Resource-Based、ABAC 都支援)。
  • [appPermitDisable]自動 disabled):需要顯示但不可操作時,維持 UX 一致(常見於 ABAC)。

提醒:前端只負責體驗與提示。真正的存取控制仍要由後端強制驗證。


權限核心:PermissionService

這是最小可運作版本,實務上你可以把 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;
  }
}

結構型 Directive:*appPermit(顯示/隱藏)

語意化地描述「誰可以看到這個元素」。

支援三種輸入: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」通過即顯示。

屬性型 Directive:[appPermitDisable](自動 disabled)

當元素需要顯示,但要禁止操作時使用(例如「你能看見這列,但不能改」)。

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>

範例頁面:與 ng-zorro Table 整合

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;
}

整合與實務建議

  • Directive 落點
    • appPermit:避免「多層 if 判斷」,增加程式碼可讀性跟可維護性。
    • [appPermitDisable]:需要顯示但禁用,以 一致的 UX 告知使用者「看得到、但不能用」。
  • 策略來源
    • 企業系統建議把 permissionspolicies 從後端載入(登入後存到 signal/store),確保一致性。
    • can(action, resource) 最好支援多資源(order|invoice|customer),可把 key 做成 \${entity}.${action}
  • 變更即時生效
    • 我們用 signal + effect,當使用者或權限更新,Directive 會自動重算並更新 DOM。
  • 安全底線
    • 前端永遠不可信。所有敏感操作(編輯、刪除、匯出)後端仍要檢查角色/資源/屬性。
    • UI 隱藏/禁用只是避免誤操作,不是最終防線

小結

  • 用兩個 Directive,把三層權限(角色 / 資源 / 屬性)一次收斂,模板更乾淨、語意更清晰。
    • *appPermit:顯示/隱藏
    • [appPermitDisable]:自動 disabled。
  • 配合 Day19 的分層思維,可以在大型專案中 先粗後細、逐步落地,維持可讀性與擴充性。

上一篇
Day 19:權限管理設計思維
系列文
Angular 進階實務 30天20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言