重大更新:

这篇文章还有一个补充篇,请看完这篇后,再看这个《从壹开始前后端分离[.netCore 不定期 ] 36 ║解决JWT权限验证过期问题》,

两个是上下衔接的,主要是解决本文中,过期时间无效的问题。

 

1、目前已经实现多用户对接口的访问,也可以通过将用户的信息自定义存入到 Token 中,然后反序列化后,将该用户的多个角色一起添加到 clain 中,实现一个用户的多角色访问。你可以搜索文中 包含以下字样的地方:

注意这个可以添加多个角色声明,请注意这是一个 list

2、网友@rookie丶    Jwt token时效不起作用,有知道的小伙伴,可以回答下

      这个问题已经解决:36 ║解决JWT权限验证过期问题

 

3、关于JWT一共三篇 姊妹篇,内容分别从简单到复杂,一定要多看多想:

      一、Swagger的使用 3.3 JWT权限验证【修改】

      二、解决JWT权限验证过期问题

      三、JWT完美实现权限与接口的动态分配

 

 

 


提要

书接上文,在前边的两篇文章中,我们简单提到了接口文档神器Swagger,《从零开始搭建自己的前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之三 || Swagger的使用 3.1》、《从零开始搭建自己的前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之四 || Swagger的使用 3.2》,两个文章中,也对常见的几个问题做了简单的讨论,最后还剩下一个小问题,

1、如何给接口实现权限验证?

其实关于这一块,我思考了下,因为毕竟我的项目中是使用的vue + api 搭建一个前台展示,大部分页面都没有涉及到权限验证,本来要忽略这一章节,可是犹豫再三,还是给大家简单分析了下,个人还是希望陪大家一直搭建一个较为强大的,只要是涉及到后端那一定就需要 登录=》验证了,本文主要是参考网友https://www.cnblogs.com/RayWang/p/9255093.html的思路,我自己稍加改动,大家都可以看看。

根据维基百科定义,JWT(读作 [/dʒɒt/]),即JSON Web Tokens,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法,从而使通信双方实现以JSON对象的形式安全的传递信息。

以上是JWT的官方解释,可以看出JWT并不是一种只能权限验证的工具,而是一种标准化的数据传输规范。所以,只要是在系统之间需要传输简短但却需要一定安全等级的数据时,都可以使用JWT规范来传输。规范是不因平台而受限制的,这也是JWT做为授权验证可以跨平台的原因。

如果理解还是有困难的话,我们可以拿JWT和JSON类比:

JSON是一种轻量级的数据交换格式,是一种数据层次结构规范。它并不是只用来给接口传递数据的工具,只要有层级结构的数据都可以使用JSON来存储和表示。当然,JSON也是跨平台的,不管是Win还是Linux,.NET还是Java,都可以使用它作为数据传输形式。

1)客户端向授权服务系统发起请求,申请获取“令牌”。

2)授权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端

3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源)


 

一、通过Jwt获取Token,并通过缓存记录,配合中间件实现验证

在之前的搭建中,swagger已经基本成型,其实其功能之多,不是我这三篇所能写完的,想要添加权限,先从服务开始

1、开启服务

在ConfigureServices中,增加以下代码

  #region Token绑定到ConfigureServices
  //添加header验证信息
  //c.OperationFilter<SwaggerHeader>();
  var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, };
  c.AddSecurityRequirement(security);
  //方案名称“Blog.Core”可自定义,上下一致即可
  c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme
  {
      Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)"",
      Name = "Authorization",//jwt默认的参数名称
      In = "header",//jwt默认存放Authorization信息的位置(请求头中)
      Type = "apiKey"
  }); 
  #endregion

 

 

最终的是这样的

