设计API系统


有了交易引擎和定序系统,我们还需要一个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头AuthorizationBasic模式:

  1. Authorization: Basic 用户名:口令

因此,我们可以尝试从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.");
  14. }

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. if (userId == null) {
  9. // 匿名身份:
  10. chain.doFilter(req, resp);
  11. } else {
  12. // 用户身份:
  13. try (UserContext ctx = new UserContext(userId)) {
  14. chain.doFilter(req, resp);
  15. }
  16. }
  17. }
  18. }

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

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

编写API Controller

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

  1. @ResponseBody
  2. @GetMapping(value = "/assets", produces = "application/json")
  3. public String getAssets() throws IOException {
  4. Long userId = UserContext.getRequiredUserId();
  5. return tradingEngineApiProxyService.get("/internal/" + userId + "/assets");
  6. }

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

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

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

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

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

代码实现如下:

  1. public class TradingApiController {
  2. // 消息refId -> DeferredResult:
  3. Map<String, DeferredResult<ResponseEntity<String>>> deferredResultMap = new ConcurrentHashMap<>();
  4. @Autowired
  5. RedisService redisService;
  6. @PostConstruct
  7. public void init() {
  8. // 订阅Redis:
  9. this.redisService.subscribe(RedisCache.Topic.TRADING_API_RESULT, this::onApiResultMessage);
  10. }
  11. @PostMapping(value = "/orders", produces = "application/json")
  12. @ResponseBody
  13. public DeferredResult<ResponseEntity<String>> createOrder(@RequestBody OrderRequestBean orderRequest) {
  14. final Long userId = UserContext.getRequiredUserId();
  15. // 消息的Reference ID:
  16. final String refId = IdUtil.generateUniqueId();
  17. var event = new OrderRequestEvent();
  18. event.refId = refId;
  19. event.userId = userId;
  20. event.direction = orderRequest.direction;
  21. event.price = orderRequest.price;
  22. event.quantity = orderRequest.quantity;
  23. event.createdAt = System.currentTimeMillis();
  24. // 如果超时则返回:
  25. ResponseEntity<String> timeout = new ResponseEntity<>(getTimeoutJson(), HttpStatus.BAD_REQUEST);
  26. // 正常异步返回:
  27. DeferredResult<ResponseEntity<String>> deferred = new DeferredResult<>(500, timeout); // 0.5秒超时
  28. deferred.onTimeout(() -> {
  29. this.deferredResultMap.remove(event.refId);
  30. });
  31. // 根据refId跟踪消息处理结果:
  32. this.deferredResultMap.put(event.refId, deferred);
  33. // 发送消息:
  34. sendMessage(event);
  35. return deferred;
  36. }
  37. // 收到Redis的消息结果推送:
  38. public void onApiResultMessage(String msg) {
  39. ApiResultMessage message = objectMapper.readValue(msg, ApiResultMessage.class);
  40. if (message.refId != null) {
  41. // 根据消息refId查找DeferredResult:
  42. DeferredResult<ResponseEntity<String>> deferred = this.deferredResultMap.remove(message.refId);
  43. if (deferred != null) {
  44. // 找到DeferredResult后设置响应结果:
  45. ResponseEntity<String> resp = new ResponseEntity<>(JsonUtil.writeJson(message.result), HttpStatus.OK);
  46. deferred.setResult(resp);
  47. }
  48. }
  49. }
  50. }

如何实现API Key认证

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

userIdemailname
100bob@example.comBob
101alice@example.comalice
102cook@example.comCook

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

userIdrandompasswd
100c47snXI7b6da12c…
101djEqC2If7b68248…

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

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

userIdapiKeyapiSecret
1015b503947f4f5d34ae57c677d4ab4c5a4
10213a867e8da13c7f692e41573e833ae13
102341a8e60baf5b824302c9e195826267f

用户使用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。让内部系统去读用户的口令或者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。

参考源码

可以从GitHubGitee下载源码。

GitHubmichaelliaowarpexchange/

▸ build)

▸ sql)

▤ schema.sql)

▤ docker-compose.yml)

▤ pom.xml)

▸ common)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ bean)

▤ AuthToken.java)

▤ OrderBookBean.java)

▤ OrderBookItemBean.java)

▤ OrderRequestBean.java)

▤ SimpleMatchDetailRecord.java)

▤ TransferRequestBean.java)

▤ ValidatableBean.java)

▸ client)

▤ RestClient.java)

▸ config)

▤ ExchangeConfiguration.java)

▸ ctx)

▤ UserContext.java)

▸ db)

▤ AccessibleProperty.java)

