angular 接入 IdentityServer4
Intro
最近把活動室預約的專案做了一個升級,預約活動室需要登入才能預約,並用 IdentityServer4 做了一個統一的登入註冊中心,這樣以後就可以把其他的需要使用者操作的應用統一到 IdentityServer 這裡,這樣就不需要在每個應用裡都做一套使用者的機制,接入 IdentityServer 就可以了。
目前活動室預約的伺服器端和基於 angular 的客戶端已經完成了 IdentityServer 的接入,並增加了使用者的相關的一些功能,比如使用者可以檢視自己的預約記錄並且可以取消自己未開始的預約,
還有一個小程式版的客戶端暫時還未完成接入,所以小程式版目前暫時是不能夠預約的
為什麼要寫這篇文章
目前在網上看到很多都是基於 implicit 模式接入 IdentityServer,這樣實現起來很簡單,但是現在 OAuth 已經不推薦這樣做了,OAuth 推薦使用 code 模式來代替 implicit
implicit 模式會有一些安全風險,implicit 模式會將 accessToken 直接返回到客戶端,而 code 模式只是會返回一個 code,accessToken 和 code 的分離的兩步,implicit 模式很有可能會將 token 洩露出去
詳細可以參考 StackOverflow 上的這個問答
除此之外,還有一個小原因,大多是直接基於 oidc-client
的 一個 npm 包來實現的,我是用了一個針對 angular 封裝的一個庫 angular-oauth2-oidc
,如果你在用 angular ,建議你可以嘗試一下,針對 angular 做了一些封裝和優化,對 angular 更友好一些
準備接入吧
API 配置
預約系統的 API 和網站管理系統是在一起的,針對需要登入才能訪問的 API 單獨設定了的 policy 訪問
services.AddAuthentication()
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["Authorization:Authority"];
options.RequireHttpsMetadata = false;
options.NameClaimType = "name";
options.RoleClaimType = "role";
})
;
services.AddAuthorization(options =>
{
options.AddPolicy("ReservationApi", builder => builder
.AddAuthenticationSchemes(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireScope("ReservationApi")
);
});
需要授權才能訪問的介面設定 Authorize
並指定 Policy 為 ReservationApi
[Authorize(Policy = "ReservationApi")]
[HttpPost]
public async Task<IActionResult> MakeReservation([FromBody] ReservationViewModel model)
IdentityServer Client 配置
首先我們需要在 IdentityServer 這邊新增一個客戶端,因為我們要使用 code 模式,所以授權型別需要配置 authorization-code 模式,不使用 implicit 模式
允許的作用域(scope) 是客戶端允許訪問的 api 資源和使用者的資訊資源,openid 必選,profile 是預設的使用者基本資訊的集合,根據自己客戶端的需要進行配置,ReservationApi
是訪問 API 需要的 scope,其他的 scope 根據客戶端需要進行配置
angular 客戶端配置
安裝 angular-oauth2-oidc
npm 包,我現在使用的是 9.2.0
版本
新增 oidc 配置:
export const authCodeFlowConfig: AuthConfig = {
issuer: 'https://id.weihanli.xyz',
// URL of the SPA to redirect the user to after login
redirectUri: window.location.origin + '/account/callback',
clientId: 'reservation-angular-client',
dummyClientSecret: 'f6f1f917-0899-ef36-63c8-84728f411e7c',
responseType: 'code',
scope: 'openid profile ReservationApi offline_access',
useSilentRefresh: false,
showDebugInformation: true,
sessionChecksEnabled: true,
timeoutFactor: 0.01,
// disablePKCI: true,
clearHashAfterLogin: false
};
在 app.module 引入 oauth 配置
imports: [
BrowserModule,
AppRoutingModule,
AppMaterialModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
BrowserAnimationsModule,
OAuthModule.forRoot({
resourceServer: {
allowedUrls: ['https://reservation.weihanli.xyz/api'],
sendAccessToken: true
}
})
]
OAuthModule 裡 resourceServer 中的 allowedUrls 是配置的資源的地址,訪問的資源符合這個地址時就會自動傳送 accessToken,這樣就不需要自己實現一個 interceptor 來實現自動在請求頭中設定 accessToken 了
在 AppComponment 的構造器中初始化 oauth 配置,並載入 ids 的發現文件
export class AppComponent {
constructor(
private oauth: OAuthService
) {
this.oauth.configure(authConfig.authCodeFlowConfig);
this.oauth.loadDiscoveryDocument();
}
// ...
}
新增一個 AuthGuard,路由守衛,需要登入才能訪問的頁面自動跳轉到 /account/login
自動登入
AuthGuard:
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { OAuthService } from 'angular-oauth2-oidc';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router, private oauthService: OAuthService) {}
canActivate() {
if (this.oauthService.hasValidAccessToken()) {
return true;
} else {
this.router.navigate(['/account/login']);
return false;
}
}
}
路由配置:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ReservationListComponent } from './reservation/reservation-list/reservation-list.component';
import { NoticeListComponent } from './notice/notice-list/notice-list.component';
import { NoticeDetailComponent } from './notice/notice-detail/notice-detail.component';
import { AboutComponent } from './about/about.component';
import { NewReservationComponent } from './reservation/new-reservation/new-reservation.component';
import { LoginComponent } from './account/login/login.component';
import { AuthGuard } from './shared/auth.guard';
import { AuthCallbackComponent } from './account/auth-callback/auth-callback.component';
import { MyReservationComponent } from './account/my-reservation/my-reservation.component';
const routes: Routes = [
{ path: '', component: ReservationListComponent },
{ path: 'reservations/new', component:NewReservationComponent, canActivate: [AuthGuard] },
{ path: 'reservations', component: ReservationListComponent },
{ path: 'notice', component: NoticeListComponent },
{ path: 'notice/:noticePath', component: NoticeDetailComponent },
{ path: 'about', component: AboutComponent },
{ path: 'account/login', component: LoginComponent },
{ path: 'account/callback', component: AuthCallbackComponent },
{ path: 'account/reservations', component: MyReservationComponent, canActivate: [AuthGuard] },
{ path: '**', redirectTo: '/'}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
AccountLogin 會將使用者引導到 ids 進行登入,登入之後會跳轉到配置的重定向 url,我配置的是 account/callback
import { Component, OnInit } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.less']
})
export class LoginComponent implements OnInit {
constructor(private oauthService: OAuthService) {
}
ngOnInit(): void {
// 登入
this.oauthService.initLoginFlow();
}
}
Auth-Callback
import { Component, OnInit } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { Router } from '@angular/router';
@Component({
selector: 'app-auth-callback',
templateUrl: './auth-callback.component.html',
styleUrls: ['./auth-callback.component.less']
})
export class AuthCallbackComponent implements OnInit {
constructor(private oauthService: OAuthService, private router:Router) {
}
ngOnInit(): void {
this.oauthService.loadDiscoveryDocumentAndTryLogin()
.then(_=> {
this.oauthService.loadUserProfile().then(x=>{
this.router.navigate(['/reservations/new']);
});
});
}
}
More
當前實現還不太完善,重定向現在始終是跳轉到的新預約的頁面,應當在跳轉登入之前記錄一下當前的地址儲存在 storage 中,在 auth-callback 裡登入成功之後跳轉到 storage 中之前的地址