.net core 3.1 Identity Server4 (Hybrid模式)

发布时间 2023-08-11 17:28:57作者: dreamw

@@IdentityServer4   hybrid

 

 

 

.netcore

.net core 3.1 Identity Server4 (Hybrid模式)

Hybrid 模式的理解

Hybrid 模式相当于(Code模式+Impact模式),所以它又被称之为混合模式。大家可以看一下下面这张图。

我们可以发现它的类型response_type既可以为,code id_token,又可以code token,还可以code id_token token。虽然这很多样,接着我们来看它与与前三种又有哪些区别呢?

code与id_token

看看下面这张图。客户端先发起身份认证和授权请求,在授权端点进行身份认证和授权,然后获得id token以及授权码Authorization Code,随即客户端向授权服务器端点发起Token请求,最后获取到id token以及Access Token

code与token

这与上面图的区别是,第一次获取了Access Token与授权码Authorization Code,第二次获取到了Access TokenId Token

code,token与id token

这与上面图的区别是,第一次获取了Id Token,Access Token与授权码Authorization Code,第二次获取到了Access TokenId Token

项目演示

这里我只展示一个(code id_token)其他都是类似的

在授权服务器中添加客户端信息(老客套了)

  1. new Client
  2. {
  3. ClientId="hybrid_client",
  4. ClientSecrets = {new Secret("hybrid_client_secret".Sha256()) },
  5. AllowedGrantTypes = GrantTypes.Hybrid,
  6. RequirePkce = false,
  7. RedirectUris =
  8. {
  9. "https://localhost:6027/signin-oidc"
  10. },
  11. BackChannelLogoutUri = "https://localhost:6027/logout",
  12. PostLogoutRedirectUris =
  13. {
  14. "https://localhost:6027/signout-callback-oidc"
  15. },
  16. // 是否需要将token放入到Claim中
  17. AlwaysIncludeUserClaimsInIdToken = false,
  18. // 获取或设置一个值,该值指示是否允许脱机访问. 默认值为 false。
  19. AllowOfflineAccess = true,
  20. AllowedScopes =
  21. {
  22. "ApiOne",
  23. IdentityServerConstants.StandardScopes.OpenId,
  24. IdentityServerConstants.StandardScopes.Profile,
  25. "rc.bc"
  26. }
  27. }

创建Hybrid客户端项目(AiDaSi.OcDemo.HybridMvc)

基本上与code模式的mvc客户端相同,我就直接贴代码了哈!首先安装客户端的包!

  1. <ItemGroup>
  2. <PackageReference Include="IdentityModel" Version="4.5.0" />
  3. <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.9" />
  4. </ItemGroup>

Startup

  1. public Startup(IConfiguration configuration)
  2. {
  3. Configuration = configuration;
  4. // 我们关闭了JWT的Claim 类型映射, 以便允许well-known claims
  5. JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. services.AddControllersWithViews();
  11. services.AddHttpClient();
  12. JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
  13. services.AddSingleton<IDiscoveryCache>(r =>
  14. {
  15. var factory = r.GetRequiredService<IHttpClientFactory>();
  16. return new DiscoveryCache("https://localhost:7200", () => factory.CreateClient());
  17. });
  18. services.AddAuthentication(options =>
  19. {
  20. options.DefaultScheme = "Cookies";
  21. options.DefaultChallengeScheme = "oidc";
  22. })
  23. .AddCookie("Cookies") // 我们用作Cookies作为首选方式
  24. .AddOpenIdConnect("oidc", options =>
  25. {
  26. options.Authority = "https://localhost:7200";
  27. options.RequireHttpsMetadata = false;
  28. options.ClientId = "hybrid_client";
  29. options.ClientSecret = "hybrid_client_secret";
  30. options.ResponseType = "code id_token";
  31. options.GetClaimsFromUserInfoEndpoint = true;
  32. options.SaveTokens = true;
  33. options.Scope.Clear(); // 清理范围
  34. options.Scope.Add("ApiOne");
  35. options.Scope.Add("openid");
  36. options.Scope.Add("profile");
  37. options.Scope.Add("rc.bc");
  38. options.Scope.Add("offline_access");// 脱机访问令牌
  39. });
  40. }
  41. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  42. {
  43. if (env.IsDevelopment())
  44. {
  45. app.UseDeveloperExceptionPage();
  46. }
  47. else
  48. {
  49. app.UseExceptionHandler("/Home/Error");
  50. }
  51. app.UseStaticFiles();
  52. app.UseRouting();
  53. app.UseAuthentication();
  54. app.UseAuthorization();
  55. app.UseEndpoints(endpoints =>
  56. {
  57. endpoints.MapControllerRoute(
  58. name: "default",
  59. pattern: "{controller=Home}/{action=Index}/{id?}");
  60. });
  61. }

修改launchSettings.json,设置端口为6027

  1. {
  2. "profiles": {
  3. "AiDaSi.OcDemo.HybridMvc": {
  4. "commandName": "Project",
  5. "launchBrowser": true,
  6. "applicationUrl": "https://localhost:6027",
  7. "environmentVariables": {
  8. "ASPNETCORE_ENVIRONMENT": "Development"
  9. }
  10. }
  11. }
  12. }

然后HomeController.cs控制器内容与页面跟code mvc是差不多的

  1. public class HomeController : Controller
  2. {
  3. private readonly ILogger<HomeController> _logger;
  4. private IHttpClientFactory _httpClientFactory;
  5. public HomeController(ILogger<HomeController> logger, IHttpClientFactory httpFactory)
  6. {
  7. _logger = logger;
  8. _httpClientFactory = httpFactory;
  9. }
  10. public IActionResult Index()
  11. {
  12. return View();
  13. }
  14. [Authorize]
  15. public async Task<IActionResult> Privacy()
  16. {
  17. var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
  18. var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
  19. var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
  20. var code = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.Code);
  21. ViewData["accessToken"] = accessToken;
  22. ViewData["idToken"] = idToken;
  23. ViewData["refreshToken"] = refreshToken;
  24. ViewData["code"] = code;
  25. // 获取接口数据
  26. var httpClient = _httpClientFactory.CreateClient();
  27. //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
  28. httpClient.SetBearerToken(accessToken);
  29. // 验证Token是否失效
  30. string tokenStr = accessToken;
  31. var handler = new JwtSecurityTokenHandler();
  32. var payload = handler.ReadJwtToken(tokenStr).Payload;
  33. var expclaim = payload.Claims.FirstOrDefault(x => x.Type == "exp");
  34. DateTime dateTime = expclaim.Value.unixtime();
  35. int compNum = DateTime.Compare(DateTime.Now, dateTime);
  36. //判断当前时间是否大于token的过期时间,如果有就刷新token,这样就能达到无缝衔接
  37. if (compNum > 0)
  38. {
  39. await RenewTokensAsync();
  40. return RedirectToAction();
  41. }
  42. var Result = await httpClient.GetAsync("http://localhost:5280/WeatherForecast");
  43. if (Result.IsSuccessStatusCode)
  44. {
  45. ViewData["Apione"] = await Result.Content.ReadAsStringAsync();
  46. }
  47. return View();
  48. }
  49. private async Task<string> RenewTokensAsync()
  50. {
  51. var client = _httpClientFactory.CreateClient();
  52. var disco = await client.GetDiscoveryDocumentAsync("https://localhost:7200");
  53. if (disco.IsError)
  54. {
  55. // 我们这里将Cookie清空掉
  56. foreach (var item in Request.Cookies)
  57. {
  58. Response.Cookies.Delete(item.Key);
  59. }
  60. // 报错
  61. return await Task.FromResult(disco.Error);
  62. // throw new Exception(disco.Error);
  63. }
  64. var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
  65. // 刷新token的操作
  66. var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
  67. {
  68. Address = disco.TokenEndpoint,
  69. ClientId = "client_id_mvc",
  70. ClientSecret = "mvc_secret",
  71. RefreshToken = refreshToken
  72. });
  73. #region 第一种写法
  74. if (tokenResponse.IsError)
  75. {
  76. // 我们这里将Cookie清空掉
  77. foreach (var item in Request.Cookies)
  78. {
  79. Response.Cookies.Delete(item.Key);
  80. }
  81. return await Task.FromResult(tokenResponse.Error);
  82. // 报错
  83. // throw new Exception(tokenResponse.Error);
  84. }
  85. var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
  86. var tokens = new[]
  87. {
  88. new AuthenticationToken
  89. {
  90. Name = OpenIdConnectParameterNames.IdToken,
  91. Value = tokenResponse.IdentityToken
  92. },
  93. new AuthenticationToken
  94. {
  95. Name = OpenIdConnectParameterNames.AccessToken,
  96. Value = tokenResponse.AccessToken
  97. },
  98. new AuthenticationToken
  99. {
  100. Name = OpenIdConnectParameterNames.RefreshToken,
  101. Value = tokenResponse.RefreshToken
  102. },
  103. new AuthenticationToken
  104. {
  105. Name = "expires_at",
  106. Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
  107. }
  108. };
  109. // 获取身份认证的结果,包含当前的pricipal和properties
  110. var currentAuthenticateResult =
  111. await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
  112. // 把新的tokens存起来
  113. currentAuthenticateResult.Properties.StoreTokens(tokens);
  114. // 登录
  115. await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
  116. currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
  117. return tokenResponse.AccessToken;
  118. #endregion
  119. #region 第二种写法
  120. //下面将修改上下文
  121. var authInfo = await HttpContext.AuthenticateAsync("Cookies");
  122. authInfo.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken);
  123. authInfo.Properties.UpdateTokenValue("id_token", tokenResponse.IdentityToken);
  124. authInfo.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken);
  125. //二次认证(更新token)
  126. await HttpContext.SignInAsync("Cookies", authInfo.Principal, authInfo.Properties);
  127. #endregion
  128. }
  129. [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
  130. public IActionResult Error()
  131. {
  132. return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
  133. }
  134. }