/// <summary>
        /// ConfigureServices 方法
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            #region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Version = "v0.1.0",
                    Title = "Blog.Core API",
                    Description = "框架说明文档",
                    TermsOfService = "None",
                    Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" }
                });

                //就是这里

                #region 读取xml信息
                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名
                var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名
                c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改
                c.IncludeXmlComments(xmlModelPath);
                #endregion

                #region Token绑定到ConfigureServices
                //添加header验证信息
                //c.OperationFilter<SwaggerHeader>();
                var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, };
                c.AddSecurityRequirement(security);
                //方案名称“Blog.Core”可自定义,上下一致即可
                c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme
                {
                    Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)"",
                    Name = "Authorization",//jwt默认的参数名称
                    In = "header",//jwt默认存放Authorization信息的位置(请求头中)
                    Type = "apiKey"
                }); 
                #endregion


            });
            #endregion

            #region Token服务注册
            services.AddSingleton<IMemoryCache>(factory =>
             {
                 var cache = new MemoryCache(new MemoryCacheOptions());
                 return cache;
             });
            services.AddAuthorization(options =>
            {
                options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
                options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
                options.AddPolicy("AdminOrClient", policy => policy.RequireRole("Admin","Client").Build());
            });
            #endregion
        }

 

然后执行代码,就可以看到效果

 


图 1
 

最新的Github 开源代码中,已经做了调整,需要在输入 Token的时候,前边加上 Bearer ,比如是这样的:

但是请注意!如果你使用的是中间件app.UseMiddleware<JwtTokenAuth>() ,要是使用 Bearer xxxx传值的时候,记得在中间件的方法中,把Token的 “Bearer ”字符给截取掉,这样的:

 

 

 

接下来,就是在项目中配置5个地方(感谢 @猫出没 提出歧义,其实不是我们要新建五个文件,而是5个地方,1.JwtHelper.cs(帮助类) + 2.JwtTokenAuth.cs(执行中间件) + 3.一个Token类(存放我们的token信息) + 4.在startup中的configure方法配置启动中间件 + 5. 添加 controller 特性),如下图

 图 3

 

具体来说:

2:JwtTokenAuth,一个中间件,用来过滤每一个http请求,就是每当一个用户发送请求的时候,都先走这一步,然后再去访问http请求的接口

     public Task Invoke(HttpContext httpContext)
        {
            //检测是否包含'Authorization'请求头
            if (!httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                return _next(httpContext);
            }
            var tokenHeader = httpContext.Request.Headers["Authorization"].ToString();

            TokenModelJWT tm = JwtHelper.SerializeJWT(tokenHeader);//序列化token,获取授权

            //授权 注意这个可以添加多个角色声明,请注意这是一个 list
            var claimList = new List<Claim>(); 
            var claim = new Claim(ClaimTypes.Role, tm.Role);
            claimList.Add(claim);
            var identity = new ClaimsIdentity(claimList);
            var principal = new ClaimsPrincipal(identity);
            httpContext.User = principal;

            return _next(httpContext);
        }

3:JwtHelper 一个帮助类,可以生成Token,和讲Token反序列成model

public class JwtHelper
    {
        public static string secretKey { get; set; } = "sdfsdfsrty45634kkhllghtdgdfss345t678fs";
        /// <summary>
        /// 颁发JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJWT(TokenModelJWT tokenModel)
        {
            var dateTime = DateTime.UtcNow;
            var claims = new Claim[]
            {
                new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//Id
                new Claim("Role", tokenModel.Role),//角色
                new Claim(JwtRegisteredClaimNames.Iat,dateTime.ToString(),ClaimValueTypes.Integer64)
            };
            //秘钥
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtHelper.secretKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var jwt = new JwtSecurityToken(
                issuer: "Blog.Core",
                claims: claims, //声明集合
                expires: dateTime.AddHours(2),
                signingCredentials: creds);

            var jwtHandler = new JwtSecurityTokenHandler();
            var encodedJwt = jwtHandler.WriteToken(jwt);

            return encodedJwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static TokenModelJWT SerializeJWT(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
            object role = new object(); ;
            try
            {
                jwtToken.Payload.TryGetValue("Role", out role);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var tm = new TokenModelJWT
            {
                Uid = (jwtToken.Id).ObjToInt(),
                Role = role != null ? role.ObjToString() : "",
            };
            return tm;
        }
    }

 注意:

ObjToInt() 和 ObjToString()//这是我封装的方法,在Blog.Core.Model层的UtilConvert类中,注意命名控制是整个解决方案的,这样全局就可以使用

  

4、补充:什么是 Claim

如果对 claim[] 定义不是很理解,可以看看dudu大神的解释《理解ASP.NET Core验证模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不读的英文博文》:

这篇英文博文是 Andrew Lock 写的 Introduction to Authentication with ASP.NET Core 。

以下是简单的阅读笔记:

-----------------------------------

ASP.NET Core 的验证模型是 claims-based authentication 。Claim 是对被验证主体特征的一种表述,比如:登录用户名是...,email是...,用户Id是...,其中的“登录用户名”,“email”,“用户Id”就是ClaimType。

You can think of claims as being a statement about...That statement consists of a name and a value.

对应现实中的事物,比如驾照,驾照中的“身份证号码:xxx”是一个claim,“姓名:xxx”是另一个claim。

一组claims构成了一个identity,具有这些claims的identity就是 ClaimsIdentity ,驾照就是一种ClaimsIdentity,可以把ClaimsIdentity理解为“证件”,驾照是一种证件,护照也是一种证件。

ClaimsIdentity的持有者就是 ClaimsPrincipal ,一个ClaimsPrincipal可以持有多个ClaimsIdentity,就比如一个人既持有驾照,又持有护照。

------------------------------------

理解了Claim, ClaimsIdentity, ClaimsPrincipal这三个概念,就能理解生成登录Cookie为什么要用下面的代码?

var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, loginName) }, "Basic");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await context.Authentication.SignInAsync(_cookieAuthOptions.AuthenticationScheme, claimsPrincipal);
要用Cookie代表一个通过验证的主体,必须包含Claim, ClaimsIdentity, ClaimsPrincipal这三个信息,以一个持有合法驾照的人做比方,ClaimsPrincipal就是持有证件的人,ClaimsIdentity就是证件,"Basic"就是证件类型(这里假设是驾照),Claim就是驾照中的信息。 

 

5:令牌类 TokenModelJWT

定义中的TokenModelJWT,是一个令牌类,主要存储个人角色信息,自己简单写一个,也可以是你登录的时候的用户信息,或者其他

    public class TokenModelJWT
    {
        /// <summary>
        /// Id
        /// </summary>
        public long Uid { get; set; }
        /// <summary>
        /// 角色
        /// </summary>
        public string Role { get; set; }
        /// <summary>
        /// 职能
        /// </summary>
        public string Work { get; set; }

    }

 

6:配置权限验证

1、然后再Startup的Configure中,将TokenAuth注册中间件

注意:HTTP管道是有先后顺序的,一定要写在 app.Mvc() 之前,否则不起作用。

 

2、在需要加权限的页面中,增加特性


 

这个时候,你运行项目,发现之前写的都报错了,


图 7

别慌!是因为每次操作请求,都会经过TokenAuth 中的Invoke方法,方法中对Header信息进行过滤,因为现在Header中,并没有相应的配置信息,看到这里,你就想到了,这个特别像我们常见的[HttpGet]等特性,没错!在.Net Core 中,到处都可以看到AOP编程,真的特别强大。

这个时候我们就用到了最开始的那个权限按钮


,图 8

没错就是这里,但是我们方法写好了,那Token如何获取呢,别急,我们新建一个LoginController,来模拟一次登录操作,简单传递几个参数,将用户角色和缓存时间传递,然后生成Token,并生成到缓存中,为之后做准备。

 

        /// <summary>
        /// 获取JWT的重写方法,推荐这种,注意在文件夹OverWrite下
        /// </summary>
        /// <param name="id">id</param>
        /// <param name="sub">角色</param>
        /// <returns></returns>
        [HttpGet]
        [Route("Token2")]
        public JsonResult GetJWTStr(long id = 1, string sub = "Admin")
        {
            //这里就是用户登录以后,通过数据库去调取数据,分配权限的操作
            TokenModelJWT tokenModel = new TokenModelJWT();
            tokenModel.Uid = id;
            tokenModel.Role = sub;

            string jwtStr = JwtHelper.IssueJWT(tokenModel);
            return Json(jwtStr);
        }

 

这个时候我们就得到了我们的Token


图 9

然后粘贴到我们的上图权限窗口中,还记得么

 


图 10

接下来,你再调用窗口,就发现都可以辣!


 

如果这里你报错了,可以在中间件里进行调试,看具体的原因是什么

 

 

 

 

二、核心知识点梳理

以下是参考大神文章:@ASP.NET Core 认证与授权[4]:JwtBearer认证 ,一定要多看多想,下边的代码我没有试验正确性,大家看个意思即可,不用纠结正确与否,重点跟着这个系列往后走就行。

1、Bearer认证

HTTP提供了一套标准的身份验证框架:服务器可以用来针对客户端的请求发送质询(challenge),客户端根据质询提供身份验证凭证。质询与应答的工作流程如下:服务器端向客户端返回401(Unauthorized,未授权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息,其中至少包含有一种质询方式。然后客户端可以在请求中添加Authorization头进行验证,其Value为身份验证的凭证信息。

HTTPAuth

在HTTP标准验证方案中,我们比较熟悉的是"Basic"和"Digest",前者将用户名密码使用BASE64编码后作为验证凭证,后者是Basic的升级版,更加安全,因为Basic是明文传输密码信息,而Digest是加密后传输。在前文介绍的Cookie认证属于Form认证,并不属于HTTP标准验证。

本文要介绍的Bearer验证也属于HTTP协议标准验证,它随着OAuth协议而开始流行,详细定义见: RFC 6570

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

Bearer验证中的凭证称为BEARER_TOKEN,或者是access_token,它的颁发和验证完全由我们自己的应用程序来控制,而不依赖于系统和Web服务器,Bearer验证的标准请求方式如下:

Authorization: Bearer [BEARER_TOKEN] 

那么使用Bearer验证有什么好处呢?

  • CORS: cookies + CORS 并不能跨不同的域名。而Bearer验证在任何域名下都可以使用HTTP header头部来传输用户信息。

  • 对移动端友好: 当你在一个原生平台(iOS, Android, WindowsPhone等)时,使用Cookie验证并不是一个好主意,因为你得和Cookie容器打交道,而使用Bearer验证则简单的多。

  • CSRF: 因为Bearer验证不再依赖于cookies, 也就避免了跨站请求攻击。

  • 标准:在Cookie认证中,用户未登录时,返回一个302到登录页面,这在非浏览器情况下很难处理,而Bearer验证则返回的是标准的401 challenge

2、JWT(JSON WEB TOKEN)

上面介绍的Bearer认证,其核心便是BEARER_TOKEN,而最流行的Token编码方式便是:JSON WEB TOKEN。

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT是由.分割的如下三部分组成:

头部(Header)

Header 一般由两个部分组成:

  • alg
  • typ

alg是是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的类型,在这里就是:JWT。

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

然后使用Base64Url编码成第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>

载荷(Payload)

这一部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。

Claims的实体一般包含用户和一些元数据,这些claims分成三种类型:

  • reserved claims:预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的原因是保证 JWT 的紧凑)。

  • public claims: 公有声明,这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。

  • private claims: 私有声明,这个部分是共享被认定信息中自定义部分。

一个简单的Pyload可以是这样子的:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

这部分同样使用Base64Url编码成第二部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>

签名(Signature)

Signature是用来验证发送者的JWT的同时也能确保在期间不被篡改。

在创建该部分时候你应该已经有了编码后的Header和Payload,然后使用保存在服务端的秘钥对其签名,一个完整的JWT如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

因此使用JWT具有如下好处:

  • 通用:因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

  • 紧凑:JWT的构成非常简单,字节占用很小,可以通过 GET、POST 等放在 HTTP 的 header 中,非常便于传输。

  • 扩展:JWT是自我包涵的,包含了必要的所有信息,不需要在服务端保存会话信息, 非常易于应用的扩展。

关于更多JWT的介绍,网上非常多,这里就不再多做介绍。下面,演示一下 ASP.NET Core 中 JwtBearer 认证的使用方式。

3、示例

模拟Token

ASP.NET Core 内置的JwtBearer验证,并不包含Token的发放,我们先模拟一个简单的实现:

[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]UserDto userDto)
{
    var user = _store.FindUser(userDto.UserName, userDto.Password);
    if (user == null) return Unauthorized();
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(Consts.Secret);
    var authTime = DateTime.UtcNow;
    var expiresAt = authTime.AddDays(7);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new Claim[]
        {
            new Claim(JwtClaimTypes.Audience,"api"),
            new Claim(JwtClaimTypes.Issuer,"http://localhost:5200"),
            new Claim(JwtClaimTypes.Id, user.Id.ToString()),
            new Claim(JwtClaimTypes.Name, user.Name),
            new Claim(JwtClaimTypes.Email, user.Email),
            new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber)
        }),
        Expires = expiresAt,
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    var tokenString = tokenHandler.WriteToken(token);
    return Ok(new
    {
        access_token = tokenString,
        token_type = "Bearer",
        profile = new
        {
            sid = user.Id,
            name = user.Name,
            auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds(),
            expires_at = new DateTimeOffset(expiresAt).ToUnixTimeSeconds()
        }
    });
}

如上,使用微软提供的Microsoft.IdentityModel.Tokens帮助类(源码地址:azure-activedirectory-identitymodel-extensions-for-dotnet),可以很容易的创建出JwtToen,就不再多说。

注册JwtBearer认证

首先添加JwtBearer包引用:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0

然后在Startup类中添加如下配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = JwtClaimTypes.Name,
            RoleClaimType = JwtClaimTypes.Role, 

            ValidIssuer = "http://localhost:5200",
            ValidAudience = "api",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Consts.Secret))

            /***********************************TokenValidationParameters的参数默认值***********************************/
            // RequireSignedTokens = true,
            // SaveSigninToken = false,
            // ValidateActor = false,
            // 将下面两个参数设置为false,可以不验证Issuer和Audience,但是不建议这样做。
            // ValidateAudience = true,
            // ValidateIssuer = true, 
            // ValidateIssuerSigningKey = false,
            // 是否要求Token的Claims中必须包含Expires
            // RequireExpirationTime = true,
            // 允许的服务器时间偏移量
            // ClockSkew = TimeSpan.FromSeconds(300),
            // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
            // ValidateLifetime = true
        };
    });
}

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();
}

JwtBearerOptions的配置中,通常IssuerSigningKey(签名秘钥)ValidIssuer(Token颁发机构)ValidAudience(颁发给谁) 三个参数是必须的,后两者用于与TokenClaims中的IssuerAudience进行对比,不一致则验证失败(与上面发放Token中的Claims对应)。

NameClaimTypeRoleClaimType需与Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes,否则会造成User.Identity.Name为空等问题。

添加受保护资源

创建一个需要授权的控制器,直接使用Authorize即可:

[Authorize]
[Route("api/[controller]")]
public class SampleDataController : Controller
{
    [HttpGet("[action]")]
    public IEnumerable<WeatherForecast> WeatherForecasts()
    {
        return ...
    }
}

运行

最后运行,直接访问/api/SampleData/WeatherForecasts,将返回一个401:

HTTP/1.1 401 Unauthorized
Server: Kestrel
Content-Length: 0
WWW-Authenticate: Bearer

让我们调用api/oauth/authenticate,获取一个JWT:

请求:
POST http://localhost:5200/api/oauth/authenticate HTTP/1.1
content-type: application/json

{
  "username": "alice",
  "password": "alice"
}

响应:
HTTP/1.1 200 OK
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc","token_type":"Bearer","profile":{"sid":1,"name":"alice","auth_time":1509464340,"expires_at":1510069140}}

最后使用该Token,再次调用受保护资源:

GET http://localhost:5200/api/SampleData/WeatherForecasts HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc

授权成功,返回了预期的数据:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[{"dateFormatted":"2017/11/3","temperatureC":35,"summary":"Chilly","temperatureF":94}]

4、扩展

自定义Token获取方式

JwtBearer认证中,默认是通过Http的Authorization头来获取的,这也是最推荐的做法,但是在某些场景下,我们可能会使用Url或者是Cookie来传递Token,那要怎么来实现呢?

其实实现起来非常简单,如前几章介绍的一样,JwtBearer也在认证的各个阶段为我们提供了事件,来执行我们的自定义逻辑:

.AddJwtBearer(o =>
{
    o.Events = new JwtBearerEvents()
    {
        OnMessageReceived = context =>
        {
            context.Token = context.Request.Query["access_token"];
            return Task.CompletedTask;
        }
    };
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ...
    };

然后在Url中添加access_token=[token],直接在浏览器中访问:

access_token_in_url

同样的,我们也可以很容易的在Cookie中读取Token,就不再演示。

除了OnMessageReceived外,还提供了如下几个事件:

  • TokenValidated:在Token验证通过后调用。

  • AuthenticationFailed: 认证失败时调用。

  • Challenge: 未授权时调用。

使用OIDC服务

在上面的示例中,我们简单模拟的Token颁发,功能非常简单,并不适合在生产环境中使用,可是微软也没有提供OIDC服务的实现,好在.NET社区中提供了几种实现,可供我们选择:

NameDescription
AspNet.Security.OpenIdConnect.Server (ASOS) Low-level/protocol-first OpenID Connect server framework for ASP.NET Core and OWIN/Katana
IdentityServer4 OpenID Connect and OAuth 2.0 framework for ASP.NET Core - officially certified by the OpenID Foundation and under governance of the .NET Foundation
OpenIddict Easy-to-use OpenID Connect server for ASP.NET Core
PwdLess Simple, stateless, passwordless authentication for ASP.NET Core

我们在这里使用IdentityServer4来搭建一个OIDC服务器,并添加如下配置:

/********************OIDC服务器代码片段********************/
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // 配置IdentitryServer
    services.AddIdentityServer()
        .AddInMemoryPersistedGrants()
        .AddInMemoryApiResources(Config.GetApis())
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryClients(Config.GetClients())
        .AddTestUsers(Config.GetUsers())
        .AddDeveloperSigningCredential();
}

new Client
{
    ClientId = "jwt.implicit",
    ClientName = "Implicit Client (Web)",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RedirectUris = { "http://localhost:5200/callback" },
    PostLogoutRedirectUris = { "http://localhost:5200/home" },
    AllowedCorsOrigins = { "http://localhost:5200" },
    AllowedScopes = { "openid", "profile", "email", "api" },
}

而JwtBearer客户端的配置就更加简单了,因为OIDC具有配置发现的功能:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(o =>
    {
        o.Authority = "https://oidc.faasx.com/";
        o.Audience = "api";
        o.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = JwtClaimTypes.Name,
            RoleClaimType = JwtClaimTypes.Role,
        };
    });
}

