配置相對較為繁瑣绿鸣,最后會放上 Github 源碼地址
新建一個 ng 項(xiàng)目
ng new angular-oidc
進(jìn)入目錄 cd angular-oidc
安裝 oidc-client
npm i oidc-client --save
配置 oidc-client 參數(shù)
打開 environment.ts
將下面的代碼覆蓋原來的內(nèi)容
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 }),
},
};
需要修改的幾個參數(shù):
-
authority
: 認(rèn)證服務(wù)器,需要修改為自己的認(rèn)證服務(wù)器 -
client_id
: 客戶端 id ,按照約定修改即可 -
redirect_uri
: 認(rèn)證服務(wù)器回調(diào)的客戶端頁面 -
post_logout_redirect_uri
: 登出回調(diào)鏈接
模塊劃分
這里我們把模塊劃分為2塊: 1) 游客模塊
2) 用戶模塊
默認(rèn)的殼組件所在的 module 作為游客模塊, 另外還需要構(gòu)建一個用戶模塊
游客模塊
為了方便理解, 游客模塊創(chuàng)建一個歡迎頁, 點(diǎn)擊繼續(xù)按鈕訪問用戶模塊.
1. 創(chuàng)建一個歡迎頁
沒什么特別的作用, 就是為了方便理解單獨(dú)設(shè)立的一個交互頁面.
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. 創(chuàng)建一個回調(diào)頁
回調(diào)頁是用戶 oidc 認(rèn)證結(jié)束后的回調(diào), 起到一個過度的作用(目前先空著)
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
, 這時候已經(jīng)能看到一點(diǎn)點(diǎn)信息了, 不過還沒有 home
路由, 下面來配置一下
用戶模塊
1. 添加一個 auth 模塊
ng g m auth/auth --flat
--flat
:不在一個單獨(dú)的文件夾創(chuàng)建
2. 將 auth 模塊添加到殼組件
打開 app-module.ts
, 主要修改一下內(nèi)容
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
內(nèi)容如下:
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
停止之前啟動項(xiàng)目的終端, ng s
重新啟動項(xiàng)目
此時的項(xiàng)目已經(jīng)可以從游客路由跳轉(zhuǎn)至用戶路由派任,但我們是不允許游客默認(rèn)訪問用戶路由的, 這時候就應(yīng)該 守衛(wèi)(Guard)
登場了赶诊。
配置守衛(wèi)(Guard)
1. 添加 auth.service (認(rèn)證相關(guān)的函數(shù))
ng g s auth/auth --flat
替換 auth.service.ts
內(nèi)容:
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 {
// 大多數(shù) oidc-client 操作都在其中
private manager: UserManager = new UserManager(environment.authConfig);
// private manager: UserManager = undefined;
// 登錄狀態(tài)改變事件
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 過期,調(diào)用 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
內(nèi)容:
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
頁
回到成功后逸贾,導(dǎo)航到 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;">登錄成功铝侵,跳轉(zhuǎn)中...</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;
}
再此重啟一下程序(往往一些奇奇怪怪的問題重新啟動后會被解決)。
這時候就已經(jīng)實(shí)現(xiàn)了一個認(rèn)證的過程咪鲜,不過 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
保持默認(rèn)內(nèi)容即可。
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 {}
重啟項(xiàng)目穴墅,這時候得到一個錯誤信息:
Error: Template parse errors:
'router-outlet' is not a known element:
這表示 auth 模塊沒有引入 RouterModule,其實(shí)是我們的 auth.module.ts 沒有引入 auth-routing.module.ts 導(dǎo)致的(routing 中有引入 RouterModule)
修改 auth.module.ts
:
...
import { AuthRoutingModule } from './auth-routing.module';
@NgModule({
...
imports: [..., AuthRoutingModule],
})
重啟項(xiàng)目温自,可以看到現(xiàn)在基本功能都已經(jīng)實(shí)現(xiàn)了玄货,不過還差一個退出功能。
退出登錄
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
自動跳轉(zhuǎn)登錄,沒問題馆里。
訪問 /home/c1
居然跳過了認(rèn)證隘世,直接進(jìn)來了!
造成這個問題的原因是但是我們的守衛(wèi)添加的方式是 canActivate
鸠踪,canActivate
只會保護(hù)本路由丙者,而不會保護(hù)其子路由。因此营密,我們還需要保護(hù)子路由械媒!
保護(hù)子路由
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 },
],
},
];
重啟項(xiàng)目,再此訪問 '/home/c1'评汰,成功跳轉(zhuǎn)纷捞,訪問 '/home',同樣成功跳轉(zhuǎn)被去。