美文网首页Angular 8
Angular 8 配置 oidc-client

Angular 8 配置 oidc-client

作者: bei6 | 来源:发表于2019-04-08 18:00 被阅读16次

    配置相对较为繁琐,最后会放上 Github 源码地址

    新建一个 ng 项目

    ng new angular-oidc

    进入目录 cd angular-oidc

    安装 oidc-client

    npm i oidc-client --save
    

    配置 oidc-client 参数

    打开 environment.ts 将下面的代码覆盖原来的内容

    import { WebStorageStateStore } from "oidc-client";
    
    export const environment = {
      production: false,
      authConfig: {
        authority: "http://localhost:57001",
        client_id: "query",
        redirect_uri: "http://localhost:4200/login-callback",
        response_type: "id_token token",
        scope: "openid profile",
        post_logout_redirect_uri: "http://localhost:4200",
        accessTokenExpiringNotificationTime: 4,
        filterProtocolClaims: true,
        silentRequestTimeout: 10000,
        loadUserInfo: true,
        userStore: new WebStorageStateStore({ store: window.localStorage }),
      },
    };
    

    需要修改的几个参数:

    • authority: 认证服务器,需要修改为自己的认证服务器
    • client_id: 客户端 id ,按照约定修改即可
    • redirect_uri: 认证服务器回调的客户端页面
    • post_logout_redirect_uri: 登出回调链接

    模块划分

    这里我们把模块划分为2块: 1) 游客模块 2) 用户模块

    默认的壳组件所在的 module 作为游客模块, 另外还需要构建一个用户模块

    游客模块

    为了方便理解, 游客模块创建一个欢迎页, 点击继续按钮访问用户模块.

    1. 创建一个欢迎页

    没什么特别的作用, 就是为了方便理解单独设立的一个交互页面.

    ng g c public/index
    

    修改 index.component.html

    <h3>WELLCOME TO ANGULAR OIDC</h3>
    <input type="button" value="visit" (click)="visitAuth()">
    

    修改 index.component.ts

    import { Component, OnInit } from "@angular/core";
    import { Router } from "@angular/router";
    
    @Component({
      selector: "app-index",
      templateUrl: "./index.component.html",
      styleUrls: ["./index.component.less"],
    })
    export class IndexComponent implements OnInit {
      constructor(private _router: Router) {}
    
      ngOnInit() {}
    
      public visitAuth(): void {
        this._router.navigate(["auth"]);
      }
    }
    
    

    2. 创建一个回调页

    回调页是用户 oidc 认证结束后的回调, 起到一个过度的作用(目前先空着)

    ng g c public/login-callback
    

    3. 配置路由

    打开 app-routing.module.ts, 对照修改

    import { NgModule } from "@angular/core";
    import { Routes, RouterModule } from "@angular/router";
    import { IndexComponent } from "./public/index/index.component";
    import { LoginCallbackComponent } from "./public/login-callback/login-callback.component";
    
    const routes: Routes = [
      {
        path: "",
        pathMatch: "full",
        component: IndexComponent,
      },
      {
        path: "login-callback",
        component: LoginCallbackComponent,
      },
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
    })
    export class AppRoutingModule {}
    

    启动程序 ng s -o, 这时候已经能看到一点点信息了, 不过还没有 home 路由, 下面来配置一下

    用户模块

    1. 添加一个 auth 模块

    ng g m auth/auth --flat
    

    --flat:在一个单独的文件夹创建

    2. 将 auth 模块添加到壳组件

    打开 app-module.ts, 主要修改一下内容

    import { AuthModule } from "./auth/auth.module";
    ...
    
    imports: [..., AuthModule],
    

    3. 添加 auth "壳组件"

    ng g c auth/auth
    

    4. 添加 auth 模块的路由

    ng g m auth/auth-routing --flat
    

    修改 auth-routing.module.ts 内容如下:

    import { NgModule } from "@angular/core";
    import { RouterModule, Routes } from "@angular/router";
    import { AuthComponent } from "./auth/auth.component";
    
    const routes: Routes = [
      {
        path: "home",
        component: AuthComponent,
      },
    ];
    
    @NgModule({
      exports: [RouterModule],
    })
    export class AuthRoutingModule {}
    

    5. 修改 app-routing.module.ts 添加 home 路由

    const routes: Routes = [
      {
        path: "",
        pathMatch: "full",
        component: IndexComponent,
      },
      {
        path: "login-callback",
        component: LoginCallbackComponent,
      },
      {
        path: "home",
        component: AuthComponent,
      },
    ];
    

    ctrl + c -> y 停止之前启动项目的终端, ng s 重新启动项目

    此时的项目已经可以从游客路由跳转至用户路由,但我们是不允许游客默认访问用户路由的, 这时候就应该 守卫(Guard) 登场了。

    配置守卫(Guard)

    1. 添加 auth.service (认证相关的函数)

    ng g s auth/auth --flat
    

    替换 auth.service.ts 内容:

    import { Injectable, EventEmitter } from '@angular/core';
    import { environment } from 'src/environments/environment';
    import { UserManager, User } from 'oidc-client';
    import { Observable, from } from 'rxjs';
    
    
    @Injectable({
      providedIn: 'root'
    })
    export class AuthService {
    
      // 大多数 oidc-client 操作都在其中
      private manager: UserManager = new UserManager(environment.authConfig);
      // private manager: UserManager = undefined;
    
      // 登录状态改变事件
      public loginStatusChanged: EventEmitter<User> = new EventEmitter();
      // localStorage 中存放用户信息的 Key
    
      private userKey = `oidc.user:${environment.authConfig.authority}:${environment.authConfig.client_id}`;
      // private userKey = `oidc.user:${this._conf.env.authConfig.authority}:${this._conf.env.authConfig.client_id}`;
    
      constructor() {
        // 如果访问用的 token 过期,调用 login()
        this.manager.events.addAccessTokenExpired(() => {
          this.login();
        });
      }
    
      login() {
        this.manager.signinRedirect();
      }
    
      logout() {
        this.manager.signoutRedirect();
      }
    
      loginCallBack() {
        return Observable.create(observer => {
          from(this.manager.signinRedirectCallback())
            .subscribe((user: User) => {
              this.loginStatusChanged.emit(user);
              observer.next(user);
              observer.complete();
            });
        });
      }
    
      tryGetUser() {
    
        return from(this.manager.getUser());
      }
    
      get type(): string {
        return 'Bearer';
      }
    
      get user(): User | null {
        const temp = localStorage.getItem(this.userKey);
        if (temp) {
          const user: User = JSON.parse(temp);
          return user;
        }
        return null;
      }
    
      get token(): string | null {
        const temp = localStorage.getItem(this.userKey);
        if (temp) {
          const user: User = JSON.parse(temp);
          return user.access_token;
        }
        return null;
      }
    
      get authorizationHeader(): string | null {
        if (this.token) {
          return `${this.type} ${this.token}`;
        }
        return null;
      }
    }
    
    

    2. 添加 auth.guard

    ng g g auth/auth --flat
    

    选择 CanActivate

    替换 auth.guard.ts 内容:

    import { Injectable } from "@angular/core";
    import {
      CanActivate,
      CanActivateChild,
      ActivatedRouteSnapshot,
      RouterStateSnapshot,
      UrlTree,
    } from "@angular/router";
    import { Observable } from "rxjs";
    import { map } from "rxjs/operators";
    import { AuthService } from "./auth.service";
    import { User } from "oidc-client";
    
    @Injectable({
      providedIn: "root",
    })
    export class AuthGuard implements CanActivate {
      constructor(private _auth: AuthService) {}
    
      canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<boolean> {
        return this.mapper(this._auth.tryGetUser());
      }
    
      private mapper = map((user: User) => {
        if (user) return true;
        this._auth.login();
        return false;
      });
    }
    

    3. 修改 app-routing.module.ts

    import { NgModule } from "@angular/core";
    import { RouterModule, Routes } from "@angular/router";
    import { AuthComponent } from "./auth/auth.component";
    import { C1Component } from "./test/c1/c1.component";
    import { C2Component } from "./test/c2/c2.component";
    
    const routes: Routes = [
      {
        path: "home",
        component: AuthComponent,
        children: [
          { path: "c1", component: C1Component },
          { path: "c2", component: C2Component },
        ],
      },
    ];
    
    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule],
    })
    export class AuthRoutingModule {}
    
    

    4. 修改 login-callback.component.ts

    回到成功后,导航到 home 页,你也可以写更多的其他逻辑。

    import { Component, OnInit } from "@angular/core";
    import { Router } from "@angular/router";
    import { User } from "oidc-client";
    import { AuthService } from "src/app/auth/auth.service";
    
    @Component({
      selector: "app-login-callback",
      templateUrl: "./login-callback.component.html",
      styleUrls: ["./login-callback.component.less"],
    })
    export class LoginCallbackComponent implements OnInit {
      constructor(private _router: Router, private _auth: AuthService) {}
    
      ngOnInit() {
        this._auth.loginCallBack().subscribe((user: User) => {
          this._router.navigate(["home"]);
        });
      }
    }
    

    顺便美化一下下样式

    login-callback.component.html:

    <div class="callback-bar">
      <span style="margin-left: 10px;">登录成功,跳转中...</span>
    </div>
    

    login-callback.component.less(我这里使用的是 less,你的可能是 css/scss/sass):

    .callback-bar {
        margin: 0px 0px 0px 0px;
        padding: 8px 0px 0px 0px;
        font-size: 24px;
        font-weight: 600px;
        color: white;
        background-color: #3881bf;
        box-shadow: 0px 3px 5px #666;
        height: 50px;
    }
    

    再此重启一下程序(往往一些奇奇怪怪的问题重新启动后会被解决)。

    这时候就已经实现了一个认证的过程,不过 auth 模块(用户模块)只有一个组件,总感觉不够直观,因此,我们需要在 auth 模块添加更多的组件,形成子路由,在观察功能。

    添加 auth 子组件、子路由

    修改 auth.component 组件

    1. auth.component.html

    <div>
      <input type="button" value="c1" (click)="goC1()">
      <input type="button" value="c2" (click)="goC2()">
    </div>
    
    <div>
      <router-outlet></router-outlet>
    </div>
    

    2. auth.component.ts

    import { Component, OnInit } from "@angular/core";
    import { Router } from "@angular/router";
    
    @Component({
      selector: "app-auth",
      templateUrl: "./auth.component.html",
      styleUrls: ["./auth.component.less"],
    })
    export class AuthComponent implements OnInit {
      constructor(private _router: Router) {}
    
      ngOnInit() {}
    
      public goC1(): void {
        this._router.navigate(["home/c1"]);
      }
    
      public goC2(): void {
        this._router.navigate(["home/c2"]);
      }
    }
    

    新建子路由

    2. 添加 c1、c2 子组件

    ng g c auth/test/c1
    ng g c auth/test/c2
    

    保持默认内容即可。

    3. 修改 auth-routing.module.ts

    import { NgModule } from "@angular/core";
    import { RouterModule, Routes } from "@angular/router";
    import { AuthComponent } from "./auth/auth.component";
    import { C1Component } from "./test/c1/c1.component";
    import { C2Component } from "./test/c2/c2.component";
    
    const routes: Routes = [
      {
        path: "home",
        component: AuthComponent,
        children: [
          { path: "c1", component: C1Component },
          { path: "c2", component: C2Component },
        ],
      },
    ];
    
    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule],
    })
    export class AuthRoutingModule {}
    

    重启项目,这时候得到一个错误信息:

    Error: Template parse errors:
    'router-outlet' is not a known element:
    

    这表示 auth 模块没有引入 RouterModule,其实是我们的 auth.module.ts 没有引入 auth-routing.module.ts 导致的(routing 中有引入 RouterModule)

    修改 auth.module.ts:

    ...
    import { AuthRoutingModule } from './auth-routing.module';
    
    @NgModule({
      ...
      imports: [..., AuthRoutingModule],
    })
    

    重启项目,可以看到现在基本功能都已经实现了,不过还差一个退出功能。

    退出登录

    1. 修改 auth.component.html

    <div>
      <input type="button" value="c1" (click)="goC1()">
      <input type="button" value="c2" (click)="goC2()">
      <input type="button" value="exit" (click)="exit()">
    </div>
    
    <div>
      <router-outlet></router-outlet>
    </div>
    

    2. 修改 auth.component.ts

    import { Component, OnInit } from "@angular/core";
    import { Router } from "@angular/router";
    import { AuthService } from "../auth.service";
    
    @Component({
      selector: "app-auth",
      templateUrl: "./auth.component.html",
      styleUrls: ["./auth.component.less"],
    })
    export class AuthComponent implements OnInit {
      constructor(private _router: Router, private _auth: AuthService) {}
    
      ngOnInit() {}
    
      public goC1(): void {
        this._router.navigate(["home/c1"]);
      }
    
      public goC2(): void {
        this._router.navigate(["home/c2"]);
      }
    
      public exit(): void {
        this._auth.logout();
      }
    }
    

    重启测试,退出成功!

    访问 /home 自动跳转登录,没问题。

    访问 /home/c1 居然跳过了认证,直接进来了!

    造成这个问题的原因是但是我们的守卫添加的方式是 canActivatecanActivate只会保护本路由,而不会保护其子路由。因此,我们还需要保护子路由!

    保护子路由

    1. 修改 auth.guard.ts

    import { Injectable } from "@angular/core";
    import {
      CanActivate,
      CanActivateChild,
      ActivatedRouteSnapshot,
      RouterStateSnapshot,
      UrlTree,
    } from "@angular/router";
    import { Observable } from "rxjs";
    import { map } from "rxjs/operators";
    import { AuthService } from "./auth.service";
    import { User } from "oidc-client";
    
    @Injectable({
      providedIn: "root",
    })
    export class AuthGuard implements CanActivate, CanActivateChild {
      constructor(private _auth: AuthService) {}
    
      canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<boolean> {
        return this.mapper(this._auth.tryGetUser());
      }
    
      canActivateChild(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ):
        | Observable<boolean | UrlTree>
        | Promise<boolean | UrlTree>
        | boolean
        | UrlTree {
        return this.mapper(this._auth.tryGetUser());
      }
    
      private mapper = map((user: User) => {
        if (user) return true;
        this._auth.login();
        return false;
      });
    }
    

    2. 修改 auth-routing.module.ts
    主要修改代码如下:

    import { AuthGuard } from "./auth.guard"; // <- here
    
    const routes: Routes = [
      {
        path: "home",
        component: AuthComponent,
        canActivateChild: [AuthGuard], // <- here
        children: [
          { path: "c1", component: C1Component },
          { path: "c2", component: C2Component },
        ],
      },
    ];
    

    重启项目,再此访问 '/home/c1',成功跳转,访问 '/home',同样成功跳转。

    Github

    angular-oidc

    相关文章

      网友评论

        本文标题:Angular 8 配置 oidc-client

        本文链接:https://www.haomeiwen.com/subject/wampiqtx.html