ASP.NET Core Web Api之JWT VS Session VS Cookie(二)

简介: ASP.NET Core Web Api之JWT VS Session VS Cookie(二)前言本文我们来探讨下JWT VS Session的问题,这个问题本没有过多的去思考,看到评论讨论太激烈,就花了一点时间去研究和总结,顺便说一句,这就是写博客的好处,一篇博客写出有的可能是经验积累,有的可能是学习分享,但都逃不过看到文章的你有更多或更好的想法,往返交流自身能收获更多,何乐而不为呢?希望本文能解惑或者能得到更多的交流。

ASP.NET Core Web Api之JWT VS Session VS Cookie(二)
前言
本文我们来探讨下JWT VS Session的问题,这个问题本没有过多的去思考,看到评论讨论太激烈,就花了一点时间去研究和总结,顺便说一句,这就是写博客的好处,一篇博客写出有的可能是经验积累,有的可能是学习分享,但都逃不过看到文章的你有更多或更好的想法,往返交流自身能收获更多,何乐而不为呢?希望本文能解惑或者能得到更多的交流。我们可直接抛出问题:使用客户端存储的JWT比服务端维持Session更好吗?

基于JWT和Session认证共同点
既然要比较JWT VS Session,那我们就得知道为何需要JWT和Session,它们共同是为了解决什么问题呢?那我们从一个场景说起,网上购物现已是再平常不过的事情了,当我们将某个商品加入购物车后,然后跳转到其他商品页面此时需要之前选择的商品依然在购物车中,此时就需要维持会话,因为HTTP无状态,所以JWT和Session共同点都是为了持久维持会话而存在,为了克服HTTP无状态的情况,JWT和Session分别是如何处理的呢?

JWT VS Session认证
Session:当用户在应用系统中登录后,此时服务端会创建一个Session(我们也称作为会话),然后SessionId会保存到用户的Cookie中,只要用户是登录状态,对于每个请求,在Cookie中的SessionId都会发送到服务端,然后服务端会将保存在内存中的SessionId和Cookie中的SessionId进行比较来认证用户的身份并响应。

JWT:当用户在应用系统中登录后,此时服务端会创建一个JWT,并将JWT发送到客户端,客户端存储JWT(一般是在Local Storage中)同时在每个请求头即Authorization中包含JWT,对于每个请求,服务端都会进行验证JWT是否合法,直接在服务端本地进行验证,比如颁发者,受理者等等,以致于无需发出网络请求或与数据库交互,这种方式可能比使用Session更快,从而加快响应性能,降低服务器和数据库服务器负载。

通过如上对JWT认证和Session认证简短的描述,我们知道二者最大的不同在于Session是存储在服务端,而JWT存储在客户端。服务端存储会话无外乎两种,一种是将会话标识符存储在数据库,一种是存储在内存中维持会话,我想大多数情况下都是基于内存来维持会话,但是这会带来一定的问题,如果系统存在大流量,也就是说若有大量用户访问系统,此时使用基于内存维持的会话则限制了水平扩展,但对基于Token的认证则不存在这样的问题,同时Cookie一般也只适用于单域或子域,如果对于跨域,假如是第三方Cookie,浏览器可能会禁用Cookie,所以也受浏览器限制,但对Token认证来说不是问题,因为其保存在请求头中。

如果我们将会话转移到客户端,也就是说使用Token认证,此时将解除会话对服务端的依赖,同时也可水平扩展,不受浏览器限制,但是与此同时也会带来一定的问题,一是令牌的传输安全性,对于令牌传输安全性我们可使用HTTPS加密通道来解决,二是与存储在Cookie中的SessionId相比,JWT显然要大很多,因为JWT中还包含用户信息,所以为了解决这个问题,我们尽量确保JWT中只包含必要的信息(大多数情况下只包含sub以及其他重要信息),对于敏感信息我们也应该省略掉从而防止XSS攻击。JWT的核心在于声明,声明在JWT中是JSON数据,也就是说我们可以在JWT中嵌入用户信息,从而减少数据库负载。所以综上所述JWT解决了其他会话存在的问题或缺点:

更灵活

更安全

减少数据库往返,从而实现水平可伸缩。

防篡改客户端声明

移动设备上能更好工作

适用于阻止Cookie的用户

综上关于JWT在有效期内没有强制使其无效的能力而完全否定JWT的好处显然站不住脚,当然不可辩驳的是若是没有如上诸多使用限制,实现其他类型的身份验证完全也是合情合理且合法的,需综合权衡,而非一家之言下死结论。到目前为止,我们一直讨论的是JWT VS Session认证,而不是JWT VS Cookie认证,但是如标题我们将Cookie也纳入了,只是想让学习者别搞混了,因为JWT VS Cookie认证这种说法是错误的,Cookie只是一种存储和传输信息介质,只能说我们可以通过Cookie存储和传输JWT。接下来我们来实现Cookie存储和传输JWT令牌。

JWT AS Cookies Identity Claim
在Startup中我们可以添加如下Cookie认证中间件,此时我们有必要了解下配置Cookie的一些选项,通过对这些选项的配置来告知Cookie身份认证中间件在浏览器中的表现形式,我们看下几个涉及到安全的选项。

复制代码

       services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
       .AddCookie(options =>
       {
           options.LoginPath = "/Account/Login";
           options.LogoutPath = "/Account/Logout";
           options.Cookie.Expiration = TimeSpan.FromMinutes(5);
           options.Cookie.HttpOnly = true;
           options.Cookie.SecurePolicy = CookieSecurePolicy.None;
           options.Cookie.SameSite = SameSiteMode.Lax;
       });

复制代码
配置HttpOnly标志着Cookie是否仅供服务端使用,而不能通过前端直接访问。

配置SecurePolicy将限制Cookie为HTTPS,在生产环境建议配置此参数同时支持HTTPS。

配置SameSite用来指示浏览器是否可以将Cookie与跨站点请求一同使用,若是对于OAuth身份认证,可设置为Lax,允许外部链接重定向发出比如POST请求而维持会话,若是Cookie认证,设置为Restrict,因为Cookie认证只适用于单站点,若是设置为None,则不会设置Cookie Header值。(注意:SameSite属性在谷歌、火狐浏览器均已实现,对于IE11好像不支持,Safari从版本12.1开始支持该属性)

在创建.NET Core默认Web应用程序时,在ConfigureServices方法中,通过中间件直接配置了全局Cookie策略,如下:

        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

当然默认配置了全局Cookie策略,同时也在Configure方法中使用其策略如下:

        app.UseCookiePolicy();

我们也可以直接在上述调用使用Cookie策略中间件的方法中来设置对应参数策略,如下:

若是我们在添加Cookie中间件的同时也配置全局Cookie策略,我们会发现对于属性HTTPOnly和SameSite都可配置,此时个人猜测会存在覆盖的情况,如下:

对于需要认证的控制器我们需要添加上[Authroize]特性,对每一个控制器我们都得添加这样一个特性,相信大部分童鞋都是这么干的。其实我们大可反向操作,对于无需认证的我们添加可匿名访问特性即可,而需要认证的控制器我们进行全局配置认证过滤器,如下:

services.AddMvc(options=> options.Filters.Add(new AuthorizeFilter()))

            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

好了到了这里,我们只是粗略的讲解了下关于Cookie中间件参数配置和Cookie全局配置策略的说明,没有太深入去研究里面的细枝末节,等遇到问题再具体分析吧。继续回到话题,Cookie认证相比JWT对API访问来讲安全系数低,所以我们完全可以在Cookie认证中结合JWT来使用。具体我们可尝试怎么搞呢?将其放到身份信息声明中,我想应该是可行的方式,我们来模拟登陆和登出试试,大概代码如下:

复制代码

public class AccountController : Controller
{
    /// <summary>
    /// 登录
    /// </summary>
    /// <returns></returns>
    [HttpPost]
    public async Task<IActionResult> Login()
    {            
        var claims = new Claim[]
        {
            new Claim(ClaimTypes.Name, "Jeffcky"),
            new Claim(JwtRegisteredClaimNames.Email, "2752154844@qq.com"),
            new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
            new Claim("access_token", GenerateAccessToken()),
        };

        var claimsIdentity = new ClaimsIdentity(
            claims, CookieAuthenticationDefaults.AuthenticationScheme);

        var authticationProperties = new AuthenticationProperties();

        await HttpContext.SignInAsync(
          CookieAuthenticationDefaults.AuthenticationScheme,
          new ClaimsPrincipal(claimsIdentity),
          authticationProperties);

        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

    string GenerateAccessToken()
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));

        var token = new JwtSecurityToken(
            issuer: "http://localhost:5000",
            audience: "http://localhost:5001",
            notBefore: DateTime.Now,
            expires: DateTime.Now.AddHours(1),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    /// <summary>
    /// 退出
    /// </summary>
    /// <returns></returns>
    [Authorize]
    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

}

复制代码
上述代码很简单,无需我再多讲,和Cookie认证无异,只是我们在声明中添加了access_token来提高安全性,接下来我们自定义一个Action过滤器特性,并将此特性应用于Action方法,如下:

复制代码

public class AccessTokenActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var principal = context.HttpContext.User as ClaimsPrincipal;

        var accessTokenClaim = principal?.Claims
          .FirstOrDefault(c => c.Type == "access_token");

        if (accessTokenClaim is null || string.IsNullOrEmpty(accessTokenClaim.Value))
        {
            context.HttpContext.Response.Redirect("/account/login", permanent: true);

            return;
        }

        var sharedKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));

        var validationParameters = new TokenValidationParameters
        {
            ValidateAudience = true,
            ValidIssuer = "http://localhost:5000",
            ValidAudiences = new[] { "http://localhost:5001" },
            IssuerSigningKeys = new[] { sharedKey }
        };

        var accessToken = accessTokenClaim.Value;

        var handler = new JwtSecurityTokenHandler();

        var user = (ClaimsPrincipal)null;

        try
        {
            user = handler.ValidateToken(accessToken, validationParameters, out SecurityToken validatedToken);
        }
        catch (SecurityTokenValidationException exception)
        {
            throw new Exception($"Token failed validation: {exception.Message}");
        }

        base.OnActionExecuting(context);
    }
}

复制代码
JWT Combine Cookie Authentication
如上是采用将JWT放到声明的做法,我想这么做也未尝不可,至少我没找到这么做有什么不妥当的地方。我们也可以将Cookie认证和JWT认证进行混合使用,只不过是在上一节的基础上添加了Cookie中间件罢了,如下图:

通过如上配置后我们就可以将Cookie和JWT认证来组合使用了,比如我们在用户登录后,如下图点击登录后显示当前登录用户名,然后点击退出,在退出Action方法上我们添加组合特性:

复制代码

    /// <summary>
    /// 退出
    /// </summary>
    /// <returns></returns>
    [Authorize(AuthenticationSchemes = "Bearer,Cookies")]
    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

复制代码

在上一节中,我们通过获取AccessToken,从而访问端口号为5001的客户端来获取当前时间,那现在我们针对获取当前时间的方法添加上需要Cookie认证,如下:

复制代码

    [Authorize(CookieAuthenticationDefaults.AuthenticationScheme)]
    [HttpGet("api/[controller]")]
    public string GetCurrentTime()
    {
        var sub = User.FindFirst(d => d.Type == JwtRegisteredClaimNames.Sub)?.Value;

        return DateTime.Now.ToString("yyyy-MM-dd");
    }

复制代码

Cookie认证撤销
在.NET Core 2.1版本通过Cookie进行认证中,当用户与应用程序进行交互修改了信息,需要在cookie的整个生命周期,也就说在注销或cookie过期之前看不到信息的更改时,我们可通过cookie的身份认证事件【撤销身份】来实现这样的需求,下面我们来看看。

复制代码

public class RevokeCookieAuthenticationEvents : CookieAuthenticationEvents
{
    private readonly IDistributedCache _cache;

    public RevokeCookieAuthenticationEvents(
      IDistributedCache cache)
    {
        _cache = cache;
    }

    public override Task ValidatePrincipal(
      CookieValidatePrincipalContext context)
    {
        var userId = context.Principal?.Claims
        .First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;

        if (!string.IsNullOrEmpty(_cache.GetString("revoke-" + userId)))
        {
            context.RejectPrincipal();

            _cache.Remove("revoke-" + userId);
        }

        return Task.CompletedTask;
    }
}

复制代码
我们通过重写CookieAuthenticationEvents事件中的ValidatePrincipal,然后判断写在内存中关于用户表示是否存在,若存在则调用 context.RejectPrincipal() 撤销用户身份。然后我们在添加Cookie中间件里配置该事件类型以及对其进行注册:

services.AddScoped();
接下来我们写一个在页面上点击【修改信息】的方法,并在内存中设置撤销指定用户,如下:

