• ionic + asp.net core webapi + keycloak实现前后端用户认证和自动生成客户端代码


    概述

    本文使用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("http://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 = "http://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中将侦听地址改为http://localhost:5000

    5、dotnet run启动

    使用浏览器打开http://localhost:5000,看到如下文档界面。

    ionic配置keycloak支持

    使用keyclock-angular快速添加对于keyclock的支持,https://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: 'http://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: 'http://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 = http://localhost:8100,也就是ionic app调试运行的地址

    然后,建立一个用户并配置密码。

    浏览器打开http://localhost:8100,如果配置正确,会看到请求被重定向到keycloak的登陆界面,输入用户名密码后跳转回ionic app。

    至此,inoic集成keycloak的工作基本完成。

    特别要说明的,集成keycloak之后,不仅能控制路由的访问,而且所有的http请求都会自动加上登陆时获取的token,方便了webapi的调用。你可以在app.module.ts的52行,添加例外。

    调用WebApi

    如果不使用OpenApi代码自动生成,调用WebApi的套路无非是编写interface,然后编写service,使用http调用webapi,有许多重复的代码和机械步骤。

    OpenApi代码生成器解决了这一问题,可以替代我们生成这些代码,很方便。

    首先,安装全局工具 https://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代码生成器自动生成客户端代码。

  • 相关阅读:
    原来是板子的硬件问题
    最简单的helloworld模块编译加载(linux3.5内核源码树建立)
    排序学习笔记
    配置开发环境遇到的一些问题及解决方法
    .NET基础之GridView控件
    .NET之页面数据缓存
    .NET基础之Calendar控件
    【转帖】DIV+CSS完美兼容IE6/IE7/FF的通用方法
    ADO.NET()Command
    .NET基础之DataList控件
  • 原文地址:https://www.cnblogs.com/wjsgzcn/p/12925016.html
Copyright © 2020-2023  润新知