21 WebAPI服务
ASP.NET Web API,是微软在.NET Framework 4.5上推出的轻量级网络服务框架,虽然作为ASP.NET MVC 4的一部分,但却是一套全新的、独立的服务平台开发框架,可支持多种(包括移动)客户端的访问,非常适合于网络平台应用的开发。
Phenixヾ在ASP.NET Web API服务框架、及其自身业务框架(封装CSLA)基础上,为跨平台应用系统的实现提供了全面的数据服务,并为服务访问提供了身份认证、权限验证等辅助功能。
21.1 启动服务
Phenixヾ的WebAPI服务需运行在.NET Framework 4.6环境,框架文件存放在“in.NET4.6”目录,如需使用,请在该目录下启动服务程序Phenix.Services.Host.x86/x64.EXE。
启动后可见到如下提示,即说明服务已就绪:
21.2 配置服务
WebAPI服务提供了如下配置参数:
l Web API HTTP Port:默认值是8080。
l 可以在任何给定时间处理的并发HttpRequestMessage实例数的上限(实际将被乘以CPU内核数):默认值是100。
l HTTP请求标头附加身份认证的name值:默认值是“Phenix-Authorization”。
l HTTP请求标头附加突破客户端HTTP代理限制(某些HTTP代理不支持任意的HTTP方法(比如“PUT”、“DELETE”)的替代方法name值:默认值是“X-HTTP-Method-Override”。
l HTTP跨域访问开关:允许访问的资源(逗号分隔)列表,使用“*”表示全部允许: 默认值是“*”。
见:
其余参数未提供配置界面,请直接到config文件里做配置:
一般情况下采取默认配置即可,否则客户端亦要做相应的调整。Phenixヾ提供的Phenix.Web.Client工程即使用了默认配置。
21.3 身份认证
将应用系统放到Internet上,首先考虑的是安全问题。在评估了现有主流的各种解决方案之后,为兼顾跨平台的环境要求、便于应用系统的开发,Phenixヾ在应用层面上所设计的的安全方案,主要考虑到了如下安全性问题:
l 口令的保密性。
l 避免假冒攻击。
l 避免重放攻击。
l SQL注入攻击。
为保证用户的口令不被泄露,最基本的要求是不能在客户端与服务端之间的数据交换中以明文的形式传递。而只要口令由用户和系统各自保存一份,我们就可以将口令当作对称加密报文方法的密钥来使用。比如,对传递的Header、Data等信息进行适当的加密,就可以用来验证数据是否是来自被授权的发送方。
21.3.1口令的保密性处理
用户在注册时,初始口令不管是谁生成的,都需要传递给对方,以保持口令的一致(拥有相同的密钥)。
Phenixヾ推荐应用系统利用用户的邮箱或者用户的手机等第三方通讯手段来传递初始口令。一般是系统自动生成一个初始口令发给用户,由用户利用初始口令登录系统:
在这个流程里,Phenixヾ为应用系统授权模块开发提供了新增用户的接口函数供调用:
Phenix.Core.Data.DefaultDatabase.ExecuteOle(Phenix.Core.Security.DataSecurityHub.AddUser, 邮箱地址, 用户名, 初始口令)
因为这个函数是直接操作数据库的,所以调用方代码需写在服务端上。
同样,如果用户忘记了登录口令,也可以如法炮制,用用户名和邮箱地址来重置口令:
Phenixヾ也为此流程提供了更新登录口令的接口函数供调用:
Phenix.Core.Data.DefaultDatabase.ExecuteOle(Phenix.Core.Security.DataSecurityHub.ChangePassword, 用户名, 初始口令)
同理,这段调用代码也要运行在服务端上。
以上方案中的授权模块实现,需应用系统自行设计和开发,以匹配自己的应用场景。
21.3.2防范假冒重发攻击
为了防范假冒重发攻击,Phenixヾ要求客户端在报文的Header中附带nonce、timestamp、signature。其中nonce是一个随机数、timestamp是时间戳、signature是对nonce + timestamp做了AES加密后的字符串,AES加密用的Key和IV就是用户的登录口令(经MD5散列算法处理)。
设计方法讲解起来有点复杂,还不如直接看代码更便于理解。以下是AJAX在每次发送报文时,自动向报文头添加身份认证(“Phenix-Authorization”)Header的代码,摘录自“Phenix.Test.使用指南.21.3.html”:
$user =
{
userNumber: "",
password: ""
};
$.ajaxSetup({
beforeSend: function(XMLHttpRequest) {
jQuery.support.cors = true;
var nonce = Math.round(Math.random() * 999999999999999);
var timestamp = new Date().toISOString();
var key = CryptoJS.MD5($user.password);
XMLHttpRequest.setRequestHeader("Phenix-Authorization",
$user.userNumber + "," + nonce + "," + timestamp + "," +
CryptoJS.AES.encrypt(nonce + timestamp, key, { iv: key, mode: CryptoJS.mode.CBC }));
}
});
注意:实际场景下,请将用户名、口令缓存在本地,口令绝不能上传到服务端。对于浏览器应用,页面之间跳转时传递的敏感数据,也不能通过服务端。
对应的Phenix.Web.Client.DLL工程,在其Phenix.Web.Client.Security.AuthenticationHandler类中也有类似的代码:
//身份认证格式: [UserNumber],[nonce],[timestamp],[signature = Encrypt(Password, nonce+timestamp)]
string nonce = Guid.NewGuid().ToString(); //也允许采用其他随机数形式, 只要在一个LogOn到LogOff周期内不发生重复即可
DateTime timestamp = DateTime.Now;
request.Headers.Add(Phenix.Web.Client.Properties.Settings.Default.WebAuthHeaderName,
String.Format("{0},{1},{2},{3}", _userNumber, nonce, timestamp, RijndaelCryptoTextProvider.Encrypt(_password, nonce + timestamp)));
可用“Phenix.Test.使用指南.21.3”工程调试下看看效果:
其他操作环境和开发语言,只要遵循上述设计方法编写,都可以与Phenixヾ的WebAPI服务交互。(后续会陆续补上其他语言的测试案例)
21.3.3基本操作功能
Phenixヾ的身份认证服务,提供了如下的基本操作功能:
功能 |
Type |
URI |
参数 |
登录 |
POST |
api/DataSecurity |
userNumber=[用户名] |
修改登录口令 |
PUT |
api/DataSecurity |
userNumber=[用户名]&encryptedNewPassword=[经加密的新口令] |
登出 |
DELETE |
api/DataSecurity |
userNumber=[用户名] |
以下代码摘录自“Phenix.Test.使用指南.21.3.html”,在Internet Explorer 11 Web浏览器上通过测试。
21.3.3.1 登录
function LogOn(userNumber, password) {
$user.userNumber = userNumber;
$user.password = password;
$.ajax({
type: "POST",
url: "http://localhost:8080/api/DataSecurity?userNumber=" + userNumber,
dataType: "JSON",
contentType: "application/json;charset=utf-8",
data: {},
complete: function(XMLHttpRequest, textStatus) {
if (XMLHttpRequest.status === 200)
alert("登录成功! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
else if (XMLHttpRequest.status === 401)
alert("登录无效! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
else
alert("登录失败! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
}
});
}
21.3.3.2 修改登录口令
function ChangePassword(userNumber, password, newPassword) {
$user.userNumber = userNumber;
$user.password = password;
var key = CryptoJS.MD5(password);
$.ajax({
type: "PUT",
url: "http://localhost:8080/api/DataSecurity?userNumber=" + userNumber +
"&encryptedNewPassword=" + CryptoJS.AES.encrypt(newPassword, key, { iv: key, mode: CryptoJS.mode.CBC }),
dataType: "JSON",
contentType: "application/json;charset=utf-8",
data: {},
complete: function(XMLHttpRequest, textStatus) {
if (XMLHttpRequest.status === 200)
alert("修改登录口令成功! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
else
alert("修改登录口令失败! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
}
});
}
21.3.3.3 登出
function LogOff(userNumber) {
$.ajax({
type: "DELETE",
url: "http://localhost:8080/api/DataSecurity?userNumber=" + userNumber,
dataType: "JSON",
contentType: "application/json;charset=utf-8",
data: {},
complete: function (XMLHttpRequest, textStatus) {
if (XMLHttpRequest.status === 200)
alert("登出成功! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
else
alert("登出失败! status: " + XMLHttpRequest.statusText + ", response: " + XMLHttpRequest.responseText);
}
});
}
21.3.3.4 试用效果