55

ASP.NET Core Web Api之JWT(一) - Jeffcky

 4 years ago
source link: https://www.cnblogs.com/CreateMyself/p/11123023.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近沉寂了一段,主要是上半年相当于休息和调整了一段时间,接下来我将开始陆续学习一些新的技术,比如Docker、Jenkins等,都会以生活实例从零开始讲解起,到时一并和大家分享和交流。接下来几节课的内容将会讲解JWT,关于JWT的原理解析等等园子里大有文章,就不再叙述,这里我们讲解使用和一些注意的地方。

为什么要使用JWT

在.NET Core之前对于Web应用程序跟踪用户登录状态最普通的方式则是使用Cookie,当用户点击登录后将对其信息进行加密并响应写入到用户浏览器的Cookie里,当用户进行请求时,服务端将对Cookie进行解密,然后创建用户身份,整个过程都是那么顺其自然,但是这是客户端是基于浏览器的情况,如果是客户端是移动app或者桌面应用程序呢?关于JWT原理可以参考系列文章https://www.cnblogs.com/RainingNight/p/jwtbearer-authentication-in-asp-net-core.html,当然这只是其中一种限制还有其他。如果我们使用Json Web Token简称为JWT而不是使用Cookie,此时Token将代表用户,同时我们不再依赖浏览器的内置机制来处理Cookie,我们仅仅只需要请求一个Token就好。这个时候就涉及到Token认证,那么什么是Token认证呢?一言以蔽之:将令牌(我们有时称为AccessToken或者是Bearer Token)附加到HTTP请求中并对其进行身份认证的过程。Token认证被广泛应用于移动端或SPA。

Json Web Token基础

JWT由三部分构成,Base64编码的Header,Base64编码的Payload,签名,三部分通过点隔开。第一部分以Base64编码的Header主要包括Token的类型和所使用的算法,例如:

{
"alg": "HS265",
"typ": "JWT"
}

第二部分以Base64编码的Payload主要包含的是声明(Claims),例如,如下:

{
    "sub": "765032130654732",
    "name": "jeffcky"
}

第三部分则是将Key通过对应的加密算法生成签名,最终三部分以点隔开,比如如下形式:

1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
2 eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSmVmZmNreSIsImVtYWlsIjoiMjc1MjE1NDg0NEBxcS5jb20iLCJleHAiOjE1NjU2MTUzOTgsIm5iZiI6MTU2MzE5NjE5OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIn0.
3 OJjlGJOnCCbpok05gOIgu5bwY8QYKfE2pOArtaZJbyI

到这里此时我们应该知道:JWT包含的信息并没有加密,比如为了获取Payload,我们大可通过比如谷歌控制台中的APi(atob)对其进行解码,如下:

那如我所说既然JWT包含的信息并没有加密,只是进行了Base64编码,岂不是非常不安全呢?当然不是这样,还没说完,第三部分就是签名,虽然我们对Payload(姑且翻译为有效负载),未进行加密,但是若有蓄意更换Payload,此时签名将能充分保证Token无效,除非将签名的Key不小心暴露在光天化日之下,否则必须是安全的。好了,到了这里,我们稍稍讲解了下JWT构成,接下来我们进入如何在.NET Core中使用JWT。

.NET Core中使用JWT

在.NET Core中如何使用JWT,那么我们必须得知晓如何创建JWT,接下来我们首先创建一个端口号为5000的APi,创建JWT,然后我们需要安装 System.IdentityModel.Tokens.Jwt 包,如下:

589642-20190715212242263-1055895515.png

我们直接给出代码来创建Token,然后一一对其进行详细解释,代码如下:

            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, "Jeffcky"),
                new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
                new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
            };

            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.AddHours(1),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            );

            var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

如上我们在声明集合中初始化声明时,我们使用了两种方式,一个是使用 ClaimTypes ,一个是 JwtRegisteredClaimNames ,那么这二者有什么区别?以及我们到底应该使用哪种方式更好?或者说两种方式都使用是否有问题呢?针对ClaimTypes则来自命名空间 System.Security.Claims ,而JwtRegisteredClaimNames则来自命名空间 System.IdentityModel.Tokens.Jwt ,二者在获取声明方式上是不同的,ClaimTypes是沿袭微软提供获取声明的方式,比如我们要在控制器Action方法上获取上述ClaimTypes.Name的值,此时我们需要F12查看Name的常量定义值是多少,如下:

589642-20190715221621470-972321835.png

