Asp.Net Core Web Api基於cookie的安全驗證


一直以來都有一個說法,在對Asp.Net Core Web Api進行安全驗證時,只能使用Token而不能使用Cookie.

事實並非如此,在對Web Api進行驗證和授權時,你可以使用Cookie,跟普通的Web Application並無區別。而且跟使用JWT Token比起來,使用Cookie配置更簡單。只是,有一點點知識你需要了解。

這篇博客是關於如何使用Cookie對Asp.Net Core Web Api進行安全驗證的,如果你想了解如何使用JWT Token,請參考 Secure a Web Api in ASP.NET Core and Refresh Tokens in ASP.NET Core Web Api

使Cookie能在Web Api中工作的一些必備配置

如果Web Api的客戶端是Web Application(如Angular app), 且Web Api和Web Application運行在不同的域名下(這是很常見的場景), 如果不做一些額外的配置,Cookie是無法工作的。

這或許就是人們默認使用JWT Token的原因吧。如果你嘗試着按傳統的Web Application(如Asp.Net MVC)那樣去配置驗證,然后通過AJAX請求進行登錄和注銷的話,很快你就會發現這根本行不通。

舉個例子,當你試圖通過JQuery登錄Web Api的時候

$.post('https://yourdomain.com/api/account/login', "username=theUsername&password=thePassword")

響應不顯示任何錯誤信息,如果你監視響應,它甚至包含Set-Cookie的Header, 但是Cookie好像被瀏覽器給忽略了。

更加讓人困惑的是,即便你正確的配置了CORS,情況依然如此。

結論就是你還需要在客戶端做一些額外的配置。接下來將會介紹你還需要做哪些工作,包括服務端和客戶端兩方面。

這里提供了一個示例項目,僅僅是一個默認模板的Asp.Net,用於驗證單一用戶賬號,並且剔除了所有的UI以便能夠做為Web Api被調用。項目中還包含一個Angular程序,用於調用Web Api

服務端的配置

服務端要做的工作是配置Asp.Net的Cookie驗證中間件和CORS, 以便你的Web Api“聲明”它接受來自客戶端所在域發來的請求。

要配置Cookie驗證中間件,你需要在Startup.cs的ConfigurateServices方法中配置驗證中間件。

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddAuthentication(options => { 
        options.DefaultScheme = "Cookies"; 
    }).AddCookie("Cookies", options => {
        options.Cookie.Name = "auth_cookie";
        options.Cookie.SameSite = SameSiteMode.None;
        options.Events = new CookieAuthenticationEvents
        {                          
            OnRedirectToLogin = redirectContext =>
            {
                redirectContext.HttpContext.Response.StatusCode = 401;
                return Task.CompletedTask;
            }
        };                
    });

在這里,我把Cookie驗證的schema命名為"Cookies"(也就是AddCookie方法的第一個參數)。后面我們實現登錄方法的時候需要用到這個名字。

我還把要創建的Cookie命名為auth_cookie(options.Cookie.Name = "auth_cookie")。如果你的Web Api的調用者是web客戶端(例如Angular程序),你不用管cookie的名字;但是如果調用者是用C#寫的客戶端,通過HttpClient來調用,你就需要手動讀取和保存cookie的值。一個顯式的命名比默認的名字—— .Asp.Net. + schema name(在這個例子中是 .Asp.Net.Cookies)——更容易記住。

至於SameSiteMode,我把它設置成了None。 SameSite在設置Cookie的時候會用到(它控制着Set-Cookie header中一個同名的屬性)。它的值既嚴格又寬松。嚴格指的是只有跟Cookie同域名的請求,瀏覽器才會發送cookie。寬松指的是瀏覽器只對同域的請求發送cookie,而跨域的語法不會有任何的副作用。

如果你像我一樣把它設置成SameSiteMode.None, set-cookie中不會包含samesite屬性,那么瀏覽器后續發送的所有請求都要攜帶cookie,這正是我們想要的。

插一句題外話,如果你想調試問題,盡量選擇FireFox的開發者工具,而不是Chrome的。因為如果不是同域的請求,Chrome不會顯示Set-Cookie頭。

最后我還重定義了當驗證失敗應該如何處理。通常Cookie中間件會給登錄頁返回302重定向, 因為我們是在構建一個Web Api, 我們需要給客戶端返回一個401未授權。這些是在自定義OnRedirectToLogin的時候要做的事情。

當使用Asp.Net Core Identity(示例項目中用的就是這個),配置有一點點不同。你不需要關心cookie shcema的名字,因為 ASP.NET Core Identity會給出一個默認值。此外,OnRedirectToLogin的重定義也有一點不同(很相似,看示例代碼就明白了)

之前說的是都是授權中間件的配置,此外,ConfigureServices方法中我們還要加CORS, 就一行代碼:

services.AddCors();

最后,在Startup.cs的 Configure方法里,給管道加上驗證和CORS中間件(在MVC管道前面)

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //...
    app.UseCors(policy =>
    {
        policy.AllowAnyHeader();
        policy.AllowAnyMethod();
        policy.AllowAnyOrigin();
        policy.AllowCredentials();
    });

    app.UseAuthentication();
    //...
    app.UseMvc();

我們的CORS配置未對潛在的客戶端作任何限制。需要強調的是AllowCredentials選項,沒有它,瀏覽器會忽略所有帶有cookie的請求的響應(參考MSDN關於CORS的文檔中的Access-Control-Allow-Credentials)。

登錄和注銷

登錄和注銷的實現方式,跟在MVC的情況很相似。唯一的不同之處在於不返回Content,只是返回一個狀態碼。

登錄方法的示例如下:

[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
    if (!IsValidUsernameAndPasswod(username, password))
        return BadRequest();

    var user = GetUserFromUsername(username);

    var claimsIdentity = new ClaimsIdentity(new[]
    {
        new Claim(ClaimTypes.Name, user.Username),
        //...
    }, "Cookies");

    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);

    return NoContent();
}

注意,這里的“Cookies"就是之前在Startup.cs中定義的schema的名字。

 

注銷方法的示例如下:

[HttpPost]
public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync();
    return NoContent();
}

我們的demo工程依賴ASP.NET Core Identity, 里面提供了UserManager和SignInManager兩個類, 這兩個類實現與上面描述的相同的功能。

 

客戶端

當使用瀏覽器作為客戶端去調用基於cookie的Web API時,你需要了解一些關於XMLHttpRequest或者Fetch API的知識;如果你的客戶端不是基於瀏覽器的(如c#程序), 你還需要知道如何保存、恢復cookie.

有兩種方式可以在瀏覽器里進行AJAX請求:XMLHttpRequest或者Fetch API。即便你使用的是一些框架或者庫(JQuery或者Angular),底層用的還是這二者中的一種。

當你執行請求時,如果你使用了某些選項,包含 Set-Cookie header的響應將會被忽略。當你要返回一個響應時,你需要把withCredentials標識設置為true才能保證該響應不會被忽略;當你要發送一個攜帶cookie的請求時,也需要設置withCredentials。總之,這個標識有兩種不同的用途。

你很可能不會手動使用XMLHttpRequest發送請求,我就不再提供關於它的使用方法的例子。下面分別提供關於JQuery、Angular和Fetch Api版本的例子。

JQuery版

當使用JQuery時,你可以通過如下方式設置withCredentials

$.ajax({
    url: 'http://yourdomain.com/api/account/login?username=theUsername&password=thePassword', 
    method: 'POST', 
    xhrFields: {
        withCredentials: true
    }
});

每次請求都要帶上withCredentials 標識

如果使用$.ajax來這么做的話,你很快就會覺得沉悶乏味。幸好你還可能通過$.ajaxSetup進行設置

$.ajaxSetup({xhrFields: {withCredentials: true}});

設置之后,接下來每個通過JQuery發送的請求($.get, $.post等)都會帶有withCredentials標識,並且設置為了true.

Angular

在Angular中,通過@angular/common/http中的HttpClient來發送請求。你可以通過如下方式來指定 withCredentials

this.httpClient.post<any>(`http://yourdomain.com/api/account/login?username=theUsername&password=thePassword`, {}, {
  withCredentials: true 
}).subscribe(....

為了更加方便,你可以創建一個 HttpInterceptor來給每個請求都加上withCredentials標識。

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class AddWithCredentialsInterceptorService implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req.clone({
            withCredentials: true
        }));
    }
}

Fetch Api

如果你使用的是更先進的 Fetch Api, 你需要給每個與cookie相關(請求攜帶cookie或者響應帶有Set-Cookie header)的請求添加credentials屬性,並把值設為 include.

下面是一個POST請求的例子

fetch('http://yourdomain.com/api/account/login?username=theUsername&password=thePassword', {
    method: 'POST',
    credentials: 'include'
}).then...

.Net 客戶

創建能發送請求的客戶端,你需要用到 HttpClient

HttpClient在收到響應后,會自己管理cookie; 並且當你發送請求時,會自動帶上cookie, 你只需要保證在后續的請求與登錄的請求使用的是同一個HttpClient實例即可。

下面是一個使用 HttpClient 例子

var client = new HttpClient();
var loginResponse = await client.PostAsync("http://yourdomain.com/api/account/login?username=theUsername&password=thePassword", null);
if (!loginResponse.IsSuccessStatusCode){
    //handle unsuccessful login
}                        

var response = await client.GetAsync("http://yourdomain.com/api/anEndpointThatRequiresASignedInUser/");

你可能還需要保存驗證的cookie,並且在隨后的某個時間點恢復它。

想象一下,用戶關閉了程序,在以后的某個時間又打開程序,你希望用戶不用再登錄。HttpClient是完全能夠實現這個功能的,但它的初始化的方式有點不同:

CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
    CookieContainer = cookieContainer
};
handler.CookieContainer = cookieContainer;
var client = new HttpClient(handler);

var loginResponse = await client.PostAsync("http://yourdomain.com/api/account/login?username=theUsername&password=thePassword", null);
if (!loginResponse.IsSuccessStatusCode){
    //handle unsuccessful login
}

var authCookie = cookieContainer.GetCookies(new Uri("http://yourdomain.com")).Cast<Cookie>().Single(cookie => cookie.Name == "auth_cookie");

//Save authCookie.ToString() somewhere
//authCookie.ToString() -> auth_cookie=CfDJ8J0_eoL4pK5Hq8bJZ8e1XIXFsDk7xDzvER3g70....

你可以通過調用CookieContainer的SetCookies方法來恢復cookie.

cookieContainer.SetCookies(new Uri("http://yourdomain.com"), "auth_cookie=CfDJ8J0_eoL4pK5Hq8bJZ8e1XIXFsDk7xDzvER3g70...");

結語

看完這個帖子,對你在Web Api中設置cookie可能會有所幫助。你只需要記住一些要點即可。即:你需要確保你產生的cookie中不帶有samesite屬性,通過檢查你的登錄方法的響應中的Set-Cookie header。

做這個檢查時,最好使用FireFox的開發者工具,而不是Chrome。對於跨域的請求,Chrome(至少我的j版本67.0.3396.99)不顯示Set-Cookie header

另一個需要記住的是:你正確的設置了CORS, 或者說核心是你的CORS策略中含有AllowsCredentials

最后,對於客戶端而言,要確保每次請求都帶上了withCredentials標識,對Fetch Api而言是帶上了credentials: 'include'

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM