C# WEBAPI Token+数字签名(上)

之前写了一个接口程序,只是利用VS自带的登录验证获取Token,然后根据调用接口的时候带入Token即可。这种方式比较简单,对方调用也很简单,但是安全性上是不够的,所以根据网上查找和自己需求就自己写了一份Token+数字签名的WebAPI程序

1、因为要自行验证数字签名,所以我们就必须利用到一个程序筛选类:ActionFilterAttribute
此类在WebMVC和WEBAPI中调用是不一样的,我会后面会写一篇详解,以下都是API下代码


```csharp
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;

namespace System.Web.Http.Filters
{
    //
    // 摘要:
    //     表示所有操作筛选器特性的基类。
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IFilter
    {
        //
        // 摘要:
        //     初始化 System.Web.Http.Filters.ActionFilterAttribute 类的新实例。
        protected ActionFilterAttribute();

        //
        // 摘要:
        //     在调用操作方法之后发生。
        //
        // 参数:
        //   actionExecutedContext:
        //     操作执行的上下文。
        public virtual void OnActionExecuted(HttpActionExecutedContext actionExecutedContext);
        public virtual Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken);
        //
        // 摘要:
        //     在调用操作方法之前发生。
        //
        // 参数:
        //   actionContext:
        //     操作上下文。
        public virtual void OnActionExecuting(HttpActionContext actionContext);
        public virtual Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken);
    }
}

2、重写此类调用前和调用后方法,可以随时监控所有调用我们程序的请求。创建一个类ApiSecurityFilter继承ActionFilterAttribute重写OnActionExecutingAsyncOnActionExecutedAsync方法

public class ApiSecurityFilter: ActionFilterAttribute
    {
		 /// <summary>
        ///   在调用操作方法之前发生。
        /// </summary>
        /// <param name="actionContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            AuthenticationModel<JObject> Auth = new AuthenticationModel<JObject>();
			return base.OnActionExecutingAsync(actionContext, cancellationToken);
		}
		  /// <summary>
        /// 在请求执行完后 记录请求的数据以及返回数据( 在调用操作方法之后发生。)
        /// </summary>
        /// <param name="actionExecutedContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
        {
          
            return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken);

        }
	}

3、完成此类编写后,要在我们的启动配置类里进行配置。在App_Start文件下的WebApiConfig.csRegister方法中加入我们刚刚写的过滤器

			//过滤器
            config.Filters.Add(new ApiSecurityFilter());

4、可以随便在外面的一个WEBAPI中进行测试,任何一个接口调用都会触发我们配置的过滤器类,并能获取传入和传出的参数信息,因此过滤器大部分用于日志记录。

5、想好我们需要如何增加我们接口方式的安全性,我当时想了两种

  • A:我们可以利用自行编写Token值获取,然后加上随机数,时间戳,参数然后加密进行数字签名。
  • B:利用服务器公钥,然后配属客户端私钥,然后客户端利用私钥对时间戳,随机数和参数进行加密,后台利用公钥对其进行解密。
    两者模式基本相同,唯一区别就是Token自行获取,时间控制过期,然后利用Token值参入进行加密,Token不泄露就不会出现安全问题。后者是利用公钥私钥加解密,逻辑基本相同,公钥私钥非对称加解密可能稍微安全点,但是原理基本是相同的,所以我们就说第一种吧。

6*、在外面的WEBAPI中创建Get_Token方法,自行编写自由Token值序列,我就直接拿GUID当Token值了。具体实现直接看代码,就是生成一个序列,然后声明一个Token类,设定id,过期时间,Token序列值,然后放入缓存中,很简单。

 		/// <summary>
        /// 获取Token
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        [Route("Get_Token"), HttpGet]
        public AuthenticationModel<JObject> Get_Token(string userId)
        {
            AuthenticationModel<JObject> Auth = new AuthenticationModel<JObject>();

            //判断参数是否合法
            if (string.IsNullOrEmpty(userId))
            {
                Auth.Status = 0;
                Auth.Message = "用户ID不正确";
                return Auth;
            } 

            //插入缓存
            Token token = (Token)HttpRuntime.Cache.Get(userId);
            if (HttpRuntime.Cache.Get(userId) == null)
            {
                token = new Token();
                token.userId = userId;
                token.SignToken = Guid.NewGuid().ToString();
                token.ExpireTime = DateTime.Now.AddMinutes(3);
                HttpRuntime.Cache.Insert(token.userId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero);
            }
            //返回token信息
            JObject _paramsObj = JObject.FromObject(token);
            Auth.Status = 1;
            Auth.data = _paramsObj;

            return Auth;
        }
 

注意一下,就是HttpRuntime.Cache是缓存,重新生成或者其他操作就会丢失。

7*、在我们刚刚写的过滤器中**OnActionExecutingAsync**增加对调用接口的控制了,不能所有的接口调用都让通过吧,这样就失去了监控的意义。

  • A:首先,我们要确定签名是那些字段进行加密,我采用UserId,时间戳,随机数,参数整合成一个字符串,在进行字母升序排序然后MD5加密形成数字签名。
    所以在OnActionExecutingAsync方法中,我们就必须判断,UserId,时间戳,随机数,数字签名是否正常上传,如果没有上传就应该拒绝其进行访问。
    B:但是,我们的Get_Token方法是不会上传这些参数的,所以它要单独拎出来。
		/// <summary>
        ///   在调用操作方法之前发生。
        /// </summary>
        /// <param name="actionContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            AuthenticationModel<JObject> Auth = new AuthenticationModel<JObject>();

            var request = actionContext.Request;
            string method = request.Method.Method;//GE,POST,PUT...

            string userid = String.Empty;//userid
            string timestamp = string.Empty;//提交时间
            string nonce = string.Empty;//随机数
            string signature = string.Empty;//签名

            var Parameter = actionContext.ActionArguments;//参数列表

            var controller = actionContext.ControllerContext.Controller;
            if (request.Headers.Contains("userid")) {
                userid = HttpUtility.UrlDecode(request.Headers.GetValues("userid").FirstOrDefault());
            }
            if (request.Headers.Contains("timestamp"))
            {
                timestamp = HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault());
            }
            if (request.Headers.Contains("nonce"))
            {
                nonce = HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault());
            }
            if (request.Headers.Contains("signature"))
            {
                signature = HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault());
            }

            //GetToken方法不需要进行签名验证
            if (actionContext.ActionDescriptor.ActionName == "Get_Token")
            {
                return base.OnActionExecutingAsync(actionContext, cancellationToken);
            }
        }

8*、判断各参数是否存在后,就要判断各参数是否是正常传入不是被别人截取直接传入了。

  • A:判断各参数是否有效
   if (string.IsNullOrEmpty(userid)  || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))
    {
        Auth.Status = 0;
        Auth.Message = "无权访问";

        HttpResponseMessage httpResponseMessage = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(Auth), System.Text.Encoding.UTF8, "application/json") };

        actionContext.Response = httpResponseMessage;
        return base.OnActionExecutingAsync(actionContext, cancellationToken);
    }
  • B:判断Token是否有效
  		Token token = (Token)HttpRuntime.Cache.Get(userid);
        string signtoken = string.Empty;
        if (token == null)
        {
            Auth.Status = 3;
            Auth.Message = "Token验证失败";
            HttpResponseMessage httpResponseMessage = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(Auth), System.Text.Encoding.UTF8, "application/json") };
            actionContext.Response = httpResponseMessage;
            return base.OnActionExecutingAsync(actionContext, cancellationToken);
        }
        else
        {
            signtoken = token.SignToken.ToString();//Token值
        }
  • C:判断时间戳是否有效
  			double ts1 = 0;
            double ts2 = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;//获取当前日期总毫秒数(时间戳)
            bool timespanvalidate = double.TryParse(timestamp, out ts1);
            double ts = ts2 - ts1;
            bool falg = ts > int.Parse("3000") * 1000;
            if (falg || (!timespanvalidate))
            {
                Auth.Status = 2;
                Auth.Message = "接口访问已过期";

                HttpResponseMessage httpResponseMessage = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(Auth), System.Text.Encoding.UTF8, "application/json") };
                actionContext.Response = httpResponseMessage;
                return base.OnActionExecutingAsync(actionContext, cancellationToken);
            }

9*、当所有参数判断完毕后,就必须要判断签名是否有效了,GET和POST有区别,Get参数是通过URL后缀传输,所以可以将参数进行签名。POST由于API关系参数要么是一个参数,要么就是一个实体对象参数,所以如果要加密就必须将客户端传入的参数进行JSON字符序列化。当然也可以不加密参数,GET请求可以多参数也可以实体对象参数,同样的理解即可,根据需求走。
由于我们采取参数按照ascii升序进行排序,单一参数不需要排序,我们只讲GET多参数请求方法。
下面就是对参数进行序列化排序方法。

		//第一步:取出所有get参数
        IDictionary<string, string> parameters = new Dictionary<string, string>();
        for (int f = 0; f < form.Count; f++)
        {
             string key = form.Keys[f];
             parameters.Add(key, form[key]);
        }

        // 第二步:把字典按Key的字母顺序排序
        IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
        IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();

        // 第三步:把所有参数名和参数值串在一起
        StringBuilder query = new StringBuilder();
        while (dem.MoveNext())
        {
             string key = dem.Current.Key;
             string value = dem.Current.Value;
             if (!string.IsNullOrEmpty(key))
             {
                 query.Append(key).Append(value);
             }
        }
        data = query.ToString();

10*、然后就需要做一个数字签名判断方法了

		/// <summary>
        /// 签名方法
        /// </summary>
        /// <param name="timeStamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        /// <param name="userId">用户ID</param>
        /// <param name="jsonData">参数Json</param>
        /// <param name="signature">传入的签名</param>
        /// <returns></returns>
        private static bool GetSignature(string timeStamp, string nonce, string userId, string signtoken, string jsonData,string signature)
        {
            var md5 = System.Security.Cryptography.MD5.Create();
            //拼接签名数据
            var signStr = timeStamp + nonce + userId + signtoken + jsonData;
            //将字符串中字符按升序排序
            var sortStr = string.Concat(signStr.OrderBy(c => c));
            //需要将字符串转成字节数组
            byte[] buffer = Encoding.Default.GetBytes(sortStr);
            //使用MD5加密
            var md5Val = md5.ComputeHash(buffer);
            //把二进制转化为小写的32进制
            StringBuilder result = new StringBuilder();
            foreach (var c in md5Val)
            {
                result.Append(c.ToString("X2"));
            }
           string strSignature= result.ToString().ToLower();
            if (strSignature == signature) {
                return true;
            }
            else {
                return false;
            }
           
        }

11*、调用此参数判断是否签名有效即可。

	bool result = GetSignature(timestamp, nonce, userid, signtoken, data, signature);
     if (!result)
     {
         Auth.Status = 4;
         Auth.Message = "接口签名验证失败";
         HttpResponseMessage httpResponseMessage = new HttpResponseMessage { Content = new 		StringContent(JsonConvert.SerializeObject(Auth), System.Text.Encoding.UTF8, "application/json") 						};
         actionContext.Response = httpResponseMessage;
    }
    return base.OnActionExecutingAsync(actionContext, cancellationToken);

12*、客户端完成调用即可(JS里进行调用,是无法确保Token不被盗取的,不管你如何加密都不行,除非你自己制作混乱代码和防止别人对你的JS进行获取)。

下一章我会写出客户端调用方法(JS)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值