Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端(包括浏览器,手机和平板电脑等移动设备)的框架,
ASP.NET Web API 是一种用于在 .NET Framework 上构建 RESTful 应用程序的理想平台。本文主要以ASP.NET
Web API 的框架实现来介绍整个Web API应用架构设计,但不局限于.NET的技术。
1、Web API的核心层设计
在目前发达的应用场景下,我们往往需要接入Winform客户端、APP程序、网站程序、以及目前热火朝天的微信应用等,这些数据应该可以由同一个服务提供,这个就是我们所需要构建的Web
API平台,基于上述的需求,很多企业的需求都是以Web API优先的理念来设计整个企业应用体系的。Web
API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。
从上图我们可以看到,整个外围的应用场景围绕着Web API核心层构建,如果我们把它换一种方式表达,那么也就是下面的设计图示,我们把微信应用、APP应用、Web应用、Winform应用,作为Web
API接口层上面的一个界面应用层来处理,这样就是基于一个API接口层,接入多个或多种方式的界面应用层,从而构建企业丰富的数据服务应用。
由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,即使为了利用各自应用的特殊性,也可以把这些应用做的很相似,这样就给用户提供了一个统一的界面表示方式,极大提高客户使用的界面体验效果,用户几乎不需要额外的界面学习,就可以熟悉整个应用体系的各个模块使用。
在整个WebAPI下面可以通过业务逻辑层整合数据存储和外部接口访问两部分工作,也就是数据访问层、外部接口层这样的分层概念,如果扩展开来,我们还可以提供给客户一些文件、图片、视频等资料的文件存储,类似微信的多媒体API接口一样,这样整个Web
API的接口层就能为所有接入的客户端提供丰富的数据接口,从而实现强大的、灵活的接入。
上面提到了如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,其实APP应用层,可以分为两种类型,一种是原生的APP类型,采用原生语言如Object
C来开发IOS应用,采用java来开发安卓的原生应用一样;还有一种是封装一个入口的原生框架+后台响应式Web页面,如下图所示。
对比原生应用,采用入口框架+响应式页面的方式,开发效率非常快、升级维护成本也可以降低很多,比较Web开发总比使用原始APP开发快捷得多。
从上面的架构分析来看,我们的Web API作为核心层,可以在上面开发我们各种企业业务应用,
在目前比较热门的会员管理、客户管理等方面,结合微信的应用催化剂,就可以做的更加符合移动的潮流,从而实现我们“互联网+”的应用落地。
2、Web API层在Winform混合框架中的应用
同样,在Winform界面里面,我们除了可以利用直接访问数据库方式,以及采用访问分布式WCF服务的方式接入,还可以使得它能够访问Web
API的数据服务,从而构建成一个适应性更加广泛、功能更加强大的混合式开发框架模式。
安全性方便,直接访问数据库方式,没有在网络上公开接口,它们只是在单机或者局域网安全的环境运行,因此只需要确保数据库的安全即可,一般可以通过加密连接字符串方式实现一定的限制即可。
WCF服务的安全性,可以通过X509证书方式实现校验,也还可以利用自定义的用户名、密码验证方式进行检查等等。
对于Web API,由于它提供的是一种无状态的接口访问,而且往往Web API一般为了多种客户端接入的需要,可能需要发布在公网上进行访问,因此我们需要更加注重Web
API接口层的安全性,这方面我们后面详细介绍。
也就是新型的混合式开发框架,除了直连数据库访问的传统模式,WCF分布式访问的WCF服务访问模式,还可以接入API分布式访问的Web
API接口模式,他们的关系构成了一个完整的Winform应用体系,如下图所示。
下面图示是我的基于传统访问数据库方式和分布式WCF数据服务访问方式的一个模块分析图,围绕着混合型框架的核心,我们可以构建很多松散耦合的模块,从而能够为我们Winform应用的开发集成提供更高的开发效率。
混合型框架可以看成是Winform框架高级版本,除了它本身是一个完整的业务系统外,它外围的所有辅助性模块均(如通用权限、通用字典、通用附件管理、通用人员管理。。。。)都实现了这种混合型的框架,因此使用非常方便,整个框架如果简化来看,就是在原有的Winform界面层,用接口调用方式,避免和业务逻辑类的紧耦合关系。由于它是通过接口方式的调用方式,它本身又可以通过配置指定指向WCF的实现,因此也囊括了WCF框架的一切特点。在完成Web
API层的开发后,基于Web API层的整合就是我下一个阶段的工作了。
我们进一步分析混合式框架的实现细节,原来考虑的传统Winform访问数据库和WCF服务访问方式,就是通过一个配置模块,确定是采用直接访问数据库方式,还是访问WCF服务的方式,它们两者是统一到一个Facade接口门面层上,如果考虑到Web
API层,基于混合式的架构,也就是在这个Facade接口门面层上增加多一个Web API的接口的封装成即可。具体整个框架的架构图如下所示。
1、Web API的接口访问分类
Web API接口的访问方式,大概可以分为几类:
1)一个是使用用户令牌,通过Web API接口进行数据访问。这种方式,可以有效识别用户的身份,为用户接口返回用户相关的数据,如包括用户信息维护、密码修改、或者用户联系人等与用户身份相关的数据。
2)一种是使用安全签名进行数据提交。这种方式提交的数据,URL连接的签名参数是经过安全一定规则的加密的,服务器收到数据后也经过同样规则的安全加密,确认数据没有被中途篡改后,再进行数据修改处理。因此我们可以为不同接入方式,如Web/APP/Winfrom等不同接入方式指定不同的加密秘钥,但是秘钥是双方约定的,并不在网络连接上传输,连接传输的一般是这个接入的AppID,服务器通过这个AppID来进行签名参数的加密对比,这种方式,类似微信后台的回调处理机制,它们就是经过这样的处理。
3)一种方式是提供公开的接口调用,不需要传入用户令牌、或者对参数进行加密签名的,这种接口一般较少,只是提供一些很常规的数据显示而已。
下面图示就是这几种接入方式的说明和大概应用场景。
2、Web 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,返回给调用端即可。
3、Web API使用安全令牌的实现
通过上面的接口,我们获取到的用户访问令牌,以后和用户相关的信息调用,我们就可以通过这个令牌参数进行传递就可以了,这个令牌带有用户的一些基础信息,如用户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移除就可以了。
4、ASP.NET Web API的开发
上面我们定义了一般的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);
}
} |
5、Web API 接口的测试
接下来我们要做的就是需要增加业务接口,以便进行具体的测试了,建议使用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 接口开发的需要,专门弄的一个调试工具,可以自动组装相关的参数,包括使用安全签名的参数,还可以把所有参数数据进行存储。
|