如上,最重要的是Authority参数,用来表示OIDC服务的地址,然后便可以自动发现IssuerIssuerSigningKey等配置,而o.Audienceo.TokenValidationParameters = new TokenValidationParameters { ValidAudience = "api" }是等效的,后面分析源码时会介绍。

OIDC兼容OAuth2协议,我们可以使用上一章介绍的授权码模式来获取Token,也可以直接用户名密码模式来获取Token:

请求:
POST https://oidc.faasx.com/connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=client.rop&client_secret=secret&grant_type=password&scope=api&username=alice&password=alice

响应:
HTTP/1.1 200 OK
Content-Type: application/json

{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDk2NzI1NjksImV4cCI6MTUwOTY3NjE2OSwiaXNzIjoiaHR0cHM6Ly9vaWRjLmZhYXN4LmNvbSIsImF1ZCI6WyJodHRwczovL29pZGMuZmFhc3guY29tL3Jlc291cmNlcyIsImFwaSJdLCJjbGllbnRfaWQiOiJjbGllbnQucm9wIiwic3ViIjoiMDAxIiwiYXV0aF90aW1lIjoxNTA5NjcyNTY5LCJpZHAiOiJsb2NhbCIsIm5hbWUiOiJBbGljZSBTbWl0aCIsImVtYWlsIjoiQWxpY2VTbWl0aEBlbWFpbC5jb20iLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbInB3ZCJdfQ.PM93LThOZA3lkgPFVwieqGQQQtgmYDCY0oSFVmudv1hpKO6UaaZsmnn4ci9QjbGl5g2433JkDks5UIZsZ0xE62Qqq8PicPBBuaNoYrCf6dxR7j-0uZcoa7-FCKGu-0TrM8OL-NuMvN6_KEpbWa3jlkwibCK9YDIwJZilVoWUOrbbIEsKTa-DdLScmzHLUzksT8GBr0PAVhge9PRFiGqg8cgMLjsA62ZeDsR35f55BucSV5Pj0SAj26anYvrBNTHKOF7ze1DGW51Dbz6DRu1X7uEIxSzWiNi4cRVJ6Totjkwk5F78R9R38o_mYEdehZBjRHFe6zLd91hXcCKqOEh5eQ","expires_in":3600,"token_type":"Bearer"}

我们使用https://jwt.io解析一下OIDC服务器颁发的Token中的Claims:

{
  "nbf": 1509672569, // 2017/11/3 1:29:29 NotBefore Token生效时间,在此之前不可用
  "exp": 1509676169, // 2017/11/3 2:29:29 Expiration Token过期时间,在此之后不可用
  "iss": "https://oidc.faasx.com", // Issuer 颁发者,通常为STS服务器地址
  "aud": [ // Audience Token的作用对象,也就是被访问的资源服务器授权标识
    "https://oidc.faasx.com/resources",
    "api"
  ],
  "client_id": "client.rop", // 客户端标识
  "sub": "001",
  "auth_time": 1509672569, // Token颁发时间
  "idp": "local",
  "name": "Alice Smith",
  "email": "AliceSmith@email.com",
  "scope": [
    "api"
  ],
  "amr": [
    "pwd"
  ]
}

 

这一篇呢,写的比较潦草,主要是讲如何使用,具体的细节知识,还是大家摸索,还是那句话,这里只是抛砖引玉的作用哟,通过阅读本文,你会了解到,什么是JWT,如何添加配置.net core 中间件,如何使用Token验证,在以后的项目里你就可以在登录的时候,调用Token,返回客户端,然后判断是否有相应的接口权限。

三、结语

好啦!项目准备阶段就这么结束了,以后咱们就可以直接用swagger来调试了,而不是每次都用F5运行等,接下来我们就要正式开始搭建项目了,主要采用的是泛型仓储模式 Repository+Service,也是一种常见的模式。

四、CODE

https://github.com/anjoy8/Blog.Core.git

 

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!