第1部分-MVC身份验证和授权
本教程将引导您完成必要的步骤,以启动并运行最少的IdentityServer。为简单起见,我们将IdentityServer和客户端托管在同一个Web应用程序中-这不是很现实的情况,但是可以让您入门而又不会太复杂。
完整的源代码可以在这里找到。
第1部分-MVC身份验证和授权
在第一部分中,我们将创建一个简单的MVC应用程序,并通过IdentityServer向其添加身份验证。然后,我们将仔细研究索赔,索赔转换和授权
创建Web应用程序
在Visual Studio 2013中,创建一个标准MVC应用程序,并将身份验证设置为“无身份验证”。

您现在可以使用属性窗口将项目切换到SSL:

重要提示 不要忘记在项目属性中更新起始URL。
添加IdentityServer
IdentityServer基于OWIN / Katana,并作为Nuget软件包分发。要将其添加到新创建的虚拟主机中,请安装以下两个软件包:
install-package Microsoft.Owin.Host.Systemweb
install-package IdentityServer3
配置IdentityServer-客户端
IdentityServer需要有关将要支持的客户端的一些信息,可以使用一个Client对象简单地提供它:
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44319/"
},
AllowAccessToAllScopes = true
}
};
}
}
备注现在,客户端可以访问所有作用域(通过AllowAccessToAllScopes设置)。对于生产应用程序,您可以将其锁定。以后再说。
配置IdentityServer-用户
接下来,我们将向IdentityServer添加一些用户-同样,这可以通过提供一个简单的C#类来完成。您可以从任何数据存储中检索用户信息,并且我们为ASP.NET Identity和MembershipReboot提供开箱即用的支持。
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith")
}
}
};
}
}
添加启动
在启动类中配置IdentityServer。在这里,我们提供有关客户端,用户,范围,签名证书和其他一些配置选项的信息。在生产中,您应该从Windows证书存储区或某些其他安全来源加载签名证书。在此示例中,我们只是将其作为文件添加到项目中(您可以从此处下载测试证书。将其添加到项目中并将其构建操作设置为Copy to output。
有关如何从Azure网站加载证书的信息,请参见此处。
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(StandardScopes.All)
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(@"{0}\bin\identityServer\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
此时,您已经具有功能齐全的IdentityServer,并且可以浏览到发现端点以检查配置:

拉姆法尔
最后一件事,请不要忘记将RAMMFAR添加到您的web.config中,否则IIS将无法正确加载某些嵌入式资产:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
添加和配置OpenID Connect身份验证中间件
要将OIDC身份验证添加到MVC应用程序,我们需要添加两个软件包:
install-package Microsoft.Owin.Security.Cookies
install-package Microsoft.Owin.Security.OpenIdConnect
使用默认值在StartUp.cs中配置cookie中间件:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
将OpenID Connect中间件(也在Startup.cs中)指向我们的IdentityServer嵌入式版本,并使用以前配置的客户端配置:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
添加受保护的资源并显示声明
要使用IdentityServer启动身份验证,您需要创建受保护的资源,例如,通过添加全局授权过滤器。对于我们的示例,我们将仅保护控制器About上的操作Home。另外,我们将声明移交给视图,以便我们可以看到IdentityServer发出了哪些声明:
[Authorize]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
相应的视图如下所示:
@model IEnumerable<System.Security.Claims.Claim>
<dl>
@foreach (var claim in Model)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
身份验证和声明
单击关于链接现在将触发身份验证。IdentityServer将显示登录屏幕,并将令牌发送回主应用程序。OpenID Connect中间件将验证令牌,提取声明并将其传递给cookie中间件,后者将依次设置身份验证cookie。用户现已登录。


添加角色声明和范围
在下一步中,我们想向用户添加一些角色声明,稍后将用于授权。
现在,我们摆脱了OIDC标准范围-让我们定义一个包含角色声明的角色范围,并将其添加到标准范围中:
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
还要将工厂更改Startup为使用新的合并范围:
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
接下来,我们向Bob添加几个角色声明:
public static class Users
{
public static IEnumerable<InMemoryUser> Get()
{
return new[]
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "Foo")
}
}
};
}
}
更改中间件配置以询问角色
默认情况下,OIDC中间件要求两个范围:openid和profile-这就是IdentityServer包含主题和名称声明的原因。现在,我们向roles范围添加一个请求:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
Scope = "openid profile roles",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});
成功认证之后,您现在应该在用户的Claims集合中看到角色声明:

索赔转换
当检查“关于”页面上的声明时,您会注意到两件事:一些声明的类型名长于奇数,并且有更多的声明超出了应用程序中可能需要的数量。
长声明名称来自Microsoft的JWT处理程序,该处理程序试图将某些声明类型映射到.NET的ClaimTypes类类型。您可以使用以下代码行(在中Startup)关闭此行为。
这也意味着您需要将反CSRF保护的配置调整为新的唯一sub声明类型:
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
现在的声明将如下所示:

这是一个改进,但是仍然存在一些典型的业务逻辑肯定不需要的低层协议声明。将原始传入的声明转换为特定于应用程序的声明的过程称为声明转换。在此过程中,您将接收传入的声明,确定要保留的声明,并可能需要联系其他数据存储来检索应用程序所需的更多声明。
OIDC中间件有一条通知,可用于进行声明转换-产生的声明将存储在cookie中:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
ClientId = "mvc",
Scope = "openid profile roles",
RedirectUri = "https://localhost:44319/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
// we want to keep first name, last name, subject and roles
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
添加以上代码后,我们的声明集现在如下所示: 
授权书
现在我们有了身份验证和一些声明,我们可以开始添加简单的授权规则。
MVC具有一个内置属性,称为[Authorize]要求经过身份验证的用户,您也可以使用此属性来注释角色成员资格要求。我们不建议使用此方法,因为这通常会导致代码混合了业务/控制器逻辑和授权策略等问题。我们宁愿建议将授权逻辑与控制器分开,以使代码更简洁,可测试性更好(在此处了解更多信息)。
资源授权
要添加新的授权基础结构和新属性,我们添加一个Nuget包:
install-package Thinktecture.IdentityModel.Owin.ResourceAuthorization.Mvc
接下来,我们在注释Contact上的动作Home控制器的属性,它表达了执行该动作是要Read在ContactDetails资源:
[ResourceAuthorize("Read", "ContactDetails")]
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
请注意,该属性并不表示允许谁读取联系人-我们将该逻辑分离为授权管理器,该管理器了解操作,资源以及允许谁在应用程序中执行哪个操作:
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "ContactDetails":
return AuthorizeContactDetails(context);
default:
return Nok();
}
}
private Task<bool> AuthorizeContactDetails(ResourceAuthorizationContext context)
{
switch (context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "Geek"));
case "Write":
return Eval(context.Principal.HasClaim("role", "Operator"));
default:
return Nok();
}
}
}
最后,我们将授权管理器连接到以下位置的OWIN管道中Startup:
app.UseResourceAuthorization(new AuthorizationManager());
运行示例并逐步执行代码以熟悉流程。
角色授权
但是,如果您选择使用,请[Authorize(Roles = "Foo,Bar")]注意,当当前用户通过身份验证时,站点可能会陷入无限重定向循环,但不属于您传递给Authorize属性的角色或用户之一(在MVC 5.2中验证)。发生这种不希望的结果是因为,Authorize当用户通过身份验证时,该属性会将操作的结果设置为401未经授权,但不是角色之一。该401结果触发重定向,以通过IdentityServer进行身份验证,身份服务器先进行身份验证,然后将用户重定向回去,然后重定向循环开始。可以通过如下重写Authorize属性的HandleUnauthorizedRequest方法来克服此行为,然后使用自定义的授权属性代替MVC附带的属性。
// Customized authorization attribute:
public class AuthAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// 403 we know who you are, but you haven't been granted access
filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Forbidden);
}
else
{
// 401 who are you? go login and then try again
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
// Usage:
[Auth(Roles = "Geek")]
public ActionResult About()
{
// ...
}
更多授权和处理拒绝访问的情况
通过向Home控制器添加新的操作方法,让我们做更多的授权:
[ResourceAuthorize("Write", "ContactDetails")]
public ActionResult UpdateContact()
{
ViewBag.Message = "Update your contact details!";
return View();
}
当您尝试通过导航到/home/updatecontactURL 来调用该操作时,将看到一个forbidden错误页面。

实际上,基于用户是否已通过身份验证的事实,您将看到不同的响应。如果不是,则MVC将重定向到登录页面,如果通过身份验证,您将看到禁止的响应。这是设计使然(在此处了解更多信息)。
您可以通过检查403状态代码来处理禁止的情况-我们提供了开箱即用的此类过滤器:
[ResourceAuthorize("Write", "ContactDetails")]
[HandleForbidden]
public ActionResult UpdateContact()
{
ViewBag.Message = "Update your contact details!";
return View();
}
HandleForbidden每当发出403时,过滤器(当然也可以是全局过滤器)将重定向到指定的视图-默认情况下,我们寻找名为的视图Forbidden。

您还可以强制使用授权管理器,这为您提供了更多选择:
[HandleForbidden]
public ActionResult UpdateContact()
{
if (!HttpContext.CheckAccess("Write", "ContactDetails", "some more data"))
{
// either 401 or 403 based on authentication state
return this.AccessDenied();
}
ViewBag.Message = "Update your contact details!";
return View();
}
添加注销
添加注销很容易,只需添加一个新操作即可Signout在Katana身份验证管理器中调用该方法:
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
这将启动到IdentityServer上所谓的endsession端点的往返。该端点将清除身份验证cookie并终止您的会话:

通常,现在最安全的做法是简单地关闭浏览器窗口以摆脱所有会话数据。但是,某些应用程序希望为用户提供以匿名用户身份返回的机会。
这是可能的,但是需要一些步骤-首先,您需要注册一个有效的URL,以在注销过程完成后返回。这是在MVC应用程序的客户端定义中完成的(请注意新PostLogoutRedirectUris设置):
new Client
{
Enabled = true,
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44319/"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44319/"
}
}
接下来,客户端必须向注销端点证明其身份,以确保我们重定向到正确的URL(而不是某些垃圾邮件发送者/网络钓鱼页面)。这是通过将身份验证过程中客户端收到的初始标识令牌发回来完成的。到目前为止,我们已经放弃了此令牌,现在是时候更改声明转换逻辑以保留它了。
这是通过将以下代码行添加到我们的SecurityTokenValidated通知中来完成的:
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
最后,我们必须在用户注销时附加id_token,然后往返于IdentityServer。这也可以使用OIDC中间件上的通知来完成:
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
通过这些更改,IdentityServer将为用户提供指向调用应用程序的链接:

