Ocelot(四)- 认证与授权
ocelot(四)- 认证与授权
作者:markjiang7m2
原文地址:
源码地址:https://gitee.com/sevenm2/ocelotdemo
本文是我关于ocelot系列文章的第四篇,认证与授权。在前面的系列文章中,我们的下游服务接口都是公开的,没有经过任何的认证,只要知道接口的调用方法,任何人都可以随意调用,因此,很容易就造成信息泄露或者服务被攻击。
正如,我要找willing干活之前,我得先到hr部门那里登记并且拿到属于我自己的工卡,然后我带着我的工卡去找willing,亮出我是公司员工的身份,并且有权利要求他帮我完成一个任务。
在这里集成一套 .net core的服务认证框架identityserver4,以及如何在ocelot中接入identityserver4的认证与授权。
跟上一篇ocelot(三)- 服务发现文章中的consul类似,这一个是关于ocelot的系列文章,我暂时也不打算详细展开说明identityserver4,在本文中也是使用identityserver4最简单的client认证模式。
关于更多的ocelot功能介绍,可以查看我的系列文章
本文中涉及案例的完整代码都可以从我的代码仓库进行下载。
identityserver4使用
identityserver4有多种认证模式,包括用户密码、客户端等等,我这里只需要实现identityserver4的验证过程即可,因此,我选择了使用最简单的客户端模式。
首先我们来看,当没有ocelot网关时系统是如何使用identityserver4进行认证的。
客户端需要先想identityserver请求认证,获得一个token,然后再带着这个token向下游服务发出请求。
我尝试根据流程图搭建出这样的认证服务。
创建identityserver服务端
新建一个空的asp.net core web api项目,因为这个项目只做identityserver服务端,因此,我将controller也直接删除掉。
使用nuget添加identityserver4,可以直接使用nuget包管理器搜索identityserver4
进行安装,或者通过vs中内置的powershell执行下面的命令行
install-package identityserver4
在appsettings.json
中添加identityserver4的配置
{ "logging": { "loglevel": { "default": "warning" } }, "ssoconfig": { "apiresources": [ { "name": "identityapiservice", "displayname": "identityapiservicename" } ], "clients": [ { "clientid": "mark", "clientsecrets": [ "markjiang7m2" ], "allowedgranttypes": "clientcredentials", "allowedscopes": [ "identityapiservice" ] } ] }, "allowedhosts": "*" }
apiresources
为数组类型,表示identityserver管理的所有的下游服务列表
- name: 下游服务名称
- displayname: 下游服务别名
clients
为数组类型,表示identityserver管理的所有的上游客户端列表
- clientid: 客户端id
- clientsecrets: 客户端对应的密钥
- allowedgranttypes: 该客户端支持的认证模式,目前支持如下:
- implicit
- implicitandclientcredentials
- code
- codeandclientcredentials
- hybrid
- hybridandclientcredentials
- clientcredentials
- resourceownerpassword
- resourceownerpasswordandclientcredentials
- deviceflow
- implicit
- allowedscopes: 该客户端支持访问的下游服务列表,必须是在
apiresources
列表中登记的
新建一个类用于读取identityserver4的配置
using identityserver4.models; using microsoft.extensions.configuration; using system; using system.collections.generic; using system.linq; using system.threading.tasks; namespace identityserver { public class ssoconfig { public static ienumerable<apiresource> getapiresources(iconfigurationsection section) { list<apiresource> resource = new list<apiresource>(); if (section != null) { list<apiconfig> configs = new list<apiconfig>(); section.bind("apiresources", configs); foreach (var config in configs) { resource.add(new apiresource(config.name, config.displayname)); } } return resource.toarray(); } /// <summary> /// 定义受信任的客户端 client /// </summary> /// <returns></returns> public static ienumerable<client> getclients(iconfigurationsection section) { list<client> clients = new list<client>(); if (section != null) { list<clientconfig> configs = new list<clientconfig>(); section.bind("clients", configs); foreach (var config in configs) { client client = new client(); client.clientid = config.clientid; list<secret> clientsecrets = new list<secret>(); foreach (var secret in config.clientsecrets) { clientsecrets.add(new secret(secret.sha256())); } client.clientsecrets = clientsecrets.toarray(); granttypes granttypes = new granttypes(); var allowedgranttypes = granttypes.gettype().getproperty(config.allowedgranttypes); client.allowedgranttypes = allowedgranttypes == null ? granttypes.clientcredentials : (icollection<string>)allowedgranttypes.getvalue(granttypes, null); client.allowedscopes = config.allowedscopes.toarray(); clients.add(client); } } return clients.toarray(); } } public class apiconfig { public string name { get; set; } public string displayname { get; set; } } public class clientconfig { public string clientid { get; set; } public list<string> clientsecrets { get; set; } public string allowedgranttypes { get; set; } public list<string> allowedscopes { get; set; } } }
在startup.cs
中注入identityserver服务
public void configureservices(iservicecollection services) { var section = configuration.getsection("ssoconfig"); services.addidentityserver() .adddevelopersigningcredential() .addinmemoryapiresources(ssoconfig.getapiresources(section)) .addinmemoryclients(ssoconfig.getclients(section)); services.addmvc().setcompatibilityversion(compatibilityversion.version_2_2); }
使用identityserver中间件
public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } app.useidentityserver(); app.usemvc(); }
配置完成,接下来用debug模式看看identityserver是否可用,尝试向identityserver进行认证。因为需要使用post方式,而且在认证请求的body中加入认证信息,所以我这里借助postman工具完成。
请求路径:<host>
+/connect/token
如果认证正确,会得到如下结果:
如果认证失败,则会返回如下:
这样,最简单的identityserver服务就配置完成了。当然,我刚刚为了快速验证identityserver服务是否搭建成功,所以使用的是debug模式,接下来要使用的话,还是要通过iis部署使用的,我这里就把identityserver服务部署到8005
端口。
下游服务加入认证
在ocelotdownapi
项目中,使用nuget添加accesstokenvalidation包,可以直接使用nuget包管理器搜索identityserver4.accesstokenvalidation
进行安装,或者通过vs中内置的powershell执行下面的命令行
install-package identityserver4.accesstokenvalidation
在appsettings.json
中加入identityserver服务信息
"identityserverconfig": { "serverip": "localhost", "serverport": 8005, "identityscheme": "bearer", "resourcename": "identityapiservice" }
这里的identityapiservice
就是在identityserver服务端配置apiresources
列表中登记的其中一个下游服务。
在startup.cs
中读取identityserver服务信息,加入identityserver验证
public void configureservices(iservicecollection services) { identityserverconfig identityserverconfig = new identityserverconfig(); configuration.bind("identityserverconfig", identityserverconfig); services.addauthentication(identityserverconfig.identityscheme) .addidentityserverauthentication(options => { options.requirehttpsmetadata = false; options.authority = $"http://{identityserverconfig.ip}:{identityserverconfig.port}"; options.apiname = identityserverconfig.resourcename; } ); services.addmvc().setcompatibilityversion(compatibilityversion.version_2_2); } public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } app.useauthentication(); app.usemvc(); }
根据前面的配置,我们添加一个需要授权的下游服务api
注意添加属性[authorize]
因为我这里只是为了演示identityserver的认证流程,所以我只是在其中一个api接口中添加该属性,如果还有其他接口需要整个认证,就需要在其他接口中添加该属性,如果是这个controller所有的接口都需要identityserver认证,那就直接在类名前添加该属性。
using microsoft.aspnetcore.authorization;
// get api/ocelot/identitywilling [httpget("identitywilling")] [authorize] public async task<iactionresult> identitywilling(int id) { var result = await task.run(() => { responseresult response = new responseresult() { comment = $"我是willing,既然你是我公司员工,那我就帮你干活吧, host: {httpcontext.request.host.value}, path: {httpcontext.request.path}" }; return response; }); return ok(result); }
重新打包ocelotdownapi
项目,并发布到8001
端口。
首先,像之前那样直接请求api,得到如下结果:
得到了401
的状态码,即未经授权。
因此,我必须先向identityserver请求认证并授权
然后将得到的token
以bearer
的方式加入到向下游服务的请求当中,这样我们就可以得到了正确的结果
可能有些朋友在这里会有点疑惑,在postman中我们在authorization
中加入这个token,但是在我们实际调用中该怎么加入token?
其实熟悉postman的朋友可能就知道怎么一回事,postman为了我们在使用过程中更加方便填入token信息而单独列出了authorization
,实际上,最终还是会转换加入到请求头当中
这个请求头的key就是authorization
,对应的值是bearer
+ (空格)
+ token
。
以上就是没有ocelot网关时,identityserver的认证流程。
案例五 ocelot集成identityserver服务
在上面的例子中,我是直接将下游服务暴露给客户端调用,当接入ocelot网关时,我们要达到内外互隔的特性,于是就把identityserver服务也托管到ocelot网关中,这样我们就能统一认证和服务请求时的入口。
于是,我们可以形成下面这个流程图:
根据流程图,我在ocelot reroutes
中添加两组路由
{ "downstreampathtemplate": "/connect/token", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 8005 } ], "upstreampathtemplate": "/token", "upstreamhttpmethod": [ "post" ], "priority": 2 }, { "downstreampathtemplate": "/api/ocelot/identitywilling", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 8001 } ], "upstreampathtemplate": "/ocelot/identitywilling", "upstreamhttpmethod": [ "get" ], "priority": 2 }
第一组是将identityserver服务进行托管,这样客户端就可以直接通过ocelot网关访问/token
就可以进行认证,第二组是将下游服务进行托管
然后,也是按照之前例子的步骤,先通过http://localhost:4727/token
认证,然后将得到的token
以bearer
的方式加入到向下游服务的请求当中
结果也是跟我预想的是一致的,可以按照这样的流程进行身份认证。
但是!!!但是!!!但是!!!
当外面随便来一个人,跟前台说他要找我做一件事情,然后前台直接告诉他我的具体位置,就让他进公司找我了,然后当我接待他的时候,我才发现这个人根本就是来搞事的,拒绝他的请求。如果一天来这么几十号人,我还要不要正常干活了?
这明显就不符合实际应用场景,外面的人(客户端)在前台(ocelot)的时候,就需要进行身份认证(identityserver),只有通过认证的人才能进公司(路由),我才会接触到这个人(响应),这才叫专人做专事。
于是,认证流程改为下图:
准备下游服务
为了保证我的案例与上面这个认证流程是一致的,我就把前面在下游服务中的认证配置去掉。而且在实际生产环境中,客户端与下游服务的网络是隔断的,客户端只能通过网关的转发才能向下游服务发出请求。
ocelotdownapi项目
public void configureservices(iservicecollection services) { //identityserverconfig identityserverconfig = new identityserverconfig(); //configuration.bind("identityserverconfig", identityserverconfig); //services.addauthentication(identityserverconfig.identityscheme) // .addidentityserverauthentication(options => // { // options.requirehttpsmetadata = false; // options.authority = $"http://{identityserverconfig.ip}:{identityserverconfig.port}"; // options.apiname = identityserverconfig.resourcename; // } // ); services.addmvc().setcompatibilityversion(compatibilityversion.version_2_2); } public void configure(iapplicationbuilder app, ihostingenvironment env) { if (env.isdevelopment()) { app.usedeveloperexceptionpage(); } //app.useauthentication(); app.usemvc(); }
同时也把api接口中的[authorize]
属性去除。
然后将ocelotdownapi
项目重新打包,部署在8001
、8002
端口,作为两个独立的下游服务。
配置identityserver
回到identityserver
项目的appsettings.json
,在apiresources
中另外添加两个服务
{ "name": "identityapiservice8001", "displayname": "identityapiservice8001name" }, { "name": "identityapiservice8002", "displayname": "identityapiservice8002name" }
在clients
中添加两个client
{ "clientid": "markfull", "clientsecrets": [ "markjiang7m2" ], "allowedgranttypes": "clientcredentials", "allowedscopes": [ "identityapiservice8001", "identityapiservice8002" ] }, { "clientid": "marklimit", "clientsecrets": [ "123456" ], "allowedgranttypes": "clientcredentials", "allowedscopes": [ "identityapiservice8001" ] }
这里我为了能让大家看出允许访问范围的效果,特意分配了两个不同的allowedscopes
。
使用markfull
登录的客户端可以同时请求identityapiservice8001
和identityapiservice8002
两个下游服务,而使用marklimit
登录的客户端只允许请求identityapiservice8001
服务。
ocelot集成identityserver认证
跟前面的例子一样,要支持identityserver认证,ocelotdemo项目就需要安装identityserver4.accesstokenvalidation
包。
ocelotdemo
项目的appsettings.json
添加identityserver信息
"identityserverconfig": { "ip": "localhost", "port": 8005, "identityscheme": "bearer", "resources": [ { "key": "apiservice8001", "name": "identityapiservice8001" }, { "key": "apiservice8002", "name": "identityapiservice8002" } ] }
当然这个配置项的结构是任意的,我这里的resources
数组配置的就是ocelot网关支持哪些服务的认证,name
就是服务的名称,同时会唯一对应一个key
。
为了能更加方便读取identityserverconfig
的信息,我定义了一个跟它同结构的类
public class identityserverconfig { public string ip { get; set; } public string port { get; set; } public string identityscheme { get; set; } public list<apiresource> resources { get; set; } } public class apiresource { public string key { get; set; } public string name { get; set; } }
然后来到startup.cs
的configureservices
方法,就能很快地将identityserver
信息进行注册
var identitybuilder = services.addauthentication(); identityserverconfig identityserverconfig = new identityserverconfig(); configuration.bind("identityserverconfig", identityserverconfig); if (identityserverconfig != null && identityserverconfig.resources != null) { foreach (var resource in identityserverconfig.resources) { identitybuilder.addidentityserverauthentication(resource.key, options => { options.authority = $"http://{identityserverconfig.ip}:{identityserverconfig.port}"; options.requirehttpsmetadata = false; options.apiname = resource.name; options.supportedtokens = supportedtokens.both; }); } }
configure
方法中添加
app.useauthentication();
最后,就是配置ocelot.json
文件。
在reroutes
中添加两组路由
{ "downstreampathtemplate": "/api/ocelot/identitywilling", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 8001 } ], "upstreampathtemplate": "/ocelot/8001/identitywilling", "upstreamhttpmethod": [ "get" ], "priority": 2, "authenticationoptions": { "authenticationproviderkey": "apiservice8001", "allowedscopes": [] } }, { "downstreampathtemplate": "/api/ocelot/identitywilling", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 8002 } ], "upstreampathtemplate": "/ocelot/8002/identitywilling", "upstreamhttpmethod": [ "get" ], "priority": 2, "authenticationoptions": { "authenticationproviderkey": "apiservice8002", "allowedscopes": [] } }
跟其他普通路由相比,这两组路由都多了一个authenticationoptions
属性,它里面的authenticationproviderkey
就是我们在前面configureservices
方法中登记过的key
。
我们来捋顺一下这个路由跟认证授权过程。以markfull的id和这里的第一组路由为例。
- 客户端拿着
markfull
的clientid向identityserver(http://localhost:4727/token
)进行认证,得到了一个的token
- 客户端带着这个token,因此有了
markfull
的身份,请求url地址http://localhost:4727/ocelot/8001/identitywilling
- ocelot网关接收到请求,根据路由表找到了认证支持关键字为
apiservice8001
,从而得到了对应的identityserver服务信息:identityserver服务地址为http://localhost:8005
,下游服务名称为identityapiservice8001
- ocelot带着token向identityserver服务(
http://localhost:8005
)进行配对,即查看markfull
身份的访问范围是否包含了identityapiservice8001
服务
- ocelot认证过
markfull
是允许访问的,将请求转发到下游服务中,根据路由配置,下游服务地址为http://localhost:8001/api/ocelot/identitywilling
下面我将ocelot运行起来,并通过postman进行验证。
markfull身份认证
使用markfull
clientid向identityserver进行认证
向8001请求
将得到的token加入到请求中,请求url地址http://localhost:4727/ocelot/8001/identitywilling
,得到下游服务返回的响应结果
向8002请求
将得到的token加入到请求中,请求url地址http://localhost:4727/ocelot/8002/identitywilling
,得到下游服务返回的响应结果
然后,更换marklimit
身份再验证一遍
marklimit身份认证
使用marklimit
clientid向identityserver进行认证
向8001请求
将得到的token加入到请求中,请求url地址http://localhost:4727/ocelot/8001/identitywilling
,得到下游服务返回的响应结果
向8002请求
将得到的token加入到请求中,请求url地址http://localhost:4727/ocelot/8002/identitywilling
,此时,我们得到了401
的状态码,即未授权。
总结
在这篇文章中就跟大家介绍了基于identityserver4为认证服务器的ocelot认证与授权,主要是通过一些案例的实践,让大家理解ocelot对客户端身份的验证过程,使用了identityserver中最简单的客户端认证模式,因为这种模式下identityserver的认证没有复杂的层级关系。但通常在我们实际开发时,更多的可能是通过用户密码等方式进行身份认证的,之后我会尽快给大家分享关于identityserver如何使用其它模式进行认证。今天就先跟大家介绍到这里,希望大家能持续关注我们。