6-9.Angular 入門教學 - 路由守衛,檢查是否有權限進入該頁面

Angular 入門教學


這是我們的根路由表

const routes: Routes = [
  {
    path: 'login',
    loadChildren: () => import('./login/login.module').then(m => m.LoginModule)
  },
  {
    path: 'manage',
    loadChildren: () => import('./manage/manage.module').then(m => m.ManageModule)
  },
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: '**', component: NotfoundComponent },
];

一般而言login頁面的功能不需要任何權限就能使用,而manage要登入後才能使用

那這個時候我們可以做一個路由守衛來做判斷,所以先產生一個路由守衛

ng g g auth

接著會產生路由守衛的基本格式檔

canActivate(
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

  return true;
}

這個裡面就可以寫你想要判斷的邏輯,那我們這邊就來判斷使用者是否有登入

canActivate(
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

  const jwt = localStorage.getItem('jwt');
  if (jwt) {
    const payload = JSON.parse(window.atob(jwt.split('.')[1]));
    const exp = new Date(Number(payload.exp) * 1000);
    if (new Date() > exp) {
      alert('JWT已過期,請重新登入');
      return false;
    }
  } else {
    alert('尚未登入');
    return false;
  }

  return true;
}

這邊的邏輯是,我先判斷localStorage是否存有jwt,如果沒有代表沒登入,跳個提示,並回傳false

如果回傳false將不會進到該頁面,反之true才可以正常進入

那如果有jwt,我們就將它base64解碼,並取出到期時間,如果大於現在時間代表過期了,就跳個提示,一樣回傳false

接著我們將我們做好的路由守衛,擺在我們想判斷的路由位置todo-routing.module.ts

const routes: Routes = [
  {
    path: 'list',
    component: TodoListComponent,
    resolve: { dataList: TodoListResolver }
  },
  {
    path: 'content/:id',
    children: [
      {
        path: ':action',
        component: TodoContentComponent,
        canActivate: [AuthGuard],
        resolve: { todoList: TodoResolver }
      },
      { path: '', redirectTo: 'All', pathMatch: 'full' }
    ]
  },
  { path: '', redirectTo: 'list', pathMatch: 'full' }
];

我們將canActivate: [AuthGuard]放在TodoContentComponent這邊,此時可以在沒有權限下,試試登入這一頁

這時候會發現一件事,雖然有正確的做出判斷,跳出了提示,但我頁面就卡在原地無法進去

所以通常我們會有UrlTree的寫法來寫

constructor(private router: Router) { }
canActivate(
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

  const jwt = localStorage.getItem('jwt');
  if (jwt) {
    const payload = JSON.parse(window.atob(jwt.split('.')[1]));
    const exp = new Date(Number(payload.exp) * 1000);
    if (new Date() > exp) {
      alert('JWT已過期,請重新登入');
      return this.router.createUrlTree(['/login']);
    }
  } else {
    alert('尚未登入');
    return this.router.createUrlTree(['/login']);
  }

  return true;
}

我們注入Router,並在本來回傳false的地方,改回傳this.router.createUrlTree(['/login']);

這樣就會變成轉址到這個路由,也就是說如果回傳false,將會停止進入路由,但這效果可能不是我們這邊想要的

因為使用者看起來會覺得程式當掉的感覺,所以我們改成將沒有權限的人返回登入頁

接著我們登入的路由守衛,應該是判斷整個manage才對,所以我們將守衛移到app-routing.module.ts

const routes: Routes = [
  {
    path: 'login',
    loadChildren: () => import('./login/login.module').then(m => m.LoginModule)
  },
  {
    path: 'manage',
    canActivate: [AuthGuard],
    loadChildren: () => import('./manage/manage.module').then(m => m.ManageModule)
  },
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: '**', component: NotfoundComponent },
];

但其實這樣並不是正確的設置,因為這樣設置它其實只判斷第一次載入manage

我們進來頁面後,子路由頁面的切換,路由守衛是不會進行判斷

canActivate只會判斷設置的那個位置的進入,兒子的切換是不會做判斷的

所以會變成,當你沒了權限,你依然可以切換子路由,大家可以自己試一試看看,我這邊的登入權限大概一分鐘至兩分鐘會失效

所以我們要改一個設定方式,這邊angular有提供另一個canActivateChild的設定方式

這樣的設定,可以在子路由切換時都做判斷,但我們這邊要回頭修改一下auth.guard.ts

canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
  return this.canActivate(childRoute, state);
}

增加一個canActivateChild,開頭也要多implements一個CanActivateChild 

export class AuthGuard implements CanActivate, CanActivateChild{}

那如果你子路由沒有額外的判斷,我們return的地方就只要回傳跟canActivate一樣的判斷就好時,就可以直接回傳this.canActivate(childRoute, state)即可

最後我們將路由表修改一下設置

const routes: Routes = [
  {
    path: 'login',
    loadChildren: () => import('./login/login.module').then(m => m.LoginModule)
  },
  {
    path: 'manage',
    canActivateChild: [AuthGuard],
    loadChildren: () => import('./manage/manage.module').then(m => m.ManageModule)
  },
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: '**', component: NotfoundComponent },
];

這樣進入後再等個一分鐘試試看,就會發現一換頁就會被導回登入的頁面

如此就簡單完成了一個是否有登入權限的判斷,那未來可能會有更多的權限判斷,就可以用這個方法,依照自己的需求去打造對應路由守衛


這邊要特別提一下,如果是剛接觸web應用或前後端分離的新手,可能會有個誤區

就是,這是權限驗證的功能是吧?我們是否就是用這個方式禁止沒有權限的人進入我的管理頁面?

答案是否定的,簡單來說,前端所有的驗證,都不是在禁止使用者進入或使用某個沒有權限的功能頁面

充其量其實只是提升使用者體驗的一環,因為前端的程式都是在你電腦上跑的

所以我想要修改什麼都可以直接修改,前端程式並沒有能力真正禁止使用者的行為

也就是說上面的路由守衛,其實只是讓正常的使用者知道,喔~我現在沒有登入,然後我們程式就貼心的幫使用者導到登入畫面

不會說今天誤入的沒有權限的頁面,然後就卡死在這邊,而是會貼心地告訴使用者原因,並做導向到正確的頁面

所以說今天路由守衛並不是防止你進入這頁,而是要貼心的提醒你沒這頁的權限,並引導你至正確的頁面的功用

也就是為什麼,我上面的jwt僅僅只判斷有沒有到期而已,因為我並不是真的要判斷這個jwt是否是真的有效的

只是在判斷正常使用者的情況下,jwt如果到期就代表沒權限了

至於自己修改一個假的但沒有到期的jwt,雖然能過得了路由守衛,但過不了後端的權限驗證

此時,可能因為沒寫對應的判斷,畫面可能會卡在那裡,不過我們也沒必要提升"非正常使用者"的體驗,因此不用理這塊

結論,所有的權限驗證都是後端在負責的,前端的驗證,只是在提升使用者體驗而已

前端只要負責,在正常使用者使用的情況下,可以順順的操作即是好的設計

 

範例檔:下載




Copyright © 凱哥寫程式 2022 | Powered by TalllKai ❤