文章分類/Infragistics
前端三大巨頭,最大的共通點即是使用 component-base 的開發結構,但使用 Angular 的開發者一定熟知 CLI 的便利性,假如沒使用過的話也無仿,大概的意思就是,透過指令產生 component template code,並自動新增相依路徑於必要檔案,十分方便。IgniteUI 本身也是採用 Angular,所以非常了解 Angular CLI 的便利性,也延用了這個概念,讓使用 IgniteUI 的開發者也能透過 Ignite CLI,自動幫專案新增 template,甚至可以直接建立一個 IgniteUI 專案。
Ignite CLI 會非常建議使用,比如建立專案時會有很好的程式碼配置,像是登入功能會獨立抽成 module,採用子路由,error handle 的頁面配置,新增 component 會有 sample code,諸如此類的小貼心設計,也省去了很多初始化配置,或是查詢並複製貼上的時間。
首先我們先安裝 Ignite CLI,所以:
npm install -g igniteui-cli
安裝成功,輸入指令測試,所以:
ig
直接輸入 ig 指令會有引導式輸入設定,或是直接提供參數建立專案,所以:
ig new newAngularProject --framework=angular --type=igx-ts --template=side-nav-auth
可以額外提供:
接著於專案中建立功能頁面,可以先查詢 template ID,再新增,所以:
ig list // 查看所有的 component templateig add grid employee-salary
如果不要切 router,可以額外加入 –skr
啟動專案,所以:
ig start
接著我們來看一下 side-nav-auth 所產生的程式碼:
style.scss 中,預設幫我們加入了 palette,可以透過調整 primary、secondary 參數定義主色、副色。
@import "~minireset.css/minireset"; @import "~igniteui-angular/lib/core/styles/themes/index"; $primary: #731963 !default; $secondary: #ce5712 !default; $app-palette: igx-palette($primary, $secondary); @include igx-core(); @include igx-typography($font-family: $material-typeface, $type-scale: $material-type-scale); @include igx-theme($app-palette); html, body { height: 100%; }
app-routing 中,可以看到 error page、not found page 已經定義好,新增的 component 也切好 router,更進一步看也有特別將 ErrorRoutingModule 拆出來。
export const routes: Routes = [ { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: 'home', component: HomeComponent, data: { text: 'Home' } }, { path: 'error', component: UncaughtErrorComponent }, { path: 'employeeinformation', component: EmployeeInformationComponent, data: { text: 'EmployeeInformation' } }, { path: 'employee-salary', component: EmployeeSalaryComponent, data: { text: 'employee-salary' } }, { path: '**', component: PageNotFoundComponent } // must always be last ]; @NgModule({ imports: [RouterModule.forRoot(routes), ErrorRoutingModule], exports: [RouterModule, ErrorRoutingModule] }) export class AppRoutingModule { }
我們進一步往 ErrorRoutingModule 中檢視,Angular 預設會將錯誤透過 console 顯示出來,這邊可以看到,針對 ErrorHandle token,特別定義了 GlobalErrorHandlerService 讓開發者實作錯誤處理,並在 production 的時後才啟用。
const providers: Provider[] = []; if (environment.production) { // register prod error handler providers.push({ provide: ErrorHandler, useClass: GlobalErrorHandlerService }); } @NgModule({ declarations: [ PageNotFoundComponent, UncaughtErrorComponent ], providers }) export class ErrorRoutingModule { }
不過最重要的莫過於是已經實作好的 Authentication,先打開 AppModule,我們可以看到被註解的三大主流登入方式,Facebook、Google、Microsoft。這邊試著把 Google 取消註解,並在正式使用時帶入 Google OAuth ClientID,即可以在 Login 的跳窗中看到 Google 登入驗證被顯示出來了。
export class AppModule { constructor(private externalAuthService: ExternalAuthService) { this.externalAuthService.addGoogle('<CLIENT_ID>'); // this.externalAuthService.addMicrosoft('<CLIENT_ID>'); // this.externalAuthService.addFacebook('<CLIENT_ID>'); } }
基本上三種登入方式都是採用 OpenID 協定來實作,我們以 addGoogle 來進一步查看做了什麼事。
從程式碼中我們可以看到,與 google 認證需要的 config 已經定義好了,並初始化了 GoogleProvider,這邊要注意 redirect_url 預設是幫我們設定 root 網域加上 redirect-google 路由,這邊要調整成與您的 google provider 設定的轉導網址一樣,可以修改下圖中的 ExternalAuthRedirectUrl 所定義的路由。如果對 OpenID 的流程實作有興趣,可以進一步往 Google Provider 中查看具體的實作方式,相信在其他的專案中有很有參考價值。
不過大致上來說,我們可以知道 IgniteUI 已經幫我們把三種熱門的登入流程建置好,只差我們將 client ID 填入即可。當我們實際按下 SIGN UP GOOGLE,會觸發圖片最下方的 login 事件,並執行 GoogleProvider 中實作的登入流程。
public addGoogle(clientID: string) { const googleConfig: ExternalAuthConfig = { provider: ExternalAuthProvider.Google, stsServer: 'https://accounts.google.com', client_id: clientID, scope: 'openid email profile', redirect_url: this.getAbsoluteUrl(ExternalAuthRedirectUrl.Google), response_type: 'id_token token', post_logout_redirect_uri: '/', post_login_route: 'redirect', auto_userinfo: false, max_id_token_iat_offset_allowed_in_seconds: 30 }; this.providers.set( ExternalAuthProvider.Google, new GoogleProvider(this.oidcConfigService, this.oidcSecurityService, googleConfig) ); } public addFacebook(clientID: string) { const fbConfig: ExternalAuthConfig = { client_id: clientID, redirect_url: ExternalAuthRedirectUrl.Facebook } as ExternalAuthConfig; this.providers.set( ExternalAuthProvider.Facebook, new FacebookProvider(fbConfig, this.router) ); } public addMicrosoft(clientID: string) { const msConfig: ExternalAuthConfig = { provider: ExternalAuthProvider.Microsoft, stsServer: 'https://login.microsoftonline.com/consumers/v2.0/', client_id: clientID, scope: 'openid email profile', redirect_url: this.getAbsoluteUrl(ExternalAuthRedirectUrl.Microsoft), response_type: 'id_token token', post_logout_redirect_uri: '/', post_login_route: '', auto_userinfo: false, max_id_token_iat_offset_allowed_in_seconds: 1000 }; this.providers.set( ExternalAuthProvider.Microsoft, new MicrosoftProvider(this.oidcConfigService, this.oidcSecurityService, msConfig) ); } public login(provider: ExternalAuthProvider) { const extProvider = this.providers.get(provider); if (extProvider) { this.activeProvider = provider; extProvider.login(); } }
當登入完成後,Google 即會跳轉回來,如同方才提到的 return url 預設會以 redirect-google 做為路由,此時可以在 AuthenticationRoutingModule 中看到,三種登入方式都會進到 RedirectComponent 中,並執行 authService.loginWith 的事件。
這個動作是要把驗證過的用戶資訊傳給自己的 API,進一步取得用戶於系統中有才有資料,並跳轉到 ProfileComponent 的頁面中,即代表登入成功。不過這個部份 IgniteUI 考慮到操作的完整性,先以 BackendProvider 構建一些 fake 資料模擬 API 的行為來處理,此做法的好處是,開發者只需要將 BackendProvider 中的實作內容,移至 API,即可與專案完美的銜接。
const authRoutes: Routes = [ { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] }, { path: ExternalAuthRedirectUrl.Google, component: RedirectComponent, data: { provider: ExternalAuthProvider.Google } }, { path: ExternalAuthRedirectUrl.Facebook, component: RedirectComponent, data: { provider: ExternalAuthProvider.Facebook } }, { path: ExternalAuthRedirectUrl.Microsoft, component: RedirectComponent, data: { provider: ExternalAuthProvider.Microsoft } } ]; @NgModule({ imports: [ RouterModule.forChild(authRoutes) ], exports: [ RouterModule ] }) export class AuthenticationRoutingModule {}
@Component({ template: '<p>Signing in...</p>' }) export class RedirectComponent implements OnInit { private provider: ExternalAuthProvider; constructor( route: ActivatedRoute, private router: Router, private user: UserService, private authService: AuthenticationService, private externalAuthService: ExternalAuthService) { this.provider = route.snapshot.data[routeData].provider as ExternalAuthProvider; } async ngOnInit() { const userInfo: ExternalLogin = await this.externalAuthService.getUserInfo(this.provider); const result = await this.authService.loginWith(userInfo); if (!result.error) { this.user.setCurrentUser(result.user!); this.router.navigate(['/profile']); } else { alert(result.error); } } }
基於頁面保護的機制,我們也可以在 AuthenticationRoutingModule 中看到 AuthGuard 應用在 ProfileComponent,意即在 AuthGuard 中驗證失敗即會 redirect 到 home 頁面,預設是以用戶資料的取得與否來判斷,但我們也可以進一步在 AuthGuard 中實作自己的驗證機制。
@Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private router: Router, private userService: UserService) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (this.userService.currentUser) { return true; } this.router.navigate([''], { queryParams: { returnUrl: state.url } }); return false; } }
最後我們來看 AuthenticationModule,2 個重要的部份。一個是 BackendProvider,務必在開發時移除,否則 http 無法有效 reach 至 API。
另一個我們看到 JwtInterceptor,這邊預設是以 HttpInterceptor 的技巧,在 http 出去之前,統一附加 token 於 header 的方式,如此在系統中所有的 http 請求,只需要專注於 API 的存取即可,如果預設邏輯不符需求,也可以自製一個 Interceptor 將 JwtInterceptor 置換即可,非常方便。
@NgModule({ imports: [ CommonModule, HttpClientModule, ReactiveFormsModule, AuthModule.forRoot(), AuthenticationRoutingModule, IgxToggleModule, IgxRippleModule, IgxDialogModule, IgxInputGroupModule, IgxIconModule, IgxAvatarModule, IgxButtonModule, IgxDropDownModule ], declarations: [ LoginBarComponent, LoginComponent, RedirectComponent, RegisterComponent, LoginDialogComponent, ProfileComponent ], providers: [ AuthGuard, OidcConfigService, { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, // TODO: DELETE THIS BEFORE PRODUCTION! BackendProvider ], exports: [ LoginBarComponent, LoginComponent, RedirectComponent, RegisterComponent, LoginDialogComponent, ProfileComponent ] }) export class AuthenticationModule { }
@Injectable({ providedIn: 'root' }) export class JwtInterceptor implements HttpInterceptor { constructor(private userService: UserService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const currentUser = this.userService.currentUser; if (currentUser && currentUser.token) { request = request.clone({ setHeaders: { Authorization: `Bearer ${currentUser.token}` } }); } return next.handle(request); } }