"ASP.NET Core micro-service real" - reading notes (Chapter 10)

Chapter 10 applications and micro-Security

Infrastructure cloud application means that the application is running can not control, so security can no longer wait until afterthoughts, not just meaningless on the checkbox checklist

Since the method is closely related to security and cloud native application, this chapter will discuss the topic of security and protection with sample demonstrates several ASP.NET Core Web application security services and micro

Secure cloud environment

Intranet applications

Companies have been supportive of the development of this application, but we need to run the PaaS based on a scalable cloud infrastructure of the development of such applications, a lot of old patterns and practices will fail soon

One of the most obvious problem is not support Windows Authentication

For a long time, ASP.NET developers have been immersed in the use of the built-in Windows credentials to protect the Web application security in conveniences

Whether public or private cloud platform deployment PaaS platform, on these platforms, operating systems support application should be considered as a temporary subsisting

Some corporate security policies require that all virtual machines need to be destroyed and rebuilt during the rollover, thereby reducing the ongoing attacks could range

When the application is running in PaaS environments, Cookie Authentication still apply

But it will also give the application additional burden

First, Forms authentication requires the application maintenance and verification of credentials

In other words, these applications need to address the safety and security of confidential information, encryption and storage

Cloud environment within the application encryption

In traditional ASP.NET application development, a common scenario is to use encryption to create secure authentication and session Cookie Cookie

In this encryption mechanism, the machine will be used when the Cookie encryption key

Then when Cookie sent back by the Web browser application, and then use the same machine key to decrypt

If you can not rely on persistent file system, you can not start the application each time the key is placed in memory, and how these keys are stored

The answer is that the storage and maintenance of encryption keys regarded as back-end services

In other words, with the state maintaining mechanism, file systems, databases, and other micro as service that is located outside the application

Bearer token

Examples in this chapter will explain OAuth and OpenID Connect (referred OIDC)

如果要以 HTTP 友好、可移植的方式传输身份证明,最常见的方法就是 Bearer 令牌

应用从 Authorization 请求头接收 Dearer 令牌

下例展示一个包含 Bearer 令牌的 HTTP 跟踪会话

POST /api/service HTTP/1.1
Host: world-domination.io
Authorization: Bearer ABC123HIJABC123HIJABC123HIJ Content-Type:
application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (XLL; Linux x86_64) etc...etc...etc...

Authorization 请求头的值中包含一个表示授权类型的单词,紧接着是包含凭据的字符序列

通常,服务在处理 Bearer 令牌时,会从 Authorization 请求头提取令牌

很多各式的令牌,例如 OAuth 2.0 (JWT),通常将 Base64 编码用作一种 URL 友好格式,因此验证令牌的第一步就是解码,以获取原有内容

如果令牌使用私钥加密,服务就需要使用公钥验证令牌确实由正确的发行方颁发

ASP.NET Core Web 应用安全

本章示例中,我们将主要关注 OpenID Connetc 和 JWT 格式的 Bearer 令牌

OpenID Connect 基础

OpenID Connect 是 OAuth2 的一个超集,它规定了身份提供方(IDP)、用户和应用之间的安全通信的规范和标准

使用 OIDC 保障 ASP.NET Core 应用的安全

作为本章第一个代码清单,我们将使用 OIDC 为一个简单的 ASP.NET Core
MVC Web 应用提供安全保障功能

创建一个空的 Web 应用

$ dotnet new mvc

使用 Auth0 账号配置身份提供方服务

现在可转到 http://auth0.com/,注册完成后进入面板,点击“创建客户端”按钮,请确保应用类型选择为“常规 Web 应用”

选择 ASP.NET Core 作为实现语言后,将转到一个 “快速开始”教程,其代码与本章将要编写的内容非常相似

使用 OIDC 中间件

GitHub链接:https://github.com/microservices-aspnetcore/secure-services

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;


