在上篇随笔《Web API应用架构设计分析(1)》,我对Web API的各类应用架构进行了归纳性的分析和设计,Web API 是一种应用接口框架,它可以构建HTTP服务以支撑更普遍的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,本篇继续这个主题,介绍如何利用ASP.NET Web API 来设计Web API层以及相关的调用处理。html
Web API接口的访问方式,大概能够分为几类:git
1)一个是使用用户令牌,经过Web API接口进行数据访问。这种方式,能够有效识别用户的身份,为用户接口返回用户相关的数据,如包括用户信息维护、密码修改、或者用户联系人等与用户身份相关的数据。github
2)一种是使用安全签名进行数据提交。这种方式提交的数据,URL链接的签名参数是通过安全必定规则的加密的,服务器收到数据后也通过一样规则的安全加密,确认数据没有被中途篡改后,再进行数据修改处理。所以咱们能够为不一样接入方式,如Web/APP/Winfrom等不一样接入方式指定不一样的加密秘钥,可是秘钥是双方约定的,并不在网络链接上传输,链接传输的通常是这个接入的AppID,服务器经过这个AppID来进行签名参数的加密对比,这种方式,相似微信后台的回调处理机制,它们就是通过这样的处理。web
3)一种方式是提供公开的接口调用,不须要传入用户令牌、或者对参数进行加密签名的,这种接口通常较少,只是提供一些很常规的数据显示而已。json
下面图示就是这几种接入方式的说明和大概应用场景。api
首先咱们为用户注册的时候,须要由咱们承认的终端发起,也就是它们须要进行安全签名,后台确认签名有效性,才能正常实现用户注册,不然遭到伪造数据,系统就失去原有的意义了。浏览器
/// <summary> /// 注册用户信息接口 /// </summary> public interface IUserApi { /// <summary> /// 注册用户处理,包括用户名,密码,身份证号,手机等信息 /// </summary> /// <param name="json">注册用户信息</param> /// <param name="signature">加密签名字符串</param> /// <param name="timestamp">时间戳</param> /// <param name="nonce">随机数</param> /// <param name="appid">应用接入ID</param> /// <returns></returns> ResultData Add(UserJson json, string signature, string timestamp, string nonce, string appid); }
其实咱们得到用户的令牌,也是须要进行用户安全签名认证的,这样咱们才有效保证用户身份令牌获取的合法性。缓存
/// <summary> /// 系统认证等基础接口 /// </summary> public interface IAuthApi { /// <summary> /// 注册用户获取访问令牌接口 /// </summary> /// <param name="username">用户登陆名称</param> /// <param name="password">用户密码</param> /// <param name="signature">加密签名字符串</param> /// <param name="timestamp">时间戳</param> /// <param name="nonce">随机数</param> /// <param name="appid">应用接入ID</param> /// <returns></returns> TokenResult GetAccessToken(string username, string password, string signature, string timestamp, string nonce, string appid); }
上面介绍到的参数,咱们说起了几个参数,一个是加密签名字符串,一个是时间戳,一个是随机数,一个是应用接入ID,咱们通常的处理规则以下所示。安全
1)Web API 为各类应用接入,如APP、Web、Winform等接入端分配应用AppID以及通讯密钥AppSecret,双方各自存储。
2)接入端在请求Web API接口时需携带如下参数:signature、 timestamp、nonce、appid,签名是根据几个参数和加密秘钥生成。
3) Web API 收到接口调用请求时需先检查传递的签名是否合法,验证后才调用相关接口。服务器
加密签名在服务端(Web API端)的验证流程参考微信的接口的处理方式,处理逻辑以下所示。
1)检查timestamp 与系统时间是否相差在合理时间内,如10分钟。
2)将appSecret、timestamp、nonce三个参数进行字典序排序
3)将三个参数字符串拼接成一个字符串进行SHA1加密
4)加密后的字符串可与signature对比,若匹配则标识该次请求来源于某应用端,请求是合法的。
C#端代码校验以下所示。
/// <summary> /// 检查应用接入的数据完整性 /// </summary> /// <param name="signature">加密签名内容</param> /// <param name="timestamp">时间戳</param> /// <param name="nonce">随机字符串</param> /// <param name="appid">应用接入Id</param> /// <returns></returns> public CheckResult ValidateSignature(string signature, string timestamp, string nonce, string appid) { CheckResult result = new CheckResult(); result.errmsg = "数据完整性检查不经过"; //根据Appid获取接入渠道的详细信息 AppInfo channelInfo = BLLFactory<App>.Instance.FindByAppId(appid); if (channelInfo != null) { #region 校验签名参数的来源是否正确 string[] ArrTmp = { channelInfo.AppSecret, timestamp, nonce }; Array.Sort(ArrTmp); string tmpStr = string.Join("", ArrTmp); tmpStr = FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1"); tmpStr = tmpStr.ToLower(); if (tmpStr == signature && ValidateUtil.IsNumber(timestamp)) { DateTime dtTime = timestamp.ToInt32().IntToDateTime(); double minutes = DateTime.Now.Subtract(dtTime).TotalMinutes; if (minutes > timspanExpiredMinutes) { result.errmsg = "签名时间戳失效"; } else { result.errmsg = ""; result.success = true; result.channel = channelInfo.Channel; } } #endregion } return result; }
一旦咱们完成对安全签名进行成功认证,也就是咱们对数据提交的来源和完整性进行了确认,就能够进行更多和安全性相关的操做了,如获取用户的访问令牌信息的操做以下所示。
第一步是验证用户的签名是否符合要求,符合要求后进行用户信息的比对,并生成用户访问令牌数据JSON,返回给调用端便可。
经过上面的接口,咱们获取到的用户访问令牌,之后和用户相关的信息调用,咱们就能够经过这个令牌参数进行传递就能够了,这个令牌带有用户的一些基础信息,如用户ID,过时时间等等,这个Token的设计思路来源于JSON Web Token (JWT),具体能够参考http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html,以及GitHub上的项目https://github.com/jwt-dotnet/jwt。
因为Web API的调用,都是一种无状态方式的调用方式,咱们经过token来传递咱们的用户信息,这样咱们只须要验证Token就能够了。
JWT的令牌生成逻辑以下所示
令牌生成后,咱们须要在Web API调用处理前,对令牌进行校验,确保令牌是正确有效的。
检查的代码,就是把令牌生成的过程逆反过来,获取相应的信息,而且对令牌签发的时间进行有效性判断,通常能够约定一个失效时间,如1天或者7天,也不用设置过短。
/// <summary> /// 检查用户的Token有效性 /// </summary> /// <param name="token"></param> /// <returns></returns> public CheckResult ValidateToken(string token) { //返回的结果对象 CheckResult result = new CheckResult(); result.errmsg = "令牌检查不经过"; if (!string.IsNullOrEmpty(token)) { try { string decodedJwt = JsonWebToken.Decode(token, sharedKey); if (!string.IsNullOrEmpty(decodedJwt)) { #region 检查令牌对象内容 dynamic root = JObject.Parse(decodedJwt); string username = root.name; string userid = root.iss; int jwtcreated = (int)root.iat; //检查令牌的有效期,7天内有效 TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1)); int timestamp = (int)t.TotalDays; if (timestamp - jwtcreated > expiredDays) { throw new ArgumentException("用户令牌失效."); } //成功校验 result.success = true; result.errmsg = ""; result.userid = userid; #endregion } } catch (Exception ex) { LogTextHelper.Error(ex); } } return result; }
通常来讲,访问令牌不能永久有效,对于访问令牌的从新更新问题,能够设置一个规则,只容许最新的令牌使用,并把它存储在接口缓存里面进行对比,应用系统退出的时候,就把内存里面的Token移除就能够了。
上面咱们定义了通常的Web API接口,以及实现相应的业务实现,若是咱们须要建立Web API层,还须要构建一个Web API项目的。
建立好相应的项目后,能够为项目添加一个Web API基类,方便控制共同的接口。
而后咱们就能够在Controller目录上建立更多的应用API控制器了。
最后咱们为了统一全部的API接口都是返回JSON方式,咱们须要对WebApiConfig里面的代码进行设置下。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服务 config.SetCorsPolicyProviderFactory(new CorsPolicyFactory()); config.EnableCors(); // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { action = "post", id = RouteParameter.Optional } ); // Remove the JSON formatter //config.Formatters.Remove(config.Formatters.JsonFormatter); // Remove the XML formatter config.Formatters.Remove(config.Formatters.XmlFormatter); } }
接下来咱们要作的就是须要增长业务接口,以便进行具体的测试了,建议使用Winform项目,对每一个接口进行一个测试,或者也能够考虑使用单元测试的方式,看我的喜爱吧。
例如咱们若是要测试用户登录的接口的话,咱们的测试代码以下所示。
/// <summary> /// 生成签名字符串 /// </summary> /// <param name="appSecret">接入秘钥</param> /// <param name="timestamp">时间戳</param> /// <param name="nonce">随机数</param> private string SignatureString(string appSecret, string timestamp, string nonce) { string[] ArrTmp = { appSecret, timestamp, nonce }; Array.Sort(ArrTmp); string tmpStr = string.Join("", ArrTmp); tmpStr = FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1"); return tmpStr.ToLower(); } private TokenResult GetTokenResult() { string timestamp = DateTime.Now.DateTimeToInt().ToString(); string nonce = new Random().NextDouble().ToString(); string signature = SignatureString(appSecret, timestamp, nonce); string appended = string.Format("&signature={0}×tamp={1}&nonce={2}&appid={3}", signature, timestamp, nonce, appId); string queryUrl = url + "Auth/GetAccessToken?username=test&password=123456" + appended; HttpHelper helper = new HttpHelper(); string token = helper.GetHtml(queryUrl); Console.WriteLine(token); TokenResult tokenResult = JsonConvert.DeserializeObject<TokenResult>(token); return tokenResult; }
若是咱们已经得到了令牌,咱们根据令牌传递参数给链接,并获取其余数据的测试处理代码以下所示。
//获取访问令牌 TokenResult tokenResult = GetTokenResult(); string queryUrl = url + "/Contact/get?token=" + tokenResult.access_token; HttpHelper helper = new HttpHelper(); string result = helper.GetHtml(queryUrl); Console.WriteLine(result);
若是须要POST数据的话,那么调用代码以下所示。
//使用POST方式 var data = new { name = "张三", certno = "123456789", }; var postData = data.ToJson(); queryUrl = url + "/Contact/Add?token=" + tokenResult.access_token; helper = new HttpHelper(); helper.ContentType = "application/json"; result = helper.GetHtml(queryUrl, postData, true); Console.WriteLine(result);
Web API后台,会自动把POST的JSON数据转换为对应的对象的。
若是是GET方式,咱们可能能够直接经过浏览器进行调试,若是是POST方式,咱们须要使用一些协助工具,如Fiddler等处理工具,可是最好的方式是本身根据须要弄一个测试工具,方便测试。
如下就是我为了本身Web API 接口开发的须要,专门弄的一个调试工具,能够自动组装相关的参数,包括使用安全签名的参数,还能够把全部参数数据进行存储。