参考资料:使用ActionFilterAttribute 记录 WebApi Action 请求和返回结果记录
客户端 (注意:SDK是客户端的东西,客户端调用SDK,通过SDK来请求我们的服务端)
什么是sdk,sdk其实就是做最傻瓜化的封装,别人能够很有好的调用你的方法
这里我们做一个客户端像服务端发起http请求的例子,在这个例子中我们封装一个.net的sdK
第一步:创建一个名字叫APISKD的类库
创建一个类:SDKResult.cs
namespace APISKD
{
/// <summary>
/// SDK返回类
/// </summary>
class SDKResult
{
/// <summary>
/// 响应报文体(内容)
/// </summary>
public string Result { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
public HttpStatusCode StatusCode { get; set; }
}
}
创建一个类:SDKClient.cs
namespace APISKD
{
/// <summary>
/// 这个类主要供内部调用,所以是私有的
/// </summary>
class SDKClient
{
private string appKey;
private string appSecret;
private string serverRoot; //网站根目录 例如:http://127.0.0.1:80/api/v1
public SDKClient(string appKey, string appSecret, string serverRoot)
{
this.appKey = appKey;
this.appSecret = appSecret;
this.serverRoot = serverRoot;
}
/// <summary>
/// 异步Get请求
/// </summary>
/// <param name="url">要请求的地址:例如Home/Get</param>
/// <param name="requestParamsDic">请求参数键值对</param>
/// <returns></returns>
public async Task<SDKResult> GetAsync(string url, IDictionary<string, object> requestParamsDic)
{
if (requestParamsDic == null)
{
throw new ArgumentNullException("querystring请求参数不能为null");
}
//对请求参数的key按照升序排序,然后再用等号将key和value连接起来:例如 name=lily
var rpOrderItems = requestParamsDic.OrderBy(r => r.Key).Select(r => r.Key + "=" + r.Value);
var requestParamsStr = string.Join("&", rpOrderItems); //用&号拼接请求参数;例如:age=26&name=lily
//使用经过升序排序后的参数字符串+appSecret 对它进行MD5加密得到签名sign
var sign = Md5Helper.CalcMD5(requestParamsStr + appSecret);
using (HttpClient hc = new HttpClient())
{
hc.DefaultRequestHeaders.Add("AppKey", appKey);//将appKey添加到请求报文头中(做安全验证)
hc.DefaultRequestHeaders.Add("Sign", sign);//将签名添加到请求报文头中(做安全验证)
//请求全路径。例如:http://127.0.0.1:80/api/v1/Home/Get
var requestUrl = Path.Combine(serverRoot, url);
var resp = await hc.GetAsync(requestUrl + "?" + requestParamsStr);//发起异步请求
SDKResult skdResult = new SDKResult();
skdResult.Result = await resp.Content.ReadAsStringAsync(); //请求内容
skdResult.StatusCode = resp.StatusCode; //请求状态码
return skdResult;
}
}
/// <summary>
/// 异步Post请求
/// </summary>
/// <param name="url">要请求的地址:例如Home/Get</param>
/// <param name="requestParamsDic">请求参数键值对</param>
/// <returns></returns>
public async Task<SDKResult> PostAsync(string url, Dictionary<string, string> requestParamsDic)
{
if (requestParamsDic == null)
{
throw new ArgumentNullException("querystring请求参数不能为null");
}
//对请求参数的key按照升序排序,然后再用等号将key和value连接起来:例如 name=lily
var rpOrderItems = requestParamsDic.OrderBy(r => r.Key).Select(r => r.Key + "=" + r.Value);
var requestParamsStr = string.Join("&", rpOrderItems); //用&号拼接请求参数;例如:age=26&name=lily
//使用经过升序排序后的参数字符串+appSecret 对它进行MD5加密得到签名sign
var sign = Md5Helper.CalcMD5(requestParamsStr + appSecret);
using (HttpClient hc = new HttpClient())
{
FormUrlEncodedContent content = new FormUrlEncodedContent(requestParamsDic);
//请求全路径。例如:http://127.0.0.1:80/api/v1/Home/Get
var requestUrl = Path.Combine(serverRoot, url);
var resp = await hc.PostAsync(requestUrl, content);
SDKResult skdResult = new SDKResult();
skdResult.Result = await resp.Content.ReadAsStringAsync();
skdResult.StatusCode = resp.StatusCode; //请求状态码
return skdResult;
}
}
}
}
现在我们就添加一个暴露给外面调用的类,例如 用户操作类,我们暂且给它取名叫UserApi(只是命名中有个api,但是他不是一个api项目)
创建一个类:UserApi.cs (暴露在外,供客户端调用)
namespace APISKD
{
/// <summary>
/// 我们在客户端直接new这个类对象,传入参数,调用方法就可以了
/// </summary>
public class UserApi
{
private string appKey;
private string appSecret;
private string serverRoot;
public UserApi(string appKey, string appSecret, string serverRoot)
{
this.appKey = appKey;
this.appSecret = appSecret;
this.serverRoot = serverRoot;
}
public async Task<long> AddAsync(string phoneNum, string nickName, string password)
{
SDKClient client = new SDKClient(appKey, appSecret, serverRoot);
Dictionary<string, string> data = new Dictionary<string, string>();
data["phoneNum"] = phoneNum;
data["nickName"] = nickName;
data["password"] = password;
var result = await client.PostAsync("User/AddNew", data);
if (result.StatusCode == System.Net.HttpStatusCode.OK)
{
//因为返回的报文体是新增id:{5}
//使用newtonsoft把json格式反序列化为long
long id = JsonConvert.DeserializeObject<long>(result.Result); //需要安装:Newtonsoft.Json
return id;
}
else
{
throw new ApplicationException("新增失败,状态码" + result.StatusCode + ",响应报文" + result.Result);
}
}
}
}
创建一个控制台应用程序:在里面调用我们封装的SDK类库的UserApi类,实现新增数据用户的目的
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
UserApi user = new UserApi("dfdfjpep","fdfjpejfefnmfdhfosh", "http://127.0.0.1:8888/api/v1/"); //初始化
var result= user.AddAsync("18620998800", "tom", "123456").Result;//调用新增类(控制台Main方法不能用async所以这里用了Result)
}
}
}
服务端:WebApi
控制器
服务端中我们就简单的放了一个控制器方法
namespace WebApi.Controllers
{
public class UserController : ApiController
{
public IUsersRepository users { get; set; }
[HttpPost]
public string GetData()
{
return "";
}
}
}
数据防篡改验证过滤器
并在webapi中我们还创建了一个身份验证的过滤器
namespace WebApi
{
public class MyAuthenticationAttribute : IAuthorizationFilter//也可以直接继承AuthorizationFilterAttribute
{
public IAppInfosRepository app { get; set; }
public bool AllowMultiple => true;
public async Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
//获得报文头中的AppKey和Sign (我们与客户端约定,在向服务端发起请求的时候,将AppKey和Sign放到请求报文头中)
IEnumerable<string> appKeys;
if (!actionContext.Request.Headers.TryGetValues("AppKey", out appKeys)) //从请求报文头中获取AppKey
{
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("报文头中的AppKey为空") };
}
}
IEnumerable<string> signs;
if (!actionContext.Request.Headers.TryGetValues("Sign", out signs)) //从请求报文头中获取Sign
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("报文头中的Sign为空") };
}
string appKey = appKeys.First();
string sign = signs.First();
var appInfo = await app.GetByAppKeyAsync(appKey);//从数据库获取appinfo这条数据(获取AppKey,AppSecret信息)
if (appInfo == null)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("不存在的AppKey") };
}
if (appInfo.IsEnable == "true")
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { Content = new StringContent("AppKey已经被封禁") };
}
string requestDataStr = ""; //请求参数字符串
List<KeyValuePair<string, string>> requestDataList = new List<KeyValuePair<string, string>>();//请求参数键值对
if (actionContext.Request.Method == HttpMethod.Post) //如果是Post请求
{
//获取Post请求数据
requestDataStr = GetRequestValues(actionContext);
if (requestDataStr.Length > 0)
{
string[] requestParamsKv = requestDataStr.Split('&');
foreach (var item in requestParamsKv)
{
string[] pkv = item.Split('=');
requestDataList.Add(new KeyValuePair<string, string>(pkv[0], pkv[1]));
}
//requestDataList就是按照key(参数的名字)进行排序的请求参数集合
requestDataList = requestDataList.OrderBy(kv => kv.Key).ToList();
var segments = requestDataList.Select(kv => kv.Key + "=" + kv.Value);//拼接key=value的数组
requestDataStr = string.Join("&", segments);//用&符号拼接起来
}
}
if (actionContext.Request.Method == HttpMethod.Get) //如果是Get请求
{
//requestDataList就是按照key(参数的名字)进行排序的请求参数集合
requestDataList = actionContext.Request.GetQueryNameValuePairs().OrderBy(kv => kv.Key).ToList();
var segments = requestDataList.Select(kv => kv.Key + "=" + kv.Value);//拼接key=value的数组
requestDataStr = string.Join("&", segments);//用&符号拼接起来
}
//计算Sign (即:计算requestDataStr+AppSecret的md5值)
string computedSign = MD5Helper.ComputeMd5(requestDataStr + appInfo.AppSecret);
//用户传进来md5值和计算出来的比对一下,就知道数据是否有被篡改过
if (sign.Equals(computedSign, StringComparison.CurrentCultureIgnoreCase))
{
return await continuation();
}
else
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new StringContent("sign验证失败") };
}
}
/// <summary>
/// 获取Post请求的请求参数内容
/// 参考资料:https://www.cnblogs.com/hnsongbiao/p/7039666.html
/// </summary>
/// <param name="actionContext"></param>
/// <returns></returns>
public string GetRequestValues(HttpActionContext actionContext)
{
Stream stream = actionContext.Request.Content.ReadAsStreamAsync().Result;
Encoding encoding = Encoding.UTF8;
/*
这个StreamReader不能关闭,也不能dispose, 关了就傻逼了
因为你关掉后,后面的管道 或拦截器就没办法读取了
所有这里不要用using
using (StreamReader reader = new StreamReader(stream, System.Text.Encoding.UTF8))
{
result = reader.ReadToEnd().ToString();
}
*/
var reader = new StreamReader(stream, encoding);
string result = reader.ReadToEnd();
/*
这里也要注意: stream.Position = 0;
当你读取完之后必须把stream的位置设为开始
因为request和response读取完以后Position到最后一个位置,交给下一个方法处理的时候就会读不到内容了。
*/
stream.Position = 0;
return result;
}
}
}
这里提供一个JWT辅助类
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using UserCenter.NETSDK;
namespace IM.Web
{
public class JWTHelper
{
private static readonly string jwt_secret;
static JWTHelper()
{
jwt_secret = ConfigurationManager.AppSettings["JWT_Secret"];
}
/// <summary>
/// 把user加密放到JWT字符串中
/// </summary>
/// <param name="user"></param>
/// <returns>JWT字符串</returns>
public static string Encrypt(User user)
{
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(user, jwt_secret);
return token;
}
/// <summary>
/// 从JWT token中解密出来User
/// </summary>
/// <param name="token"></param>
/// <returns>如果token错误、被篡改或者过期,则返回null</returns>
public static User Decrypt(string token)
{
try
{
IJsonSerializer serializer = new JsonNetSerializer();
IDateTimeProvider provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
var user = decoder.DecodeToObject<User>(token, jwt_secret, verify: true);
return user;
}
catch (TokenExpiredException)
{
return null;
}
catch (SignatureVerificationException)
{
return null;
}
}
/// <summary>
/// 获得当前登录用户的User(供asp.net mvc用)
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public static User GetUser(HttpContextBase httpContext)
{
var cookie = httpContext.Request.Cookies["JWTToken"];
if(cookie==null)
{
return null;
}
string token = cookie.Value;
return Decrypt(token);
}
/// <summary>
/// 获得当前登录用户的User(供SignalR的Hub用)
/// </summary>
/// <param name="hubContext"></param>
/// <returns></returns>
public static User GetUser(HubCallerContext hubContext)
{
if(!hubContext.RequestCookies.ContainsKey("JWTToken"))
{
return null;
}
string token = hubContext.RequestCookies["JWTToken"].Value;
return Decrypt(token);
}
}
}