实践剖析.NET Core如何支持Cookie和JWT混合认证、授权

前言

为防止JWT Token被窃取,我们将Token置于Cookie中,但若与第三方对接,调用我方接口进行认证、授权此时仍需将Token置于请求头,通过实践并联系理论,我们继续开始整活!首先我们实现Cookie认证,然后再次引入JWT,最后在结合二者使用时联系其他我们可能需要注意的事项

Cookie认证

在startup中我们添加cookie认证服务,如下:

复制代码
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
    options.Cookie.Name = "user-session";
    options.SlidingExpiration = true;
});
复制代码

接下来则是使用认证和授权中间件,注意将其置于路由和终结点终结点之间,否则启动也会有明确异常提示

复制代码
app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
  ......
});
复制代码

我们给出测试视图页,并要求认证即控制器添加特性

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

当进入首页,未认证默认进入account/login,那么接下来创建该视图

复制代码
public class AccountController : Controller
{
    [AllowAnonymous]
    public IActionResult Login()
    {
      return View();
    }
    ......
}
复制代码

我们启动程序先看看效果

如上图,自动跳转至登录页,此时我们点击模拟登录按钮,发起请求去模拟登录(发起ajax请求代码就不占用篇幅了)

复制代码
/// <summary>
/// 模拟登录
/// </summary>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> TestLogin()
{
    var claims = new Claim[]
    {
      new Claim(ClaimTypes.Name, "Jeffcky"),
    };

    var claimsIdentity = new ClaimsIdentity(claims, "Login");

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

    return Ok();
}
复制代码

上述无非就是构建身份以及该身份下所具有的身份属性,类似个人身份证唯一标识个人,身份证上各个信息即表示如上声明,同时呢,肯定要调用上下文去登录,在整个会话未过期之前,根据认证方案获取对应处理方式,最后将相关信息进行存储等等,有兴趣的童鞋可以去了解其实现细节哈

当我们请求过后,再次访问首页,将看到生成当前会话信息,同时我们将会话过期设置为1分钟,在1分钟内未进行会话,将自动重定向至登录页,注意如上标注并没有值,那么这个值可以设置吗?当然可以,在开始配置时我们并未给出,那么这个属性又代表什么含义呢?

options.Cookie.MaxAge = TimeSpan.FromMinutes(2);

那么结合ExpireTimeSpan和MaxAge使用,到底代表什么意思呢?我们暂且撇开滑动过期设置

 

ExpireTimeSpan表示用户身份认证票据的生命周期,它是认证cookie的有效负载,存储的cookie值是一段加密字符串,在每次请求时,web应用程序都会根据请求对其进行解密

 

MaxAge控制着cookie的生命周期,若cookie过期,浏览器将会自动清除,如果没有设置该值,实质上它的生命周期就是ExpireTimeSpan,那么它到底有何意义呢?

 

上述我们设置票据的生命周期为1分钟,同时我们控制cookie的生命周期为2分钟,若在2分钟内关闭浏览器或重启web应用程序,此时cookie生命周期并未过期,所以仍将处于会话状态即无需登录,若未设置MaxAge,关闭浏览器或重启后将自动清除其值即需登录,当然一切前提是未手动清除浏览器cookie

 

问题又来了,在配置cookie选项中,还有一个也可以设置过期的属性

options.Cookie.Expiration = TimeSpan.FromMinutes(3);

当配置ExpireTimeSpan或同时配置MaxAge时,无需设置Expiration,因为会抛出异常

JWT认证

上述已经实现Cookie认证,那么在与第三方进行对接时,我们要使用JWT认证,我们又该如何处理呢?首先我们添加JWT认证服务

复制代码
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")),
      ValidateIssuer = true,
      ValidIssuer = "http://localhost:5000",
      ValidateAudience = true,
      ValidAudience = "http://localhost:5001",
      ValidateLifetime = true,
      ClockSkew = TimeSpan.FromMinutes(5)
    };
});
复制代码

将JWT Token置于cookie中,此前文章已有讲解,这里我们直接给出代码,先生成Token

复制代码
private string GenerateToken(Claim[] claims)
{
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));

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

    return new JwtSecurityTokenHandler().WriteToken(token);
}
复制代码

在登录方法中,将其写入响应cookie中,如下这般

复制代码
/// <summary>
/// 模拟登录
/// </summary>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> TestLogin()
{
    var claims = new Claim[]
    {
      new Claim(ClaimTypes.Name, "Jeffcky"),
    };

    var claimsIdentity = new ClaimsIdentity(claims, "Login");

    Response.Cookies.Append("x-access-token", GenerateToken(claims),
      new CookieOptions()
      {
        Path = "/",
        HttpOnly = true
      });

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

 return Ok();
}
复制代码

那么JWT是如何验证Token的呢?默认是从请求去取Bearer Token值,若成功取到这赋值给如下context.Token,所以此时我们需要手动从cookie中取出token并赋值

复制代码
options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var accessToken = context.Request.Cookies["x-access-token"];

        if (!string.IsNullOrEmpty(accessToken))
        {
            context.Token = accessToken;
        }

        return Task.CompletedTask;
    }
};
复制代码

一切已就绪,接下来我们写个api接口测试验证看看

复制代码
[Authorize("Bearer")]
[Route("api/[controller]/[action]")]
[ApiController]
public class JwtController : ControllerBase
{
    [HttpGet]
    public IActionResult Test()
    {
      return Ok("test jwt");
    }
}
复制代码

思考一下,我们通过Postman模拟测试,会返回401吗?结果会是怎样的呢?

问题不大,主要在于该特性参数为声明指定策略,但我们需要指定认证方案即scheme,修改成如下:

如此在与第三方对接时,请求返回token,后续将token置于请求头中即可验证通过,同时上述取cookie中token并手动赋值,对于对接第三方则是多余,不过是为了诸多其他原因而已

[Authorize(AuthenticationSchemes = "Bearer,Cookies")]

注意混合认证方案设置存在顺序,后者将覆盖前者即如上设置,此时将走cookie认证

滑动过期思考扩展

若我们实现基于Cookie滑动过期,同时使用signalr进行数据推送,势必存在问题,因为会一直刷新会话,那么将导致会话永不过期问题,从安全层面角度考虑,我们该如何处理呢?

 

我们知道票据生命周期存储在上下文AuthenticationProperties属性中,所以在配置Cookie选项事件中我们可以进行自定义处理

复制代码
public class CookieAuthenticationEventsExetensions : CookieAuthenticationEvents
{
    private const string TicketIssuedTicks = nameof(TicketIssuedTicks);

    public override async Task SigningIn(CookieSigningInContext context)
    {
        context.Properties.SetString(
          TicketIssuedTicks,
          DateTimeOffset.UtcNow.Ticks.ToString());

        await base.SigningIn(context);
    }

    public override async Task ValidatePrincipal(
      CookieValidatePrincipalContext context)
    {
        var ticketIssuedTicksValue = context
          .Properties.GetString(TicketIssuedTicks);

        if (ticketIssuedTicksValue is null ||
          !long.TryParse(ticketIssuedTicksValue, out var ticketIssuedTicks))
        {
          await RejectPrincipalAsync(context);
          return;
        }

        var ticketIssuedUtc =
          new DateTimeOffset(ticketIssuedTicks, TimeSpan.FromHours(0));

        if (DateTimeOffset.UtcNow - ticketIssuedUtc > TimeSpan.FromDays(3))
        {
          await RejectPrincipalAsync(context);
          return;
        }

        await base.ValidatePrincipal(context);
    }

    private static async Task RejectPrincipalAsync(
      CookieValidatePrincipalContext context)
    {
        context.RejectPrincipal();
        await context.HttpContext.SignOutAsync();
    }
}
复制代码

在添加Cookie服务时,有对应事件选项,使用如下

 options.EventsType = typeof(CookieAuthenticationEventsExetensions);

扩展事件实现表示在第一次会话到当前时间截止超过3天,则自动重定向至登录页,最后将上述扩展事件进行注册即可

更新(2022-01-07)

看到评论中有园友提出疑问,其实本文已有大致说明,本文只是从开头到最后做了演化,未详细说明,这里做明确澄清!毫无疑问,NET Core本身支持混合认证方式,从其特性为复数也可猜测得知!文中之所以将token放在cookie中,是由于最开始未实现cookie认证,完全使用JWT以及项目上种种原因导致,所以在最初就实现混合认证后,将token放在cookie中完全没必要

[Authorize(AuthenticationSchemes = "Bearer,Cookies")]

平台使用Cookie认证,无需指定认证方案,因为底层将直接获取配置的Cookie认证方案处理方式

[Authorize]

若与第三方对接使用JWT认证,将指定对接控制器明确指定使用JWT认证即Bearer Token

[Authorize(AuthenticationSchemes = "Bearer")]

因为在认证授权时,底层会通过配置的认证方案,然后获取其认证处理方式即AuthenticationHandler....,在写此文时,大致看了下源码实现,源码类名貌似是以此前缀开头。

总结

暂无,下次再会!


为了方便大家在移动端也能看到我分享的博文,现已注册个人公众号,扫描上方左边二维码即可,欢迎大家关注,有时间会及时分享相关技术博文。

感谢花时间阅读此篇文章,如果您觉得这篇文章你学到了东西也是为了犒劳下博主的码字不易不妨打赏一下吧,让楼主能喝上一杯咖啡,在此谢过了!
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!
本文版权归作者和博客园共有,来源网址:http://www.cnblogs.com/CreateMyself)/欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
posted @   Jeffcky  阅读(5720)  评论(14编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2020-01-06 Spring MVC系列之Hello World(SpringBoot)(六)
2020-01-06 SpringBoot系列之注解@Autowired VS @Qualifier VS @Primary(五)
2017-01-06 EntityFramework Core Raw SQL
点击右上角即可分享
微信分享提示