《ASP.NET Core 微服务实战》-- 读书笔记(第10章)
第 10 章 應(yīng)用和微服務(wù)安全
云應(yīng)用意味著應(yīng)用運(yùn)行所在的基礎(chǔ)設(shè)施無法掌控,因此安全不能再等到事后再考慮,也不能只是檢查清單上毫無意義的復(fù)選框
由于安全與云原生應(yīng)用密切相關(guān),本章將討論安全話題,并用示例演示幾種保障 ASP.NET Core Web 應(yīng)用和微服務(wù)安全的方法
云環(huán)境中的安全
內(nèi)網(wǎng)應(yīng)用
企業(yè)一直在開發(fā)這種支持性的應(yīng)用,但當(dāng)我們需要基于運(yùn)行在可縮放的云基礎(chǔ)設(shè)施之的 PaaS 開發(fā)此類應(yīng)用時(shí),很多舊的模式和實(shí)踐將很快失效
一個(gè)最明顯的問題就是無法支持 Windows 身份驗(yàn)證
長(zhǎng)期以來,ASP.NET 開發(fā)人員一直沉浸在借助內(nèi)置的 Windows 憑據(jù)來保障 Web 應(yīng)用安全的便利中
不管是公有云平臺(tái)還是私有部署的 PaaS 平臺(tái),在這些平臺(tái)上,支撐應(yīng)用的操作系統(tǒng)應(yīng)被視為臨時(shí)存續(xù)的
有些企業(yè)的安全策略要求所有虛擬機(jī)在滾動(dòng)更新期間需要銷毀并重新構(gòu)建,從而縮小持續(xù)攻擊的可能范圍
Cookie 和 Forms 身份驗(yàn)證
當(dāng)應(yīng)用運(yùn)行于 PaaS 環(huán)境中時(shí),Cookie 身份驗(yàn)證仍然適用
不過它也會(huì)給應(yīng)用增加額外負(fù)擔(dān)
首先,Forms 身份驗(yàn)證要求應(yīng)用對(duì)憑據(jù)進(jìn)行維護(hù)并驗(yàn)證
也就是說,應(yīng)用需要處理好這些保密信息的安全保障、加密和存儲(chǔ)
云環(huán)境中的應(yīng)用內(nèi)加密
在傳統(tǒng) ASP.NET 應(yīng)用開發(fā)中,常見的加密使用場(chǎng)景是創(chuàng)建安全的身份驗(yàn)證 Cookie 和會(huì)話 Cookie
在這種加密機(jī)制中,Cookie 加密時(shí)會(huì)用到機(jī)器密鑰
然后當(dāng) Cookie 由瀏覽器發(fā)回 Web 應(yīng)用時(shí),再使用同樣的機(jī)器密鑰對(duì)其進(jìn)行解密
如果無法依賴持久化文件系統(tǒng),又不可能在每次啟動(dòng)應(yīng)用時(shí)將密鑰置于內(nèi)存中,這些密鑰將如何存儲(chǔ)
答案是,將加密密鑰的存儲(chǔ)和維護(hù)視為后端服務(wù)
也就是說,與狀態(tài)維持機(jī)制、文件系統(tǒng)、數(shù)據(jù)庫(kù)和其他微服務(wù)一樣,這個(gè)服務(wù)位于應(yīng)用之外
Bearer 令牌
本章的示例將講解 OAuth 和 OpenID Connect (簡(jiǎn)稱 OIDC)
如果要以 HTTP 友好、可移植的方式傳輸身份證明,最常見的方法就是 Bearer 令牌
應(yīng)用從 Authorization 請(qǐng)求頭接收 Dearer 令牌
下例展示一個(gè)包含 Bearer 令牌的 HTTP 跟蹤會(huì)話
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 請(qǐng)求頭的值中包含一個(gè)表示授權(quán)類型的單詞,緊接著是包含憑據(jù)的字符序列
通常,服務(wù)在處理 Bearer 令牌時(shí),會(huì)從 Authorization 請(qǐng)求頭提取令牌
很多各式的令牌,例如 OAuth 2.0 (JWT),通常將 Base64 編碼用作一種 URL 友好格式,因此驗(yàn)證令牌的第一步就是解碼,以獲取原有內(nèi)容
如果令牌使用私鑰加密,服務(wù)就需要使用公鑰驗(yàn)證令牌確實(shí)由正確的發(fā)行方頒發(fā)
ASP.NET Core Web 應(yīng)用安全
本章示例中,我們將主要關(guān)注 OpenID Connetc 和 JWT 格式的 Bearer 令牌
OpenID Connect 基礎(chǔ)
OpenID Connect 是 OAuth2 的一個(gè)超集,它規(guī)定了身份提供方(IDP)、用戶和應(yīng)用之間的安全通信的規(guī)范和標(biāo)準(zhǔn)
使用 OIDC 保障 ASP.NET Core 應(yīng)用的安全
作為本章第一個(gè)代碼清單,我們將使用 OIDC 為一個(gè)簡(jiǎn)單的 ASP.NET Core
MVC Web 應(yīng)用提供安全保障功能
創(chuàng)建一個(gè)空的 Web 應(yīng)用
$ dotnet new mvc使用 Auth0 賬號(hào)配置身份提供方服務(wù)
現(xiàn)在可轉(zhuǎn)到 http://auth0.com/,注冊(cè)完成后進(jìn)入面板,點(diǎn)擊“創(chuàng)建客戶端”按鈕,請(qǐng)確保應(yīng)用類型選擇為“常規(guī) Web 應(yīng)用”
選擇 ASP.NET Core 作為實(shí)現(xiàn)語言后,將轉(zhuǎn)到一個(gè) “快速開始”教程,其代碼與本章將要編寫的內(nèi)容非常相似
使用 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);}};}} }與之前各章代碼的第一點(diǎn)區(qū)別在于,我們創(chuàng)建了一個(gè)名為 OpenIdSettings 的選項(xiàng)類,從配置系統(tǒng)讀入后,以 DI 的服務(wù)方式提供給應(yīng)用
它是一個(gè)簡(jiǎn)單類,其屬性僅用于存儲(chǔ)每種 OIDC 客戶端都會(huì)用到的四種元信息:
授權(quán)域名
客戶端 ID
客戶端密鑰
回調(diào) 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 類中執(zhí)行的兩部操作是,讓 ASP.NET Core 使用 Cookie 身份驗(yàn)證和 OpenID Connect 身份驗(yàn)證
添加一個(gè) account 控制器,提供的功能包括登錄、注銷、以及使用一個(gè)視圖顯示用戶身份中的所有特征
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 視圖代碼,它從特征集合中逐個(gè)取出特征的類型和值,并呈現(xiàn)在表格中,同時(shí),視圖還顯示用戶頭像
<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>現(xiàn)在,我們已經(jīng)基于一個(gè)模板生成的空白 ASP.NET Core Web 應(yīng)用,建立了與第三方云友好的身份提供服務(wù)的連接
這讓云應(yīng)用能夠利用 Bearer 令牌和 OIDC 標(biāo)準(zhǔn)的優(yōu)勢(shì),從手工管理身份驗(yàn)證的負(fù)擔(dān)中解放出來
OIDC 中間件和云原生
我們已經(jīng)討論過在使用 Netflix OSS 技術(shù)棧時(shí),如何借助 Steeltoe 類庫(kù)支持應(yīng)用配置和服務(wù)發(fā)現(xiàn)
我們可以使用來自 Steeltoe 的 NuGet 模塊 Steeltoe.Security.DataProtection.Redis
它專門用于將數(shù)據(jù)保護(hù) API 所用的存儲(chǔ)從本地磁盤遷移到外部的 Redis 分布式緩存中
在這個(gè)類庫(kù),可使用以下方式在 Startup 類的 ConfigureServices 方法中配置由外部存儲(chǔ)支持的數(shù)據(jù)保護(hù)功能
services.AddMvc();services.AddRedisConnectionMultiplexer(Configuration); services.AddDataProtection().PersisitKeysToRedis().SetApplicationName("myapp-redis-keystore");services.AddDistributedRedisCache(Configuration);services.AddSession();接著,我們?cè)?Configure 方法中調(diào)用 app.UseSession() 以完成外部會(huì)話狀態(tài)的配置
保障 ASP.NET Core 微服務(wù)的安全
本節(jié),我們討論為微服務(wù)提供安全保障的幾種方法,并通過開發(fā)一個(gè)使用 Bearer 令牌提供安全功能的微服務(wù)演示其中的一種方法
使用完整 OIDC 安全流程保障服務(wù)的安全
在這個(gè)流程中,用戶登錄的流程前面已經(jīng)討論過,即通過幾次瀏覽器重定向完成網(wǎng)站和 IDP 之間的交互
當(dāng)網(wǎng)站獲取到合法身份后,會(huì)向 IDP 申請(qǐng)?jiān)L問令牌,申請(qǐng)時(shí)需要提供身份證令牌以及正在被請(qǐng)求的資源的信息
使用客戶端憑證保障服務(wù)的安全
首先,只允許通過 SSL 與服務(wù)通信
此外,消費(fèi)服務(wù)的代碼需要在調(diào)用服務(wù)時(shí)附加憑據(jù)
這種憑據(jù)通常就是用戶名和密碼
在一些不存在人工交互的場(chǎng)景中,將其稱為客戶端標(biāo)識(shí)和客戶端密鑰更準(zhǔn)確
使用 Bearer 令牌保障服務(wù)的安全
在服務(wù)的 Startup 類型的 Configure 方法中啟用并配置 JWT Bearer 身份驗(yàn)證
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 令牌期間要執(zhí)行的各種驗(yàn)證,包括頒發(fā)方簽名證書、頒發(fā)方名稱、接收名稱以及令牌的時(shí)效
在上面的代碼中,我們禁用了頒發(fā)方和接收方名稱驗(yàn)證,其過程都是相當(dāng)簡(jiǎn)單的字符串對(duì)比檢查
開啟驗(yàn)證時(shí),頒發(fā)方和接收方名稱必須與令牌中包含的頒發(fā)方式和接收方式名稱嚴(yán)格匹配
要?jiǎng)?chuàng)建一個(gè)密鑰,用于令牌簽名時(shí)所用的密鑰進(jìn)行對(duì)比,我們需要一個(gè)保密密鑰,并從它創(chuàng)建一個(gè) SymmetricSecurityKey
string SecretKey = "sericouslyneverleavethissitting in yourcode"; SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));為了消費(fèi)安全的服務(wù),我們需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的控制臺(tái)應(yīng)用,它從一組 Claim 對(duì)象生成一個(gè) JwtSecurityToken 實(shí)例,并作為 Bearer 令牌放入 Authorization 請(qǐng)求頭發(fā)給服務(wù)端
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());下面是一個(gè)受安全機(jī)制保護(hù)的控制器方法,它將枚舉從客戶端發(fā)來的身份特征
[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"; }如果要控制特定客戶端能夠訪問的控制器方法,我們可以利用策略概念,策略是在授權(quán)檢查過程中執(zhí)行一小段代碼
[Authorize( Policy = "CheeseburgerPolicy")] [HttpGet("policy")] public string GetWithPolicy() {return "this is from the super secret area w/policy enforcement."; }在 ConfigureServices 方法中配置策略的過程很簡(jiǎn)單
public void ConfigureServices(IServiceCollection services){services.AddMvc();services.AddOptions();services.AddAuthorization( options => {options.AddPolicy("CheeseburgePolicy",policy =>policy.RequireClaim("icanhazcheeseburger", "true"));}); }現(xiàn)在,只要修改控制臺(tái)應(yīng)用,在其中添加這種類型的特征并將值指定為 true,就既能調(diào)用普通受保護(hù)的控制器方法,又能調(diào)用標(biāo)記了 CheeseburgerPolicy 策略的方法
該策略需要特定的身份特征、用戶名、條件以及角色
還可以通過實(shí)現(xiàn) IAuthorizationRequirement 接口定義定制的需求,這樣就可以添加自定義驗(yàn)證邏輯而不會(huì)影響各個(gè)控制器
總結(jié)
以上是生活随笔為你收集整理的《ASP.NET Core 微服务实战》-- 读书笔记(第10章)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 研发协同平台持续集成2.0架构演进
- 下一篇: 软硬件协同编程 - C#玩转CPU高速缓