AspNetCore网关集成Swagger访问使用IdentityServer保护的webapi项目
创建webapi项目
创建四个webapi项目,两个处理业务,一个网关,一个验证中心。四个项目对应的端口如下,
apigateway:1999
identityserver:16690
services.api1:2108
services.api2:2343
添加swagger支持
在两个业务项目中分别引用swashbuckle.aspnetcore,目前是最新版本是4.0.1。在项目属性面板,设置输出xml文档,swagger可以读取xml注释生成json文件,在swagger ui页面中显示。但是选择输出xml文档后,对于没有xml注释的类和方法会显示警告,可以在项目属性面板中【错误和警告】选项,取消显示警告中添加1591,这样就可以不显示缺少xml注释的警告了。对于强迫症的你暂时可以平复一下了,当然,真的强迫症的话,肯定会全部加上xml注释的。(¬_¬)瞄
startup.configureservices
public void configureservices(iservicecollection services) { services.addmvc().setcompatibilityversion(compatibilityversion.version_2_2); services.addswaggergen(options => { //swaggerdoc的第一个参数要与configure中swaggerendpoint中的版本名称一致 //既可以使用版本号,也可以使用自定义名称 options.swaggerdoc("serviceapitwo", new info { title = "services.api #two", version = "v1", description = "服务api #two", license = new license { name = "apl2.0", url = "https://opensource.org/licenses/apache-2.0" }, contact = new contact { name = "原来是李", url = "https://www.cnblogs.com/falltakeman" } }); var xmlfile = $"{assembly.getexecutingassembly().getname().name}.xml"; var xmlpath = path.combine(appdomain.currentdomain.basedirectory, xmlfile); options.includexmlcomments(xmlpath); }); }
startup.configure
public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); app.useswagger(c=> { c.routetemplate = "{documentname}/swagger.json"; }); app.useswaggerui(u => { u.swaggerendpoint("/serviceapitwo/swagger.json", "serviceapitwo"); u.documenttitle = "service api #2 文档"; }); } app.usemvc(); }
配置好启动项目看看。
通过swagger发起请求,响应正常。
配置网关项目
使用目前比较流行的开源网关服务ocelot,apigateway项目添加引用ocelot、ocelot.provider.consul、ocelot.provider.polly、ocelot.cache.cachemanager,目前用到的版本是13.5.2。要在网关中统一查看各个服务的swagger文档,还需引用swash.aspnetcore。consul的配置先不说了。
在项目根路径下添加ocelotconfig.json配置文件。处理正常的路由转发外,要在网关swagger页面访问业务api,还需要配置swagger路由转发。
{ "globalconfiguration": { //外部访问路径 "baseurl": "http://localhost:1999", //限速配置 "ratelimitoptions": { //白名单 "clientwhitelist": [], "enableratelimiting": true, //限制时间段,例如1s,5m,1h,1d "period": "1s", //等待重试等待的时间间隔(秒) "periodtimespan": 1, //限制 "limit": 1, //自定义消息 "quotaexceededmessage": "单位时间内请求次数超过限制。", "httpstatuscode": 999 }, //服务发现配置 "servicediscoveryprovider": { "host": "localhost", "port": 8500, "type": "pollconsul", "pollinginterval": 1000 }, //熔断配置 "qosoptions": { "exceptionsallowedbeforebreaking": 3, "durationofbreak": 5, //超时值(毫秒) "timeoutvalue": 5000 } }, "reroutes": [ // api#one项目配置 { "upstreampathtemplate": "/gateway/one/{url}", //上游路径模板 "upstreamhttpmethod": [ "get", "post", "put", "delete" ], //上游http请求方法 "downstreampathtemplate": "/api/{url}", //下游路径模板 "downstreamscheme": "http", //下游协议 https/http "servicename": "serviceapione", //服务名称(结合服务发现使用) "useservicediscovery": true, //启用服务发现 "loadbalancer": "roundrobin", //负载均衡算法:roundrobin-轮询;leastconnection-最少连接数(最空闲的服务器);noloadbalancer-总是发送往第一个请求或者服务发现 //下游主机与端口,允许配置多个 "downstreamhostandports": [ //{ // "host": "ip", // "port": 80 //}, { "host": "localhost", "port": 2108 } ], //熔断配置,在请求下游服务时使用断路 "qosoptions": { "exceptionsallowedbeforebreaking": 3, "durationofbreak": 10, "timeoutvalue": 5000 }, //权限配置 //"authenticationoptions": { // "authenticationproviderkey": "bearer", // "allowedscopes": [] //} }, // api#two项目配置 { "upstreampathtemplate": "/gateway/two/{url}", "upstreamhttpmethod": [ "get", "post", "put", "delete" ], "downstreampathtemplate": "/api/{url}", "downstreamscheme": "http", "servicename": "serviceapitwo", "useservicediscovery": true, "loadbalancer": "roundrobin", "downstreamhostandports": [ //{ // "host": "ip", // "port": 80 //}, { "host": "localhost", "port": 2343 } ], "qosoptions": { "exceptionsallowedbeforebreaking": 3, "durationofbreak": 10, "timeoutvalue": 5000 }, //"authenticationoptions": { // "authenticationproviderkey": "bearer", // "allowedscopes": [] //} }, //swagger api2配置 { "upstreampathtemplate": "/serviceapitwo/swagger.json", "upstreamhttpmethod": [ "get", "post", "put", "delete" ], "downstreampathtemplate": "/serviceapitwo/swagger.json", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 2343 } ] }, //swagger api1多版本配置v1.0 { "upstreampathtemplate": "/serviceapione/1.0/swagger.json", "upstreamhttpmethod": [ "get", "post", "put", "delete" ], "downstreampathtemplate": "/serviceapione/1.0/swagger.json", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 2108 } ] }, //swagger api1多版本配置v2.0 { "upstreampathtemplate": "/serviceapione/2.0/swagger.json", "upstreamhttpmethod": [ "get", "post", "put", "delete" ], "downstreampathtemplate": "/serviceapione/2.0/swagger.json", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 2108 } ] } ] }
startup.configureservices注册swagger和ocelot网关服务,configureservices中的swagger配置和业务api中一样,
services.addocelot(configuration) .addconsul() .addcachemanager(c => c.withdictionaryhandle()) .addpolly(); services.addswaggergen(options => { options.swaggerdoc(configuration["swagger:name"], new info { title = configuration["swagger:title"], version = configuration["swagger:version"], description = configuration["swagger:description"], license = new license { name = configuration["swagger:license:name"], url = configuration["swagger:license:url"] }, contact = new contact { name = configuration["swagger:contact:name"], email = configuration["swagger:contact:email"], url = configuration["swagger:contact:url"] } }); });
startup.configure中,我是使用了配置文件,将业务api的swagger节点写在了配置文件中。
public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); // 配置文件中的swaggername为业务api中是swaggerendpoint名称,有版本号的带上版本号 var apis = configuration["swaggerapis:swaggername"].split(';').tolist(); app.useswagger(); app.useswaggerui(options=> { //显示注册到网关的api接口 apis.foreach(key => { options.swaggerendpoint($"/{key}/swagger.json", key); }); options.documenttitle = "api网关"; }); } else { app.useexceptionhandler("/home/error"); } app.usestaticfiles(); app.usecookiepolicy(); app.usemvc(routes => { routes.maproute( name: "default", template: "{controller=home}/{action=index}/{id?}"); }); app.useocelot().wait(); // 使用ocelot网关中间件 }
修改apigateway项目program.cs将配置文件添加进来。
public static iwebhostbuilder createwebhostbuilder(string[] args) => webhost.createdefaultbuilder(args) .configureappconfiguration((hostingcontext,config)=> { var env = hostingcontext.hostingenvironment; //根据环境变量加载不同的json配置 config.addjsonfile($"appsettings.{env.environmentname}.json", optional: true, reloadonchange: true) .addjsonfile("ocelotconfig.json")//网关配置 .addenvironmentvariables();//环境变量 }) .configurelogging((hostingcontext,logging)=> { logging.addconfiguration(hostingcontext.configuration.getsection("logging")); logging.addconsole(); //添加调试日志 logging.adddebug(); }) .usestartup<startup>();
网关配置了,swagger也配置了,启动业务api和网关服务看看效果。
两个业务api的swagger文档都可以正常查看。发请求看一下,结果响应404,仔细看一下,请求的服务地址是网关服务的地址,而不是业务api的地址,难道是ocelot网关路由配置错了?使用postman发一个get请求看看,localhost:1999/gateway/two/values,网关转发到localhost:2343/api/values,响应正常。
看swashbuckle文档这一段,将业务api中configure加上这一段后再次通过网关发起请求,结果出现typeerror。既然出错了,打开浏览器调试工具看一下就明白了,failed to load http://localhost:2343/api/apitwo: no 'access-control-allow-origin' header is present on the requested resource. origin 'http://localhost:1999' is therefore not allowed access.
网关请求业务api跨域了,要让业务api允许来自网关的请求,需要设置业务api跨域请求政策。加上下面的配置后,网关请求正常了。
修改startup.configure
public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); app.useswagger(c=> { //处理网关通过swagger访问 c.preserializefilters.add((swaggerdoc, httpreq) => swaggerdoc.host = httpreq.host.value); c.routetemplate = "{documentname}/swagger.json"; }); app.useswaggerui(u => { u.swaggerendpoint("/serviceapitwo/swagger.json", "serviceapitwo"); u.documenttitle = "service api #2 文档"; }); } // 允许网关访问 app.usecors(options => { options.withorigins("http://localhost:1999") .allowanyheader() .allowanymethod(); }); app.usemvc(); }
使用identityserver4保护webapi
先前已经创建identityserver项目,添加引用identityserver4.aspnetidentity(2.5.0)、identityserver4.entityframework(2.5.0)。新建一个类identityserverconfig,里面定义四个方法getapiresources、getidentityresources、getclients、gettestusers,具体代码就不贴了,看一下startup。生产环境的话,当然要用数据库,这里不讨论identityserver4的使用。
public void configureservices(iservicecollection services) { services.addmvc().setcompatibilityversion(compatibilityversion.version_2_2); services.addidentityserver() .adddevelopersigningcredential() .addinmemorypersistedgrants() .addtestusers(identityserverconfig.gettestusers()) .addinmemoryidentityresources(identityserverconfig.getidentityresources()) .addinmemoryapiresources(identityserverconfig.getapiresources()) .addinmemoryclients(identityserverconfig.getclients(configuration)); } // this method gets called by the runtime. use this method to configure the http request pipeline. public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } app.useidentityserver(); app.usemvc(); }
将网关和两个api项目对应的apiresources和clients分别为api-gateway、service-api-one、service-api-two,两个api客户端allowedscope为自己,网关的allowedscope为自己和两个api客户端。在需要保护的三个项目中添加引用identityserver4.accesstokenvalidation(2.7.0),修改startup的configureservices,添加如下代码。
//使用identityserver4 services.addauthentication(identityserverauthenticationdefaults.authenticationscheme) .addidentityserverauthentication(options => { options.apiname = "service-api-two"; options.authority = "http://localhost:16690"; // identityserver验证服务 options.requirehttpsmetadata = false; options.enablecaching = true; });
startup.configure中添加app.useauthentication();
要在swagger中访问需要验证的api,需要在swagger配置中添加安全验证。
services.addswaggergen(options => { //swaggerdoc的第一个参数要与configure中swaggerendpoint中的版本名称一致 //既可以使用版本号,也可以使用自定义名称 options.swaggerdoc("serviceapitwo", new info { title = "services.api #two", version = "v1", description = "服务api #two", license = new license { name = "mit", url = "https://mit-license.org/" }, contact = new contact { name = "原来是李", url = "http://www.cnblogs.com/falltakeman" } }); var xmlfile = $"{assembly.getexecutingassembly().getname().name}.xml"; var xmlpath = path.combine(appdomain.currentdomain.basedirectory, xmlfile); options.includexmlcomments(xmlpath); // swagger访问需要验证的api options.addsecuritydefinition("bearer", new apikeyscheme { in = "header", name = "authorization", type = "apikey", description = "bearer {token}" }); options.addsecurityrequirement(new dictionary<string, ienumerable<string>> { { "bearer", enumerable.empty<string>() } }); });
在api控制器中,在需要保护的api上添加[authorize]特性,没有授权的情况下访问受限api会报401错误。
使用postman获取token,在swagger中填写token,再次发起请求,响应正常。
在apigateway的startup.configureservices添加authentication,在services.addswaggergen添加相应代码,启动项目在app.useocelot().wait()抛出异常:scheme already exists: beareridentityserverauthenticationjwt. 最终使用了下面的方式。在apigateway项目中通过swagger也可以访问业务api了。
action<identityserverauthenticationoptions> isaopt = option => { option.authority = configuration["identityservice:uri"]; option.requirehttpsmetadata = convert.toboolean(configuration["identityservice:usehttps"]); option.apiname = configuration["identityservice:apiname"]; option.apisecret = configuration["identityservice:apisecret"]; option.supportedtokens = supportedtokens.both; }; services.addauthentication().addidentityserverauthentication(configuration["identityservice:defaultscheme"], isaopt);
但是配置中的identityservice:defaultscheme不可以是"bearer",试验配置的是"identitybearer",不知为何不可以是"bearer",不知道有没有懂这个的可以指点一二。
the end...