WebApi基于Token和签名的验证
最近一段时间在学习WebApi,涉及到验证部分的一些知识觉得自己并不是太懂,所以来博客园看了几篇博文,发现一篇讲的特别好的,读了几遍茅塞顿开(都闪开,我要装逼了),刚开始读有些地方不理解,所以想了很久,因此对原文中省略的部分这里做一点个人的理解和补充,非常基础,知道的园友就不需要了,只是帮助初次学习的园友理解。原文传送门:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
本篇博文中的所有代码均来自上述链接,如果你觉得有帮助,请点击链接给原文大牛一个推荐,开搞!!
1.基于Token令牌 + 签名的验证思路梳理
客户端首先向服务端请求Token令牌,客户获取Token后计算对应的签名。签名由时间戳、随机数、Token令牌、参数拼接字符串四部分组成,客户端发送请求的时候需要带上对应的身份ID、时间戳、随机数和计算出的签名。
服务端过滤器拦截请求,验证请求参数的合法性、是否过期,Token令牌是否合法、是否过期,全部通过后重新计算签名,与传递的签名参数对比,一致则执行对应的Api请求,否则返回错误消息。如果服务端计算的签名与传递的参数签名不一样,请求不合法(可能被篡改),为什么这么说呢,因为客户端与服务端拥有相同的签名计算方式,如果请求被修改,那么服务端计算的签名肯定与客户端计算的签名肯定不一致。
1.1 客户端请求Token令牌流程
客户端请求Token的凭证是对应的身份ID,当然可以是其他的,这里假设用的是身份ID。
(1)首先客户端向服务端发送获取Token令牌的请求,这个Token令牌是一个GUID码,生成后服务端会将其存在缓存中,当再次请求的时候会先从缓存中查找。请求Token令牌的代码:
public static ProductResultMsg.TokenResultMsg GetSignToken(int staffId)
{
string tokenApi = AppSettingsConfig.GetTokenApi;
Dictionary<string, string> parames = new Dictionary<string, string>();
parames.Add("staffid", staffId.ToString());
Tuple<string, string> parameters = GetQueryString(parames);
ProductResultMsg.TokenResultMsg token = WebApiHelper.Get<ProductResultMsg.TokenResultMsg>
(tokenApi, parameters.Item1, staffId.ToString(), staffId, false);
return token;
}
代码解释:
1.tokenApi是配置在webConfig中的接口Url
2.Parames字典对象用来封装参数的,因为请求Token时可能不止一个参数。
3.Parameter:是元组类型,元组可以承载任何的数据类型,这里用来接收GetQueryString方法返回的拼接字符串
4.token:客户端用来承载接口返回的Token令牌的类的实例,TokenResultMSg结构如下:
public class TokenResultMsg : HttpResponseMsg
{
public Tokens Result
{
get
{
if (StatusCode == (int)StatusCodeEnum.Success)
{
return JsonConvert.DeserializeObject<Tokens>(Data.ToString());
}
return null;
}
}
}
可以看到这个类实际是封装了一个Token类实例,因为Api接口返回的是Json数据类型,所以这里进行了反序列化。Token类结构在最上面,里面包含身份ID、 Token令牌、 过期时间三个属性,最后返回包含Token令牌的Token类给主程序。Token类结构如下:
public class Tokens
{
/// <summary>
/// 用户名
/// </summary>
public int StaffId { get; set; } /// <summary>
/// 用户名对应签名Token
/// </summary>
public Guid SignToken { get; set; } /// <summary>
/// Token过期时间
/// </summary>
public DateTime ExpireTime { get; set; }
}
(2)上面调用了GetQueryString方法,这个方法是用来整理参数以及参数的拼接字符串,参数用来随URL传递,参数的拼接字符串用来生成对应的Sign签名,方法如下:
public static Tuple<string, string> GetQueryString(Dictionary<string, string> parames)
{
//第一步:把字典按key的字母顺序排序
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parames);
IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator(); //第二部 把所有的名字和参数值串在一起
StringBuilder query = new StringBuilder("");//签名字符串
StringBuilder queryStr = new StringBuilder("");//url参数
if (parames == null || parames.Count == )
{
return new Tuple<string, string>("", "");
} while (dem.MoveNext())
{
string key = dem.Current.Key;
string value = dem.Current.Value;
if (!string.IsNullOrEmpty(key))
{
query.Append(key).Append(value);
queryStr.Append("&").Append(key).Append("=").Append(value);
}
}
return new Tuple<string, string>(query.ToString(), queryStr.ToString().Substring(, queryStr.Length - ));
}
(3)此时Url以及参数已经准备完成,之后调用了 Get 方法,发送获取Token的请求到WepApi接口,这里描述一下思路,请求的Get方法你可以做成单独的,也可以做成公共的,公共的是什么意思呢,就是这个Get方法不止可以用来请求Token令牌,请求的参数视安全程度决定,这里为了演示只传递了一个身份ID,在实际的操作过程中可以传递更多的验证数据,Get方法代码如下:(对于后台请求Api接口的方式这里就不做详解了)
public static T Get<T>(string webApi, string query, string queryStr, int staffId, bool sign = true)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(webApi + "/" + queryStr);
string timeStamp = GetTimeStamp();
string nonce = GetRandom(); //加入头信息
request.Headers.Add("staffid", staffId.ToString());//当前请求用户的Staffid
request.Headers.Add("timestamp", timeStamp);//发起请求的时间戳(单位:毫秒)
request.Headers.Add("nonce", nonce);//发起请求的随机数 if (sign)
request.Headers.Add("signature", GetSignature(timeStamp, nonce, staffId, query));//当前请求内容的数字签名
request.Method = "GET";
request.ContentType = "application/json";
request.Timeout = ;
request.Headers.Set("Pragma", "no-cache");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream streamReceive = response.GetResponseStream();
StreamReader streamReader = new StreamReader(streamReceive, Encoding.UTF8);
string strResult = streamReader.ReadToEnd(); streamReader.Close();
streamReceive.Close();
request.Abort();
response.Close(); return JsonConvert.DeserializeObject<T>(strResult);
}
方法中有两个地方需要在这里说一下,因为博主当时第一次看的时候没注意,那就是时间戳和随机数的生成,代码如下:
/// 获得时间戳
/// </summary>
/// <returns></returns>
private static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(, , , , , , );
return Convert.ToInt64(ts.TotalMilliseconds).ToString();
}
/// <summary>
/// 获取随机数
/// </summary>
/// <returns></returns>
private static string GetRandom()
{
Random rd = new Random(DateTime.Now.Millisecond);
int i = rd.Next(, int.MaxValue);
return i.ToString();
}
(4)当代码执行到HttpWebResponse response = (HttpWebResponse)request.GetResponse();时客户端发送请求发到Api接口,此时我们来看Api接口收到请求的处理程序:
public class ServiceController : ApiController
{
[HttpGet]
public HttpResponseMessage GetToken(string id)
{
string staffId = id;
ResultMsg resultMsg = null;
int ID = ; //判断参数是否合法
if (string.IsNullOrEmpty(staffId) || (!int.TryParse(staffId, out ID)))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
string returnErrJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg);
return new HttpResponseMessage { Content = new StringContent(returnErrJson, System.Text.Encoding.UTF8) };
}
//插入缓存
Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString());
if (HttpRuntime.Cache.Get(id.ToString()) == null)
{
token = new Tokens();
token.StaffId = ID;
token.SignToken = Guid.NewGuid();
token.ExpireTime = DateTime.Now.AddDays();
HttpRuntime.Cache.Insert(token.StaffId.ToString(), token, null, token.ExpireTime, TimeSpan.Zero);
} //返回Token信息
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.Success;
resultMsg.Info = "";
resultMsg.Data = token;
string returnJson = Newtonsoft.Json.JsonConvert.SerializeObject(resultMsg);
return new HttpResponseMessage { Content = new StringContent(returnJson, System.Text.Encoding.UTF8) };
}
代码分析:
服务端收到请求后,接受传过来的身份ID,首先判断ID是否为空以及是否合法以及其它一些验证,如果有什么地方不合法,就返回错误信息到客户端。
如果身份ID符合所有验证,则根据ID去缓存中查询对应的Token令牌,如果缓存中没有,则新生成对应身份ID的Token令牌,令牌是一段GUID码,生成之后存入缓存中。
最后,返回一个包含Token的 ResultMsg实体,记得转换为Json数据类型,ResultMsg类结构如下,Data属性是object类型,用来承载Token类对象:
public class ResultMsg
{
/// <summary>
/// 状态码
/// </summary>
public int StatusCode { get; set; } /// <summary>
/// 操作信息
/// </summary>
public string Info { get; set; } /// <summary>
/// 返回数据
/// </summary>
public object Data { get; set; }
}
到此,Token令牌就返回到了客户端,这个过程的执行状态可以根据实际情况决定,比如是请求一次之后将Token令牌存在客户端,或者是每次发送请求到客户端都需要请求一次Token令牌。
1.2 发送数据请求完整描述
在客户端得到Token令牌之后每次请求都需要带上它,当然,只有Token令牌还是不够的,还需要编码的签名。签名由4个部分组成:时间戳、随机数、Token令牌、数据参数,编码的方式如下,首先将这四部分进行拼接字符串,然后将字符串中字符按照升序排序,之后转换为二进制数据流,然后进行MD5哈希加密,MD5是哈希加密的一种,接着循环遍历加密后的二进制字节流,这个时候字节流的长度时128位的,为了使用方便和节约网络传输流量我们需要把它转化为16进制的字符串,最后将所有的字符串转换为大写。至此,加密签名完成。生成Signature签名的代码如下:
public static string GetSignature(string timeStamp, string nonce, int staffId, string data)
{
Tokens token = null;
var resultMsg = GetSignToken(staffId);
if (resultMsg != null)
{
if (resultMsg.StatusCode == (int)StatusCodeEnum.Success)
{
token = resultMsg.Result;
}
else
{
throw new Exception(resultMsg.Data.ToString());
}
}
else
{
throw new Exception("token为null,员工编号为:" + staffId);
} var hash = System.Security.Cryptography.MD5.Create();
//拼接签名数据
var signStr = timeStamp + nonce + staffId + token.SignToken.ToString() + data;
//将字符串中的字符按升序排序
var sortStr = string.Concat(signStr.OrderBy(c => c));
var bytes = Encoding.UTF8.GetBytes(sortStr);
//使用MD5加密
var md5Val = hash.ComputeHash(bytes);
//把二进制转化为大写的十六进制
StringBuilder result = new StringBuilder();
foreach (var v in md5Val)
{
result.Append(v.ToString("X2"));
}
return result.ToString().ToUpper();
}
在发送数据请求的时候,需要传递四个参数,时间戳(用来判断请求是否过期)、随机数(用来强化请求的安全性)、身份ID(服务端用来查询对应的Token令牌)、Signature签名。
那么服务端应该怎么验证用户的请求是否合法呢?服务端通过一个全局的过滤器(Filter)来拦截所有的客户端请求(不包含请求Token令牌),过滤器拦截到请求后首先判断请求方式,根据不同的请求方式获取请求中所有的参数(比如Get是QueryString,Post是InputStream),然后通过参数名称(key)得到参数的值(value),然后对参数进行相应的验证(是否为空或null),通过TimeSpan时间戳判断请求是否过期,如果过期则返回对应的错误信息;通过身份ID查询缓存中的Token令牌,与参数中令牌对比是否正确,如果验证都通过则对最后的签名做验证。
服务端验证签名的方式是这样的,使用与客户端计算签名同样的算法,重新计算签名字符串,然后与请求中的前民字符串做对比,这里有个问题是,我在注册为全局过滤器的时候,系统提示需要实现System.Web.Mvc中四个接口中的一个,但是原文中实现的是System.Web.Http命名空间下的标签类,博主有点迷糊了,如果有懂得园友大牛可以指点一下,服务端过滤器代码如下:
public class ApiSecurityFilter:ActionFilterAttribute,IActionFilter
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
ResultMsg resultMsg = null;
var request = actionContext.Request;
string method = request.Method.Method;
string staffid = String.Empty, timestamp = string.Empty, nonce = string.Empty, signature = string.Empty;
int id = ; if (request.Headers.Contains("staffid"))
{
staffid = HttpUtility.UrlDecode(request.Headers.GetValues("staffid").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 == "GetToken")
{
if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id)) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
return;
}
else
{
base.OnActionExecuting(actionContext);
}
} //判断请求头是否包含以下参数
if (string.IsNullOrEmpty(staffid) || (!int.TryParse(staffid, out id)) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) || string.IsNullOrEmpty(signature))
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.ParameterError;
resultMsg.Info = StatusCodeEnum.ParameterError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
} //判断timespan是否有效
double ts1 = ;
double ts2 = (DateTime.UtcNow - new DateTime(, , , , , , )).TotalMilliseconds;
bool timespanvalidate = double.TryParse(timestamp, out ts1);
double ts = ts2 - ts1;
bool falg = ts > int.Parse(WebSettingsConfig.UrlExpireTime) * ;
if (falg || (!timespanvalidate))
{
//此时说明时间戳已过期
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.URLExpireError;
//错误信息
resultMsg.Info = StatusCodeEnum.URLExpireError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
} //判断Token是否有效
Tokens token = (Tokens)HttpRuntime.Cache.Get(id.ToString());
string signtoken = string.Empty;
if (HttpRuntime.Cache.Get(id.ToString()) == null)
{
//说明身份ID对应的token令牌不存在
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.TokenInvalid;
resultMsg.Info = StatusCodeEnum.TokenInvalid.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
else
{
//缓存中存在ID对应的 Token
signtoken = token.SignToken.ToString();
} //根据请求类型拼接参数
NameValueCollection form = HttpContext.Current.Request.QueryString;
string data = string.Empty;
switch (method)
{
case "POST":
Stream stream = HttpContext.Current.Request.InputStream;
string responseJson = string.Empty;
StreamReader streamReader = new StreamReader(stream);
data = streamReader.ReadToEnd();
break;
case "GET":
//第一步:取出所有的get参数
IDictionary<string, string> parameters = new Dictionary<string, string>();
for (int f = ; 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();
break; default://两者都不是返回错误信息
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.HttpMehtodError;
resultMsg.Info = StatusCodeEnum.HttpMehtodError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
bool result = SignExtension.Validate(timestamp, nonce, id, signtoken, data, signature); if (!result)
{
resultMsg = new ResultMsg();
resultMsg.StatusCode = (int)StatusCodeEnum.HttpRequestError;
resultMsg.Info = StatusCodeEnum.HttpRequestError.GetEnumText();
resultMsg.Data = "";
actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(resultMsg), System.Text.Encoding.GetEncoding("UTF-8"), "application/json") };
base.OnActionExecuting(actionContext);
return;
}
else
{
base.OnActionExecuting(actionContext);
}
}
}
如果服务端与客户端签名也一致,所有验证通过,根据请求执行对应的Api方法,返回结果。这里就不写代码了,再次奉上原文大牛连接:
http://www.cnblogs.com/MR-YY/p/5972380.html#!comments
如果你觉得有帮助,请给原文大牛一个推荐,谢谢。
WebApi基于Token和签名的验证的更多相关文章
- 在ASP.NET Web API 2中使用Owin基于Token令牌的身份验证
		
基于令牌的身份验证 基于令牌的身份验证主要区别于以前常用的常用的基于cookie的身份验证,基于cookie的身份验证在B/S架构中使用比较多,但是在Web Api中因其特殊性,基于cookie的身份 ...
 - ASP.NET Core WebApi基于JWT实现接口授权验证
		
一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...
 - 基于token的后台身份验证(转载)
		
几种常用的认证机制 HTTP Basic Auth HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RES ...
 - Springboot token令牌验证解决方案 在SpringBoot实现基于Token的用户身份验证
		
1.首先了解一下Token 1.token也称作令牌,由uid+time+sign[+固定参数]组成: uid: 用户唯一身份标识 time: 当前时间的时间戳 sign: 签名, 使用 hash/e ...
 - 从零开始的SpringBoot项目 ( 八 ) 实现基于Token的用户身份验证
		
1.首先了解一下Token uid: 用户唯一身份标识 time: 当前时间的时间戳 sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接 固定参数(可 ...
 - WebApi 基于token的多平台身份认证架构设计
		
1 概述 在存在账号体系的信息系统中,对身份的鉴定是非常重要的事情. 随着移动互联网时代到来,客户端的类型越来越多, 逐渐出现了 一个服务器,N个客户端的格局 . 不同的客户端产生了不同的用户使用 ...
 - 基于 Token 的身份验证:JSON Web Token(附:Node.js 项目)
		
最近了解下基于 Token 的身份验证,跟大伙分享下.很多大型网站也都在用,比如 Facebook,Twitter,Google+,Github 等等,比起传统的身份验证方法,Token 扩展性更强, ...
 - ASP.NET WebApi 基于OAuth2.0实现Token签名认证
		
一.课程介绍 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将是我们需要思考的问题.为了保护我们的WebApi数 ...
 - ASP.NET WebApi 基于JWT实现Token签名认证
		
一.前言 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将会是需要思考的问题.在ASP.NET WebServi ...
 
随机推荐
- setAttribute()
			
●节点分为不同的类型:元素节点.属性节点和文本节点等. ●getElementById()方法将返回一个对象,该对象对应着文档里的一个特定的元素节点. ●getElementsByTagNam ...
 - Jquery 搭配 css 使用,简单有效
			
前几篇博客中讲了Jquery的基础和点击实际,下面来说一下和css搭配着来怎么做 还是和往常一样,举个例子 好几个方块,然后设置颜色 <!DOCTYPE html PUBLIC "-/ ...
 - Asp.net MVC 传递数据 从前台到后台,包括单个对象,多个对象,集合
			
今天为大家分享下 Asp.net MVC 将数据从前台传递到后台的几种方式. 环境:VS2013,MVC5.0框架 1.基本数据类型 我们常见有传递 int, string, bool, double ...
 - Xshell 连接CentOS服务器解密
			
平台之大势何人能挡? 带着你的Net飞奔吧!http://www.cnblogs.com/dunitian/p/4822808.html Xshell生成密钥key(用于Linux 免密码登录)htt ...
 - nginx的使用
			
1.nginx的下载 解压后文件目录: 2.nginx的常用命令 nginx -s stop 强制关闭 nginx -s quit 安全关闭 nginx -s reload 改变配置文件的时候,重 ...
 - Ubuntu 16.10 开启PHP错误提示
			
两个步骤: 修改php.ini配置文件中的error_reporting 和 display_errors两地方内容: sudo vim /etc/php/7.0/apache2/php.ini er ...
 - 动手做第一个Chrome插件
			
Chrome插件是令人惊讶的简单,一旦你弄懂它的工作和实现原理.它是由一部分HTML,一部分Js,然后混合了一个叫做manifest.json的Json文件组合而成的整体.这意味着你可以使用你最擅长的 ...
 - 【走过巨坑】android studio对于jni调用及运行闪退无法加载库的问题解决方案
			
相信很多小伙伴都在android开发中遇到调用jni的各种巨坑,因为我们不得不在很多地方用到第三方库so文件,然而第三方官方通常都只会给出ADT环境下的集成方式,而谷歌亲儿子android studi ...
 - Java 时间类-Calendar、Date、LocalDate/LocalTime
			
1.Date 类 java.util.Date是一个"万能接口",它包含日期.时间,还有毫秒数,如果你只想用java.util.Date存储日期,或者只存储时间,那么,只有你知道哪 ...
 - 显示本地openssl支持的加密算法
			
参考页面: http://www.yuanjiaocheng.net/webapi/parameter-binding.html http://www.yuanjiaocheng.net/webapi ...