IdentityServer3、WebAPI、MVC、ASP.NET Identity、Specflow:五大要素






4.60/5 (21投票s)
本文展示了如何在需要对存储在 SQL Server 中的用户使用 WebAPI/MVC 进行身份验证和授权时,配置 IdentityServer3。
如何获取代码
请在 https://github.com/icedage/IdentityServer 查看代码。
本文内容
- 解决方案结构
- 如何运行解决方案
- ID Token 与 Access Token 对比
- 在授权服务器中注册 Scope
- 在授权服务器中注册客户端
- 授权流程
- 如何实现 ASP.NET Identity
- MVC 客户端中的身份验证如何工作
- WebAPI 客户端中的身份验证如何工作
- 基于角色的授权如何工作
- 日志
解决方案结构
该解决方案由 4 个项目构成
Security.AuthorizationServer
:IdentityServer3
的基础设施。在此处我们注册客户端并定义 Scope。Security.IdentityManagementTool IdentityManagement
:一个 MVC 应用程序。它用于创建用户、角色并将角色分配给现有用户。它使用隐式流程,并在AuthorizationServer
中注册。Security.WebAPI UserController
:提供需要基于角色授权的终结点。它使用资源所有者流程,并在AuthorizationServer
中注册。Security.Scenarios
:可用于运行 WebAPI 的两个场景。
请在 GitHub 中查看 README.md,了解如何运行解决方案和触发 IdentityServer
。
假设您已成功运行解决方案,为了验证 IdentityServer
是否已配置,请打开 Identity ManagementTool
并单击菜单上的 Roles。
渲染 Roles
视图的 Endpoint
已用 Authorize
属性进行修饰。这意味着,如果您尚未登录,IdentityServer
将通过重定向到 IdentityServer
的登录页面来要求您进行身份验证,如下所示。请注意,您也可以自定义默认登录页面。
您会看到您被重定向到另一个 URL。那是 IdentityServers
的地址。该应用程序通过将用户重定向到身份提供者来请求 OpenID Connect Scope(openId
、profile
等)的 OAuth 2.0 授权。
创建您的第一个用户或使用现有用户
您可以使用现有用户和角色来测试系统。要查看用户及其角色,请打开 Security.IdentityManagementTool/Migrations/Configuration.cs。在使用之前,您需要为 IdentityManagementTool
执行 update-database 命令。
或者,您也可以使用 IdentityManagementTool
创建自己的用户和角色。
单击 New User,这样您就可以使用表单向系统中添加用户。
接下来,使用您刚刚创建的用户凭据进行登录。用户通过身份验证后,IdentityServer
会返回用户的身份,形式为 Id_Token
,如下所示。
要测试基于角色的授权,您需要创建一个 Role
。我使用了 Admin 角色,因此您需要创建一个 Admin 角色并将其分配给用户。要创建角色,只需单击菜单上的 New Role。单击 Users,然后选择您的用户,并单击 Gridview
上方的 Add Roles 按钮。这将弹出一个弹出框,您可以在其中选择角色。
ID Token 与 Access Token 对比
通过 OpenID
Connect 身份验证,还有一种额外的 OAuth Token 类型:ID Token。ID Token,或 id_token
,代表被身份验证的用户的身份。这是与用于检索用户配置文件信息或其他在同一授权流中请求的用户数据访问令牌不同的令牌。它被传递到 Check ID Endpoint 以防止重放攻击。Check ID Endpoint 用于验证 OAuth 提供者颁发的凭据是否颁发给了正确的应用程序。
在授权服务器中注册 Scope
Scope 是客户端想要访问的资源的标识符。此标识符在身份验证或令牌请求期间发送到 OP。
OpenID Connect 客户端使用作用域 (scope)值,来指定为 Access Tokens 请求的访问权限。与 Access Tokens 关联的 Scopes 决定了在使用它们访问受保护资源时可用的资源。受保护资源终结点可以根据所请求的 Access Token 的 Scope 值和其他参数执行不同的操作并返回不同的信息。
Scope 可用于请求客户端所需的特定信息集。IdentityServer
提供两种类型的 Scope:Identity
和 Resource
Scope。
Identity
用于与用户身份相关的信息集,如角色和声明。
Resource
作用域标识 Web API(也称为资源服务器)。
下面,我需要两种类型的 Scope。我需要有关用户身份的信息(Name="roles"
),并且还需要访问 WebAPI(Name = "WebAPI"
)。
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 = "WebAPI",
Name = "WebAPI",
Description = "Secure WebAPI",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Name),
new ScopeClaim(Constants.ClaimTypes.Role),
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
OpenID Connect 客户端使用作用域 (scope)值。那么,客户端是谁?
在授权服务器中注册客户端
“客户端是向 IdentityServer 请求令牌的软件,它可以用于身份验证用户或访问资源(也经常称为信赖方或 RP)。客户端必须在 OP 中注册。”
在该解决方案中,您会发现有两个客户端需要从 IdentityServer
获取令牌:WebAPI
和 IdentityManagementTool
。两者都需要注册,如下所示。
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientName = "WebAPI Client",
ClientId = "api",
Flow = Flows.ResourceOwner,
ClientSecrets = new List<Secret>
{
new Secret(SecretApi.Sha256())
},
AllowedScopes = new List<string>
{
"WebAPI"
}
},
new Client
{
Enabled = true,
ClientName = "Identity Management Tool",
ClientId = "IdentityManagementTool",
Flow = Flows.Implicit,
RequireConsent = true,
AllowRememberConsent = true,
RedirectUris = new List<string>
{
"https://:55112/"
},
IdentityTokenLifetime = 360,
AccessTokenLifetime = 3600,
AllowedScopes = new List<string>()
{ "openid", "profile" , "roles", "WebAPI" }
},
};
}
授权流程
如您所见,对于每个 Client
,我们需要指定一个 Flow。
每个客户端都需要与适当的协议流关联,以从资源所有者那里获取对其数据的访问权限。OAuth 2.0 协议定义了四个主要“授权类型”。我将重点介绍我在上述现有客户端列表中使用的类型。
隐式流和资源所有者密码凭据流
隐式授权(Implicit grant)适用于基于浏览器的客户端应用程序:隐式授权是所有流程中最简单的,并且针对在浏览器中运行的客户端 Web 应用程序进行了优化。资源所有者授予应用程序访问权限,然后立即生成一个新的访问令牌并将其传回应用程序。
资源所有者密码凭据流(Resource owner password-based grant):此授权类型允许使用资源所有者的用户名和密码来换取 OAuth 访问令牌。虽然用户的密码仍然暴露给客户端,但无需将其存储在设备上。初始身份验证后,只需存储 OAuth 令牌。由于不存储密码,用户可以在不更改密码的情况下撤销对应用程序的访问权限,并且令牌的范围仅限于一组有限的数据,因此此授权类型仍然比传统的用户名/密码身份验证提供了增强的安全性。
如何实现 ASP.NET Identity
我们需要将用户和角色保存在 SQL Server 数据库中。Identity 框架正是这样做的。它提供了一个丰富的 API 来管理用户和声明。我们如何“告诉它”与 IdentityServer
交互,以便 AuthorizationServer
只为我们本地数据库中存储的用户颁发令牌?答案是 UserService
类。有关更多信息,请查看 此链接。
IUserService
接口提供了用于用户使用本地帐户以及外部帐户进行身份验证的语义。
用户服务上的方法分为与身份验证相关的方法和与用户配置文件及为令牌颁发声明相关的方法。
每当用户使用用户名和密码对话框时,就会触发 AuthenticateLocalAsync
。如果我们打算与本地存储交互并使用 Identity
API,我们可以选择覆盖它。
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var con = new IdentityDbContext();
var userStore = new UserStore<IdentityUser>(con);
var userManager = new UserManager<IdentityUser>(userStore);
var user = userManager.Users.SingleOrDefault(x => x.UserName == context.UserName);
if (user != null)
{
context.AuthenticateResult =
new IdentityServer3.Core.Models.AuthenticateResult(user.Id, user.UserName);
}
return Task.FromResult(0);
}
MVC 客户端中的身份验证如何工作
IdentityManagementTool
(即 MVC 客户端)还需要在其 Startup.cs 中注册与 AuthenticationServer
的交互。此类中会发生许多事情。
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://:44396/identity",
ClientId = "IdentityManagementTool",
//In the scopes, we define 6 Scopes.
//In the Scope we ask what to include
Scope = "openid profile roles",
RedirectUri = "https://:55112/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
var sub = id.FindFirst(IdentityServer3.Core.Constants.ClaimTypes.Subject);
var roles = id.FindAll(IdentityServer3.Core.Constants.ClaimTypes.Role);
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
IdentityServer3.Core.Constants.ClaimTypes.Name
, IdentityServer3.Core.Constants.ClaimTypes.Role
);
nid.AddClaim(sub);
nid.AddClaims(roles);
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
IdentityServer
可以同时支持 OAuth 和 OpenID Connect。当前解决方案使用的是 OpenID Connect。因此,客户端需要指定我们需要在 OWIN 运行时中添加哪个协议。app.UseOpenIdConnectAuthentication
允许我们这样做。我们添加 OpenID Connect 的一个实例,并配置 IdentityServer
作为向我们的客户端颁发令牌的 Authority。客户端 IdentityManagementTool
由给定的 ClientId
标识。请记住 AutorizationServer
中的 Clients.cs,我们在其中注册了所有需要访问令牌的 Client
应用程序。每个 Client
都有一个 uniqueId
。该 Id
与您在 ClientId
属性中指定的 ClientId
相同。这就是授权服务器和客户端相互了解的方式。授权服务器知道它需要支持的客户端,而客户端知道负责颁发令牌的 Authority。
WebAPI 客户端中的身份验证如何工作
在在授权服务器中注册 Scope 部分,我讨论了 Scope,它们有两种类型:Identity
和 Resource
。Service
需要注册为 Resource
。那么实际的 Resource
,也就是 Web API 呢?在其 Startup.cs 中,有几件事正在发生。
app.UseIdentityServerBearerTokenAuthentication
(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://:44396/identity",
RequiredScopes = new[] { "WebAPI" },
});
AntiForgeryConfig.UniqueClaimTypeIdentifier =
IdentityServer3.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
ConfigureAuth(app)
基于角色的授权如何工作
在 Web API 中,其中一个终结点需要基于角色的授权。这意味着,仅仅通过身份验证用户是不够的,我们还需要确保用户对某些资源有访问权限。您需要检索访问令牌。您可以在两种不同的上下文中检索它。首先,您可能需要从 ClaimsPrinicipal
(隐式流)检索访问令牌,或者您可能需要通过访问 connect/token 来以编程方式检索访问令牌。在当前解决方案中,我同时使用了这两种方法。
Security.Tests
— 我们需要在调用 api/users 之前“令牌化”请求。您需要通过调用 connect/token 来检索令牌。您需要传递凭据信息来换取令牌。public HttpRequestWrapper TokenizeRequest(User user, string clientid) { var token = GetToken(user, clientid).Result; _request.AddHeader("Authorization", $"Bearer {token.AccessToken}"); return this; } private async Task<TokenResponse> GetToken(User user, string clientid) { var client = new TokenClient(Constants.TokenEndpoint, clientid, SecretApi); return client.RequestResourceOwnerPasswordAsync (user.UserName, user.Password, "WebAPI").Result; }
TokenEndpoint
是 connect/token。因此,在底层,它会调用 connect/token 终结点。有关更多信息,请阅读 OpenID 规范中的 Token endpoint。- http://openid.net/specs/openid-connect-core-1_0.html (3.1.3.1. Token Request)
IdentityManagementTool
— 您从ClaimsPrincipal
检索令牌,这意味着您需要通过 MVC 客户端(隐式流)进行身份验证。在这种情况下,您需要进行以下更改。- 首先,请记住
id_token
和访问令牌之间的区别。对于基于角色的授权流程,我们需要用户的信息或在同一授权流程中请求的其他用户数据。在这种情况下,我们需要请求IdentityServer
返回访问令牌以及id_token
。在SecurityTokenValidated
委托中,我们添加以下内容: -
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.IdToken
- 我们需要扩展
IdentityManagementlTool
的 Scope,因为我们现在需要访问另一个区域:WebAPI。在本文的开头,我们在AuthorizationServer
中注册了 Scope 和客户端。Scope 之一是 WebAPI。因此,我们还需要告知IdentityManagementTool
客户端有关此新 Scope 的信息。在IdentityManagementlTool
的 StartUp.cs 中,将 WebAPI 添加到 Scope 列表中。 -
Scope = "openid profile roles WebAPI",
- 我们还需要更新授权服务器中的客户端
AllowedScopes
。在您 Clients.cs 中的IdentityManagementTool
客户端中,请添加“WebAPI
”。 -
AllowedScopes = new List<string>() { "openid", "profile" , "roles", "WebAPI"
- 首先,请记住
日志
非常重要!也许我应该在文章开头就提到这一点。在配置过程中,许多事情可能会出错(并且确实会出错)。您需要检查日志,看看哪里出了问题。
Serilog.Log.Logger =
new LoggerConfiguration().MinimumLevel.Debug()
.WriteTo.RollingFile(pathFormat: @"c:\logs\IdSvrAdmin-{Date}.log")
.CreateLogger()
您应该可以在 C:\logs\IdSvrAdmin-{Date}.log 中访问您的日志。
结束
安全是软件工程中最难的章节之一。我不敢称自己为专家,所以如果您发现任何需要纠正的地方,请告知我。我试图列出在配置 IdentityServer
时可能会遇到的问题。我个人不得不反复阅读文档、帖子、论坛、评论、抱怨……此外,如果您觉得它有帮助但遇到问题,请给我留言,我会尽力解决。
有用资源
- https://identityserver.github.io/Documentation/
- http://openid.net/specs/openid-connect-core-1_0.html (OpenID 框架)
- https://tools.ietf.org/html/rfc6749 (OAuth2 框架)
- https://github.com/IdentityServer/IdentityServer3.Samples