• 实践剖析.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天,则自动重定向至登录页,最后将上述扩展事件进行注册即可

    总结

    暂无,下次再会!

    你所看到的并非事物本身,而是经过诠释后所赋予的意义
  • 相关阅读:
    CodeForces979D:Kuro and GCD and XOR and SUM(Trie树&指针&Xor)
    HDU4188:RealPhobia (连分数的运用之一)
    从HDU2588:GCD 到 HDU5514:Frogs (欧拉公式)
    SPOJ:Eagle and Dogs(求树上每个点最远可以走到哪里---树的直径||DP)
    【字符串】BZOJ上面几个AC自动机求最为字串出现次数的题目
    codeforces round #405 B. Bear and Friendship Condition
    codeforces round #419 C. Karen and Game
    codeforces round #419 B. Karen and Coffee
    codeforces round #419 A. Karen and Morning
    【ZOJ 3609】Modular Inverse 最小乘法逆元
  • 原文地址:https://www.cnblogs.com/CreateMyself/p/15755657.html
Copyright © 2020-2023  润新知