接触过很多电商的WebService,有种一看就蛋疼的设计,今天要从这个反例说一说 WebService 的设计。
[WebMethod]
public string QueryOrderDetail(string xml)
{
...
}
二. 异常
(1) 使用框架中定义的Exception类型,比如:SoapException, FaultException(WCF)。
(2) 尽量避免将异常定义在返回值中,通过返回值定义错误那么无论服务端还是客户端都要写很多if ... else 分支。
(3) 系统异常和业务异常要区分好,比如使用 SoapException 可以用 Code 来区分,比如:System.Error 表示系统错误,Bussiness.Error 表示业务错误。
(4) 补充:.net framework 如果没有包装那么默认有两种 fautCode: soap:Client 和 soap:Server。假设客户端传入BadRequest 基本就是 soap:Client 错误,其他 没有自定义code的则就是 soap:Server 的错误。
三. 安全
无论何时都要保证系统的安全性,我觉得安全也分系统安全和业务安全两种:
(1) 系统安全主要是指客户端的认证授权,调用次数(需要考虑会不会拖垮业务系统) 等
(2) 业务安全主要是指数据查询/操作权限,当然这个主要是从业务角度考虑的。
四. 日志
日志可以方便排查错误,还可以通过日志来分析服务基本信息(比如:调用次数,失败次数等),必要时还可以通过日志来进行重试。
另外要考虑开发的便捷,设计统一的日志拦截处理。
以 WebService Application (.NET 3.5) 为例,记录几种常用的编程技巧。
原始的 WebService 如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using WebService1.Entity;
using WebService1.Service;
using System.Web.Services.Protocols;
namespace WebService1
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class Service1 : System.Web.Services.WebService
{
[WebMethod]
public PageResult QueryOrder(Query queryInfo)
{
OrderService service = new OrderService();
return service.Query(queryInfo);
}
}
}
PageResult
using System;
using System.Collections.Generic;
namespace WebService1.Entity
{
[Serializable]
public class PageResult
{
public int PageNo { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int PageCount { get; set; }
public bool HasNextPage { get; set; }
public List Data { get; set; }
}
}
using System;
using System.Collections.Generic;
namespace WebService1.Entity
{
[Serializable]
public class Query
{
public int PageNo { get; set; }
public int PageSize { get; set; }
public T Condition { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Web.Services.Protocols;
using log4net;
using System.Xml;
namespace WebService1.Common
{
public class TraceExtension : SoapExtension
{
private ILog logger = LogManager.GetLogger(typeof(TraceExtension));
Stream oldStream;
Stream newStream;
public override System.IO.Stream ChainStream(System.IO.Stream stream)
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeDeserialize:
log4net.ThreadContext.Properties["ip"] = HttpContext.Current.Request.UserHostAddress;
log4net.ThreadContext.Properties["action"] = message.Action;
WriteInput(message);
break;
case SoapMessageStage.AfterDeserialize:
break;
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
WriteOutput(message);
break;
default:
throw new Exception("Invalid Stage");
}
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attr)
{
return null;
}
public override void Initialize(object initializer)
{
//filename = (string)initializer;
}
public void WriteOutput(SoapMessage message)
{
string soapString = (message is SoapServerMessage) ? "SoapResponse" : "SoapRequest";
string content = GetContent(newStream);
// 为了Format XML,如果从性能考虑应该去掉此处的处理
if (!string.IsNullOrEmpty(content))
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(content);
using (StringWriter sw = new StringWriter())
{
using (XmlTextWriter xtw = new XmlTextWriter(sw))
{
xtw.Formatting = Formatting.Indented;
xmlDoc.WriteTo(xtw);
content = sw.ToString();
}
}
}
logger.Info(soapString + ":\n" + content);
Copy(newStream, oldStream);
}
public void WriteInput(SoapMessage message)
{
Copy(oldStream, newStream);
string soapString = (message is SoapServerMessage) ? "SoapRequest" : "SoapResponse";
string content = GetContent(newStream);
logger.Info(soapString + ":\n" + content);
}
void Copy(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
string GetContent(Stream stream)
{
stream.Position = 0;
TextReader reader = new StreamReader(stream);
string content = reader.ReadToEnd();
stream.Position = 0;
return content;
}
}
}
TraceAttribute 实现如下:
using System;
using System.Web.Services.Protocols;
namespace WebService1.Common
{
[AttributeUsage(AttributeTargets.Method)]
public class TraceAttribute : SoapExtensionAttribute
{
private int priority = 0;
public override Type ExtensionType
{
get { return typeof(TraceExtension); }
}
public override int Priority
{
get { return priority; }
set { priority = value; }
}
}
}
[WebMethod]
[Trace]
public PageResult QueryOrder(Query queryInfo)
{
OrderService service = new OrderService();
return service.Query(queryInfo);
}
输出日志如下:
2014-05-25 22:05:02,292 [8] INFO [127.0.0.1] [http://tempuri.org/QueryOrder] - SoapRequest:
1
1
?
?
?
?
2014-05-25 22:05:02,357 [8] INFO [127.0.0.1] [http://tempuri.org/QueryOrder] - SoapResponse:
1
1
3
1
false
1
2014-05-25 22:05:02
SHOP001
PRD001
1
59
...
using System;
using System.Web.Services.Protocols;
namespace WebService1.Common
{
public class Authentication : SoapHeader
{
public string UserName { get; set; }
public string Password { get; set; }
}
}
AuthCheckExtension 的实现:在 SoapMessage AfterDeserialize 这个阶段,取出客户端传的 SoapHeader 验证 UserName 和 Password 在服务端是否存在。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Web.Services.Protocols;
using WebService1.Config;
namespace WebService1.Common
{
public class AuthCheckExtension : SoapExtension
{
public override void ProcessMessage(SoapMessage message)
{
if (message.Stage == SoapMessageStage.AfterDeserialize)
{
foreach (SoapHeader header in message.Headers)
{
if (header is Authentication)
{
var authHeader = header as Authentication;
var isValidUser = true;
var users = AuthConfiguration.AuthSettings.Users;
if (users != null && users.Count > 0)
{
isValidUser = users.Any(u => u.UserName == authHeader.UserName && u.Password == authHeader.Password);
}
if (!isValidUser)
throw new BizException("no auth !");
}
}
}
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override void Initialize(object initializer)
{
// 初始化 AuthSettings
AuthConfiguration.Config();
}
}
}
然后给 WebMethod 加上 [SoapHeader("Authentication"), AuthCheck] 就OK了。
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using WebService1.Entity;
using WebService1.Service;
using System.Web.Services.Protocols;
using WebService1.Common;
namespace WebService1
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class Service1 : System.Web.Services.WebService
{
public Authentication Authentication { get; set; }
[WebMethod]
[Trace]
[SoapHeader("Authentication"), AuthCheck]
public PageResult QueryOrder(Query queryInfo)
{
OrderService service = new OrderService();
return service.Query(queryInfo);
}
}
}
JAVA CXF Client 代码:
public static void main(String[] args) {
try {
Service1 service1 = new Service1();
Service1Soap service1Soap = service1.getService1Soap();
BindingProvider provider = (BindingProvider)service1Soap;
List headers = new ArrayList();
Authentication authentication = new Authentication();
authentication.setUserName("fangxing");
authentication.setPassword("123456");
Header authHeader = new Header(ObjectFactory._Authentication_QNAME, authentication,
new JAXBDataBinding(Authentication.class));
headers.add(authHeader);
provider.getRequestContext().put(Header.HEADER_LIST, headers);
QueryOfOrderCondition queryInfo = new QueryOfOrderCondition();
queryInfo.setPageNo(1);
queryInfo.setPageSize(1000);
OrderCondition condition = new OrderCondition();
condition.setShopId("SHOP001");
condition.setStartTime("2014-05-01 00:00:00");
condition.setEndTime("2014-05-10 23:59:59");
queryInfo.setCondition(condition);
PageResultOfOrder result = service1Soap.queryOrder(queryInfo);
System.out.println("get order size: " + result.getData().getOrder().size());
} catch (Exception e) {
e.printStackTrace();
}
}