第 10 章 应用和微服务安全

云应用意味着应用运行所在的基础设施无法掌控,因此安全不能再等到事后再考虑,也不能只是检查清单上毫无意义的复选框

由于安全与云原生应用密切相关,本章将讨论安全话题,并用示例演示几种保障 ASP.NET Core Web 应用和微服务安全的方法

云环境中的安全

内网应用

企业一直在开发这种支持性的应用,但当我们需要基于运行在可缩放的云基础设施之的 PaaS 开发此类应用时,很多旧的模式和实践将很快失效

一个最明显的问题就是无法支持 Windows 身份验证

长期以来,ASP.NET 开发人员一直沉浸在借助内置的 Windows 凭据来保障 Web 应用安全的便利中

不管是公有云平台还是私有部署的 PaaS 平台,在这些平台上,支撑应用的操作系统应被视为临时存续的

有些企业的安全策略要求所有虚拟机在滚动更新期间需要销毁并重新构建,从而缩小持续攻击的可能范围

Cookie 和 Forms 身份验证

当应用运行于 PaaS 环境中时,Cookie 身份验证仍然适用

不过它也会给应用增加额外负担

首先,Forms 身份验证要求应用对凭据进行维护并验证

也就是说,应用需要处理好这些保密信息的安全保障、加密和存储

云环境中的应用内加密

在传统 ASP.NET 应用开发中,常见的加密使用场景是创建安全的身份验证 Cookie 和会话 Cookie

在这种加密机制中,Cookie 加密时会用到机器密钥

然后当 Cookie 由浏览器发回 Web 应用时,再使用同样的机器密钥对其进行解密

如果无法依赖持久化文件系统,又不可能在每次启动应用时将密钥置于内存中,这些密钥将如何存储

答案是,将加密密钥的存储和维护视为后端服务

也就是说,与状态维持机制、文件系统、数据库和其他微服务一样,这个服务位于应用之外

Bearer 令牌

本章的示例将讲解 OAuth 和 OpenID Connect (简称 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/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

《ASP.NET Core 微服务实战》-- 读书笔记(第10章)的更多相关文章

  1. JavaScript DOM编程艺术 - 读书笔记1-3章

    1.JavaScript语法 准备工作 一个普通的文本编辑器,一个Web浏览器. JavaScript代码必须通过Html文档才能执行,第一种方式是将JavaScript代码放到文档<head& ...

  2. JavaScript DOM编程艺术 读书笔记

    2. JavaScript语法 2.1 注释      HTML允许使用"<!--"注释跨越多个行,但JavaScript要求这种注释的每行都必须在开头加上"< ...

  3. JavaScript DOM编程艺术读书笔记(四)

    第十章 实现动画效果 var repeat = "moveElement('"+elementID+"',"+final_x+","+fin ...

  4. JavaScript DOM编程艺术读书笔记(三)

    第七章 动态创建标记 在web浏览器中往文档添加标记,先回顾下过去使用的技术: <body> <script type="text/javascript"> ...

  5. JavaScript DOM编程艺术读书笔记(二)

    第五章 最佳实践 平稳退化(graceful degradation):如果正确使用了JavaScript脚本,可以让访问者在他们的浏览器不支持JavaScript的情况下仍能顺利地浏览你网站.虽然某 ...

  6. JavaScript DOM编程艺术读书笔记(一)

    第一章,第二章 DOM:是一套对文档的内容进行抽象和概念化的方法. W3C中的定义:一个与系统平台和编程语言无关的接口,程序和脚本可以通过这个接口动态的访问和修改文档的内容,结构和样式. DHTML( ...

  7. JavaScript DOM编程艺术-学习笔记(第二章)

    1.好习惯从末尾加分号:开始 2.js区分大小写 3.程序界万能的命名法则:①不以,数字开头的数字.字母.下划线.美元符号 ②提倡以下划线命名法来命名变量,以驼峰命名法来命名函数.但是到了公司往往会身 ...

  8. 《javascript dom编程艺术》笔记(一)——优雅降级、向后兼容、多个函数绑定onload函数

    刚刚开始自学前端,如果不对请指正:欢迎各位技术大牛指点. 开始学习<javascript dom编程艺术>,整理一下学习到的知识.今天刚刚看到第六章,记下get到的几个知识点. 优雅降级 ...

  9. JavaScript DOM编程艺术学习笔记(一)

    嗯,经过了一周的时间,今天终于将<JavaScript DOM编程艺术(第2版)>这本书看完了,感觉受益匪浅,我和作者及出版社等等都不认识,无意为他们做广告,不过本书确实值得一看,也值得推 ...

  10. JavaScript DOM编程艺术-学习笔记

    发现基础不是很好,补习一下.37买了2本书(dom编程和高级程序设计). 以前读书总是自己勾勾画画,有点没意思.现在写下来,说不定会成为传世经典.哈哈...........随便扯扯淡. 第一天(201 ...

随机推荐

  1. Java标签在循环中的使用

    定义 标签,类似--label1: 放在循环外部,用于内部多重循环语句的跳出 例子 public static void main(String[] args) { Scanner sc = new ...

  2. 什么是 doris,为什么几乎国内大厂都会使用它

    转载至我的博客 https://www.infrastack.cn ,公众号:架构成长指南 今天给各位分享一个非常牛的实时分析型数据库Apache Doris,几乎国内的一二线大厂都在使用它做数据分析 ...

  3. Linux-软件包管理-rpm-yum-apt

  4. [转帖]docker exec 失败问题排查之旅

    https://plpan.github.io/docker-exec-%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5%E4%B9%8B% ...

  5. [转帖]linux audit审计(7-1)--读懂audit日志

    https://www.cnblogs.com/xingmuxin/p/8807774.html  auid=0 auid记录Audit user ID,that is the loginuid.当我 ...

  6. [转帖]jmeter_采样器sampler简介

    1.取样器介绍 取样器是用来模拟用户操作的,向服务器发送请求以及接收服务器的响应数据. 取样器是在线程组内部的元件,也就是说取样器只能在线程组中添加. 取样器(Sampler)是性能测试中向服务器发送 ...

  7. Jmeter学习之一_连接与测试Redis

    Jmeter学习之一_连接与测试Redis 简介 下载: https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.zip 注意事项: D ...

  8. [转帖]《AWK程序设计语言》笔记(1)—— AWK入门与简单案例

    原文为 <The AWK Programming Language>,GitHub上有中译版,不过有些内容翻译的比较奇怪,建议跟原版对照着看 https://github.com/wuzh ...

  9. [转贴]loadrunner 场景设计-添加Unix、Linux Resources计数器

    loadrunner 场景设计-添加Unix.Linux Resources计数器   https://www.cnblogs.com/shouke/p/10158239.html 场景设计-添加Un ...

  10. vue如何在render函数中循环(3)

    h函数的三个参数 第一个参数是必须的. 类型:{String | Object | Function} 一个 HTML 标签名.一个组件.一个异步组件.或一个函数式组件. 是要渲染的html标签. 第 ...