标签:单点 options return context services new 认证 public IdentityServer4
前言
上一篇文章介绍了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
标签:单点,options,return,context,services,new,认证,public,IdentityServer4 来源: https://www.cnblogs.com/cwsheng/p/13663789.html
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。
