ASP.NET Web API基础(05)--- 基于JWT的身份认证

5.1 Web API中的过滤器

WebApi下的过滤器和MVC下的过滤器有一些区别。

(1)       所处命名空间不同。

Web API 过滤器额命名空间是“System.Web.Http”,而MVC过滤器命名空间位于“System.Web.MVC”。

(2)       Web API 没有结果过滤器

Web API 中的ActionFilterAttribute这个类并没有继承IResultFilter这个接口,只继承了IActionFilter这个接口,重写了OnActionExecuted和OnActionExecuting两个方法(包括对应的异步方法),并没有重写:OnResultExecuted和OnResultExecuting两个方法。

Web API过滤器的方法的执行顺序为:

OnAuthorization→OnActionExecuting-> Action方法执行 ->OnActionExecuted。

下面是Web API中过滤器的基本用法。

// 授权过滤器

public class MyAuthorize : AuthorizeAttribute

{

   public override void OnAuthorization(HttpActionContext actionContext)

   {

      //1.如果保留如下代码,则会运行.net framework定义好的身份验证,如果希望自定义身份验证,则删除如下代码

      // base.OnAuthorization(actionContext);

      //2.获取控制器作用的Controller和action的名字

      string controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower();

      string actionName = actionContext.ActionDescriptor.ActionName.ToLower();

      HttpContext.Current.Response.Write("身份验证过滤器作用于" + controllerName + "控制器下的" + actionName + "方法");

        }

}

//行为过滤器

public class MyAction: ActionFilterAttribute

{

    /// <summary>

    /// 在Action方法运行之前调用

    /// </summary>

    public override void OnActionExecuting(HttpActionContext actionContext)

    {

        //2.获取控制器作用的Controller和action的名字

        string controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower();

        string actionName = actionContext.ActionDescriptor.ActionName.ToLower();

        HttpContext.Current.Response.Write("行为过滤器OnActionExecuting作用于" + controllerName + "控制器下的" + actionName + "方法运行之前");

    }

    /// <summary>

    /// 在Action方法运行之后调用

    /// </summary>

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)

    {

        base.OnActionExecuted(actionExecutedContext);

       //2.获取控制器作用的Controller和action的名字

       string controllerName = actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName.ToLower();

       string actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName.ToLower();

       HttpContext.Current.Response.Write("行为过滤器OnActionExecuted作用于" + controllerName + "控制器下的" + actionName + "方法运行之后");

    }

}

// 异常过滤器

public class MyException : FilterAttribute,IExceptionFilter

{

    public async Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)

    {

        //1.获取异常信息

        string errorMsg = actionExecutedContext.Exception.ToString();

        //2.对获取的异常信息进行处理

        using (StreamWriter writer = File.AppendText("d:/err.txt"))

        {

            await writer.WriteLineAsync(errorMsg);

        }

     }

}

5.2 JWT

WebApi常见的实现方式有:FORM身份验证、集成WINDOWS验证、Basic基础认证、Digest摘要认证。在前面MVC课程部分,我们学习过基于Form的身份验证。我们知道,Form身份验证是基于Cookie实现的,存在很多弊端,比如Cookie不允许跨域读取,不宜与前后端分离等。JWT的出现可以使这些问题得到改善。

5.2.1 认识JWT

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

JWT的主要优点如下:

l  JWT是无状态的,不需要服务器端保存会话信息,减轻服务器端的读取压力(存储在客户端上),同时易于扩展、易于分布式部署。

l  JWT可以跨语言支持。

l  便于传输,JWT的构成很简单,字节占用空间少,所以是非常便于传输的。

l  自身构成有payload部分,可以存储一下业务逻辑相关的非敏感信息。

JWT是由三段信息构成的,将这三段信息文本用“.”链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT由三部分组成,如下图,分别是:Header头部、Payload负载、Signature签名。

(1). 头部(Header)

通常包括两部分,类型(如 “typ”:“JWT”)和加密算法(如“alg”:"HS256"),当然你也可以添加其它自定义的一些参数,然后对这个对象机型base64编码,生成一段字符串。

例如,完整的头部就像下面这样的JSON:

{

  'typ': 'JWT',

  'alg': 'HS256'

}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

 注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

(2). 负载(Payload)

通常用来存放一些业务需要但不敏感的信息,比如:用户编号(userId)、用户账号(userAccount)、权限等等,该部分也有一些默认的声明,如下所示,但不常用。

l  iss: jwt签发者

