认证
认证是安全体系的第一道屏障。当访问者的请求进入的时候,认证体系通过验证对方提供的凭证来确定它的真实身份。认证体系只有在证实了访问者的真实身份之后才允许它进入。.NET Core提供了多种认证方式。但是它们的实现都是一样的。都是基于同一个认证模型的。
ASP.NET Core的认证功能是通过内置的一个认证组件来提供的。这个组件在处理分发给它的请求时会按照指定的认证方案从请求中提取能够验证用户真实身份的数据。我们一般把这种数据称为安全令牌。在ASP.NET Core应用下的安全令牌也叫做认证票据。
认证组件实现的整个流程涉及上图所示的三种针对认证票据的操作。也就是认证票据的颁发、验证、撤销。我们将这三种操作对应的角色称为认证票据的颁发者、验证者、撤销者。在大部分场景下这三种角色其实是由同一个主体来扮演的。
颁发认证票据的过程其实就是登录的操作。一般来说,用户试图通过登录应用以获取认证票据的时候需要提供可用来证明自身身份的用户凭证。最常见的用户凭证的就是用户名加密码。认证方在确定了对方真实身份之后就会颁发一个认证票据。这个票据携带的该用户有关的身份权限,以及其它相关的信息。一旦客户端拥有了由认证方颁发的认证票据。我们就可以按照双方协商的方式在请求种携带这个认证票据。并以此票据声明的身份执行目标操作, 或则访问目标资源。
一般来说,这个认证票据是有时效性的。一旦过期就会无效。我们有的时候会希望在过期之前就让认证票据无效。这就是所谓的注销操作。由撤销者来完成。
ASP.NET Core认证系统目的就在于构建一个标准的模型,用来完成针对请求的认证以及相关登录和注销操作。
示例:
public class demo21
{
private static readonly Dictionary<string, string> Account = new Dictionary<string, string>
{
{ "Admin","123"},
{ "UserA","123"},
{ "UserB","123"}
};
public static void Run()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
builder => builder
.ConfigureServices(
collection => collection
.AddRouting()
.AddAuthentication(
options => options
.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme
)
.AddCookie()
)
.Configure(app => app
.UseAuthentication()
.UseRouting()
.UseEndpoints(endpoints => {
endpoints.Map("/",RenderHomePageAsync);
endpoints.Map("Account/Login",SignInAsync);
endpoints.Map("Account/Logout",SignOutAsync);
})
)
)
.Build()
.Run();
}
public static async Task RenderHomePageAsync(HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
await context.Response.WriteAsync(
@"<html>
<head><title>Index</title></head>
<body>" +
$"<h3>Welcome {context.User.Identity.Name}</h3>" +
@"<a href='/Account/Logout'>Sign out</a>
</body>
</html>"
);
}
else
{
await context.ChallengeAsync();
}
}
public static async Task SignInAsync(HttpContext context)
{
if (string.CompareOrdinal(context.Request.Method, "GET") == 0)
{
await RenderLoginPageAsync(context, null, null, null);
}
else
{
var userName = context.Request.Form["username"];
var password = context.Request.Form["password"];
if (Account.TryGetValue(userName, out var pwd) && pwd == password)
{
var identity = new GenericIdentity(userName, "Passord");
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync(principal);
}
else
{
await RenderLoginPageAsync(context, userName, password, "Invalid user name or password");
}
}
}
private static Task RenderLoginPageAsync(HttpContext context, string userName, string password,string errorMessage)
{
context.Response.ContentType = "text/html";
return context.Response.WriteAsync(
@"<html>
<head><title>Login</title></head>
<body>
<form method='post'>" +
$"<input type='text' name='username' placeholder='User name' value='{userName}'/>" +
$"<input type='password' name='password' placeholder='Password' value='{password}'/>" +
@"<input type='submit' value='Sign in' />
</form>" +
$"<p style='color:red'>{errorMessage}</p>"+
@"</body>
</html>");
}
public static async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync();
context.Response.Redirect("/");
}
}
IPrincipal
IPrincipal接口表示接收认证的用户。
源代码位置:\runtime\src\libraries\System.Private.CoreLib\src\System\Security\Principal\IPrincipal.cs
源代码:
public interface IPrincipal
{
// Retrieve the identity object
// 用来表示身份
IIdentity? Identity { get; }
// Perform a check for a specific role
// 用来确定当前用户是否被添加到指定的某一个角色中。如果采用基于角色的授权方式,我们就可以直接调用这个方法来确定当前用户是否有访问目标的资源或者访问目标的权限。
bool IsInRole(string role);
}
IIdentity
IIdentity接口表示用户的身份。
源代码位置:\runtime\src\libraries\System.Private.CoreLib\src\System\Security\Principal\IIdentity.cs
源代码:
public interface IIdentity
{
// Access to the name string
string? Name { get; }
// Access to Authentication 'type' info
string? AuthenticationType { get; }
// Determine if this represents the unauthenticated identity
bool IsAuthenticated { get; }
}
Claim
Claim对象用来描述用户的身份、权限、以及其它与用户相关的信息。
源代码位置:\runtime\src\libraries\System.Security.Claims\src\System\Security\Claims\Claim.cs
源代码:太长,只列出部分。
public class Claim
{
// 表明声明当前的颁发者
private readonly string _issuer;
// 表明声明最初的颁发者
private readonly string _originalIssuer;
// 声明陈述的主体对象
private readonly ClaimsIdentity? _subject;
// 声明陈述的类型
private readonly string _type;
// 声明陈述的类型值
private readonly string _value;
}
正常来说我们可以定义任何的声明的类型,但是在.NET中已经定义了常用的类型。
public static class ClaimTypes
{
internal const string ClaimTypeNamespace = "http://schemas.microsoft.com/ws/2008/06/identity/claims";
public const string AuthenticationInstant = ClaimTypeNamespace + "/authenticationinstant";
public const string AuthenticationMethod = ClaimTypeNamespace + "/authenticationmethod";
public const string CookiePath = ClaimTypeNamespace + "/cookiepath";
public const string DenyOnlyPrimarySid = ClaimTypeNamespace + "/denyonlyprimarysid";
public const string DenyOnlyPrimaryGroupSid = ClaimTypeNamespace + "/denyonlyprimarygroupsid";
public const string DenyOnlyWindowsDeviceGroup = ClaimTypeNamespace + "/denyonlywindowsdevicegroup";
public const string Dsa = ClaimTypeNamespace + "/dsa";
public const string Expiration = ClaimTypeNamespace + "/expiration";
public const string Expired = ClaimTypeNamespace + "/expired";
public const string GroupSid = ClaimTypeNamespace + "/groupsid";
public const string IsPersistent = ClaimTypeNamespace + "/ispersistent";
public const string PrimaryGroupSid = ClaimTypeNamespace + "/primarygroupsid";
public const string PrimarySid = ClaimTypeNamespace + "/primarysid";
public const string Role = ClaimTypeNamespace + "/role";
public const string SerialNumber = ClaimTypeNamespace + "/serialnumber";
public const string UserData = ClaimTypeNamespace + "/userdata";
public const string Version = ClaimTypeNamespace + "/version";
public const string WindowsAccountName = ClaimTypeNamespace + "/windowsaccountname";
public const string WindowsDeviceClaim = ClaimTypeNamespace + "/windowsdeviceclaim";
public const string WindowsDeviceGroup = ClaimTypeNamespace + "/windowsdevicegroup";
public const string WindowsUserClaim = ClaimTypeNamespace + "/windowsuserclaim";
public const string WindowsFqbnVersion = ClaimTypeNamespace + "/windowsfqbnversion";
public const string WindowsSubAuthority = ClaimTypeNamespace + "/windowssubauthority";
//
// From System.IdentityModel.Claims
//
internal const string ClaimType2005Namespace = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims";
public const string Anonymous = ClaimType2005Namespace + "/anonymous";
public const string Authentication = ClaimType2005Namespace + "/authentication";
public const string AuthorizationDecision = ClaimType2005Namespace + "/authorizationdecision";
public const string Country = ClaimType2005Namespace + "/country";
public const string DateOfBirth = ClaimType2005Namespace + "/dateofbirth";
public const string Dns = ClaimType2005Namespace + "/dns";
public const string DenyOnlySid = ClaimType2005Namespace + "/denyonlysid"; // NOTE: shown as 'Deny only group SID' on the ADFSv2 UI!
public const string Email = ClaimType2005Namespace + "/emailaddress";
public const string Gender = ClaimType2005Namespace + "/gender";
public const string GivenName = ClaimType2005Namespace + "/givenname";
public const string Hash = ClaimType2005Namespace + "/hash";
public const string HomePhone = ClaimType2005Namespace + "/homephone";
public const string Locality = ClaimType2005Namespace + "/locality";
public const string MobilePhone = ClaimType2005Namespace + "/mobilephone";
public const string Name = ClaimType2005Namespace + "/name";
public const string NameIdentifier = ClaimType2005Namespace + "/nameidentifier";
public const string OtherPhone = ClaimType2005Namespace + "/otherphone";
public const string PostalCode = ClaimType2005Namespace + "/postalcode";
public const string Rsa = ClaimType2005Namespace + "/rsa";
public const string Sid = ClaimType2005Namespace + "/sid";
public const string Spn = ClaimType2005Namespace + "/spn";
public const string StateOrProvince = ClaimType2005Namespace + "/stateorprovince";
public const string StreetAddress = ClaimType2005Namespace + "/streetaddress";
public const string Surname = ClaimType2005Namespace + "/surname";
public const string System = ClaimType2005Namespace + "/system";
public const string Thumbprint = ClaimType2005Namespace + "/thumbprint";
public const string Upn = ClaimType2005Namespace + "/upn";
public const string Uri = ClaimType2005Namespace + "/uri";
public const string Webpage = ClaimType2005Namespace + "/webpage";
public const string X500DistinguishedName = ClaimType2005Namespace + "/x500distinguishedname";
internal const string ClaimType2009Namespace = "http://schemas.xmlsoap.org/ws/2009/09/identity/claims";
public const string Actor = ClaimType2009Namespace + "/actor";
}
ClaimsIdentity
ClaimsIdentity携带声明的身份,也可以称为身份声明持有对象,是IIdentity接口的实现类。
源代码位置:\runtime\src\libraries\System.Security.Claims\src\System\Security\Claims\ClaimsIdentity.cs
源代码:太长,只列出部分。
namespace System.Security.Claims
{
/// <summary>
/// An Identity that is represented by a set of claims.
/// </summary>
public class ClaimsIdentity : IIdentity
{
private readonly List<Claim> _instanceClaims = new List<Claim>();
// 一个ClaimsIdentity对象,提供的信息中除了表示认证类型的AuthenticationType属性外,还需要在创建的时候指定其它的信息。而这些信息都是根据申明所计算出来的。例:
/// <summary>
/// Gets the Name of this <see cref="ClaimsIdentity"/>.
/// </summary>
/// <remarks>Calls <see cref="FindFirst(string)"/> where string == NameClaimType, if found, returns <see cref="Claim.Value"/> otherwise null.</remarks>
public virtual string? Name
{
// just an accessor for getting the name claim
get
{
Claim? claim = FindFirst(_nameClaimType);
if (claim != null)
{
return claim.Value;
}
return null;
}
}
/// <summary>
/// Retrieves the first <see cref="Claim"/> where Claim.Type equals <paramref name="type"/>.
/// </summary>
/// <param name="type">The type of the claim to match.</param>
/// <returns>A <see cref="Claim"/>, null if nothing matches.</returns>
/// <remarks>Comparison is: StringComparison.OrdinalIgnoreCase.</remarks>
/// <exception cref="ArgumentNullException">if 'type' is null.</exception>
public virtual Claim? FindFirst(string type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
foreach (Claim claim in Claims)
{
if (claim != null)
{
if (string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase))
{
return claim;
}
}
}
return null;
}
}
}
ClaimsPrincipal
一个ClaimsPrincipal代表一个真实的用户,而一个用户是可以有多个身份的。可以简单理解一个ClaimsPrincipal是多个ClaimsIdentity的封装。
源码位置:\runtime\src\libraries\System.Security.Claims\src\System\Security\Claims\ClaimsPrincipal.cs
源代码:太长,只列出部分。
namespace System.Security.Claims
{
/// <summary>
/// Concrete IPrincipal supporting multiple claims-based identities
/// </summary>
public class ClaimsPrincipal : IPrincipal
{
// ClaimsPrincipal是多个ClaimsIdentity的封装的由来。
private readonly List<ClaimsIdentity> _identities = new List<ClaimsIdentity>();
/// <summary>
/// IsInRole answers the question: does an identity this principal possesses
/// contain a claim of type RoleClaimType where the value is '==' to the role.
/// </summary>
/// <param name="role">The role to check for.</param>
/// <returns>'True' if a claim is found. Otherwise 'False'.</returns>
/// <remarks>Each Identity has its own definition of the ClaimType that represents a role.</remarks>
public virtual bool IsInRole(string role)
{
for (int i = 0; i < _identities.Count; i++)
{
if (_identities[i] != null)
{
if (_identities[i].HasClaim(_identities[i].RoleClaimType, role))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Gets the identity of the current principal.
/// 虽然一个用户可以具有多个身份,但是还是需要确定一个主身份,用Identity属性来表示。
/// </summary>
public virtual System.Security.Principal.IIdentity? Identity
{
get
{
if (s_identitySelector != null)
{
return s_identitySelector(_identities);
}
else
{
return SelectPrimaryIdentity(_identities);
}
}
}
/// <summary>
/// Gets the claims as <see cref="IEnumerable{Claim}"/>, associated with this <see cref="ClaimsPrincipal"/> by enumerating all <see cref="Identities"/>.
/// 用户返回当前这个ClaimsIdentity携带的所有身份
/// </summary>
public virtual IEnumerable<Claim> Claims
{
get
{
foreach (ClaimsIdentity identity in Identities)
{
foreach (Claim claim in identity.Claims)
{
yield return claim;
}
}
}
}
}
}
AuthenticationTicket
认证票据对象,实际上就是对ClaimsPrincipal对象的封装。
源码位置:\aspnetcore\src\Http\Authentication.Abstractions\src\AuthenticationTicket.cs
源代码:
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Claims;
namespace Microsoft.AspNetCore.Authentication;
/// <summary>
/// Contains user identity information as well as additional authentication state.
/// </summary>
public class AuthenticationTicket
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationTicket"/> class
/// </summary>
/// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param>
/// <param name="properties">additional properties that can be consumed by the user or runtime.</param>
/// <param name="authenticationScheme">the authentication scheme that was responsible for this ticket.</param>
public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties? properties, string authenticationScheme)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
AuthenticationScheme = authenticationScheme;
Principal = principal;
Properties = properties ?? new AuthenticationProperties();
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationTicket"/> class
/// </summary>
/// <param name="principal">the <see cref="ClaimsPrincipal"/> that represents the authenticated user.</param>
/// <param name="authenticationScheme">the authentication scheme that was responsible for this ticket.</param>
public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme)
: this(principal, properties: null, authenticationScheme: authenticationScheme)
{ }
/// <summary>
/// Gets the authentication scheme that was responsible for this ticket.
/// 认证方案
/// </summary>
public string AuthenticationScheme { get; }
/// <summary>
/// Gets the claims-principal with authenticated user identities.
/// 认证对象,用户信息
/// </summary>
public ClaimsPrincipal Principal { get; }
/// <summary>
/// Additional state values for the authentication session.
/// 身份验证属性,其中包含了很多认真票据过期时间,重定向地址等。
/// </summary>
public AuthenticationProperties Properties { get; }
/// <summary>
/// Returns a copy of the ticket.
/// </summary>
/// <remarks>
/// The method clones the <see cref="Principal"/> by calling <see cref="ClaimsIdentity.Clone"/> on each of the <see cref="ClaimsPrincipal.Identities"/>.
/// </remarks>
/// <returns>A copy of the ticket</returns>
public AuthenticationTicket Clone()
{
var principal = new ClaimsPrincipal();
foreach (var identity in Principal.Identities)
{
principal.AddIdentity(identity.Clone());
}
return new AuthenticationTicket(principal, Properties.Clone(), AuthenticationScheme);
}
}
授权
认证只是在确定用户的身份,而授权则是通过权限控制,使我们的用户只能做它允许的事情。
授权的本质就是通过设置一个策略来决定具有何种特性的用户才会被授权访问某个资源或则执行某个操作。
我们可以采取任何一种授权策略。比如说根据用户角色授权、用户等级授权、用户部门授权,甚至可以根据用户的年龄、性别等授权。
认证后所体现的ClaimsPrincipal对象所携带的声明不仅仅用于描述用户的身份还携带了构建这些授权策略的元素。
所以说,授权实际上就是检查认证用户携带的声明是否与授权策略一致的过程。
ASP.NET Core的应用实际上并没有对如何定义授权策略做硬性的规定。我们完全可以根据用户具有的任意特性来判断是否具有获取目标资源或执行目标操作的权限。但是针对角色的授权策略依然是最常用的。
这里只描述对于角色的授权。角色实际上就是对一组权限集合的描述。将一个用户添加某一个角色就是为了将对应的权限赋予某一个用户。某一个请求去实施这种授权策略以确定该用户是否具有访问目标资源或执行某个操作的权限只有在请求通过认证后才有意义。所以说我们需要先在应用中实现认证以及相关的登录与注销操作。
要在一个ASP.NET Core应用中去实现这种基于角色的授权,我们需要先解决如何存储用户、角色以及他们两者的映射关系。