乐趣区

Angular 8 配置 oidc

配置相对较为繁琐,最后会放上 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 居然跳过了认证,直接进来了!
造成这个问题的原因是但是我们的守卫添加的方式是 canActivate,canActivate 只会保护本路由,而不会保护其子路由。因此,我们还需要保护子路由!
保护子路由
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

退出移动版