设计API系统


相比事件驱动的交易引擎,API系统就比较简单,因为它就是一个标准的Web应用。

在编写API之前,我们需要对请求进行认证,即识别出是哪个用户发出的请求。用户认证放在Filter中是最合适的。认证方式可以是简单粗暴的用户名+口令,也可以是Token,也可以是API Key+API Secret等模式。

我们先实现一个最简单的用户名+口令的认证方式。需要注意的是,API和Web页面不同,Web页面可以给用户一个登录页,登录成功后设置Session或Cookie,后续请求检查的是Session或Cookie。API不能使用Session,因为Session很难做无状态集群,API也不建议使用Cookie,因为API域名很可能与Web UI的域名不一致,拿不到Cookie。要在API中使用用户名+口令的认证方式,可以用标准的HTTP头的模式:

因此,我们可以尝试从Authorization中获取用户名和口令来认证:

  1. Long parseUserFromAuthorization(String auth) {
  2. if (auth.startsWith("Basic ")) {
  3. // 用Base64解码:
  4. String eap = new String(Base64.getDecoder().decode(auth.substring(6)));
  5. // 分离email:password
  6. int pos = eap.indexOf(':');
  7. String email = eap.substring(0, pos);
  8. String passwd = eap.substring(pos + 1);
  9. // 验证:
  10. UserProfileEntity p = userService.signin(email, passwd);
  11. return p.userId;
  12. }
  13. throw new ApiException(ApiError.AUTH_SIGNIN_FAILED, "Invalid Authorization header.");

ApiFilter中完成认证后,使用UserContext传递用户ID:

  1. public class ApiFilter {
  2. @Override
  3. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
  4. throws IOException, ServletException {
  5. // 尝试认证用户:
  6. String authHeader = req.getHeader("Authorization");
  7. Long userId = authHeader == null ? null : parseUserFromAuthorization(authHeader);
  8. // 匿名身份:
  9. chain.doFilter(req, resp);
  10. } else {
  11. // 用户身份:
  12. try (UserContext ctx = new UserContext(userId)) {
  13. chain.doFilter(req, resp);
  14. }
  15. }
  16. }
  17. }

Basic模式很简单,需要注意的是用户名:口令使用:分隔,然后整个串用Base64编码,因此,读取的时候需要先用Base64解码。

虽然Basic模式并不安全,但是有了一种基本的认证模式,我们就可以把API-定序-交易串起来了。后续我们再继续添加其他认证模式。

对于认证用户的操作,例如,查询资产余额,可通过UserContext获取当前用户,然后通过交易引擎查询并返回用户资产余额:

因为交易引擎返回的结果就是JSON字符串,没必要先反序列化再序列化,可以以String的方式直接返回给客户端,需要标注@ResponseBody表示不要对String再进行序列化处理。

对于无需认证的操作,例如,查询公开市场的订单簿,可以直接返回Redis缓存结果:

  1. @ResponseBody
  2. @GetMapping(value = "/orderBook", produces = "application/json")
  3. String data = redisService.get(RedisCache.Key.ORDER_BOOK);
  4. return data == null ? OrderBookBean.EMPTY : data;
  5. }

但是对于创建订单的请求,处理就麻烦一些,因为API收到请求后,仅仅通过消息系统给定序系统发了一条消息。消息系统本身并不是类似HTTP的请求-响应模式,我们拿不到消息处理的结果。这里先借助Spring的异步响应模型DeferredResult,再借助Redis的pub/sub模型,当API发送消息时,使用全局唯一refId跟踪消息,当交易引擎处理完订单请求后,向Redis发送pub事件,API收到Redis推送的事件后,根据找到DeferredResult,设置结果后由Spring异步返回给客户端:

  1. ┌─────────┐ ┌─────────┐
  2. ──▶│ API │◀────────────────│ Redis
  3. └─────────┘ └─────────┘
  4. ┌─────────┐
  5. MQ pub
  6. └─────────┘
  7. ┌─────────┐ ┌─────────┐ ┌─────────┐
  8. Sequencer│──▶│ MQ │──▶│ Engine
  9. └─────────┘ └─────────┘ └─────────┘

代码实现如下:

如何实现API Key认证

身份认证的本质是确认用户身份。用户身份其实并不包含密码,而是用户ID、email、名字等信息,可以看作数据库中的user_profiles表:

使用口令认证时,通过添加一个password_auths表,存储哈希后的口令,并关联至某个用户ID,即可完成口令认证:

并不是每个用户都必须有口令,没有口令的用户仅仅表示该用户不能通过口令来认证身份,但完全可以通过其他方式认证。

使用API Key认证同理,通过添加一个api_auths表,存储API Key、API Secret并关联至某个用户ID:

用户使用API Key认证时,提供API Key,以及用API Secret计算的Hmac哈希,服务器验证Hmac哈希后,就可以确认用户身份,因为其他人不知道该用户的API Secret,无法计算出正确的Hmac。

