asp.net core系列 60 Ocelot 构建服务认证示例
一.概述
在ocelot中,为了保护下游api资源,用户访问时需要进行认证鉴权,这需要在ocelot 网关中添加认证服务。添加认证后,reroutes路由会进行身份验证,并使用ocelot的基于声明的功能。在startup.cs中注册认证服务,为每个注册提供一个方案 (authenticationproviderkey身份验证提供者密钥)。
//下面是在网关项目中,添加认证服务 public void configureservices(iservicecollection services) { var authenticationproviderkey = "testkey"; services.addauthentication() .addjwtbearer(authenticationproviderkey, x => { //.. }); }
其中testkey是此提供程序已注册的方案,将映射到reroute的配置中
"authenticationoptions": { "authenticationproviderkey": "testkey", "allowedscopes": [] }
当ocelot运行时,会查看此configuration.json中的authenticationproviderkey节点,并检查是否使用给定密钥,该密钥是否已注册身份验证提供程序。如果没有,那么ocelot将无法启动。如果有,则reroute将在执行时使用该提供程序。
本次示例有四个项目:
apigateway网关项目 http://localhost:9000
authserver项目生成jwt令牌服务 http://localhost:9009
customerapiservices 是web api项目 http://localhost:9001
clientapp项目 模拟客户端httpclient
当客户想要访问web api服务时,首先访问api网关的身份验证模块。我们需要首先访问authserver以获取访问令牌,以便我们可以使用access_token访问受保护的api服务。开源github地址, 架构如下图所示:
二. authserver项目
此服务主要用于,为用户请求受保护的api,需要的jwt令牌。生成jwt关键代码如下:
/// <summary> ///用户使用 用户名密码 来请求服务器 ///服务器进行验证用户的信息 ///服务器通过验证发送给用户一个token ///客户端存储token,并在每次请求时附送上这个token值, headers: {'authorization': 'bearer ' + token} ///服务端验证token值,并返回数据 /// </summary> /// <param name="name"></param> /// <param name="pwd"></param> /// <returns></returns> [httpget] public iactionresult get(string name, string pwd) { //验证用户,通过后发送一个token if (name == "catcher" && pwd == "123") { var now = datetime.utcnow; //添加用户的信息,转成一组声明,还可以写入更多用户信息声明 var claims = new claim[] { //声明主题 new claim(jwtregisteredclaimnames.sub, name), //jwt id 唯一标识符 new claim(jwtregisteredclaimnames.jti, guid.newguid().tostring()), //发布时间戳 issued timestamp new claim(jwtregisteredclaimnames.iat, now.touniversaltime().tostring(), claimvaluetypes.integer64) }; //下面使用 microsoft.identitymodel.tokens帮助库下的类来创建jwttoken //安全秘钥 var signingkey = new symmetricsecuritykey(encoding.ascii.getbytes(_settings.value.secret)); //生成jwt令牌(json web token) var jwt = new jwtsecuritytoken( //jwt发行方 issuer: _settings.value.iss, //jwt订阅者 audience: _settings.value.aud, //jwt一组声明 claims: claims, notbefore: now, //jwt令牌过期时间 expires: now.add(timespan.fromminutes(2)), //签名凭证: 安全密钥、签名算法 signingcredentials: new signingcredentials(signingkey, securityalgorithms.hmacsha256) ); //序列化jwt对象,写入一个字符串encodedjwt var encodedjwt = new jwtsecuritytokenhandler().writetoken(jwt); var responsejson = new { access_token = encodedjwt, expires_in = (int)timespan.fromminutes(2).totalseconds }; //以json形式返回 return json(responsejson); } else { return json(""); } } }
在之前讲is4的第55篇中,讲resourceownerpasswords项目,获取token也是要发送用户名和密码,那是由is4来完成,包括自动:验证用户,生成jwttoken。这里由system.identitymodel.tokens类库来生成jwttoken。最后返回jwt令牌token给用户。
当catcher用户请求:http://localhost:9009/api/auth?name=catcher&pwd=123服务时,产生jwt令牌token,下面是换了行的token, 如下所示:
{"access_token":"eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9
.eyjzdwiioijjyxrjagvyiiwianrpijoizwjmnwiyzgitndg5ys00otbjltk0njutodzmote5ywezmdrjiiwiawf0ijoimjaxos80lzi1ide6ntc6mjailcjuymyioje1ntyxntc0ndasimv4cc
i6mtu1nje1nzu2mcwiaxnzijoiahr0cdovl3d3dy5jlxnoyxjwy29ybmvylmnvbs9tzw1izxjzl2nhdgnozxitd29uzyisimf1zci6iknhdgnozxigv29uzyj9
.o2ji7nsnothl9agbr0vhmdobsxhdeoxkynougasekkg","expires_in":120}
简单了解下jwt(json web token),它是在web上以json格式传输的token。该token被设计为紧凑声明表示格式,意味着字节少,它可以在get url中,header中,post parameter中进行传输。
jwt一般由三段构成(header.payload.signature),用"."号分隔开,是base64编码的,可以把该字符串放到https://jwt.io/中进行查看,如下所示:
在header中:alg:声明加密的算法,这里为hs256。typ:声明类型,这里为jwt。
在payload中:
sub: 主题, jwt发布者名称。
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。也就是请求生成的token不一样。
iat: 签发时间
nbf: 在什么时间之前,该jwt都是不可用的,是时间戳格式。
exp:jwt的过期时间,这个过期时间必须要大于签发时间。
adu: 订阅者,接收jwt的一方。
iss: jwt的发行方。
signature(数字签名,防止信息被篡改):
包含了:base64后的header,payload ,secret,secret就是用来进行jwt的签发和jwt的验证。相当于服务端的私钥。该secret在示例中,用在authserver和customerapiservices项目中。
三. customerapiservices项目
在该web api 项目中启用身份验证来保护api服务,使用jwtbearer,将默认的身份验证方案设置为testkey。添加身份验证代码如下:
public void configureservices(iservicecollection services) { //获取当前用户(订阅者)信息 var audienceconfig = configuration.getsection("audience"); //获取安全秘钥 var signingkey = new symmetricsecuritykey(encoding.ascii.getbytes(audienceconfig["secret"])); //token要验证的参数集合 var tokenvalidationparameters = new tokenvalidationparameters { //必须验证安全秘钥 validateissuersigningkey = true, issuersigningkey = signingkey, //必须验证发行方 validateissuer = true, validissuer = audienceconfig["iss"], //必须验证订阅者 validateaudience = true, validaudience = audienceconfig["aud"], //是否验证token有效期,使用当前时间与token的claims中的notbefore和expires对比 validatelifetime = true, // 允许的服务器时间偏移量 clockskew = timespan.zero, //是否要求token的claims中必须包含expires requireexpirationtime = true, }; //添加服务验证,方案为testkey services.addauthentication(o => { o.defaultauthenticatescheme = "testkey"; }) .addjwtbearer("testkey", x => { x.requirehttpsmetadata = false; ////在jwtbeareroptions配置中,issuersigningkey(签名秘钥)、validissuer(token颁发机构)、validaudience(颁发给谁)三个参数是必须的。 x.tokenvalidationparameters = tokenvalidationparameters; }); services.addmvc(); }
新建一个customerscontroller类,在api方法中使用authorize属性。
[route("api/[controller]")] public class customerscontroller : controller { //authorize]:加了该标记,当用户请求时,需要发送有效的jwt [authorize] [httpget] public ienumerable<string> get() { return new string[] { "catcher wong", "james li" }; } //未加授权标记,不受保护,任何用户都可以获取 [httpget("{id}")] public string get(int id) { return $"catcher wong - {id}"; } }
下面运行,在浏览器中直接访问http://localhost:9001/api/customers 报http 500错误,而访问http://localhost:9001/api/customers/1 则成功http 200,显示“catcher wong - 1”
四. apigateway网关
添加认证服务,基本与customerapiservices项目中的认证服务一样。代码如下:
public void configureservices(iservicecollection services) { //获取当前用户(订阅者)信息 var audienceconfig = configuration.getsection("audience"); //获取安全秘钥 var signingkey = new symmetricsecuritykey(encoding.ascii.getbytes(audienceconfig["secret"])); //token要验证的参数集合 var tokenvalidationparameters = new tokenvalidationparameters { //必须验证安全秘钥 validateissuersigningkey = true, issuersigningkey = signingkey, //必须验证发行方 validateissuer = true, validissuer = audienceconfig["iss"], //必须验证订阅者 validateaudience = true, validaudience = audienceconfig["aud"], //是否验证token有效期,使用当前时间与token的claims中的notbefore和expires对比 validatelifetime = true, // 允许的服务器时间偏移量 clockskew = timespan.zero, //是否要求token的claims中必须包含expires requireexpirationtime = true, }; //添加服务验证,方案为testkey services.addauthentication(o => { o.defaultauthenticatescheme = "testkey"; }) .addjwtbearer("testkey", x => { x.requirehttpsmetadata = false; //在jwtbeareroptions配置中,issuersigningkey(签名秘钥)、validissuer(token颁发机构)、validaudience(颁发给谁)三个参数是必须的。 x.tokenvalidationparameters = tokenvalidationparameters; }); //这里也可以使用is4承载令牌 /* var authenticationproviderkey = "testkey"; action<identityserverauthenticationoptions> options = o => { o.authority = "https://whereyouridentityserverlives.com"; o.apiname = "api"; o.supportedtokens = supportedtokens.both; o.apisecret = "secret"; }; services.addauthentication() .addidentityserverauthentication(authenticationproviderkey, options); */ //添加ocelot网关服务时,包括secret秘钥、iss发布者、aud订阅者 services.addocelot(configuration); }
在is4中是由authority参数指定oidc服务地址,oidc可以自动发现issuer, issuersigningkey等配置,而o.audience与x.tokenvalidationparameters = new tokenvalidationparameters { validaudience = "api" }是等效的。
下面应该修改configuration.json文件。添加一个名为authenticationoptions的新节点,并使authenticationproviderkey与我们在startup类中定义的相同。
"reroutes": [ { "downstreampathtemplate": "/api/customers", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 9001 } ], "upstreampathtemplate": "/customers", "upstreamhttpmethod": [ "get" ], "authenticationoptions": { "authenticationproviderkey": "testkey", "allowedscopes": [] } }
apigateway网关项目和customerapiservices项目的appsettings.json文件,都配置了订阅者信息如下:
{ "audience": { "secret": "y2f0y2hlciuymhdvbmclmjbsb3zljtiwlm5lda==", "iss": "http://www.c-sharpcorner.com/members/catcher-wong", "aud": "catcher wong" } }
五. clientapp项目
最后使用的客户端应用程序,来模拟api网关的一些请求。首先,我们需要添加一个方法来获取access_token。
/// <summary> /// 获取jwttoken /// </summary> /// <returns></returns> private static string getjwt() { httpclient client = new httpclient(); //9000是网关,会自动转发到下游服务器, client.baseaddress = new uri( "http://localhost:9000"); client.defaultrequestheaders.clear(); //转发到authserver的9009 var res2 = client.getasync("/api/auth?name=catcher&pwd=123").result; dynamic jwt = jsonconvert.deserializeobject(res2.content.readasstringasync().result); return jwt.access_token; }
接着,编写了三段代码 , 通过api gateway网关, 来访问customerapiservices项目中的api服务:
static void main(string[] args) { httpclient client = new httpclient(); client.defaultrequestheaders.clear(); client.baseaddress = new uri("http://localhost:9000"); // 1. 需要授权的api访问,没有token时,返回http状态401 var reswithouttoken = client.getasync("/customers").result; console.writeline($"sending request to /customers , without token."); console.writeline($"result : {reswithouttoken.statuscode}"); //2. 需要授权的api访问,获取令牌请求api,返回http状态200正常 client.defaultrequestheaders.clear(); console.writeline("\nbegin auth...."); var jwt = getjwt(); console.writeline("end auth...."); console.writeline($"\ntoken={jwt}"); client.defaultrequestheaders.add("authorization", $"bearer {jwt}"); var reswithtoken = client.getasync("/customers").result; console.writeline($"\nsend request to /customers , with token."); console.writeline($"result : {reswithtoken.statuscode}"); console.writeline(reswithtoken.content.readasstringasync().result); //3.不需要授权的api访问,返回http状态200正常 console.writeline("\nno auth service here "); client.defaultrequestheaders.clear(); var res = client.getasync("/customers/1").result; console.writeline($"send request to /customers/1"); console.writeline($"result : {res.statuscode}"); console.writeline(res.content.readasstringasync().result); console.read(); }
参考文献
在asp.net核心中使用ocelot构建api网关 - 身份验证