l  sub: jwt所面向的用户

l  aud: 接收jwt的一方

l  exp: jwt的过期时间,这个过期时间必须要大于签发时间

l  nbf: 定义在什么时间之前,该jwt都是不可用的.

l  iat: jwt的签发时间

l  jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

其中最常用的就是exp过期时间,要和1970年1月1日那个点进行比对,用法如下,下面表示生成jwt字符串后20分钟过期。

 

最后对该部分组装成的对象进行base64编码,得到如下编码:

“eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ”

(3). 签名(Signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

l  header (base64后的)

l  payload (base64后的)

l  secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// 伪代码如下:

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret');

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

5.2.2 在Asp.net WebAPI 中使用JWT

在Web API项目中,使用NuGet 安装 Jwt 包,如图所示。

 

(1)安装完成后,创建 JWT的帮助类。

public  class JwtToken

{

    //HMACSHA256加密

static IJwtAlgorithm algorithm = new HMACSHA256Algorithm();

//序列化和反序列

static IJsonSerializer serializer = new JsonNetSerializer();

//Base64编解码

static IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();

//UTC时间获取

    static IDateTimeProvider provider = new UtcDateTimeProvider();        const string secret = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQK";//服务端

[HttpGet]

    public string JiaM()

    {

        //设置过期时间(可以不设置,下面表示签名后 20分钟过期)

        double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;

        var payload = new Dictionary<string, object>

        {

            { "UserId", 123 },

            { "UserName", "admin" },

            {"exp",exp }   //该参数也可以不写

        };

        

        //注意这个是额外的参数,默认参数是 typ 和alg

        var headers = new Dictionary<string, object>

        {

            { "typ1", "1234" },

            { "alg2", "admin" }

        };

         

        IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder,algorithm);

        var token = encoder.Encode(headers, payload, secret);

        return token;

   }

   [HttpGet]

   public string JieM(string token)

   {

       try

       {

           //用于验证JWT的类

       IJwtValidator validator = new JwtValidator(serializer, provider);

//用于解析JWT的类

           IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder,);

           var json = decoder.Decode(token, secret, true);

           return json;

        }   

        catch (TokenExpiredException e)

        {

            //过期了自动进入这里

            return "Token has expired";

        }

        catch (SignatureVerificationException e)

        {

            //校验未通过自动进入这里

            return "Token has invalid signature";

        }

        catch (Exception e)

        {

            //其它错误,自动进入到这里

            return "other error";

        }

(2)模拟登陆接口

在登录接口中,模拟数据库校验,即账号和密码为admin和12345,即校验通过,然后把账号和userId(实际应该到数据库中查),这里也可以设置一下过期时间,比如20分钟,一同存放到PayLoad中,然后生成JWT字符串,返回给客户端。

/// <summary>
/// 模拟登陆
/// </summary>

[HttpGet]
public string Login1(string userAccount, string pwd)
{
    try
    {
        //这里模拟数据操作,只要是admin和123456就验证通过
        if (userAccount == "admin" && pwd == "123456")
        {
             //1. 进行业务处理(这里模拟获取userId)
             string userId = "0806";
             //过期时间(可以不设置,下面表示签名后 20分钟过期)
             double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;
             //进行组装
             var payload = new Dictionary<string, object>
             {
                  {"userId", userId },
                  {"userAccount", userAccount },
                  {"exp",exp }
             };
             //2. 进行JWT签名
             var token = JWTHelp.JWTJiaM(payload);
             var result = new { result = "ok", token = token };
             return JsonConvert.SerializeObject(result);
         }
         else
         {
              var result = new { result = "error", token = "" };
              return JsonConvert.SerializeObject(result);
         }
     }
     catch (Exception)
     {
         var result = new { result = "error", token = "" };
         return JsonConvert.SerializeObject(result);
     }
}

(3) 客户端调用登录接口

这里只是单纯为了测试,使用的get请求,实际项目中建议post请求,且配置Https,请求成功后,把jwt字符串存放到localStorage中。

//1.登录
$('#j_jwtLogin').on('click', function () {
      $.Ajax({

url: "/api/Seventh/Login1",

type: “get”,

data: { userAccount: "admin", pwd: "123456" },

success: function (data) {
             var jsonData = JSON.parse(data);
             if (jsonData.result == "ok") {
                  console.log(jsonData.token);
                  //存放到本地缓存中
                  window.localStorage.setItem("token", jsonData.token);
                  alert("登录成功,ticket=" + jsonData.token);
             } else {
                   alert("登录失败");
             }
         });
});

(4) 服务器端过滤器

 代码中写了两种获取header中信息的方式,获取到“auth”后,进行校验,校验不通过的话,通过状态码401返回给客户端,校验通过的话,则使用 actionContext.RequestContext.RouteData.Values.Add("auth", result); 进行解密值的存储,方便后续action的直接获取。

/// <summary>
/// 验证JWT算法的过滤器
/// </summary>
public class JWTCheck : AuthorizeAttribute
{
     public override void OnAuthorization(HttpActionContext actionContext)
     {
          //获取表头Header中值的几种方式
          //方式一:
          //{
          //    var authHeader2 = from t in actionContext.Request.Headers
          //                      where t.Key == "auth"
          //                      select t.Value.FirstOrDefault();
          //    var token2 = authHeader2.FirstOrDefault();
          //}
 
          //方式二:
          IEnumerable<string> auths;
          if (!actionContext.Request.Headers.TryGetValues("auth", out auths))
          {
              //HttpContext.Current.Response.Write("报文头中的auth为空");
              //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获),注意:这句话并不能阶段该过滤器,还会继续往下走,要借助if-else       
              actionContext.Response = actionContext.Request.

CreateErrorResponse(HttpStatusCode.Unauthorized,

new HttpError("报文头中的auth为空"));
          }
          else
          {
              var token = auths.FirstOrDefault();
              if (token != null)
              {
                  if (!string.IsNullOrEmpty(token))
                  {
                      var result = JWTHelp.JWTJieM(token);
                       if (result == "expired")
                       {
                           //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)       
                           actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("expired"));
                         }
                       else if (result == "invalid")
                       {
                           //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)  
                           actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("invalid"));
                       }
                       else if (result == "error")
                       {
                           //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)  
                           actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("error"));
                       }
                       else
                       {
                           //表示校验通过,用于向控制器中传值
                             actionContext.RequestContext.RouteData.Values.Add("auth", result);
                       }
                   }
               }
               else
               {
                   //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)  
                   actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("token 空"));
               }
           }        

}
}

