• Angular: 为Angular SPA程序添加Authorization支持


    本篇详细描述怎么为Angular SPA程序添加Authorization的全记录。相对应的,本篇中使用了Identity Server (.Net Core开源项目)作为Identity Provider。

    AccessToken和Refresh Token

    权限控制无所不在,基于OAuth, OpenID这些解决方案在今时今日的开发中几乎是必不可少的。

    这里只强调下Access Token和Refresh Token的关联与区别:

    • Access Token的生命周期通常是比较短暂。譬如Identity Server设置一般设置为3600秒,即一个小时就会过期;
    • Refresh Token。显然每个小时就要终端客户重输密码来登录不是个很好的设计,所以Refresh Token提出来用以简化登录次数。通常Refresh Token默认比较长。Identity Server中一般设置为30天。

    那么Access Token怎么跟Refresh Token协同工作呢?一般来说,整个

    • Client进行登录,Server会同时发放Access Token和Refresh token;通常Client会保存住Refresh Token;
    • 当Client察觉到Access Token过期时,它会Refresh token要求刷新Access token;
    • Server会根据Refresh Token的有效性并下发最新的Access Token;
    • 重复上述两步(这两步均无客户干预),直至Refresh Token也失效;
    • Refresh Token也失效时,重新登录。

    由于Refresh Token这个特效,在开发库中,其也被称为Offline Access。

    安装依赖

    如果是Angular CLI创建的应用程序,添加:

    ng add angular-auth-oidc-client
    
    • 1

    当然也可以使用NPM/YARN来安装。

    当开始执行时,首先要求确认:

    ℹ Using package manager: npm
    ✔ Found compatible package version: angular-auth-oidc-client@14.1.5.
    ✔ Package information loaded.
    
    The package angular-auth-oidc-client@14.1.5 will be installed and executed.
    Would you like to proceed? (Y/n) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当选择Y之后,会进行安装,并要求输入一些必要信息。下列中的(XXXX)是项目特定信息,需要按照项目的实际填写。

    ✔ Package successfully installed.
    ? What flow to use? OIDC Code Flow PKCE using refresh tokens
    ? Please enter your authority URL or Azure tenant id or Http config URL (XXXX)
        🔎 Running checks...
        ✅️ Project found, working with 'myproject'
        ✅️ Added "angular-auth-oidc-client" 14.1.5
        🔍 Installing packages...
        ✅️ Installed
        ✅️ 'src/app/auth/auth-config.module.ts' will be created
        ✅️ 'AuthConfigModule' is imported in 'src/app/app.module.ts'
        ✅️ All imports done, please add the 'RouterModule' as well if you don't have it imported yet.
        ✅️ No silent-renew entry in assets array needed
        ✅️ No 'silent-renew.html' needed
    CREATE src/app/auth/auth-config.module.ts (703 bytes)
    UPDATE package.json (2281 bytes)
    UPDATE src/app/app.module.ts (3951 bytes)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这时,项目中多了一个src\auth的文件夹,其中只有一个Module。

    @NgModule({
        imports: [AuthModule.forRoot({
            config: {
                  authority: 'XXXX.com',
                  redirectUrl: window.location.origin,
                  postLogoutRedirectUri: window.location.origin,
                  clientId: 'please-enter-clientId',
                  scope: 'please-enter-scopes', // 'openid profile offline_access ' + your scopes
                  responseType: 'code',
                  silentRenew: true,
                  useRefreshToken: true,
                  renewTimeBeforeTokenExpiresInSeconds: 30,
              }
          })],
        exports: [AuthModule],
    })
    export class AuthConfigModule {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    其中有些信息需要更新:scopeclientId等。

    如果需要silent renew(自动更新Access Token),需要在scope中加上offline_access,并且在Identity Provider也设置为Allow Offlien Access。

    以Identity Server 6为例:

        new Client
        {
            ClientName = "My App",
            ClientId = "myangularapp",
            AllowedGrantTypes = GrantTypes.Code,
            RequireClientSecret = false,
            RequirePkce = true,
    
            AllowAccessTokensViaBrowser = true,
            AllowOfflineAccess = true, // For refresh token
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    更新Angular SPA程序

    创建Unauthorized的Module和Component

    Unauthorized的Module和Component用来向客户显示错误信息。

    首先创建Module:

    ng g m pages\Unauthorized --routing
    
    • 1

    然后是Component:

    ng g c pages\Unauthorized -m pages\unauthorized
    
    • 1

    可以在pages\unauthorized\unauthorized.html中填充显示给终端客户的权限检查失败的信息。

    譬如:

    <h1>You are not unauthorized to accessh1>
    
    • 1

    更新Unauthorized Module中的路由(即文件unauthorized-routing.module.ts)来添加标准跳转:

    const routes: Routes = [{
      path: '', component: UnauthorizedComponent
    }];
    
    • 1
    • 2
    • 3

    添加Angular程序的路由

    在Angular程序中添加路由,用来支持跳转到上述刚刚创建的unauthorized的页面。

    通常,在app-routing.module.ts中添加路由项:

      { path: 'unauthorized', loadChildren: () => import('./pages/unauthorized/unauthorized.module').then(m => m.UnauthorizedModule) },
    
    • 1

    这时,在Angular SPA程序中的路由unauthorized已经添加完成。

    添加路由保护

    对需要Authorization的路由添加保护:

    @Injectable({
      providedIn: 'root'
    })
    export class AuthGuardService implements CanActivate {
    
      constructor(private authService: OidcSecurityService, private router: Router) { }
    
      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        const url: string = state.url;
    
        return firstValueFrom(this.checkLogin(url));
      }
    
      checkLogin(url: string): Observable<boolean> {
        return this.authService.isAuthenticated().pipe(map((rst: boolean) => {
          if (!rst) {
            this.authService.authorize();
          }
          return true;
        }));
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    更新路由项:

      {
        path: 'protected-path',
        canActivate: [AuthGuardService],
        loadChildren: () => import('./pages/protected-path/protected-path.module').then(m => m.ProtectedPathModule),
      },
    
    • 1
    • 2
    • 3
    • 4
    • 5

    登录及登出操作

    登录(Login)和登出(Logout)操作一般放在主Component中进行,即,通常都是app.component.ts中:

    在构造函数中添加:

        constructor(public oidcSecurityService: OidcSecurityService,) {
            // Other codes...
        }
    
    • 1
    • 2
    • 3

    添加登录函数:

      public onLogon(): void {
        this.oidcSecurityService.authorize();
      }
    
    • 1
    • 2
    • 3

    登出函数:

      public onLogon(): void {
        this.oidcSecurityService.logoffAndRevokeTokens().subscribe();
      }
    
    • 1
    • 2
    • 3

    通常在ngOnInit中添加相应Subscription来接受Logon的回调:

      ngOnInit(): void {
        this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken, idToken }) => {
          if (isAuthenticated) {        
            this.oidcSecurityService.getUserData().subscribe(val => {
              this.currentUser = `${val.name}(${val.sub})`;
            });
          }
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用Access Token

    如果申请到的Access Token是用来访问被保护的API,那么Access Token就需要传给对应的API(authService也是注入在Constructor中的OidcSecurityService的实例):

        return this.authService.isAuthenticated().pipe(mergeMap(islogin => {
          if (!islogin) {
            return of({totalCount: 0, items: []});
          }
    
          let headers: HttpHeaders = new HttpHeaders();
          headers = headers.append(this.contentType, this.appJson)
            .append(this.strAccept, this.appJson);
      
          let params: HttpParams = new HttpParams();
          params = params.append('$top', top.toString());
          params = params.append('$skip', skip.toString());
          return this.authService.getAccessToken().pipe(mergeMap(token => {
            headers = headers.append('Authorization', 'Bearer ' + token);
      
            return this.http.get(apiurl, {
              headers,
              params,
            })
              .pipe(map(response => {
                // Success received the response
                return {
                  items
                };
              }),
              catchError((error: HttpErrorResponse) => throwError(() => new Error(error.statusText + '; ' + error.error + '; ' + error.message))));
          }));  
        }));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
  • 相关阅读:
    洛谷 P1962 斐波那契数列(矩阵快速幂, 水题)
    HTTP有什么缺陷,HTTPS是怎么解决的
    Vue3中组合式API(简单用法)
    【快应用】deeplink 3种跳转格式现象总结
    C++ 11:多线程相关问题
    【云原生实战】KubeSphere实战——多租户系统实战
    云服务器CentOS8.2安装部署Docker一文详解
    流量分析(5.5信息安全铁人三项赛数据赛题解)
    Redis 客户端缓存
    Qt实现网络拓扑图实现程度
  • 原文地址:https://blog.csdn.net/alvachien/article/details/127705081