之前写了一个接口程序,只是利用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重写OnActionExecutingAsync和OnActionExecutedAsync方法
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.cs类Register方法中加入我们刚刚写的过滤器
//过滤器
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进行获取)。