(5) 服务器端获取信息的方法

将上说过滤器以特性的形式作用在该方法中,然后通过 RequestContext.RouteData.Values["auth"] 获取到解密后的值,进而进行其它业务处理。

/// <summary>
/// 加密后的获取信息
/// </summary>
[JWTCheck]
[HttpGet]
public string GetInfor()
{
     var userData = JsonConvert.DeserializeObject<userData>(RequestContext.RouteData.Values["auth"].ToString()); ;
     if (userData == null)
     {
          var result = new { Message = "error", data = "" };
          return JsonConvert.SerializeObject(result);
     }
     else
     {
          var data = new { userId = userData.userId, userAccount = userData.userAccount };
          var result = new { Message = "ok", data =data };
          return JsonConvert.SerializeObject(result);
     }
}

(6) 客户端调用获取信息的方法 

前端获取到localStorage中token值,采用自定义header的方式以“auth”进行传递调用服务器端的方法,由于服务器的验证token不正确的时候,是以状态码的形式返回,所以这里要采用error方法,通过xhr.status==401进行判断,凡是进入到这个401中,均是token验证没有通过,具体是什么原因,可以通过xhr.responseText获取详细的值进行判断。

//2.获取信息
$('#jwtGetInfor').on('click', function () {
     //从本地缓存中读取token值
     var token = window.localStorage.getItem("token");
     $.ajax({
         url: "/api/Seventh/GetInfor",
         type: "Get",
         data: {},
         datatype: "json",
         //设置header的方式1
         headers: { "auth": token},
         //设置header的方式2
         //beforeSend: function (xhr) {
              //    xhr.setRequestHeader("auth", token)
         //},          
         success: function (data) {
              console.log(data);
              var jsonData = JSON.parse(data);
              if (jsonData.Message == "ok") {
                  var myData = jsonData.data;
                  console.log("获取成功");
                  console.log(myData.userId);
                  console.log(myData.userAccount);
              } else {
                  console.log("获取失败");
              }             
          },
          //当安全校验未通过的时候进入这里
          error: function (xhr) {
              if (xhr.status == 401) {
                  console.log(xhr.responseText);
                  var jsonData = JSON.parse(xhr.responseText);
                  console.log("授权失败,原因为:" + jsonData.Message);
              }
           }
       });
});

原文地址:https://www.cnblogs.com/mrfang/p/13911798.html