码字,杂谈

IdentityServer4深入使用(一)-- 认证与授权(上)

开始之前

更多学习内容,可以看我的 .NET 学习之路系列-认证与授权

先贴上官方地址:
IdentityServer 官方文档(英文)

英文麻烦的,可以看中文,但并不是官方的,同时内容也不是很全:
中文文档

学习之前,需要了解:

OAuth2 和 OpenID Connect 两种协议机制。

OAuth 2.0

OAuth2是一种协议,允许应用程序从安全令牌服务请求访问令牌并使用它们与API进行通信。由于可以集中身份验证和授权,因此这种委派降低了客户端应用程序和API的复杂性。

OpenID Connect

OpenID Connect是一种身份验证协议。它被认为是未来,因为它在现代应用程序中具有最大的潜力。它从一开始就针对移动应用程序场景而构建,并且旨在实现API友好。

结合 OAuth2 与 OpenID Connect

实际上,OpenID Connect是OAuth 2.0的扩展。身份验证和API访问这两个基本的安全问题被组合为一个协议,通常只需一次往返于安全令牌服务即可。在可预见的将来,OpenID Connect和OAuth 2.0的结合是保护现代应用程序安全的最佳方法。

《IdentityServer4深入使用(一)-- 认证与授权(上)》

IdentityServer

IdentityServer是将符合规范的OpenID Connect和OAuth 2.0端点添加到任意ASP.NET Core应用程序的中间件。

认证与授权

在学习 IdentityServer 之前,需要系统的了解认证与授权。

认证介绍

认证又称身份验证、鉴权等,其英文都是 Authentication。

认证是对用户进行身份校验。由用户提供凭据,然后将其与存储在服务端的凭据进行比较,如果匹配,则身份验证成功。只有在用户认证通过之后,系统才会执行向其授权的操作。

授权介绍

授权(Authorization)是指确定用户可执行的操作的过程。例如,允许管理用户创建文档库、添加文档、编辑文档和删除文档。使用库的非管理用户仅获得读取文档的权限。

授权有多种形式。 ASP.NET Core 的授权提供简单的声明性角色和基于策略的丰富模型。

对比认证与授权的例子

简单来说,就是你要去 ATM 机取钱:

  • 首先你要输密码,密码正确才能进入取钱的界面,否则可能被吞卡。这个过程就是认证。
  • 进入取钱界面,发现你是个普通用户,每天只能取 5000 块,所以你取 5000 块之内都是可以的。这就是授权。
  • 超过 5000 块怎么办?当然是被拒绝,因为没有权限嘛。这时候就要尝试提升权限啥的,这都是后话了。

认证

认证是确定用户身份的过程。在 .NET 中,身份验证由 IAuthenticationService 负责,而它供身份验证中间件使用。身份验证由 Startup.ConfigureServices 中的注册身份验证服务指定。

添加认证服务

例如,为程序添加 cookie 和 JWT 持有者分身验证方案:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options));

其中,JwtBearerDefaults.AuthenticationScheme 是方案名称,为请求特定方案时,都会默认使用此名称。

如果使用了多个方案,授权策略可指定对用户进行身份验证时要依据的一个或多个身份验证方案。如上,可通过指定 cookie 方案的名称来使用该方案进行验证。

添加认证中间件

在 Startup.Configure 中添加身份验证中间件,通过调用 UseAuthenticaiton 扩展方法来实现。中间件的添加需要顺序,要保持:

  • UseRouting 之后调用,以便路由信息可用于身份验证。
  • UseEndpoints 之前调用,以便用户在经过身份验证之后才能访问端点。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();

    app.UseRouting();
    // 鉴权放在路由之后,授权之前
    app.UseAuthentication();
    // 授权
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

使用 Cookie 认证

添加一个简单的 Cookie 认证方案,每次请求需要携带对应的 Cookie 才可以访问。具体代码可以看 示例代码

