• 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'

  • 相关阅读:
    OS X进程管理之launchctl
    varnish-4.x 之varnishlog,varnishstat,varnishtop,varnishhist
    Java ClassLoader详解(转载)
    java线程中断[interrupt()函数] (转载)
    Struts2实例详解(转载)
    java 重定向和转发(转载)
    Java中Class.forName()的作用(转载)
    JSP页面的编码设置(转载)
    Struts2 实例(转载)
    Spring AOP(转载)
  • 原文地址:https://www.cnblogs.com/xclw/p/12986458.html
Copyright © 2020-2023  润新知