Privacy.cshtml

  1. @{
  2. ViewData["Title"] = "Privacy Policy";
  3. }
  4. <h1>@ViewData["Title"]</h1>
  5. <h2>Access Token:</h2>
  6. <p>@ViewData["accessToken"]</p>
  7. <h2>Id Token:</h2>
  8. <p>@ViewData["idToken"]</p>
  9. <h2>Refresh Token:</h2>
  10. <p>@ViewData["refreshToken"]</p>
  11. <h2>Code:</h2>
  12. <p>@ViewData["code"]</p>
  13. <h2>Apione:</h2>
  14. <p>@ViewData["Apione"]</p>
  15. <dl>
  16. @foreach (var claim in User.Claims)
  17. {
  18. <dt>@claim.Type</dt>
  19. <dd>@claim.Value</dd>
  20. }
  21. </dl>

  1. public static class TimeHelper
  2. {
  3. //将unix时间戳转换成系统时间
  4. public static DateTime unixtime(this string time)
  5. {
  6. DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
  7. long lTime = long.Parse(time + "0000000");
  8. TimeSpan toNow = new TimeSpan(lTime);
  9. DateTime dtResult = dtStart.Add(toNow);
  10. return dtResult;
  11. }
  12. //将系统时间转换成unix时间戳
  13. public static long timeunix2(this DateTime dt)
  14. {
  15. DateTimeOffset dto = new DateTimeOffset(dt);
  16. return dto.ToUnixTimeSeconds();
  17. }
  18. //将系统时间转换成unix时间戳
  19. public static DateTime unixtime2(this double d)
  20. {
  21. System.DateTime time = System.DateTime.MinValue;
  22. System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
  23. time = startTime.AddMilliseconds(d);
  24. return time;
  25. }

处理Claims

下面是客户端必须要的,在Startup.csAddOpenIdConnect中添加

  1. options.ClaimActions.Remove("nbf");
  2. options.ClaimActions.Remove("amr");
  3. options.ClaimActions.Remove("exp");

下面呢是在客户端中不需要的,但是如果在请求Api中是必要的话则在客户端中是不会呈现出来的(如:sid)

  1. options.ClaimActions.DeleteClaim("sid");
  2. options.ClaimActions.DeleteClaim("sub");
  3. options.ClaimActions.DeleteClaim("idp");

退出设置

服务器上设置退出

设置退出页面的路径,并写对应的退出代码

在服务器端我们对它跳转回客户端的地址是:https://localhost:6027/signout-callback-oidc,也可以修改为客户端的其他路径

客户端上设置退出

找到客户端的_Layout.cshtml

添加一个退出的按钮,并在HomeController中添加一个Logout的方法

  1. @if (User.Identity.IsAuthenticated)
  2. {
  3. <li class="nav-item">
  4. <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a>
  5. </li>
  6. }

  1. public async Task<IActionResult> Logout()
  2. {
  3. return SignOut("Cookies", "oidc");
  4. }

运行项目,点击退出按钮成功退出

RBAC(角色控制)

全称为:Role-based Access Control ,通过预定义的角色赋予访问权限,每个角色规定了一套权限。

在授权服务器中定义角色

首先我们在Create_Test_Users扩展方法中添加用户的时候定义管理员普通两个角色

接着在GetIdentityResources方法中创建身份资源

  1. new IdentityResource("roles","角色",new List<string>{ JwtClaimTypes.Role }),

设置客户端配置

在客户端中更新角色

更新客户端代码配置

创建RbacController控制器,添加角色所对应的方法(由于这里呢,比较偷懒就全部都跳到了index方法中),接着创建/Rbac/index.cshtml页面

  1. public class RbacController : Controller
  2. {
  3. [Authorize]
  4. public IActionResult Index()
  5. {
  6. return View();
  7. }
  8. [Authorize(Roles = "管理员")]
  9. public IActionResult Admin()
  10. {
  11. return View("Index");
  12. }
  13. [Authorize(Roles = "普通")]
  14. public IActionResult Common()
  15. {
  16. return View("Index");
  17. }
  18. [Authorize(Roles = "管理员,普通")]
  19. public IActionResult Share()
  20. {
  21. return View("Index");
  22. }
  23. }
  1. <div>
  2. <h1> 是不是管理员:@User.IsInRole("管理员") </h1>
  3. <h1> 是不是普通用户:@User.IsInRole("普通") </h1>
  4. </div>

我们先用bob管理员用户运行测试看看

稍后解决这个问题…

接着我们去访问https://localhost:6027/Rbac/index页面进行角色判断

退出后,我们再用aidasi普通用户运行测试看看

我们看到我们都成功的验证出客户端的身份了,但接着我们会产生两个问题。
(1). 当我们的身份资源过多我们只需要其中的部分资源权限的时候怎么做?比如我只要role这个角色权限的Claim,这里却还有了rc.bc资源。
(2). 如果中间人知道接口资源的地址,如何进行身份认证?

解决问题一

首先在授权服务器中将AlwaysIncludeUserClaimsInIdToken更改为false,其属性默认也为false

  1. // 是否需要将所有token中的Data放入到Claim中
  2. AlwaysIncludeUserClaimsInIdToken = false,

然后在客户端中添加如下代码,添加对role的映射。

  1. options.GetClaimsFromUserInfoEndpoint = true;
  2. // 从json用户数据中选择一个具有给定键名和 将其添加为声明。如果索赔实体已包含索赔,则此项不起作用使用给定的ClaimType。如果找不到键或值为空的。
  3. // 当授权服务器端 AlwaysIncludeUserClaimsInIdToken = false 时
  4. options.ClaimActions.MapUniqueJsonKey("role", "role");

运行测试一下,我们会发现没有rc.bc的资源了,并且可以访问需要授权的方法


解决问题二

当访问接口资源时,我们的接口以bb.api.bc作为角色权限,当Api资源获取WeatherForecastController中数据时其值设定为bb.api.cookie

在授权服务器上重新分配角色资源,只让bob独享经济,呸!独享bb.api.bc资源.

在客户端中添加bb.api.bc的映射.

  1. options.ClaimActions.MapUniqueJsonKey("bb.api.bc", "bb.api.bc");

接着运行测试

解决未授权问题

当我们用bob用户去访问普通用户的接口时,会发现它也是访问不了的,会跳转到一个无授权的页面。例如我们访问一下/Rbac/Common接口,它会去访问无授权的网页。(默认地址是:/Account/AccessDenied

接着我们可以创建相关的ControllerView,我们这里自定义未授权路径为/UnAuthorized/AccessDenied

UnAuthorized控制器中添加如下代码

  1. public class UnAuthorizedController : Controller
  2. {
  3. /// <summary>
  4. /// 未授权访问
  5. /// /UnAuthorized/AccessDenied
  6. /// </summary>
  7. /// <param name="ReturnUrl"></param>
  8. /// <returns></returns>
  9. public IActionResult AccessDenied(string ReturnUrl)
  10. {
  11. ViewBag.ReturnUrl = ReturnUrl;
  12. return View();
  13. }
  14. }

添加视图

  1. <div>
  2. <h1> 你所访问的地址"@ViewBag.ReturnUrl"未进行授权 </h1>
  3. <h1> <a href="@ViewBag.ReturnUrl">是否再次尝试</a></h1>
  4. </div>

最后在Startup.cs类中ConfigureServices下添加未授权访问页面的路径,最后运行测试。

  1. .AddCookie("Cookies",option=> {
  2. //添加未授权的访问页面
  3. option.AccessDeniedPath = "/UnAuthorized/AccessDenied";
  4. })

ABAC

Attribute-based Access Control 表示通过策略授予权限策略可能将多个属性/claims组合到一起允许复杂的权限规则。也简称ABAC

添加服务器端政策授权Claim

在添加政策授权字段时,添加FamilyNamelocation字段。

同时需要添加身份资源授权

  1. new IdentityResource("locations","地点",new List<string>{ "location" }),

如果需要访问api也能获取得到相关Claim的话,可以在后面直接添加。

最后在Client中添加身份认证资源

添加客户端端政策授权

在客户端Startup中,添加相关的Scope

  1. options.Scope.Add("locations");

添加locationsClaim的映射

  1. options.ClaimActions.MapUniqueJsonKey("location", "location");

随后在AddAuthorization服务注册中添加相关Policy

  1. services.AddAuthorization(option =>
  2. {
  3. option.AddPolicy("BobInAllWhere", builder =>
  4. {
  5. // 需要身份验证的用户
  6. builder.RequireAuthenticatedUser();
  7. // 需要名为FamilyName的Claim值为"He"政策为有效
  8. builder.RequireClaim(JwtClaimTypes.FamilyName, "He");
  9. // 需要名为location的Claim值为"allwhere"政策为有效
  10. builder.RequireClaim("location", "allwhere");
  11. });
  12. });

添加AbacController控制器,并对Index.cshtml视图添加BobInAllWhere的政策。

  1. public class AbacController : Controller
  2. {
  3. /// <summary>
  4. /// /Abac/Index
  5. /// </summary>
  6. /// <returns></returns>
  7. [Authorize(Policy = "BobInAllWhere")]
  8. public IActionResult Index()
  9. {
  10. return View();
  11. }
  12. }

添加Index.cshtml的内容

  1. 只有 Policy = "BobInAllWhere" 与 FamilyName = "He" 的可以访问

运行测试

当我们用bob用户登录的时候

当我们用aidasi用户登录的时候

自定义客户端政策处理

添加如下文件到客户端 (HybridMvc) 中

  1. public class BobInAllWhereRequirement: IAuthorizationRequirement
  2. {
  3. public BobInAllWhereRequirement()
  4. {
  5. }
  6. }
  1. public class BobInAllWhereHandler : AuthorizationHandler<BobInAllWhereRequirement>
  2. {
  3. protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BobInAllWhereRequirement requirement)
  4. {
  5. // 获取Claim中的familyName的值
  6. var familyName = context.User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.FamilyName)?.Value;
  7. // 获取Claim中的location的值
  8. var location = context.User.Claims.FirstOrDefault(c => c.Type == "location")?.Value;
  9. // 判断familyName的值为He,location 的值为allwhere,并且要用户是登录的状态
  10. if (familyName == "He" && location == "allwhere" && context.User.Identity.IsAuthenticated)
  11. {
  12. // 放行通过
  13. context.Succeed(requirement);
  14. return Task.CompletedTask;
  15. }
  16. // 验证失败,通不过
  17. context.Fail();
  18. return Task.CompletedTask;
  19. }
  20. }

Startup.cs中添加自定义策略,其策略规则不变。

  1. services.AddAuthorization(option =>
  2. {
  3. //option.AddPolicy("BobInAllWhere", builder =>
  4. //{
  5. // // 需要身份验证的用户
  6. // builder.RequireAuthenticatedUser();
  7. // // 需要名为FamilyName的Claim值为"He"政策为有效
  8. // builder.RequireClaim(JwtClaimTypes.FamilyName, "He");
  9. // // 需要名为location的Claim值为"allwhere"政策为有效
  10. // builder.RequireClaim("location", "allwhere");
  11. //});
  12. option.AddPolicy("BobInAllWhere", builder =>
  13. {
  14. builder.AddRequirements(new BobInAllWhereRequirement());
  15. });
  16. });
  17. services.AddSingleton<IAuthorizationHandler, BobInAllWhereHandler>();

HandleRequirementAsync方法中如果没有对context进行调用验证通过或验证不通过的方法,都为验证不通过的方法。


欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739

 

转 https://www.tnblog.net/hb/article/details/5391