首先在 Startup.cs 中添加服务:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, config =>
    {
        config.Cookie.Name = "MyCookie";
        // 没有授权时,跳转到该页面
        config.LoginPath = "/Home/Auth";
    });

然后在 HomeController 控制器前添加授权方案:

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Secret()
    {
        return View();
    }
}

并在控制器中添加一个名为 Auth 的应用:

/// <summary>
/// 认证
/// </summary>
/// <returns></returns>
// 不需要任何授权
[AllowAnonymous]
public IActionResult Auth()
{
    // 添加一些声明
    var claims1 = new List<Claim>()
    {
        new Claim(ClaimTypes.Name, "Jz"),
        new Claim(ClaimTypes.Email, "Jz@qq.com"),
        // 声明的键是可以自定义的
        new Claim("Custom-Claim", "This is a custom claim.")
    };

    // 创建一个身份声明
    var identity1 = new ClaimsIdentity(claims1, "Claims1");

    // 添加第二个声明
    var claims2 = new List<Claim>()
    {
        new Claim(ClaimTypes.Name, "Tom"),
        new Claim(ClaimTypes.Email, "Tom@qq.com")
    };

    // 创建第二个身份声明
    var identity2 = new ClaimsIdentity(claims2, "Claims2");

    // 将两个身份声明放到用户身份中
    var principal = new ClaimsPrincipal(new[] {identity1, identity2});

    // 签入身份验证方案的主体
    HttpContext.SignInAsync(principal);

    return RedirectToAction("Index");
}

是的,这样就完成了一个非常简单的认证。当我们第一次登录 Index 的时候,因为没有权限,会跳转到我们配置好的目录:/Home/Auth,然后获取到身份验证内容后,重新跳转回 Index,这一切都是这么的丝滑。

使用 JWT 认证

什么是 JWT

Jwt,Json Web Token 是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方法,用于在各方之间安全地将信息作为 JSON 对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用密码(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对对 JWT 进行签名。

基于 Token 的鉴权机制类似于 http 协议,也是无状态的,它不需要在服务端保留用户的认证信息,这也就意味着基于 Token 认证机制的应用不需要去考虑用户在哪一台设备登录,这为应用扩展提供了便利。

什么时候使用 JWT

以下是 JWT 有用的一些情况:

  • 授权:这是使用 JWT 最常见的方案。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌锁允许的路由、服务和资源。单一登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

  • 信息交换:JWT 是在各方之间安全地传输信息的一种好方法。因为可以对 JWT 进行签名,所以您可以确定发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否被篡改。

JWT 的结构

JWT 以紧凑的形式将三部分内容由点(.)分隔,其三部分内容分别是:

  • 标头
  • 有效载荷
  • 签名

因此,JWT 通常如下所示:

xxxxx.yyyyy.zzzzz
标头 Header

标头通常由两部分组成:令牌类型(即 JWT)和所使用的签名算法。

算法通常为:

  • HMAC
  • SHA256
  • RSA

例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,此 JSON 以 Base64Url 编码格式形成 JWT 的第一部分。

有效载荷 Payload

JWT 的第二部分是有效载荷,其中包含声明。声明有关实体和其他数据声明。声明有三种类型:注册声明、公共声明以及私有声明。

  • 注册声明:这些是一组非强制性的但建议使用的预定义声明,以提供一组有用的可互操作的声明。包含:

    • iss(发布者)
    • exp(到期时间)
    • sub(主题)
    • aud (受众)

    等。需要注意,声明名称是三个字符,因为 JWT 的含义是紧凑的。

  • 公共声明:这些可以由使用 JWT 的人员随意定义。但为了避免冲突,应在 IANA JSON Web Token 注册表中定义它们,或将其定义为包含抗冲突名称空间的 URI。

  • 私有声明:这些是自定义声明,旨在在同意使用它们的各方之间共享信息,既不是注册声明也不是公共声明。

有效载荷的示例:

{
  "sub": "1234567890",
  "name": "Jeremy Jone",
  "admin": true
}

然后,对此载荷进行 Base64Url 编码,以形成 JWT 的第二部分。

注意:对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取,除非将其加密,否则请勿将机密信息放入其中。

签名 Signature

要创建签名部分,您必须获取编码的标头、有效载荷、秘钥和标头中制定算法,并对其进行签名。

例如,如果要使用 HMAC SHA256 算法,则将通过以下方式创建签名:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  www.jermeyjone.com)