接下来则是获取声明Name的值,如下:

 var sub = User.FindFirst(d => d.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;

589642-20190715221909592-917098992.png

那么如果我们想要获取声明JwtRegisterClaimNames.Sub的值,我们是不是应该如上同样去获取呢?我们来试试。

var sub = User.FindFirst(d => d.Type == JwtRegisteredClaimNames.Sub)?.Value;

此时我们发现为空没有获取到,这是为何呢?这是因为获取声明的方式默认是走微软定义的一套映射方式,如果我们想要走JWT映射声明,那么我们需要将默认映射方式给移除掉,在对应客户端Startup构造函数中,添加如下代码:

 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

589642-20190715222440355-1392368167.png

如果用过并熟悉IdentityServer4的童鞋关于这点早已明了,因为在IdentityServer4中映射声明比如用户Id即(sub)是使用的JWT,也就是说使用的JwtRegisteredClaimNames,此时我们再来获取Sub看看。

589642-20190715222739282-1956288627.png

所以以上对于初始化声明两种方式的探讨并没有用哪个更好,因为对于使用ClaimTypes是沿袭以往声明映射的方式,如果要出于兼容性考虑,可以结合两种声明映射方式来使用。接下来我们来看生成签名代码,生成签名是如下代码:

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

如上我们给出签名的Key是1234567890123456,是不是给定Key的任意长度皆可呢,显然不是,关于Key的长度至少是16,否则会抛出如下错误

589642-20190715223328594-37341950.png

接下来我们再来看实例化Token的参数,即如下代码:

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

issuer代表颁发Token的Web应用程序,audience是Token的受理者,如果是依赖第三方来创建Token,这两个参数肯定必须要指定,因为第三方本就不受信任,如此设置这两个参数后,我们可验证这两个参数。要是我们完全不关心这两个参数,可直接使用JwtSecurityToken的构造函数来创建Token,如下:

            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, "Jeffcky"),
                new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
                new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
                new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMilliseconds(1)).ToUnixTimeSeconds()}"),
                new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}")
            };

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

            var jwtToken = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));

这里需要注意的是Exp和Nbf是基于Unix时间的字符串,所以上述通过实例化DateTimeOffset来创建基于Unix的时间。到了这里,我们已经清楚的知道如何创建Token,接下来我们来使用Token获取数据。我们新建一个端口号为5001的Web应用程序,同时安装包【 Microsoft.AspNetCore.Authentication.JwtBearer 】接下来在Startup中ConfigureServices添加如下代码:

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .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)
                };
            });

如上述若Token依赖于第三方而创建,此时必然会配置issuer和audience,同时在我方也如上必须验证issuer和audience,上述我们也验证了签名,我们通过设置 ValidateLifetime  为true,说明验证过期时间而并非Token中的值,最后设置 ClockSkew  有效期为5分钟。对于设置 ClockSkew  除了如上方式外,还可如下设置默认也是5分钟。

 ClockSkew = TimeSpan.Zero

如上对于认证方案我们使用的是 JwtBearerDefaults.AuthenticationScheme 即Bearer,除此之外我们也可以自定义认证方案名称,如下:

589642-20190715231947071-1553777114.png

最后别忘记添加认证中间件在Configure方法中,认证中间件必须放在使用MVC中间件之前,如下:

            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

到了这里,我们通过端口为5000的Web Api创建了Token,并配置了端口号为5001的Web应用程序使用JWT认证,接下来最后一步则是调用端口号为5000的APi获取Token,并将Token设置到请求头中Authorization键的值,格式如下(注意Bearer后面有一个空格):

('Authorization', 'Bearer ' + token);

我们在页面上放置一个按钮点击获取端口号为5000的Token后,接下来请求端口号为5001的应用程序,如下:

    $(function () {
        $('#btn').click(function () {
            $.get("http://localhost:5000/api/token").done(function (token) {
                $.ajax({
                    type: 'get',
                    contentType: 'application/json',
                    url: 'http://localhost:5001/api/home',
                    beforeSend: function (xhr) {
                        if (token !== null) {
                            xhr.setRequestHeader('Authorization', 'Bearer ' + token);
                        }
                    },
                    success: function (data) {
                        alert(data);
                    },
                    error: function (xhr) {
                        alert(xhr.status);
                    }
                });
            });
        });
    });

589642-20190715230922215-2069769217.png

589642-20190715231248170-2118911205.png

本节我们讲解了在.NET Core中使用JWT进行认证以及一点点注意事项,比较基础性的东西,下一节讲解完在JWT中使用刷新Token,开始正式进入Docker系列,感谢阅读,下节见。

scan.gif

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

感谢花时间阅读此篇文章,如果您觉得这篇文章你学到了东西也是为了犒劳下博主的码字不易不妨打赏一下吧,让楼主能喝上一杯咖啡,在此谢过了!
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!
本文版权归作者和博客园共有,来源网址:http://www.cnblogs.com/CreateMyself)/欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK