前言
上一篇文章介紹了IdentityServer4的各種授權模式,本篇繼續介紹使用IdentityServer4實現單點登入效果。
單點登入(SSO)
SSO( Single Sign-On ),中文意即單點登入,單點登入是一種控制多個相關但彼此獨立的系統的訪問許可權,擁有這一許可權的使用者可以使用單一的ID和密碼訪問某個或多個系統從而避免使用不同的使用者名稱或密碼,或者通過某種配置無縫地登入每個系統。
概括就是:一次登入,多處訪問
案例場景:
1、提供資源服務(WebApi):訂單:Order(cz.Api.Order)、商品:Goods(cz.Api.Goods)……
2、業務中存在多個系統:門戶系統、訂單系統、商品系統……
3、實現使用者登入門戶後,跳轉訂單系統、商品系統時,不需要登入認證(單點登入效果)
一、環境準備:
調整專案如下圖結構:
在身份認證專案(cz.IdentityServer)中InMemoryConfig中客戶端列表中新增以下客戶端內容:(其他內容同上一篇設定相同)
new Client { ClientId = "main_client", ClientName = "Implicit Client", ClientSecrets = new [] { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { "http://localhost:5020/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5020/signout-callback-oidc" }, //是否顯示授權提示介面 RequireConsent = true, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }, new Client { ClientId = "order_client", ClientName = "Order Client", ClientSecrets = new [] { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, AllowedScopes = { "order","goods", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile }, RedirectUris = { "http://localhost:5021/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5021/signout-callback-oidc" }, //是否顯示授權提示介面 RequireConsent = true, }, new Client { ClientId = "goods_client", ClientName = "Goods Client", ClientSecrets = new [] { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "http://localhost:5022/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5022/signout-callback-oidc" }, //是否顯示授權提示介面 RequireConsent = true, AllowedScopes = { "goods", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }
二、程式實現:
1、訂單、商品Api專案:
a)訂單API專案調整:新增Nuget包引用:
Install-Package IdentityServer4.AccessTokenValidation
b)調整Statup檔案:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //IdentityServer services.AddMvcCore() .AddAuthorization(); //配置IdentityServer services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => { options.RequireHttpsMetadata = false; //是否需要https options.Authority = $"http://localhost:5600"; //IdentityServer授權路徑 options.ApiName = "order"; //需要授權的服務名稱 }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
c)新增控制器:OrderController
namespace cz.Api.Order.Controllers { [ApiController] [Route("[controller]")] [Authorize]
public class OrderController : ControllerBase { private static readonly string[] Summaries = new[] { "Order1", "Order2", "Order3", "Order4", "Order5", "Order6", "Order7", "Order8", "Order9", "Order10" }; private readonly ILogger<OrderController> _logger; public OrderController(ILogger<OrderController> logger) { _logger = logger; } //模擬返回資料 [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(); } } }
d)商品專案同步調整,調整Api和方法
2、門戶專案:
新增Nuget引用:
Install-Package IdentityServer4.AccessTokenValidation
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect
a)調整HomeController如下內容:
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } [Authorize] public IActionResult Index() { //模擬返回應用列表 List<AppModel> apps = new List<AppModel>(); apps.Add(new AppModel() { AppName = "Order Client", Url = "http://localhost:5021" }); apps.Add(new AppModel() { AppName = "Goods Client", Url = "http://localhost:5022" }); return View(apps); } [Authorize] public IActionResult Privacy() { return View(); } public IActionResult Logout() { return SignOut("oidc", "Cookies"); } }
b)調整主頁檢視:
@model List<AppModel> @{ ViewData["Title"] = "Home Page"; } <style> .box-wrap { text-align: center; /* background-color: #d4d4f5;*/ overflow: hidden; } .box-wrap > div { width: 31%; padding-bottom: 31%; margin: 1%; border-radius: 10%; float: left; background-color: #36A1DB; } </style> <div class="text-center"> <div class="box-wrap"> @foreach (var item in Model) { <div class="box"> <a href="@item.Url" target="_blank">@item.AppName</a> </div> } </div> </div>
c)調整Statup檔案中ConfigureServices方法:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.Lax; }); JwtSecurityTokenHandler.DefaultMapInboundClaims = false; services.AddControllersWithViews(); services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.RequireHttpsMetadata = false; options.Authority = "http://localhost:5600"; options.ClientId = "main_client"; options.ClientSecret = "secret"; options.ResponseType = "id_token"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; //事件 options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents() { //遠端故障 OnRemoteFailure = context => { context.Response.Redirect("/"); context.HandleResponse(); return Task.FromResult(0); }, //訪問拒絕 OnAccessDenied = context => { //重定向到指定頁面 context.Response.Redirect("/"); //停止此請求的所有處理並返回給客戶端 context.HandleResponse(); return Task.FromResult(0); }, }; }); }
3、訂單、商品客戶端專案:
新增Nuget引用:
Install-Package IdentityServer4.AccessTokenValidation
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect
a)修改HomeController內容如下:
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } [Authorize] public IActionResult Index() { return View(); } public async Task<IActionResult> PrivacyAsync() { var accessToken = await HttpContext.GetTokenAsync("access_token"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var content = await client.GetStringAsync("http://localhost:5601/order"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var contentgoods = await client.GetStringAsync("http://localhost:5602/goods"); ViewData["Json"] = $"Goods:{contentgoods}\r\n " + $"Orders:{content}"; return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } public IActionResult Logout() { return SignOut("oidc", "Cookies"); } }
b)調整對應檢視內容:
#####Home.cshtml
@{ ViewData["Title"] = "Home Page"; } @using Microsoft.AspNetCore.Authentication <h2>Claims</h2> <div class="text-center"> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl> </div> <div class="text-center"> <h2>Properties</h2> <dl> @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) { <dt>@prop.Key</dt> <dd>@prop.Value</dd> } </dl> </div>
#####Privacy.cshtml
@{
ViewData["Title"] = "API Result";
}
<h1>@ViewData["Title"]</h1>
<p>@ViewData["Json"]</p>
c)Statup中設定客戶端資訊
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.Lax; }); JwtSecurityTokenHandler.DefaultMapInboundClaims = false; services.AddControllersWithViews(); services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.RequireHttpsMetadata = false; options.Authority = "http://localhost:5600"; options.ClientId = "order_client"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.SaveTokens = true; options.Scope.Add("order"); options.Scope.Add("goods"); options.GetClaimsFromUserInfoEndpoint = true; //事件 options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents() { //遠端故障 OnRemoteFailure = context => { context.Response.Redirect("/"); context.HandleResponse(); return Task.FromResult(0); }, //訪問拒絕 OnAccessDenied = context => { //重定向到指定頁面 context.Response.Redirect("/"); //停止此請求的所有處理並返回給客戶端 context.HandleResponse(); return Task.FromResult(0); }, }; }); }
d)商品客戶端調整按照以上內容調整類似。
三、演示效果:
1、設定專案啟動如下圖:
2、示例效果:
四、總結:
通過以上操作,整理單點登入流程如下圖:
踩坑:當登入取消、授權提示拒絕時,總是跳轉錯誤介面。
解決辦法:客戶端定義時,定義事件:對訪問拒絕新增處理邏輯。
//事件
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents()
{
//遠端故障
OnRemoteFailure = context =>
{
context.Response.Redirect("/");
context.HandleResponse();
return Task.FromResult(0);
},
//訪問拒絕
OnAccessDenied = context =>
{
//重定向到指定頁面
context.Response.Redirect("/");
//停止此請求的所有處理並返回給客戶端
context.HandleResponse();
return Task.FromResult(0);
},
};
GitHub地址:https://github.com/cwsheng/IdentityServer.Demo.git