package cn.iocoder.yudao.module.im.controller.admin.rtc; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitClient; import cn.iocoder.yudao.module.im.framework.rtc.core.LiveKitWebhookEventDTO; import cn.iocoder.yudao.module.im.service.rtc.ImRtcCallService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * LiveKit 厂商对接入口(Webhook 回调) *

* 安全由请求签名(JWT + body sha256)保证,不走登录鉴权;伪造请求会被签名校验直接拒绝 * * @author 芋道源码 */ @Tag(name = "LiveKit Webhook 回调") @RestController @RequestMapping("/im/livekit") @Slf4j public class ImRtcLiveKitController { @Resource private LiveKitClient liveKitClient; @Resource private ImRtcCallService rtcCallService; /** * LiveKit Webhook 回调入口 * * @param request HTTP 请求;用于取 Authorization 头做签名校验 * @param rawBody 原始 JSON body;签名校验需要原始字节 * @return 是否处理成功;伪造 / 非法事件返回 false */ @PostMapping("/webhook") @Operation(summary = "LiveKit Webhook 回调;接收成员离开 / 房间结束等事件做业务态兜底清理") @PermitAll @TenantIgnore public CommonResult webhook(HttpServletRequest request, @RequestBody String rawBody) { // 1.1 校验签名;伪造请求直接 200 但忽略,避免给攻击者反馈 if (!liveKitClient.verifyWebhookSignature(request.getHeader("Authorization"), rawBody)) { log.warn("[webhook][签名校验失败 ip={} bodyLength={}]", request.getRemoteAddr(), rawBody == null ? 0 : rawBody.length()); return success(false); } // 1.2 解析事件载荷;非法 / 空 event 直接忽略 LiveKitWebhookEventDTO event = JsonUtils.parseObject(rawBody, LiveKitWebhookEventDTO.class); if (event == null || StrUtil.isBlank(event.getEvent())) { return success(false); } // 2. 交给 service 处理;幂等由 service 自己保证 log.info("[webhook][事件处理 event={} room={}]", event.getEvent(), event.getRoom() == null ? null : event.getRoom().getName()); try { rtcCallService.handleLiveKitEvent(event); } catch (Exception e) { log.error("[webhook][事件处理失败 event={} body={}]", event.getEvent(), rawBody, e); } return success(true); } }