签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证 JWT 的发送者是它所说的真实身份。

拼在一起

将上述三部分拼在一起,得到一个完整的 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkplcmVteSBKb25lIiwiYWRtaW4iOnRydWV9.Vyj5QsI_6T4R5lJH-1uS1lpwDo1uIfosYmrOIdd9L18

将其放在 jwt.io 中进行验证:

《IdentityServer4深入使用(一)-- 认证与授权(上)》

JWT 如何工作

在身份验证中,当用户使用凭据登录成功时,服务器将返回 JWT。由于 token 是凭据,因此必须格外小心,防止出现安全问题。通常,token 的保留时间不应超过要求的时间。

每当用户想要访问受保护的路由或资源时,user agent 都会发送 JWT,通常使用 Bearer 模式的 Authorization 标头,内容如下:

Authorization: Bearer <token>

在某些情况下,这可以是无状态授权机制。服务器的受保护路由将在 Authorization 标头中检查有效的 JWT,如果存在,则将允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可以减少查询数据库以进行某些操作的需求,尽管这种情况并非总是如此。

如果 token 在授权标头中发送,则跨域资源共享(CORS)不会成为问题,因为它不使用 cookie。

在 .NET 中使用 JWT

创建一个 web 控制器项目,具体代码可以参考 示例代码

安装

在 NuGet 中搜索 Microsoft.AspNetCore.Authentication.JwtBearer 并安装。

生成 token

在控制器添加一个 GetToken 的方法:

/// <summary>
/// 颁发 token
/// </summary>
/// <returns></returns>
[HttpGet("token")]
public ActionResult GetToken()
{
    // 秘钥,绝对私有的,使用该秘钥可以生成和验证所有 token
    var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("www.jeremyjone.com"));
    // 创建令牌
    var token = new JwtSecurityToken(
        // 发行人
        issuer: "jeremyjone@qq.com",
        // 接收人
        audience: "jeremyjone",
        // 有效时间
        expires: DateTime.UtcNow.AddHours(1),
        // 数字签名,使用指定的加密方式对秘钥进行加密
        signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
        // 其他声明,这里可以任意填写
        claims: new Claim[]
        {
            // 角色需要在这里填写
            new Claim(ClaimTypes.Role, "Admin"),
            // 多个角色可以重复写,生成的 JWT 会是一个数组
            new Claim(ClaimTypes.Role, "Super")
        });

    // 写入 token 并生成 JWT
    return Ok(new JwtSecurityTokenHandler().WriteToken(token));
}

验证 token

Startup.cs 中的服务添加验证即可:

// 注入认证
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    // 需要认证哪些内容,就填写哪些
    .AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters
    {
        // 这里将 audience 的值修改。如果不验证,则通过,需要验证则不通过,可以修改 false 为 true 测试
        ValidateAudience = false,
        ValidAudience = "jeremyjone1", // 应该是 jeremyjone

        // 所有验证内容需要和颁发时的内容一致
        ValidateIssuer = true,
        ValidIssuer = "jeremyjone@qq.com",

        // 尤其是该秘钥字段,该字段属于绝密内容
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("www.jeremyjone.com")),

        // 验证有效期
        ValidateLifetime = true

        // 上面的内容属于建议但不强制验证
        // 还可以添加其他内容
    });

添加授权

给控制器中的 Get 方法添加 [Authorize] 属性。

测试

可以通过 Postman 进行测试,因为验证中 ValidAudience 不匹配,所以不能访问到 Get 方法了。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注