本文共 13675 字,大约阅读时间需要 45 分钟。
简而言之,他就是一个向客户端发送安全令牌的软件。
身份认证服务器的主要功能
主要支持的协议
使用客户端的人
客户端是一种从IdentityServer请求令牌的软件-用于认证用户(请求身份令牌)或访问资源(请求访问令牌)。客户端必须先向授权服务器注册,然后才能请求令牌。
客户端可以是 web 应用程序,或者桌面应用程序,或移动程序
ID4 要保护的 API
或 身份数据
(因为我主要是webapi,所以后边主要使用 webapi 举例子)
API
都有一个独一无二的名字,客户端使用这个名字来指定他要访问的是哪个 API
。 表示身份认证完成后的结果
他至少包括用户的身份信息,什么时候和怎么样去使用权限。
Access Token 是用来获取 API 的。客户端接受 access token,并将将她专送给 受保护的 API。
Access Token 里面包含客户端和使用者的信息(如果有的话),
如何理解 Identity token 和 Access Token 的关系?
可以把 Identity token 想象成在门卫那里领到的,证明你身份的条,而 Access Token是你可以 要进入 API 的钥匙 。(这是我自己的理解,不一定准确)
id4官方给的例子
网址
没有用户参与的程序。使用的是 Client Credentials(客户端证书)这种授权方式。
里面用到的三个项目的 nuget 包是不一样的。
1.用类的方式,声明配置文佳
public static class Config { //定义API public static IEnumerableApiScopes => new List { new ApiScope("api1", "My API") }; //定义客户端 // 定义访问 API 的客户端应用 // 这种情况下,客户端没有交互式的用户,只能通过客户端密钥进行身份验证。 // ClientId 和 Client Secret 可以视为登录名和密码。让身份认证服务器知道是哪个用户。 public static IEnumerable Clients => new List { new Client { // 定义一个客户端 ClientId = "client", // 认证类型。 //使用客户端密钥的方式进行验证。 AllowedGrantTypes = GrantTypes.ClientCredentials, // 认证的密码 ClientSecrets = { new Secret("secret".Sha256()) }, // 这个客户端端可以访问的 API AllowedScopes = { "api1" } } };
2.在 Startup 里面做调用设置
public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var builder = services.AddIdentityServer() .AddInMemoryApiScopes(Config.ApiScopes) // 那个api可以使用 .AddInMemoryClients(Config.Clients)// 哪个client可以使用 .AddDeveloperSigningCredential() ;//解决连接不上的问题,实际中,签名需要一对公钥和私钥,他会帮我们将公钥和私钥存储到硬盘上 } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseIdentityServer();//使用 Identity 4 app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
重生成,而使用命令行运行一下
dotnet … --urls http://*:5001
1.建立要访问受保护的 Action
[ApiController] public class IdentityController : ControllerBase { [HttpGet("identity")] [Authorize] public IActionResult Get() { return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); } [Authorize] [HttpGet("test")] public string Getst() { return "受保护的 API 访问成功"; } }
2.在startup 里面进行配置
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //接受授服务器发送的任何访问令牌。 services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:5001"; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false }; }); // 允许检测客户端请求的令牌中是否存在作用域 services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "api1"); }); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthentication();//每次都要执行身份验证. app.UseAuthorization(); //授权,确保匿名客户端无法访问我们的 api 端点 app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
3.建立用来访问的客户端
public class Program { // IdentityModel包括一个与发现端点一起使用的客户端库。这样,您只需要知道IdentityServer的基地址-可以从元数据中读取实际的端点地址: //找到授权的服务器 public static async System.Threading.Tasks.Task Main(string[] args) { //找到授权服务器 var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001"); //异步的方法名前边必须声明 async if (disco.IsError) { Console.WriteLine($"连接失败****{disco.Error}"); return; } //向授权服务器请求 token var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "client", ClientSecret = "secret", Scope = "api1" }); if (tokenResponse.IsError) { Console.WriteLine("token返回错误", tokenResponse.Error); return; } Console.WriteLine(tokenResponse.Json); //将访问令牌发送给 API var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken); var response = await apiClient.GetAsync("http://localhost:6001/test"); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); Console.WriteLine("访问失败"); } else { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine("访问成功"); // Console.WriteLine(JArray.Parse(content)); Console.WriteLine(content); } Console.ReadLine(); }
具体查看gitee代码:
这种是有用户参与的,使用的是 Password Grant (用名名密码)这种授权方式。
运行的顺序和字母的顺序是一致的:
所以在这个过程之中,即对用户进行身份认证,也对浏览器进行身份认证。
具体操作看代码吧。我也没用过MVC
使用 Refresh Token 刷新 Access Token。
new Client { ClientId = "mvc client", ClientName = "ASP.NET Core MVC Client", // 随便写 AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,//允许两种授权方式. ClientSecrets = { new Secret("mvc secret".Sha256()) },//密码. //下边都是固定的地址。 RedirectUris = { "http://localhost:5002/signin-oidc" }, FrontChannelLogoutUri = "http://localhost:5002/signout-oidc", PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, AlwaysIncludeUserClaimsInIdToken = true, AllowOfflineAccess = true, // offline_access //默认是一个小时。现在改成 60s 。 AccessTokenLifetime = 60, // 60 seconds AllowedScopes = { "api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.Address, IdentityServerConstants.StandardScopes.Phone, IdentityServerConstants.StandardScopes.Profile } },
但是仅仅这样设置,过了一分钟以后,进入 MVC ,依然还能得到 API 的相关信息 。这是因为我们的 API 没有设置对于 对于token检验。
services.AddAuthentication("Bearer")//bearer 的授权方式。 .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:5000"; options.RequireHttpsMetadata = false;//不需要https options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false }; //每隔一分钟,验证一次这个token options.TokenValidationParameters.RequireExpirationTime = true; //必须要有超时时间这个参数。 options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1); });
在 MVC 的控制器里面,再添加一个 action ,用来刷新 Token。
//刷新 token private async TaskRenewTokensAsync() { //发现文档 var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000"); if (disco.IsError) { throw new Exception(disco.Error); } //获取 refreshToken var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); //请求token var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = disco.TokenEndpoint, ClientId = "mvc client", ClientSecret = "mvc secret", Scope = "api1 openid profile email phone address", GrantType = OpenIdConnectGrantTypes.RefreshToken, RefreshToken = refreshToken }); if (tokenResponse.IsError) { throw new Exception(tokenResponse.Error); } //超时时间,使用的是 UTC 时间。 var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn); // 把 token 数组全部集中起来,做成一个数组。 var tokens = new[] { new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = tokenResponse.IdentityToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = tokenResponse.AccessToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = tokenResponse.RefreshToken }, new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) } }; // 获取身份认证的结果,包含当前的pricipal和properties var currentAuthenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 把获得的token再更新一遍。 currentAuthenticateResult.Properties.StoreTokens(tokens); // 再进行以下登录动作. await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, currentAuthenticateResult.Principal, currentAuthenticateResult.Properties); // 再把 access token 返回过去. return tokenResponse.AccessToken; }
在前边用到token的地方,再更新一下这个方法:
var response = await client.GetAsync("http://localhost:5001/identity"); if (!response.IsSuccessStatusCode) { if (response.StatusCode == HttpStatusCode.Unauthorized) { await RenewTokensAsync(); return RedirectToAction(); } throw new Exception(response.ReasonPhrase); }
使用 Hybrid 保护 api 资源。
之前使用的 OpenId Connect 隐式流,所有的令牌都是通过浏览器来传输,这对于 ID token 来说当然没有问题,但是我们还想请求一个 access token,accesstoken 更加敏感,所以在没有必要的时候,我们不会想把他暴露给外部世界, OpenID Connect 包含了一个叫做 “混合流(Hybrid flowe)”,它为我们提供了两方面的优点,身份令牌通过浏览器频道来传输,这样客户端就能能够在做任何工作前验证他,如果验证成功,客户端就会打开一个后端通道,来链接令牌,以检索访问令牌。
OAuth2.和相比OpenID 就是相当于多出来一个 hybirid Flow
根据 respons_type 的不同,分为三种情况:
在注册的用户里面直接声明 管理员角色
使用的时候,直接声明;
[Authorize(Roles ="管理员,普通用户")] [HttpGet("test")] public string Getst() { return "受保护的 API 访问成功"; }
比如:定义把名字为Smith,地址为Somewhere的作为条件。
总的来讲,基于策略的更加的灵活,我们可以自己定义:比如只让 firstname== “ 张 ”的人登录
在客户端声明:
services.AddAuthorization(options => { options.AddPolicy("SmithInSomewhere", builder => { builder.RequireAuthenticatedUser(); builder.RequireClaim(JwtClaimTypes.FamilyName, "Smith"); builder.RequireClaim("location", "somewhere"); });
在API调用
[Authorize(Policy ="sithInSomewhere")] public IActionResult Index() { var user = User.Identity; return View(); }
[1]
[2] [3] [4]转载地址:http://xuwl.baihongyu.com/