发送API Key认证时,可以定义如下的HTTP头:

  1. API-Key: 5b503947f4f5d34a
  2. API-Timestamp: 20220726T092137Z <- 防止重放攻击的时间戳
  3. API-Signature: d7a567b6cab85bcd

计算签名的原始输入可以包括HTTP Method、Path、Timestamp、Body等关键信息,具体格式可参考AWS API签名方式

一个用户可以关联多个API Key认证,还可以给每个API Key附加特定权限,例如只读权限,这样用API Key认证就更加安全。

很多时候,内部系统也需要调用API,并且需要以特定用户的身份调用API。让内部系统去读用户的口令或者API Key都是不合理的,更好的方式是使用一次性Token,还是利用Authorization头的Bearer模式:

  1. Authorization: Bearer 5NPtI6LW...

构造一次性Token可以用userId:expires:hmac,内部系统和API共享同一个Hmac Key,就可以正确计算并验证签名。外部用户因为无法获得Hmac Key而无法伪造Token。

如何跟踪API性能

可以使用Spring提供的HandlerInterceptorDeferredResultProcessingInterceptor跟踪API性能,它们分别用于拦截同步API和异步API。

可以从GitHub或下载源码。

GitHub ▸ ▸ warpexchange

▸ build)

)

▤ schema.sql)

)

▤ pom.xml)

)

▸ src/main)

)

▸ bean)

)

▤ OrderBookBean.java)

)

▤ OrderRequestBean.java)

)

▤ TransferRequestBean.java)

)

▸ client)

)

▸ config)

)

▸ ctx)

)

▸ db)

)

▤ Criteria.java)

)

▤ DbTemplate.java)

)

▤ Limit.java)

)

▤ OrderBy.java)

)

▤ Where.java)

)

▤ AssetEnum.java)

)

▤ Direction.java)

)

▤ OrderStatus.java)

)

▸ message)

)

▤ AbstractEvent.java)

)

▤ OrderRequestEvent.java)

)

▤ AbstractMessage.java)

)

▤ NotificationMessage.java)

)

▸ messaging)

)

▤ MessageProducer.java)

)

▤ Messaging.java)

)

▤ MessagingFactory.java)

)

▸ quotation)

)

▸ support)

)

▸ trade)

)

▤ EventEntity.java)

)

▤ OrderEntity.java)

)

▤ UniqueEventEntity.java)

)

▤ ApiKeyAuthEntity.java)

)

▤ UserEntity.java)

)

▸ redis)

)

▤ RedisConfiguration.java)

)

▤ SyncCommandCallback.java)

)

▤ AbstractApiController.java)

)

▤ AbstractFilter.java)

)

▸ user)

)

▸ util)

)

▤ ClassPathUtil.java)

)

▤ IdUtil.java)

)

▤ JsonUtil.java)

)

▤ ApiError.java)

)

▤ ApiException.java)

)

▸ redis)

)

▤ logback-spring.xml)

)

▸ config)

)

▸ java/com/itranswarp/exchange/config)

)

▸ resources)

)

▤ pom.xml)

)

▤ application-default.yml)

)

▤ application.yml)

)

▤ quotation.yml)

)

▤ trading-engine.yml)

)

▤ ui-default.yml)

)

▸ parent)

)

▸ push)

)

▸ java/com/itranswarp/exchange/push)

)

▸ resources)

)

▤ pom.xml)

)

▸ java/com/itranswarp/exchange)

)

▸ resources)

)

▤ pom.xml)

)

▸ src/main)

)

▸ service)

)

▤ SendEventService.java)

)

▸ web)

)

▤ TradingApiController.java)

)

▤ ApiFilterRegistrationBean.java)

)

▸ resources)

)

▤ pom.xml)

)

▸ src)

)

▸ java/com/itranswarp/exchange)

)

▤ Asset.java)

)

▤ Transfer.java)

)

▤ ClearingService.java)

)

▤ MatchDetailRecord.java)

)

▤ MatchResult.java)

)

▤ OrderKey.java)

)

▤ OrderService.java)

)

▤ StoreService.java)

)

▤ InternalTradingEngineApiController.java)

)

▤ TradingEngineService.java)

)

▤ application.yml)

)

▸ assets)

)

▸ match)

)

▤ TradingEngineServiceTest.java)

)

▸ trading-sequencer)

)

▸ java/com/itranswarp/exchange)

)

▤ SequenceHandler.java)

)

▤ TradingSequencerApplication.java)

)

▤ application.yml)

)

▸ ui)

)

▸ java/com/itranswarp/exchange)

)

▸ resources)

)

▤ pom.xml)

)

▤ LICENSE)

)

小结

API系统负责认证用户身份,并提供一个唯一的交易入口。