Identity Server 4
2018-10-24 本文已影响129人
xtddw
准备
- https://identityserver4.readthedocs.io/en/release/
- OpenID Connect & OAuth 2.0 framework for ASP.NET Core 2.
- 建立Identity Provider项目
- IdentityServer4.Templates
- https://github.com/IdentityServer/IdentityServer4.Templates
- 安装工具:
dotnet new -i identityserver4.templates
- 重置 “dotnet new” 功能列表:
dotnet new --debug:reinit
- 模板:
- dotnet new is4empty
- dotnet new is4ui
- dotnet new is4inmem
dotnet new is4aspid
- dotnet new is4ef
- dotnet new is4admin (收费)
创建项目
dotnet new -i identityserver4.templates
dotnet new is4aspid --name BlogIdp
- 升级为.NET Core 2.1, 更新Nuget包
- 配置Hsts|HttpsRedirection
//注册Hsts
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(60);
});
//配置HTTP重定向
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
options.HttpsPort = 6001;
});
MVC 测试
- 安全/机密客户端(Confidential Client), 它是传统的服务器端Web应用.
- 它需要长时间访问(long-lived access), 所以需要refresh token. 那么它可以使用Authorization Code Flow或Hybrid Flow.
- Hybrid Flow是相对高级一些的, 它可以让客户端首先从授权端点获得一个ID Token并通过
浏览器(front-channel)
传递过来, 这样我们就可以验证这个ID Token. 如果验证成功然后, 客户端再打开一个后端通道(back-channel)
, 从Token端点获取Access Token.
-
身份认证请求
- 第一行的URI: "/authorize" 就是授权端点(Authorization Endpoint), 它位于身份提供商(Identity provider, IDP)那里. 这个URI可以从前面介绍的discovery document里面找到.
- 第二行 response_type=code id_token, 它决定了采取了哪一种Hybrid流程(参考上面那三个图).
- 第三行 client_id=xxxx, 这是客户端的身份标识.
- 第四行 redirect_uri=https...., 这是客户端那里的重定向端点(Redirection Endpoint).
- 第五行 scope=openid profile email, 这就是客户端所请求的scopes.
-
Hybrid Flow
Hybrid Flow
- 为什么要返回两次ID Token呢? 这是因为第(4)步里面请求Token的时候要求客户端身份认证, 这时请求Token的时候需要提供Authorization Code, Client ID和 Client Secret, 这些secret并不暴露给外界, 这些东西是由客户端服务器通过
后端通道
传递给Token端点的. 而第一次获得的ID Token是从前端通道(浏览器)
返回的. 当这个ID Token被验证通过之后, 也就证明了当前用户到底是谁.
code demo
- 新建MVC
- 修改端口为7000,7001
- 修改Idp项目config.cs
new Client
{
ClientId = "mvcclient",
ClientName = "MVC客户端",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
RedirectUris = { "http://localhost:7001/signin-oidc" },
FrontChannelLogoutUri = "http://localhost:7001/signout-oidc",
PostLogoutRedirectUris = { "http://localhost:7001/signout-callback-oidc" },
AllowOfflineAccess = true, //offline_access(refresh token)
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId }// { "openid", "profile", "api1" }
},
- 修改mvc项目Startup
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
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.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.AccessDeniedPath = "/Authorization/AccessDenied";
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://localhost:6001";
options.RequireHttpsMetadata = true;
options.ClientId = "mvcclient";
options.ResponseType = "code id_token";
options.Scope.Clear();
options.Scope.Add("openid"); //与Idp中config对应
//options.Scope.Add("profile");
//options.Scope.Add("email");
//options.Scope.Add("restapi");
options.SaveTokens = true;
options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0";
options.GetClaimsFromUserInfoEndpoint = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
- HomeController 测试
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
......
跳转验证
- 添加更多获取资源
public async Task<IActionResult> About()
{
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
ViewData["idToken"] = idToken;
return View();
}
- Config.cs添加
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
}
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource("restapi", "My RESTful API")
};
}
AllowOfflineAccess = true, //offline_access(开启refresh token)
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"restapi"
}// { "openid", "profile", "api1" }
- 修改html
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>IdToken:@ViewData["idToken"]</h3>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
保护API资源
- 安装包 `IdentityServer4.AccessTokenValidation
- 注册
// 注册IdentityServer
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:6001";
options.ApiName = "restapi";
});
- config
app.UseAuthentication();
- api中加验证
- 属性标签
[Authorize]
- 全局filter
- 属性标签
//设置全局filter保护api需要认真用户才可访问
services.Configure<MvcOptions>(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
-
NoAuth
401 error - MVC客户端
- 安装
IdentityModel
- 修改HomeController
public async Task<IActionResult> Contact()
{
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://localhost:5001")
};
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.enfi.hateoas+json")
);
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
ViewData["accessToken"] = accessToken;
httpClient.SetBearerToken(accessToken);
var res = await httpClient.GetAsync("api/posts").ConfigureAwait(false);
if (res.IsSuccessStatusCode)
{
var json = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
var objects = JsonConvert.DeserializeObject<dynamic>(json);
ViewData["json"] = objects;
return View();
}
if (res.StatusCode == HttpStatusCode.Unauthorized)
{
return RedirectToAction("AccessDenied", "Authorization");
}
throw new Exception($"Error Occurred:${res.ReasonPhrase}");
}
- 添加 Authorization/AccessDenied
public class AuthorizationController : Controller
{
public IActionResult AccessDenied()
{
return View();
}
}
@{ViewData["Title"] = "AccessDenied";}
<div class="container">
<h2>Access Denied</h2>
</div>
- https://jwt.io 网站解析token