IdentityServer4与ocelot实现认证与客户端统一入口
关于identityserver4与ocelot博客园里已经有很多介绍我这里就不再重复了。
ocelot与identityserver4组合认证博客园里也有很多,但大多使用ocelot内置的认证,而且大多都是用来认证api的,查找了很多资料也没看到如何认证oidc,所以这里的ocelot实际只是作为统一入口而不参与认证,认证的完成依然在客户端。代码是使用identityserver4的quickstart5_hybridandapi 示例修改的。项目结构如下
一 ocelot网关
我们先在示例添加一个网关。
修改launchsettings.json中的端口为54660
"nanofabricapplication": { "commandname": "project", "launchbrowser": true, "applicationurl": "http://localhost:54660", "environmentvariables": { "aspnetcore_environment": "development" } }
配置文件如下
{ "reroutes": [ { // mvcclient "downstreampathtemplate": "/mvcclient/{route}", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50891 } ], "upstreampathtemplate": "/mvcclient/{route}", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } }, { // signin-oidc "downstreampathtemplate": "/signin-oidc", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50891 } ], "upstreampathtemplate": "/signin-oidc", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } }, { // signout-callback-oidc "downstreampathtemplate": "/signout-callback-oidc", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50891 } ], "upstreampathtemplate": "/signout-callback-oidc", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } }, { // myapi "downstreampathtemplate": "/myapi/{route}", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50890 } ], "upstreampathtemplate": "/myapi/{route}", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } }, { // identityserver "downstreampathtemplate": "/{route}", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50875 } ], "upstreampathtemplate": "/identityserver/{route}", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } }, { // identityserver "downstreampathtemplate": "/{route}", "downstreamscheme": "http", "downstreamhostandports": [ { "host": "localhost", "port": 50875 } ], "upstreampathtemplate": "/{route}", "upstreamheadertransform": { "x-forwarded-for": "{remoteipaddress}" } } ] }
这里我们定义3个下游服务,mvcclient,myapi,identityserver,并使用路由特性把signin-oidc,signout-callback-oidc导航到mvcclient,由mvcclient负责生成最后的cooike。并将默认路由指定到identityserver服务。
在configureservices中添加ocelot服务。
services.addocelot() .addcachemanager(x => { x.withdictionaryhandle(); }) .addpolly()
在configure中使用ocelot中间件
app.useocelot().wait();
ocelot网关就部署完成了。
二 修改quickstartidentityserver配置
首先依然是修改launchsettings.json中的端口为50875
在configureservices中修改addidentityserver配置中的publicorigin和issueruri的url为http://localhost:54660/identityserver/
services.addidentityserver(option => { option.publicorigin = "http://localhost:54660/identityserver/"; option.issueruri = "http://localhost:54660/identityserver/"; }) .adddevelopersigningcredential() .addinmemoryidentityresources(config.getidentityresources()) .addinmemoryapiresources(config.getapiresources()) .addinmemoryclients(config.getclients()) .addtestusers(config.getusers());
这样一来发现文档中的identityserver地址就变为网关的地址了,进一步实现identityserver的负载均衡也是没有问题的。
修改config.cs中mvc客户端配置如下
clientid = "mvc", clientname = "mvc client", allowedgranttypes = granttypes.hybridandclientcredentials, clientsecrets = { new secret("secret".sha256()) }, // accesstokentype = accesstokentype.reference, requireconsent = true, redirecturis = { "http://localhost:54660/signin-oidc" }, postlogoutredirecturis = { "http://localhost:54660/signout-callback-oidc" }, allowedscopes = { identityserverconstants.standardscopes.openid, identityserverconstants.standardscopes.profile, "api1" }, allowofflineaccess = true, //直接返回客户端需要的claims alwaysincludeuserclaimsinidtoken = true,
主要修改redirecturis和postlogoutredirecturis为网关地址,在网关也设置了signin-oidc和signout-callback-oidc转发请求到mvc客户端。
三 修改mvcclient
修改mvcclient的launchsettings.json端口为50891。
修改mvcclient的authority地址为http://localhost:54660/identityserver和默认路由地址mvcclient/{controller=home}/{action=index}/{id?}
jwtsecuritytokenhandler.defaultinboundclaimtypemap.clear(); services.addauthentication(options => { options.defaultscheme = "cookies"; options.defaultchallengescheme = "oidc"; //options.defaultsigninscheme = cookieauthenticationdefaults.authenticationscheme; }) .addcookie("cookies",options=> { options.expiretimespan = timespan.fromminutes(30); options.slidingexpiration = true; }) .addopenidconnect("oidc", options => { options.signinscheme = "cookies"; options.authority = "http://localhost:54660/identityserver"; options.requirehttpsmetadata = false; options.clientid = "mvc"; options.clientsecret = "secret"; options.responsetype = "code id_token"; options.savetokens = true; options.getclaimsfromuserinfoendpoint = true; options.scope.add("api1"); options.scope.add("offline_access"); });
app.usemvc(routes => { routes.maproute( name: "default", template: "mvcclient/{controller=home}/{action=index}/{id?}"); });
修改homecontroller,将相关地址修改为网关地址
public async task<iactionresult> callapiusingclientcredentials() { var tokenclient = new tokenclient("http://localhost:54660/identityserver/connect/token", "mvc", "secret"); var tokenresponse = await tokenclient.requestclientcredentialsasync("api1"); var client = new httpclient(); client.setbearertoken(tokenresponse.accesstoken); var content = await client.getstringasync("http://localhost:54660/myapi/identity"); viewbag.json = jarray.parse(content).tostring(); return view("json"); } public async task<iactionresult> callapiusinguseraccesstoken() { var accesstoken = await httpcontext.gettokenasync("access_token"); //openidconnectparameternames var client = new httpclient(); client.setbearertoken(accesstoken); var content = await client.getstringasync("http://localhost:54660/myapi/identity"); viewbag.json = jarray.parse(content).tostring(); return view("json"); }
四 修改api项目
api项目修改多一点。
将mvcclient的homecontroller和相关视图复制过来,模拟mvc与api同时存在的项目。
修改api的launchsettings.json端口为50890。
修改startup
public class startup { public void configureservices(iservicecollection services) { services.adddataprotection(options => options.applicationdiscriminator = "00000").setapplicationname("00000"); services.addmvc(); jwtsecuritytokenhandler.defaultinboundclaimtypemap.clear(); services.addauthentication(options => { options.defaultscheme = "cookies"; options.defaultchallengescheme = "oidc"; }).addcookie("cookies") .addopenidconnect("oidc", options => { options.signinscheme = "cookies"; options.authority = "http://localhost:54660/identityserver"; options.requirehttpsmetadata = false; options.clientid = "mvc"; options.clientsecret = "secret"; options.responsetype = "code id_token"; options.savetokens = true; options.getclaimsfromuserinfoendpoint = true; options.scope.add("api1"); options.scope.add("offline_access"); }) .addidentityserverauthentication("bearer", options => { options.authority = "http://localhost:54660/identityserver"; options.requirehttpsmetadata = false; options.apisecret = "secret123"; options.apiname = "api1"; options.supportedtokens= supportedtokens.both; }); services.addauthorization(option => { //默认 只写 [authorize],表示使用oidc进行认证 option.defaultpolicy = new authorizationpolicybuilder("oidc").requireauthenticateduser().build(); //apicontroller使用这个 [authorize(policy = "apipolicy")],使用jwt认证方案 option.addpolicy("apipolicy", policy => { policy.addauthenticationschemes(jwtbearerdefaults.authenticationscheme); policy.requireauthenticateduser(); }); }); } public void configure(iapplicationbuilder app, ihostingenvironment env) { //var options = new forwardedheadersoptions //{ // forwardedheaders = forwardedheaders.xforwardedfor | forwardedheaders.xforwardedproto | forwardedheaders.xforwardedhost, // forwardlimit = 1 //}; //options.knownnetworks.clear(); //options.knownproxies.clear(); //app.useforwardedheaders(options); //if (env.isdevelopment()) //{ // app.usedeveloperexceptionpage(); //} //else //{ // app.useexceptionhandler("/home/error"); //} app.useauthentication(); app.usestaticfiles(); app.usemvc(routes => { routes.maproute( name: "default", template: "myapi/{controller=maccount}/{action=login}/{id?}"); }); } }
主要添加了oidc认证配置和配置验证策略来同时支持oidc认证和bearer认证。
修改identitycontroller中的[authorize]特性为[authorize(policy = "apipolicy")]
依次使用调试-开始执行(不调试)并选择项目名称启动quickstartidentityserver,gateway,mvcclient,api,启动方式如图
应该可以看到gateway启动后直接显示了identityserver的默认首页
在浏览器输入http://localhost:54660/mvcclient/home/index进入mvcclient
点击secure进入需要授权的页面,这时候会跳转到登陆页面(才怪
实际上我们会遇到一个错误,这是因为ocelot做网关时下游服务获取到的host实际为localhost:50891,而在identityserver中设置的redirecturis为网关的54660,我们可以通过ocelot转发x-forwarded-host头,并在客户端通过useforwardedheaders中间件来获取头。但是useforwardedheaders中间件为了防止ip欺骗攻击需要设置knownnetworks和knownproxies以实现严格匹配。当然也可以通过清空knownnetworks和knownproxies的默认值来不执行严格匹配,这样一来就有可能受到攻击。所以这里我直接使用硬编码的方式设置host,实际使用时应从配置文件获取,同时修改mvcclient和api相关代码
app.use(async (context, next) => { context.request.host = hoststring.fromuricomponent(new uri("http://localhost:54660/")); await next.invoke(); }); //var options = new forwardedheadersoptions //{ // forwardedheaders = forwardedheaders.xforwardedfor | forwardedheaders.xforwardedproto | forwardedheaders.xforwardedhost, // forwardlimit = 1 //}; //options.knownnetworks.clear(); //options.knownproxies.clear(); //app.useforwardedheaders(options);
在反向代理情况下通过转发x-forwarded-host头来获取host地址应该时常见设置不知道还有没有其他更好的解决办法。
再次启动mvcclient并输入http://localhost:54660/mvcclient/home/secure。
使用bob,password登陆一下
点击yes, allow返回http://localhost:54660/mvcclient/home/secure,此时可以查看到登陆后的信息
分别点击call api using user token和call api using application identity来验证一下通过access_token和clientcredent模式请求来请求api
成功获取到返回值。
输入http://localhost:54660/myapi/home/index来查看api情况
请求成功。
点击secure从api项目查看用户信息,此时展示信息应该和mvcclient一致
嗯,并没有看到用户信息而是又到了授权页.....,这是因为.netcore使用dataprotection来保护数据(),api项目不能解析由mvcclient生成的cookie,而被重定向到了identityserver服务中。
在mvcclient和api的configureservices下添加如下代码来同步密钥环。
services.adddataprotection(options => options.applicationdiscriminator = "00000").setapplicationname("00000");
再次启动mvcclient和api项目并在浏览器中输入http://localhost:54660/mvcclient/home/secure,此时被要求重新授权,点击yes, allow后看到用户信息
再输入http://localhost:54660/myapi/home/secure从api项目查看用户信息
分别点击call api using user token和call api using application identity来验证一下通过access_token和clientcredent模式请求来请求api
请求成功。
如此我们便实现了通过ocelot实现统一入口,通过identityserver4来实现认证的需求
源代码 https://github.com/saber-wang/quickstart5_hybridandapi
参考