ionic + asp.net core webapi + keycloak實現前後端用戶認證和自動生成客戶端程式碼
- 2020 年 5 月 21 日
- 筆記
- Angular, code generator, dotnet core, ionic, keycloak, open api
概述
本文使用ionic/angular開發網頁前台,asp.net core webapi開發restful service,使用keycloak保護前台頁面和後台服務,並且利用open api自動生成程式碼功能,減少了重複程式碼編寫。
準備工作
1、使用docker搭建並啟動keycloak伺服器,新建名稱為test的realm,並建立幾個測試用戶,並且建立1個名稱為my_client的客戶端,注意客戶端的回調url要正確。
2、安裝ionic,使用 ionic start myApp tabs,初始化一個tabs格式的前端應用。
3、使用dotnet new webapi命令創建一個webapi。
WebApi設置
1、控制器使用[Authorize]保護
namespace WebApi1.Controllers { /// <summary> /// 天氣預報服務 /// </summary> [Authorize] [ApiController] [Route("[controller]")] [Produces("application/json")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } /// <summary> /// 獲取全部天氣預報資訊 /// </summary> /// <returns></returns> [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } } }
2、修改項目文件
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> <NoWarn>$(NoWarn);1591</NoWarn> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" /> </ItemGroup> </Project>
1591那一段主要是為了編譯時生成xml格式的注釋文檔,該文檔給OpenApi使用,用來給方法和屬性添加註釋。
JwtBearer用於實現基於JWT的身份認證,Swashbuckle.AspNetCore用於自動生成OpenApi文檔以及圖形介面。
3、修改Startup
1 namespace WebApi1 2 { 3 public class Startup 4 { 5 readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; 6 7 public Startup(IConfiguration configuration) 8 { 9 Configuration = configuration; 10 } 11 12 public IConfiguration Configuration { get; } 13 14 // This method gets called by the runtime. Use this method to add services to the container. 15 public void ConfigureServices(IServiceCollection services) 16 { 17 services.AddCors(options => 18 { 19 options.AddPolicy(name: MyAllowSpecificOrigins, 20 builder => 21 { 22 builder.WithOrigins("//localhost:8100").AllowAnyHeader().AllowAnyMethod(); 23 }); 24 }); 25 26 services.AddControllers(); 27 28 services.AddSwaggerGen(c => 29 { 30 c.SwaggerDoc("v1", new OpenApiInfo { Title = "一個測試用的天氣預報服務", Version = "v1" }); 31 32 var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 33 var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 34 c.IncludeXmlComments(xmlPath); 35 }); 36 37 services.AddAuthentication(options => 38 { 39 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 40 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 41 }).AddJwtBearer(options => 42 { 43 options.Authority = "//localhost:8180/auth/realms/test"; 44 options.RequireHttpsMetadata = false; 45 options.Audience = "account"; 46 options.TokenValidationParameters = new TokenValidationParameters 47 { 48 NameClaimType = "preferred_username" 49 }; 50 }); 51 } 52 53 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 54 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 55 { 56 if (env.IsDevelopment()) 57 { 58 app.UseDeveloperExceptionPage(); 59 } 60 61 app.UseHttpsRedirection(); 62 63 // Enable middleware to serve generated Swagger as a JSON endpoint. 64 app.UseSwagger(); 65 66 // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 67 // specifying the Swagger JSON endpoint. 68 app.UseSwaggerUI(c => 69 { 70 c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); 71 c.RoutePrefix = string.Empty; 72 }); 73 74 app.UseRouting(); 75 76 app.UseAuthentication(); 77 app.UseAuthorization(); 78 79 app.UseCors(MyAllowSpecificOrigins); 80 81 app.UseEndpoints(endpoints => 82 { 83 endpoints.MapControllers(); 84 }); 85 } 86 } 87 }
17行程式碼添加CORS支援,此處只允許來自我的客戶端的訪問。
28行配置OpenApi文檔生成邏輯。
68行生成OpenApi文檔介面,使用c.RoutePrefix使得一打開網站就能看到文檔介面,而不是打開404.
37行配置JWT參數,連接到keycloak服務的test realm。
4、修改偵聽埠
為了方便配置回調介面,在lauchSettings.json中將偵聽地址改為//localhost:5000
5、dotnet run啟動
使用瀏覽器打開//localhost:5000,看到如下文檔介面。
ionic配置keycloak支援
使用keyclock-angular快速添加對於keyclock的支援,//github.com/mauriciovigolo/keycloak-angular。
npm i –save keycloak-angular
npm i –save keycloak-js@version
這裡的version我設置的是9.0.3,最新的是10,但是keyclock-angular安裝時明確指定要求版本小於10,不知道是不是一個bug。
安裝完畢後,修改app.module.ts,
1 import { BASE_PATH } from './../../services/variables'; 2 import { NgModule, APP_INITIALIZER} from '@angular/core'; 3 import { BrowserModule } from '@angular/platform-browser'; 4 import { RouteReuseStrategy } from '@angular/router'; 5 6 import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 7 import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 8 import { StatusBar } from '@ionic-native/status-bar/ngx'; 9 10 import { AppRoutingModule } from './app-routing.module'; 11 import { AppComponent } from './app.component'; 12 import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular'; 13 import { HttpClientModule } from '@angular/common/http'; 14 15 @NgModule({ 16 declarations: [AppComponent], 17 entryComponents: [], 18 imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, KeycloakAngularModule, HttpClientModule], 19 providers: [ 20 StatusBar, 21 SplashScreen, 22 { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 23 { 24 provide: APP_INITIALIZER, 25 useFactory: initializer, 26 multi: true, 27 deps: [KeycloakService] 28 }, 29 { 30 provide: BASE_PATH, useValue: '//localhost:5000' 31 } 32 ], 33 bootstrap: [AppComponent] 34 }) 35 36 export class AppModule {} 37 38 function initializer(keycloak: KeycloakService): () => Promise<any> { 39 return (): Promise<any> => { 40 return new Promise(async (resolve, reject) => { 41 try { 42 await keycloak.init({ 43 config: { 44 url: '//localhost:8180/auth', 45 realm: 'test', 46 clientId: 'my-client' 47 }, 48 initOptions: { 49 onLoad: 'login-required', 50 checkLoginIframe: false 51 }, 52 bearerExcludedUrls: [] 53 }); 54 resolve(); 55 } catch (error) { 56 reject(error); 57 } 58 }); 59 }; 60 }
首先,第2行增加引入APP_INITIALIZER;
然後,12行引入keyclock相關組件;
然後,23行增加provider,其實就是指定程式啟動時執行的腳本為initializer;
最後,38行編寫initializer方法,配置keyclock啟動參數,並且配置了應用啟動時直接調用登錄(可選)
接下來,實現如下的CanAuthenticationGuard,用來控制路由。
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular'; @Injectable({ providedIn: 'root' }) export class CanAuthenticationGuard extends KeycloakAuthGuard implements CanActivate { constructor(protected router: Router, protected keycloakAngular: KeycloakService) { super(router, keycloakAngular); } isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { return new Promise((resolve, reject) => { if (!this.authenticated) { this.keycloakAngular.login() .catch(e => console.error(e)); return reject(false); } const requiredRoles: string[] = route.data.roles; if (!requiredRoles || requiredRoles.length === 0) { return resolve(true); } else { if (!this.roles || this.roles.length === 0) { resolve(false); } resolve(requiredRoles.every(role => this.roles.indexOf(role) > -1)); } }); } }
在app-routing.module.ts中引用該guard,並且在需要控制的路由如下處理
const routes: Routes = [ { path: '', loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule) }, { path: 'hero/list', canActivate: [CanAuthenticationGuard], loadChildren: () => import('./hero/list/list.module').then( m => m.ListPageModule) },
主要是canActive一行。
配置keyclock
略去如何安裝以及運行keyclock的詳細說明,我是通過docker安裝運行的。
首先,新建realm = test
然後,新建client = my-client,url = //localhost:8100,也就是ionic app調試運行的地址
然後,建立一個用戶並配置密碼。
瀏覽器打開//localhost:8100,如果配置正確,會看到請求被重定向到keycloak的登陸介面,輸入用戶名密碼後跳轉回ionic app。
至此,inoic集成keycloak的工作基本完成。
特別要說明的,集成keycloak之後,不僅能控制路由的訪問,而且所有的http請求都會自動加上登陸時獲取的token,方便了webapi的調用。你可以在app.module.ts的52行,添加例外。
調用WebApi
如果不使用OpenApi程式碼自動生成,調用WebApi的套路無非是編寫interface,然後編寫service,使用http調用webapi,有許多重複的程式碼和機械步驟。
OpenApi程式碼生成器解決了這一問題,可以替代我們生成這些程式碼,很方便。
首先,安裝全局工具 //github.com/OpenAPITools/openapi-generator
該工具提供了npm包,但只是一個封裝,還是需要系統有java環境的。
npm install @openapitools/openapi-generator-cli -g
使用如下命令生成程式碼:
openapi-generator -i {swagger文件url} -g typescript-angular -o {程式碼存放目錄}
運行完畢後,看到程式碼目錄下生成了一堆文件,暫時不必理會這些文件,也不要修改這些文件。
找到任意一個page的ts文件,添加程式碼,使用生成的客戶端:
import { WeatherForecast } from './../../../../services/model/weatherForecast'; import { WeatherForecastService } from './../../../../services/api/weatherForecast.service'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-list', templateUrl: './list.page.html', styleUrls: ['./list.page.scss'], }) export class ListPage implements OnInit { weathers: WeatherForecast[]; constructor(private weatherforcastService: WeatherForecastService) { } ngOnInit() { this.weatherforcastService.weatherForecastGet() .subscribe(data => this.weathers = data); } }
程式碼相當簡單,WeatherForcast和WeatherForecastService已經幫我們自動生成了,直接使用就可以,是不是很cool呢?!
接下來,你可能有疑問了,光看到service,也不能修改生成的源碼,那麼去哪兒修改service的地址呢?很簡單,翻看前面的app.module.ts,第1行引入BASE_PATH,然後在provider中替換它的內容即可。
總結
至此,我們實現了在angular/ionic中使用keycloak進行oauth認證並且訪問webapi資源,還實現了使用openapi程式碼生成器自動生成客戶端程式碼。