▤ Criteria.java)

▤ CriteriaQuery.java)

▤ DbTemplate.java)

▤ From.java)

▤ Limit.java)

▤ Mapper.java)

▤ OrderBy.java)

▤ Select.java)

▤ Where.java)

▸ enums)

▤ AssetEnum.java)

▤ ClearingType.java)

▤ Direction.java)

▤ MatchType.java)

▤ OrderStatus.java)

▤ UserType.java)

▸ message)

▸ event)

▤ AbstractEvent.java)

▤ OrderCancelEvent.java)

▤ OrderRequestEvent.java)

▤ TransferEvent.java)

▤ AbstractMessage.java)

▤ ApiResultMessage.java)

▤ NotificationMessage.java)

▤ TickMessage.java)

▸ messaging)

▤ BatchMessageHandler.java)

▤ MessageConsumer.java)

▤ MessageProducer.java)

▤ MessageTypes.java)

▤ Messaging.java)

▤ MessagingConfiguration.java)

▤ MessagingFactory.java)

▸ model)

▸ quotation)

▤ TickEntity.java)

▸ support)

▤ EntitySupport.java)

▸ trade)

▤ ClearingEntity.java)

▤ EventEntity.java)

▤ MatchDetailEntity.java)

▤ OrderEntity.java)

▤ TransferLogEntity.java)

▤ UniqueEventEntity.java)

▸ ui)

▤ ApiKeyAuthEntity.java)

▤ PasswordAuthEntity.java)

▤ UserEntity.java)

▤ UserProfileEntity.java)

▸ redis)

▤ RedisCache.java)

▤ RedisConfiguration.java)

▤ RedisService.java)

▤ SyncCommandCallback.java)

▸ support)

▤ AbstractApiController.java)

▤ AbstractDbService.java)

▤ AbstractFilter.java)

▤ LoggerSupport.java)

▸ user)

▤ UserService.java)

▸ util)

▤ ByteUtil.java)

▤ ClassPathUtil.java)

▤ HashUtil.java)

▤ IdUtil.java)

▤ IpUtil.java)

▤ JsonUtil.java)

▤ RandomUtil.java)

▤ ApiError.java)

▤ ApiErrorResponse.java)

▤ ApiException.java)

▸ resources)

▸ redis)

▤ update-orderbook.lua)

▤ logback-spring.xml)

▤ pom.xml)

▸ config)

▸ src/main)

▸ java/com/itranswarp/exchange/config)

▤ ConfigApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ config-repo)

▤ application-default.yml)

▤ application-test.yml)

▤ application.yml)

▤ push.yml)

▤ quotation.yml)

▤ trading-api.yml)

▤ trading-engine.yml)

▤ trading-sequencer.yml)

▤ ui-default.yml)

▤ ui.yml)

▸ parent)

▤ pom.xml)

▸ push)

▸ src/main)

▸ java/com/itranswarp/exchange/push)

▤ PushApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ quotation)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ QuotationApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-api)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ service)

▤ HistoryService.java)

▤ SendEventService.java)

▤ TradingEngineApiProxyService.java)

▸ web)

▸ api)

▤ TradingApiController.java)

▤ TradingInternalApiController.java)

▤ ApiFilterRegistrationBean.java)

▤ TradingApiApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-engine)

▸ src)

▸ main)

▸ java/com/itranswarp/exchange)

▸ assets)

▤ Asset.java)

▤ AssetService.java)

▤ Transfer.java)

▸ clearing)

▤ ClearingService.java)

▸ match)

▤ MatchDetailRecord.java)

▤ MatchEngine.java)

▤ MatchResult.java)

▤ OrderBook.java)

▤ OrderKey.java)

▸ order)

▤ OrderService.java)

▸ store)

▤ StoreService.java)

▸ web/api)

▤ InternalTradingEngineApiController.java)

▤ TradingEngineApplication.java)

▤ TradingEngineService.java)

▸ resources)

▤ application.yml)

▸ test/java/com/itranswarp/exchange)

▸ assets)

▤ AssetServiceTest.java)

▸ match)

▤ MatchEngineTest.java)

▤ TradingEngineServiceTest.java)

▤ pom.xml)

▸ trading-sequencer)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ sequencer)

▤ SequenceHandler.java)

▤ SequenceService.java)

▤ TradingSequencerApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ ui)

▸ src/main)

▸ java/com/itranswarp/exchange)

▤ UIApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▤ .gitignore)

▤ LICENSE)

▤ README.md)

小结

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

读后有收获可以支付宝请作者喝咖啡:

设计API系统 - 图1