.Net Core 集成JWT授权验证 从页面到API详解

.Net Core 集成JWT授权验证 从页面到API详解

1.什么是JWT

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。
JWT的官网地址:link.
通俗来讲,JWT定义了一种紧凑的、自包含的方式,可以将各方之间的信息作为JSON对象安全的传输,它是代表用户身份的一个象征令牌,可以在api接口中校验用户的身份以确认用户是否有访问api的权限。

2.JWT的使用场景

用户授权: 需要实现单点登录的时候可以使用,例如在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,一次登录能得到其他系统的信任。用户使用JWT授权登录成功后会返回一个token值给当前用户,用户访问其他的模块的时候携带该token进行请求,当token过期或被纂改,则不允许访问。
信息交换: JWT是服务器、客户端之间安全的进行传输信息的好方式。因为在颁发JWT的时候可以对JWT进行签名,多方之间可以通过约定好的加密秘钥进行数据解析。

3.JWT较之Session认证的区别

Session认证

1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

基于token的鉴权机制

1、用户使用用户名密码来请求服务器。
2、服务器进行验证用户的信息。
3、服务器通过验证颁发给用户一个token。
4、客户端存储token,并在每次请求时附送上这个token值。
5、服务端验证token值,并返回数据。

基于Session认证模式的问题在于,扩展性不好,单机使用没有问题,如果使用到了服务器集群或者是跨域的服务导向架构,就会需要每台服务器都要能够读取到session。例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。用这种方案架构看上去清晰,但是工程量大。
而另外一种方案是不在服务器保存session数据,将数据保存在客户端,在用户发起的每次请求的时候都带到服务器,它不需要在服务端保留用户的会话状态信息,不用考虑在哪一台服务器进行登录,为应用的扩展提供了便利。但是JWT在Payload里面包含了附件信息,占用的空间比Session大,在http传输的过程中会造成性能影响。所以在设计的时候不要在JWT中存储太多的claim,避免发生巨大的请求。

4.JWT的结构

令牌由三部分组成,这些部分由 (.)分隔开,分别是:

  • 标题:算法和代币类型
  • 有效载荷:数据
  • 验证签名

因此,JWT通常是这种形式使用 qqqqq.wwwww.eeeee
标题: 通常有两部分组成,令牌类型和使用的签名算法。

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

有效载荷: payload部分是一个json对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号
    除了官方定义的字段,还可以在这个部分定义自己的私有字段。

验证签名: 签名是将第一部分(header)、第二部分(payload)、密钥(key)通过指定算法(HMAC、RSA)进行加密生成的。
详细内容可看下图:在这里插入图片描述
前言介绍完毕,接下来应用下如何获取JWT并用于访问API或服务器资源。流程如下:

  • 应用程序向授权服务器请求授权。
  • 校验用户身份,校验成功,返回token。
  • 应用程序使用访问令牌访问受保护的资源。

5.Asp.Net Core 集成JWT

开发工具:vs2019

首先新建一个asp.net core的web项目,项目版本选择3.1及以上。
在这里插入图片描述在这里插入图片描述
接下来在appsettings.json添加配置信息

{
    
    
  "Logging": {
    
    
    "LogLevel": {
    
    
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "JwtSettings": {
    
    
    "Issuer": "https://localhost:51945",
    "Audience": "https://localhost:51945",
    "SecretKey": "Hello-key----------"
  },
  "TokenValidMinutes": "1",
  "TokenCacheMinutes": "5"
}
Issuer是签发人,Audience是受众,使用者、信息传播的接受者。SecretKey是定义的秘钥。
TokenValidMinutes是自己定义的有效分钟数,
TokenCacheMinutes是自己定义的缓存分钟数,

然后去StartUp类中添加配置。
在ConfigureServices方法中添加代码如下:

//添加身份验证
services.AddAuthentication(options =>
{
    
    
    //认证middleware配置
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
    
    
    //jwt token参数设置
    o.TokenValidationParameters = new TokenValidationParameters
    {
    
    
        NameClaimType = JwtClaimTypes.Name,
        RoleClaimType = JwtClaimTypes.Role,
        //Token颁发机构
        ValidIssuer = Appsettings.Issuer,
        //颁发给谁
        ValidAudience = Appsettings.Audience,
        //这里的key要进行加密
        IssuerSigningKey = new SymmetricSecurityKey
        (Encoding.UTF8.GetBytes(Appsettings.SecretKey)),

    };
});

在Configure方法中添加代码如下:

app.UseAuthentication();

在创建的项目中新建一个文件夹,里面建立一个控制器,层级关系如下图所示:
在这里插入图片描述
该控制器需要继承ActionFilterAttribute,控制器具体代码如下(上面的命名空间就不复制了vs可以直接导):

 public class CheckJWTFilter : ActionFilterAttribute
 {
    
    
     /// <summary>
     /// 检查是否登陆
     /// </summary>
     public bool IsCheck {
    
     get; set; } = true;

     /// <summary>
     /// 检查颁发的令牌
     /// </summary>
     /// <param name="context"></param>
     public override void OnActionExecuting(ActionExecutingContext filterContext)
     {
    
    
         bool ignoreCheckSession = filterContext.ActionDescriptor.FilterDescriptors
         .Select(f => f.Filter)
         .OfType<TypeFilterAttribute>()
         .Any(f => f.ImplementationType.Equals(typeof(IgnoreCheckJWTFilter)));
         if (ignoreCheckSession)
         {
    
    
             base.OnActionExecuting(filterContext);
             return;
         }

         if (IsCheck)
         {
    
    
             var claimIdentity = (ClaimsIdentity)filterContext.HttpContext.User.Identity;
             if (claimIdentity.Claims.Count() > 0 && JWTUtil.ValidateLogin(claimIdentity))
             {
    
    
                 base.OnActionExecuting(filterContext);
             }
             else
             {
    
    
                 filterContext.Result = new UnauthorizedResult();
             }
         }
         else
         {
    
    
             base.OnActionExecuting(filterContext);
         }
     }
 }

接下来在Controllers文件夹中新建一个HomeController,创建一个简单的页面,页面上包含一个文本框和几个简单的按钮即可。

<input type="text" id="name" />
<input type="text" id="pwd" />
<button onclick="btnLogin()">登录</button>
<button onclick="logOut()">退出登录</button>
<button onclick="getUserInfo()">获取用户信息</button>

然后再创建一个User控制器,用于接收页面提交过来的请求。代码如下:

[Route("GetToken")]
        [HttpGet]
        public IActionResult GetToken(string data)
        {
    
    
            LoginViewModel model = JsonConvert.DeserializeObject<LoginViewModel>(data);
            model.Id = "1";
            model.Phone = "138****8521";
            model.Password = "123";
            ResponseResult responseResult = new ResponseResult();
            responseResult.Success = true;
            responseResult.Data = JWTUtil.GetToken(model);
            return Ok(responseResult);
        }

        [Route("GetUserInfo")]
        [HttpGet]
        [CheckJWTFilter]
        public IActionResult GetUserInfo()
        {
    
    
            //获取当前请求用户的信息,包含token信息
            var claimIdentity = (ClaimsIdentity)HttpContext.User.Identity;
            string name = claimIdentity.FindFirst(JwtClaimTypes.Name).Value;
            string phoneNumber = claimIdentity.FindFirst(JwtClaimTypes.PhoneNumber).Value;
            string expirationTimeStamp = claimIdentity.FindFirst(JwtClaimTypes.Expiration).Value;
            DateTime expiration = DateTimeUtil.Unix2Datetime(Convert.ToInt64(expirationTimeStamp));
            int code = GetStatusCode(expiration, Appsettings.TokenCacheMinutes);
            return new JsonResult(new ResponseResult() {
    
      Data = name + "用户资料" });
        }

        [CheckJWTFilter]
        [HttpGet]
        [Route("RefreshToken")]
        public IActionResult RefreshToken()
        {
    
    
            string token = HttpContext.Request.Headers["Authorization"].ToString();
            string[] tokenArray = token.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            var claimIdentity = (ClaimsIdentity)HttpContext.User.Identity;
            string expirationTimeStamp = claimIdentity.FindFirst(JwtClaimTypes.Expiration)?.Value;
            if (string.IsNullOrEmpty(expirationTimeStamp))
            {
    
    
                return new JsonResult(new ResponseResult() {
    
     Success = false, Data = tokenArray.Length > 1 ? tokenArray[1] : tokenArray[0] });
            }
            DateTime expiration = DateTimeUtil.Unix2Datetime(Convert.ToInt64(expirationTimeStamp));
            if (DateTime.Now > expiration && DateTime.Now <= expiration.AddMinutes(Appsettings.TokenCacheMinutes))
            {
    
    
                ResponseResult responseResult = new ResponseResult();
                responseResult.Success = true;
                responseResult.Data = JWTUtil.GetToken(claimIdentity);
                return Ok(responseResult);
            }
            return new JsonResult(new ResponseResult() {
    
     Success = true, Data = tokenArray.Length > 1 ? tokenArray[1] : tokenArray[0], Code = 200 });
        }

        private int GetStatusCode(DateTime expiration, int tokenCacheMinutes)
        {
    
    
            if (expiration < DateTime.Now)
            {
    
    
                if (expiration.AddMinutes(tokenCacheMinutes) < DateTime.Now)
                {
    
    
                    //token已失效+不在缓冲期内
                    return 9002;
                }
                else
                {
    
    
                    //token已失效+在缓冲期内
                    return 9001;
                }
            }
            return 200;
        }

在页面上需要通过ajax提交的方式,将前台输入的信息传输到api。

    <script type="text/javascript">
        
        //规定ajax请求即将发送时运行的函数
        $(document).ajaxSend(function (e, jqxhr, opt) {
    
    
            //统一为ajax请求添加Header
            jqxhr.setRequestHeader("Authorization", "Bearer " + sessionStorage.getItem("token"));
        });

        //规定ajax请求成功完成时运行的函数
        $(document).ajaxSuccess(function (event, jqxhr, opt) {
    
    
            if (jqxhr.responseJSON.Success) {
    
    
                handleStatusCode(jqxhr.responseJSON.Code);
            }
        });
		//点击登陆
        function btnLogin() {
    
    
            let name = $("#name").val();
            let pwd = $("#pwd").val();
            $.ajax({
    
    
                type: "GET",
                url: "/User/GetToken",
                contentType: "application/json",
                data: {
    
    
                    data: JSON.stringify({
    
    
                        loginName: name,
                        password: pwd
                    })
                }
            }).success(function (data) {
    
    
                if (data.Success) {
    
    
                    sessionStorage.setItem("token", data.Data);
                } 
            });
        }
		//登出 清除token
        function logOut() {
    
    
            sessionStorage.removeItem("token");
        }
		//获取用户信息
        function getUserInfo() {
    
    
            $.ajax({
    
    
                type: "get",
                url: "/User/GetUserInfo"
            }).done(function (data) {
    
    
                if (data.Success) {
    
    
                    console.log(data.Data);
                } else {
    
    
                    alert(data.ErrorMsg);
                }
            });
        }
    </script>

我们启动程序,模拟一下应用场景。
在这里插入图片描述
我们先不传递账号密码等数据,直接提交,然后再将返回的token进行获取用户信息。
在这里插入图片描述
在这里插入图片描述
用户资料是空的,因为我们在生成token的时候,没有将文本框内的数据存储到有效载荷内容中去,所以我们调用查询用户资料接口时,根据Authorization传递过去的token参数进行解析,也是拿不到数据的。

接下来传递文本框数据再次进行解析,可以很清晰的从返回值中看到api能够从我们传递过去的token中解析出内容。
在这里插入图片描述

小结

使用JWT控制接口和服务器资源的访问,需要在接口上添加特性,表示需要有校验通过可用的令牌才能访问,这里只是简单的演示了一下生成jwt以及解析jwt,后期实战进阶的时候有很多地方需要补充以及完善。
例如token失效过期后的客户端的响应、如何强制在token未过期时让客户端的token失效、如何无刷新交换新的token等等。

感谢阅读,敬请斧正。

猜你喜欢

转载自blog.csdn.net/weixin_42794881/article/details/121750126
今日推荐