提示在上IdentityServerOptions可以找到一个AuthenticationOptions对象。此属性称为EnablePostSignOutAutoRedirect。如您所料,将其设置为true将在注销后自动重定向回客户端。
添加Google身份验证
接下来,我们要启用外部身份验证。这是通过向IdentityServer添加附加的Katana身份验证中间件来完成的-在我们的示例中,我们将使用Google。
向Google注册IdentityServer
首先,我们需要在Google开发人员控制台上注册IdentityServer。这包括几个步骤。
首先导航到:
https://console.developers.google.com
创建一个新项目

启用Google+ API

使用电子邮件地址和产品名称配置同意屏幕

创建一个客户端应用程序

创建客户端应用程序后,开发人员控制台将显示一个客户端ID和一个客户端密码。稍后配置Google中间件时,我们将需要这两个值。
添加Google身份验证中间件
通过安装以下软件包来添加中间件:
install-package Microsoft.Owin.Security.Google
配置中间件
将以下方法添加到您的Startup:
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
AuthenticationType = "Google",
Caption = "Sign-in with Google",
SignInAsAuthenticationType = signInAsType,
ClientId = "...",
ClientSecret = "..."
});
}
接下来,我们将IdentityServer选项类指向此方法:
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions
{
IdentityProviders = ConfigureIdentityProviders
}
});
而已!用户下次登录时-登录页面上将显示“使用Google登录”按钮:

请注意,role使用Google登录时,缺少该声明。这是有道理的,因为Google没有角色的概念。准备好并非所有身份提供者都将提供相同的声明类型。
第2部分-添加和调用Web API
在这一部分中,我们将向解决方案添加Web API。该API将由IdentityServer保护。接下来,我们的MVC应用程序将使用信任子系统和身份委托方法来调用API。
添加Web API项目
创建干净的API项目的最简单方法是添加一个空的Web项目。