namespace StatlerWaldorfCorp.SecureWebApp
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)                
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(
                options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
            
            // Add framework services.
            services.AddMvc();

            services.AddOptions();

            services.Configure<OpenIDSettings>(Configuration.GetSection("OpenID"));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                    ILoggerFactory loggerFactory,
                    IOptions<OpenIDSettings> openIdSettings)
        {

            Console.WriteLine("Using OpenID Auth domain of : " + openIdSettings.Value.Domain);
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();                
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseCookieAuthentication( new CookieAuthenticationOptions
            {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true
            });

            var options = CreateOpenIdConnectOptions(openIdSettings);
            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("name");
            options.Scope.Add("email");
            options.Scope.Add("picture");

            app.UseOpenIdConnectAuthentication(options);

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

        private OpenIdConnectOptions CreateOpenIdConnectOptions(
            IOptions<OpenIDSettings> openIdSettings)
        {
            return new OpenIdConnectOptions("Auth0")
            {
                Authority = $"https://{openIdSettings.Value.Domain}",
                ClientId = openIdSettings.Value.ClientId,
                ClientSecret = openIdSettings.Value.ClientSecret,
                AutomaticAuthenticate = false,
                AutomaticChallenge = false,

                ResponseType = "code",
                CallbackPath = new PathString("/signin-auth0"),

                ClaimsIssuer = "Auth0",
                SaveTokens = true,
                Events = CreateOpenIdConnectEvents()
            };
        }

        private OpenIdConnectEvents CreateOpenIdConnectEvents()
        {
            return new OpenIdConnectEvents()
            {
                OnTicketReceived = context =>
                {
                    var identity = 
                        context.Principal.Identity as ClaimsIdentity;
                    if (identity != null) {
                        if (!context.Principal.HasClaim( c => c.Type == ClaimTypes.Name) &&
                        identity.HasClaim( c => c.Type == "name"))
                        identity.AddClaim(new Claim(ClaimTypes.Name, identity.FindFirst("name").Value));
                    }
                    return Task.FromResult(0);
                }
            };
        }
    }
}

与之前各章代码的第一点区别在于,我们创建了一个名为 OpenIdSettings 的选项类,从配置系统读入后,以 DI 的服务方式提供给应用

它是一个简单类,其属性仅用于存储每种 OIDC 客户端都会用到的四种元信息:

  • 授权域名
  • 客户端 ID
  • 客户端密钥
  • 回调 URL

由于这些信息的敏感性,我们的 appsettings.json 文件没有签入到 GitHub,不过以下代码清单列出了它的大致格式

{
    "OpenID": {
        "Domain": "Your Auth0 domain",
        "ClientId": "Your Auth0 Client Id",
        "ClientSecret": "Your Auth0 Client Secret",
        "CallbackUrl": "http://localhost:5000/signin-auth0"
    }
}

接下来要在 Startup 类中执行的两部操作是,让 ASP.NET Core 使用 Cookie 身份验证和 OpenID Connect 身份验证

添加一个 account 控制器,提供的功能包括登录、注销、以及使用一个视图显示用户身份中的所有特征

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Authorization;
using System.Linq;
using System.Security.Claims;

namespace StatlerWaldorfCorp.SecureWebApp.Controllers
{
    public class AccountController : Controller
    {
        public IActionResult Login(string returnUrl = "/")
        {
            return new ChallengeResult("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
        }

        [Authorize]
        public IActionResult Logout()
        {
            HttpContext.Authentication.SignOutAsync("Auth0");
            HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            return RedirectToAction("Index", "Home");
        }

        [Authorize]
        public IActionResult Claims()
        {
            ViewData["Title"] = "Claims";
            var identity = HttpContext.User.Identity as ClaimsIdentity;
            ViewData["picture"] = identity.FindFirst("picture").Value;
            return View();
        }
    }
}

Claims 视图代码,它从特征集合中逐个取出特征的类型和值,并呈现在表格中,同时,视图还显示用户头像

<div class="row">
    <div class="col-md-12">

        <h3>Current User Claims</h3>

        <br/>  
        <img src="@ViewData["picture"]" height="64" width="64"/><br/>

        <table class="table">
            <thead>
                <tr>
                    <th>Claim</th><th>Value</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var claim in User.Claims)
                {
                    <tr>
                        <td>@claim.Type</td>
                        <td>@claim.Value</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>

现在,我们已经基于一个模板生成的空白 ASP.NET Core Web 应用,建立了与第三方云友好的身份提供服务的连接

这让云应用能够利用 Bearer 令牌和 OIDC 标准的优势,从手工管理身份验证的负担中解放出来

OIDC 中间件和云原生

我们已经讨论过在使用 Netflix OSS 技术栈时,如何借助 Steeltoe 类库支持应用配置和服务发现

我们可以使用来自 Steeltoe 的 NuGet 模块 Steeltoe.Security.DataProtection.Redis

它专门用于将数据保护 API 所用的存储从本地磁盘迁移到外部的 Redis 分布式缓存中

在这个类库,可使用以下方式在 Startup 类的 ConfigureServices 方法中配置由外部存储支持的数据保护功能

services.AddMvc();

services.AddRedisConnectionMultiplexer(Configuration);
services.AddDataProtection()
        .PersisitKeysToRedis()
        .SetApplicationName("myapp-redis-keystore");

services.AddDistributedRedisCache(Configuration);

services.AddSession();

接着,我们在 Configure 方法中调用 app.UseSession() 以完成外部会话状态的配置

保障 ASP.NET Core 微服务的安全

本节,我们讨论为微服务提供安全保障的几种方法,并通过开发一个使用 Bearer 令牌提供安全功能的微服务演示其中的一种方法

使用完整 OIDC 安全流程保障服务的安全

在这个流程中,用户登录的流程前面已经讨论过,即通过几次浏览器重定向完成网站和 IDP 之间的交互

当网站获取到合法身份后,会向 IDP 申请访问令牌,申请时需要提供身份证令牌以及正在被请求的资源的信息

使用客户端凭证保障服务的安全

首先,只允许通过 SSL 与服务通信

此外,消费服务的代码需要在调用服务时附加凭据

这种凭据通常就是用户名和密码

在一些不存在人工交互的场景中,将其称为客户端标识和客户端密钥更准确

使用 Bearer 令牌保障服务的安全

在服务的 Startup 类型的 Configure 方法中启用并配置 JWT Bearer 身份验证

app.UseJwtBearerAuthentication(new JwtBearerOptions)
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = signingKey,
        ValidateIssuer = false,
        ValidIssuer = "http://fake.issuer.com",
        ValidateAudience = false,
        ValidAudience = "http://sampleservice.example.com",
        ValidateLifetime = true,
    }
};

我们可控制在接收 Bearer 令牌期间要执行的各种验证,包括颁发方签名证书、颁发方名称、接收名称以及令牌的时效

在上面的代码中,我们禁用了颁发方和接收方名称验证,其过程都是相当简单的字符串对比检查

开启验证时,颁发方和接收方名称必须与令牌中包含的颁发方式和接收方式名称严格匹配

要创建一个密钥,用于令牌签名时所用的密钥进行对比,我们需要一个保密密钥,并从它创建一个 SymmetricSecurityKey

string SecretKey = "sericouslyneverleavethissitting in yourcode";
SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));

为了消费安全的服务,我们需要创建一个简单的控制台应用,它从一组 Claim 对象生成一个 JwtSecurityToken 实例,并作为 Bearer 令牌放入 Authorization 请求头发给服务端

var claims = new []
{
    new Claim(JwtRegisteredClaimNames.Sub, "AppUser_Bob"),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DataTime.Now).ToString(), ClaimValueTypes.Integer64),
};
var jwt = new JwtSecurityToken(
    issuer : "issuer",
    audience : "audience",
    claims : claims,
    notBefore : DateTiem.UtcNow,
    expires : DateTime.UtcNow.Add(TimeSpan.FromMinutes(20)),
    signingCredentials: creds)
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", encodedJwt);

var result = httpClient.GetAsync("http://localhost:5000/api/secured").Result;
Console.WriteLine(result.StatusCode);
Console.WriteLine(result.Content.ToString());

下面是一个受安全机制保护的控制器方法,它将枚举从客户端发来的身份特征

[Authorize]
[HttpGet]
public string Get()
{
    foreach (var claim in HttpContext.User.Claims){
        Console.WriteLine($"{claim.Type}:{claim.Value}");
    }
    return "this is from the super secret area";
}

如果要控制特定客户端能够访问的控制器方法,我们可以利用策略概念,策略是在授权检查过程中执行一小段代码

[Authorize( Policy = "CheeseburgerPolicy")]
[HttpGet("policy")]
public string GetWithPolicy()
{
    return "this is from the super secret area w/policy enforcement.";
}

在 ConfigureServices 方法中配置策略的过程很简单

public void ConfigureServices(IServiceCollection services){
    services.AddMvc();
    services.AddOptions();
    services.AddAuthorization( options => {
        options.AddPolicy("CheeseburgePolicy",
        policy =>
        policy.RequireClaim("icanhazcheeseburger", "true"));
    });
}

现在,只要修改控制台应用,在其中添加这种类型的特征并将值指定为 true,就既能调用普通受保护的控制器方法,又能调用标记了 CheeseburgerPolicy 策略的方法

该策略需要特定的身份特征、用户名、条件以及角色

还可以通过实现 IAuthorizationRequirement 接口定义定制的需求,这样就可以添加自定义验证逻辑而不会影响各个控制器

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 ([email protected]) 。

Guess you like

Origin www.cnblogs.com/MingsonZheng/p/12293531.html