复制代码

    [HttpPost]
    public IActionResult ModifyInformation()
    {
        var principal = HttpContext?.User as ClaimsPrincipal;

        var userId = principal?.Claims
          .First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;

        if (!string.IsNullOrEmpty(userId))
        {
            _cache.SetString("revoke-" + userId, userId);
        }
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

复制代码

从如上动图中我们可以看到,当点击修改信息后,然后将撤销的用户标识写入到内存中,然后跳转到Index页面,此时调用我们写的撤销事件,最终重定向到登录页,且此时用户cookie仍未过期,所以我们能够在左上角看到用户名,不清楚这种场景在什么情况下才会用到。

重定向至登录携带或移除参数
当我们在某个页面进行操作时,若此时Token或Cookie过期了,此时则会自动引导用户且将用户当前访问的URL携带并重定向跳转到登录页进行登录,比如关于博客园如下跳转URL:

https://account.cnblogs.com/signin?returnUrl=http%3a%2f%2fi.cnblogs.com%2f
但是如果我们有这样的业务场景:用于跳转至登录页时,在URL上需要携带额外的参数,我们需要获取此业务参数才能进行对应业务处理,那么此时我们应该如何做呢?我们依然是重写CookieAuthenticationEvents事件中的RedrectToLogin方法,如下:

复制代码

public class RedirectToLoginCookieAuthenticationEvents : CookieAuthenticationEvents
{
    private IUrlHelperFactory _helper;
    private IActionContextAccessor _accessor;
    public RedirectToLoginCookieAuthenticationEvents(IUrlHelperFactory helper,
        IActionContextAccessor accessor)
    {
        _helper = helper;
        _accessor = accessor;
    }

    public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
    {
        //获取路由数据
        var routeData = context.Request.HttpContext.GetRouteData();

        //获取路由数据中的路由值
        var routeValues = routeData.Values;

        var uri = new Uri(context.RedirectUri);

        //解析跳转URL查询参数
        var returnUrl = HttpUtility.ParseQueryString(uri.Query)[context.Options.ReturnUrlParameter];

        //add extra parameters for redirect to login
        var parameters = $"id={Guid.NewGuid().ToString()}";

        //添加额外参数到路由值中
        routeValues.Add(context.Options.ReturnUrlParameter, $"{returnUrl}{parameters}");

        var urlHelper = _helper.GetUrlHelper(_accessor.ActionContext);

        context.RedirectUri = UrlHelperExtensions.Action(urlHelper, "Login", "Account", routeValues);

        return base.RedirectToLogin(context);
    }
}

复制代码
这里需要注意的是因为上述我们用到了IActionContextAccessor,所以我们需要将其进行对应如下注册:

services.AddSingleton();
最终我们跳转到登录页将会看到我们添加的额外参数id也将呈现在url上,如下:

http://localhost:5001/Account/Login?ReturnUrl=%2FAccount%2FGetCurrentTime%3Fid%3Da309f451-e2ff-4496-bf18-65ba5c3ace9f
总结
本节我们讲解了Session和JWT的优缺点以及Cookie认证中可能我们需要用到的地方,下一节也是JWT最后一节内容,我们讲讲并探讨如何实现刷新Token,感谢阅读。
原文地址https://www.cnblogs.com/CreateMyself/p/11197497.html

相关文章
|
9月前
|
人工智能 搜索推荐 API
自学记录鸿蒙API 13:实现人脸比对Core Vision Face Comparator
在完成文本识别和人脸检测项目后,我深入学习了HarmonyOS Next API 13中的Core Vision Face Comparator API,开发了一个简单的人脸比对工具。该API能进行高精度人脸比对并给出相似度评分,应用场景广泛,如照片分类、身份认证、个性化服务等。通过初始化服务、加载图片、实现比对功能和构建用户界面,最终实现了可靠的人脸比对功能。未来计划将此技术应用于更复杂的场景,如照片管理和个性化服务,并探索与其他AI能力的结合。如果你也对人脸比对感兴趣,不妨从简单的比对功能开始,逐步实现自己的创意!
242 61
|
9月前
|
人工智能 自然语言处理 文字识别
自学记录鸿蒙API 13:实现智能文本识别Core Vision Text Recognition
在完成语音助手项目后,我尝试了HarmonyOS Next API 13中的Core Vision Text Recognition API,体验其强大的文本识别功能。该API支持多语言高精度识别,能快速将图像中的文本提取为结构化信息,适用于文档扫描、票据管理和实时翻译等场景。通过权限配置、初始化服务、实现识别功能和构建用户界面,我完成了文本识别应用的开发,并探索了性能优化与功能扩展。鸿蒙生态的强大支持让开发者能更便捷地实现复杂功能。未来计划将此技术应用于实际项目,如票据管理或实时翻译工具。如果你也对文本识别感兴趣,不妨一起探索!
181 11
|
9月前
|
人工智能 监控 安全
自学记录鸿蒙 API 13:骨骼点检测应用Core Vision Skeleton Detection
骨骼点检测技术能够从图片中识别出人体的关键骨骼点位置,如头部、肩部、手肘等,广泛应用于运动健身指导、游戏交互、医疗辅助、安全监控等领域。我决定深入学习HarmonyOS Next API 13中的Skeleton Detection API,并开发一个简单的骨骼点检测应用。通过理解API核心功能、项目初始化与配置、实现检测功能、构建用户界面,以及性能优化和功能扩展,逐步实现这一技术的应用。未来计划将其应用于健身指导和智能监控领域,探索与其他AI能力的结合,开发更智能的解决方案。如果你也对骨骼点检测感兴趣,不妨一起进步!
319 9
|
10月前
|
开发框架 .NET 程序员
驾驭Autofac,ASP.NET WebApi实现依赖注入详细步骤总结
Autofac 是一个轻量级的依赖注入框架,专门为 .NET 应用程序量身定做,它就像是你代码中的 "魔法师",用它来管理对象的生命周期,让你的代码更加模块化、易于测试和维护
372 4
驾驭Autofac,ASP.NET WebApi实现依赖注入详细步骤总结
|
9月前
|
人工智能 监控 安全
自学记录鸿蒙 API 13:实现人脸检测 Core Vision Face Detector
本文介绍了基于HarmonyOS Next API 13中的Core Vision Face Detector API实现人脸检测小应用的过程。通过研究发现,该API不仅支持人脸检测框的定位,还能识别关键点(如眼睛、鼻子和嘴角位置)及人脸姿态信息。文章详细记录了开发历程,包括项目初始化、权限配置、图像加载与人脸检测、用户界面设计,以及性能优化和功能扩展的思路。应用场景涵盖身份验证、照片管理和实时交互等。未来计划将技术应用于智能照片管理工具,提供更高效的照片分类体验。欢迎对人脸检测技术感兴趣的读者一起探讨和进步。
330 7
|
存储 API 数据库
如何使用 ef core 的 code first(fluent api)模式实现自定义类型转换器?
本文介绍了如何在 EF Core 的 Code First 模式下使用自定义类型转换器实现 JsonDocument 和 DateTime 类型到 SQLite 数据库的正确映射。通过自定义 ValueConverter,实现了数据类型的转换,并展示了完整的项目结构和代码实现,包括实体类定义、DbContext 配置、Repositories 仓储模式及数据库应用迁移(Migrations)操作。
238 7
如何使用 ef core 的 code first(fluent api)模式实现自定义类型转换器?
|
12月前
|
开发框架 监控 前端开发
在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
【9月更文挑战第27天】操作筛选器是ASP.NET Core MVC和Web API中的一种过滤器,可在操作方法执行前后运行代码,适用于日志记录、性能监控和验证等场景。通过实现`IActionFilter`接口的`OnActionExecuting`和`OnActionExecuted`方法,可以统一处理日志、验证及异常。创建并注册自定义筛选器类,能提升代码的可维护性和复用性。
176 3
|
UED 存储 自然语言处理
【语言无界·体验无疆】解锁Vaadin应用全球化秘籍:从代码到文化,让你的应用畅游世界每一个角落!
【8月更文挑战第31天】《国际化与本地化实战:构建多语言支持的Vaadin应用》详细介绍了如何使用Vaadin框架实现应用的国际化和本地化,提升用户体验和市场竞争力。文章涵盖资源文件的创建与管理、消息绑定与动态加载、日期和数字格式化及文化敏感性处理等方面,通过具体示例代码和最佳实践,帮助开发者构建适应不同语言和地区设置的Vaadin应用。通过这些步骤,您的应用将更加灵活,满足全球用户需求。
164 0
|
1月前
|
JSON API 数据格式
淘宝/天猫图片搜索API接口,json返回数据。
淘宝/天猫平台虽未开放直接的图片搜索API,但可通过阿里妈妈淘宝联盟或天猫开放平台接口实现类似功能。本文提供基于淘宝联盟的图片关联商品搜索Curl示例及JSON响应说明,适用于已获权限的开发者。如需更高精度搜索,可选用阿里云视觉智能API。
|
1月前
|
JSON API 数据安全/隐私保护
深度分析淘宝卖家订单详情API接口,用json返回数据
淘宝卖家订单详情API(taobao.trade.fullinfo.get)是淘宝开放平台提供的重要接口,用于获取单个订单的完整信息,包括订单状态、买家信息、商品明细、支付与物流信息等,支撑订单管理、ERP对接及售后处理。需通过appkey、appsecret和session认证,并遵守调用频率与数据权限限制。本文详解其使用方法并附Python调用示例。