接下来,我们将使用Nuget添加Web API和Katana托管:
install-package Microsoft.Owin.Host.SystemWeb
install-package Microsoft.Aspnet.WebApi.Owin
添加测试控制器
以下控制器将所有声明返回给调用者-这将使我们能够检查将发送到API的令牌。
[Route("identity")]
[Authorize]
public class IdentityController : ApiController
{
public IHttpActionResult Get()
{
var user = User as ClaimsPrincipal;
var claims = from c in user.Claims
select new
{
type = c.Type,
value = c.Value
};
return Json(claims);
}
}
在启动中连接Web API和安全性
与基于Katana的托管一样,所有配置都在Startup以下位置进行:
public class Startup
{
public void Configuration(IAppBuilder app)
{
// web api configuration
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
另外,我们想使用IdentityServer保护我们的API-为此需要做两件事:
- 仅接受由IdentityServer发行的令牌
- 仅接受为我们的API发出的令牌-为此,我们将给该API一个名称sampleApi(也称为
scope)
为此,我们添加了一个Nuget包:
install-package IdentityServer3.AccessTokenValidation
..并将它们用于Startup:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44319/identity",
RequiredScopes = new[] { "sampleApi" }
});
// web api configuration
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
注意 IdentityServer发行标准的JSON Web令牌(JWT),并且您可以使用普通的Katana JWT中间件来验证它们。我们的中间件只是一种方便,因为它可以使用IdentityServer发现文档(元数据)自动配置自己。
在IdentityServer中注册API
接下来,我们需要注册API-通过扩展范围来完成。这次,我们添加了一个所谓的资源范围:
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
注册Web API客户端
接下来,我们将调用API。您可以通过使用客户端凭据(例如服务帐户)或委派用户身份来实现。我们将从客户凭证开始。
首先,我们需要为MVC应用程序注册一个新客户端。出于安全原因,IdentityServer每个客户端只允许一个流,并且由于我们现有的MVC客户端已经使用隐式流,因此我们需要为服务之间的通信创建一个新的客户端。
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientName = "MVC Client",
ClientId = "mvc",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44319/"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44319/"
},
AllowedScopes = new List<string>
{
"openid",
"profile",
"roles",
"sampleApi"
}
},
new Client
{
ClientName = "MVC Client (service communication)",
ClientId = "mvc_service",
Flow = Flows.ClientCredentials,
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
"sampleApi"
}
}
};
}
}
备注上面的代码段锁定了各种客户端可以使用该AllowedScopes设置访问的范围。
调用API
调用API包括两部分:
- 使用客户端凭证从IdentityServer请求API的令牌
- 使用访问令牌调用API
为了简化与OAuth2令牌端点的交互,请通过Nuget将客户端包添加到MVC项目中:
install-package IdentityModel
在控制器下,添加新的类CallApiController。以下代码段使用客户端凭据为sampleApi请求令牌:
private async Task<TokenResponse> GetTokenAsync()
{
var client = new TokenClient(
"https://localhost:44319/identity/connect/token",
"mvc_service",
"secret");
return await client.RequestClientCredentialsAsync("sampleApi");
}
而以下代码段使用请求的访问令牌调用我们的身份终结点:
private async Task<string> CallApi(string token)
{
var client = new HttpClient();
client.SetBearerToken(token);
var json = await client.GetStringAsync("https://localhost:44321/identity");
return JArray.Parse(json).ToString();
}
综上所述,新添加的控制器将调用该服务并在视图上显示结果声明:
public class CallApiController : Controller
{
// GET: CallApi/ClientCredentials
public async Task<ActionResult> ClientCredentials()
{
var response = await GetTokenAsync();
var result = await CallApi(response.AccessToken);
ViewBag.Json = result;
return View("ShowApiResult");
}
// helpers omitted
}
创建ShowApiResult.cshtml文件,以简单的视图查看结果:
<h2>Result</h2>
<pre>@ViewBag.Json</pre>
结果将如下所示:

换句话说-API知道调用者:
- 发行者名称,受众和有效期(由令牌验证中间件使用)
- 令牌的发布范围(由范围验证中间件使用)
- 客户编号
令牌中的所有声明都将转换为ClaimsPrincipal,并且可以通过控制器上的.User属性使用。
代表用户调用API
接下来,我们要使用用户的身份来调用API。这是通过将sampleApi范围添加到OpenID Connect中间件配置的范围列表中来完成的。我们还需要通过更改响应类型来表明我们要请求访问令牌:
Scope = "openid profile roles sampleApi",
ResponseType = "id_token token"
一旦token请求了响应类型,IdentityServer就会停止将声明包括在身份令牌中。这是出于优化目的,因为您现在拥有一个访问令牌,该令牌允许从userinfo端点检索声明并同时保持身份令牌较小。
访问userinfo端点并不难- UserInfoClient该类可以使之更加简单。此外,我们现在还将访问令牌存储在cookie中,因此我们可以在代表用户访问API的任何时候使用它:
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
// get userinfo data
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Item1, ui.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
另一种选择是在IdentityServer中重新配置作用域,并在作用域声明中设置AlwaysIncludeInIdToken标志,以强制将声明包含在身份令牌中-我将其留给读者练习。
调用API
由于访问令牌现在存储在cookie中,因此我们可以简单地从声明主体中检索它并使用它来调用服务:
// GET: CallApi/UserCredentials
public async Task<ActionResult> UserCredentials()
{
var user = User as ClaimsPrincipal;
var token = user.FindFirst("access_token").Value;
var result = await CallApi(token);
ViewBag.Json = result;
return View("ShowApiResult");
}
登录后,您现在可以在结果页面上看到sub包含该声明的信息,这意味着API现在可以代表用户使用:

如果现在role向该sampleApi范围添加范围声明-用户角色也将包含在访问令牌中:
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}

https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html
浙公网安备 33010602011771号