视频面试

This commit is contained in:
戈经莹
2026-07-31 22:48:45 +08:00
parent 6c176aeb4a
commit 3b6b92b5aa
4 changed files with 242 additions and 150 deletions

View File

@@ -1,38 +1,57 @@
# 面试视频房间接口 # 面试视频 Token 接口
## 1. 接口说明 ## 1. 接口用途
本组接口用于创建一场面试视频通话,并分别获取面试官和候选人的 MyRTC 连接凭据 面试官和候选人分别调用同一个 Token 接口,但每次请求只返回当前调用者自己的连接凭据
后端会在一次请求中完成以下操作: ```text
企业端调用一次 → 面试官 peerId + token
候选人端调用一次 → 候选人 peerId + token
```
1. 自动生成一个唯一的 `roomId` 企业端不会拿到候选人的 token候选人端也不会拿到面试官的 token。
2. 生成面试官专用的 `peerId`
3. 生成候选人专用的 `peerId`
4. 使用与 MyRTC 服务一致的 JWT 密钥,分别签发两枚一次性 token
5. 保证两枚 token 绑定同一个 `roomId`
前端不需要调用房间详情查询和房间销毁接口。房间在 MyRTC 客户端成功执行 `join` 后创建;双方离开后由 MyRTC 自动回收空房间 两次请求必须关联到同一条面试邀约记录,后端会为同一个 `interviewId` 复用同一个 `roomId`,并为每次调用生成新的 `peerId` 和一次性 token
## 2. 创建房间并获取双方凭据 ## 2. 面试官获取 Token
### 请求 企业端使用后台用户登录态调用:
```http ```http
POST /app/interview/rtc/token POST /cms/interview/rtc/token?interviewId=123
Authorization: Bearer <业务系统登录Token> Authorization: Bearer <企业端登录Token>
Content-Type: application/json Content-Type: application/json
``` ```
请求体为空即可,不需要传入 `roomId``peerId` 或 token。 请求体为空
```json ```json
{} {}
``` ```
### 成功响应 企业端接口只返回当前面试官的一套凭据。
接口遵循后端统一的 `AjaxResult` 返回格式: ## 3. 候选人获取 Token
候选人端使用移动端登录态调用:
```http
POST /app/interview/rtc/token?interviewId=123
Authorization: Bearer <候选人登录Token>
Content-Type: application/json
```
请求体为空:
```json
{}
```
候选人端接口只返回当前候选人的一套凭据。
## 4. 成功响应
两个接口的返回格式相同,但每次只对应当前调用者:
```json ```json
{ {
@@ -40,17 +59,9 @@ Content-Type: application/json
"msg": "操作成功", "msg": "操作成功",
"data": { "data": {
"roomId": "room-0a4c3fbd8f9e4b70a0e2de99f6b5f2a8", "roomId": "room-0a4c3fbd8f9e4b70a0e2de99f6b5f2a8",
"expiresIn": "5m", "peerId": "peer-4a78d8f0c4a5468fb0d8d05a8199b3bb",
"interviewer": { "token": "eyJhbGciOiJIUzI1NiJ9...",
"role": "interviewer", "expiresIn": "5m"
"peerId": "interviewer-4a78d8f0c4a5468fb0d8d05a8199b3bb",
"token": "eyJhbGciOiJIUzI1NiJ9..."
},
"candidate": {
"role": "candidate",
"peerId": "candidate-1d2f7e2e2ae8462da3bb514d2f39b8e3",
"token": "eyJhbGciOiJIUzI1NiJ9..."
}
} }
} }
``` ```
@@ -61,113 +72,126 @@ Content-Type: application/json
| --- | --- | --- | | --- | --- | --- |
| `code` | number | `200` 表示成功 | | `code` | number | `200` 表示成功 |
| `msg` | string | 接口提示信息 | | `msg` | string | 接口提示信息 |
| `data.roomId` | string | 本次面试房间 ID,双方必须使用同一个值 | | `data.roomId` | string | 当前面试房间 ID;同一个 `interviewId` 的双方得到相同值 |
| `data.peerId` | string | 当前调用者在房间内的唯一 ID |
| `data.token` | string | 当前调用者的一次性 MyRTC JWT |
| `data.expiresIn` | string | token 有效期,当前通常为 `5m` | | `data.expiresIn` | string | token 有效期,当前通常为 `5m` |
| `data.interviewer.role` | string | 固定为 `interviewer` |
| `data.interviewer.peerId` | string | 面试官在该房间内的唯一 ID |
| `data.interviewer.token` | string | 面试官的一次性 MyRTC JWT |
| `data.candidate.role` | string | 固定为 `candidate` |
| `data.candidate.peerId` | string | 候选人在该房间内的唯一 ID |
| `data.candidate.token` | string | 候选人的一次性 MyRTC JWT |
## 3. 前端使用流程 ## 5. 用流程
### 3.1 调用后端接口 假设面试邀约 ID 为 `123`
### 5.1 企业端
```javascript ```javascript
const response = await request.post('/app/interview/rtc/token', {}) const response = await request.post(
'/cms/interview/rtc/token?interviewId=123',
{}
)
if (response.code !== 200) { if (response.code !== 200) {
throw new Error(response.msg || '创建面试视频房间失败') throw new Error(response.msg || '获取面试视频凭据失败')
} }
const session = response.data const rtc = response.data
const roomId = session.roomId await rtcClient.connect({
const interviewer = session.interviewer roomId: rtc.roomId,
const candidate = session.candidate peerId: rtc.peerId,
token: rtc.token
})
``` ```
注意:`roomId` 在响应的 `data.roomId` 中,不是在响应最外层。 ### 5.2 候选人端
### 3.2 面试官加入房间
面试官客户端使用:
```javascript ```javascript
{ const response = await request.post(
roomId: session.roomId, '/app/interview/rtc/token?interviewId=123',
peerId: session.interviewer.peerId, {}
token: session.interviewer.token )
if (response.code !== 200) {
throw new Error(response.msg || '获取候选人视频凭据失败')
} }
const rtc = response.data
await rtcClient.connect({
roomId: rtc.roomId,
peerId: rtc.peerId,
token: rtc.token
})
``` ```
### 3.3 候选人加入房间 两端的 `roomId` 必须相同,但两端的 `peerId``token` 必须使用各自接口返回的值,不能互换。
候选人客户端使用: ## 6. MyRTC WebSocket 连接
```javascript 当前 MyRTC 服务的信令地址是:
{
roomId: session.roomId,
peerId: session.candidate.peerId,
token: session.candidate.token
}
```
两端的 `roomId` 必须完全相同,但 `peerId``token` 必须使用各自对应角色的数据,不能交叉使用。
## 4. MyRTC WebSocket 连接参数
MyRTC 的 WebSocket 地址由 MyRTC 服务部署地址决定。当前服务的信令路径是 `/signaling`JWT 通过 WebSocket 握手阶段的 `token` 查询参数传入,例如:
```text ```text
wss://rtc.zhaopinzao8dian.com/signaling?token=<url-encoded-token> wss://rtc.zhaopinzao8dian.com/signaling?token=<url-encoded-token>
``` ```
如果使用 `WebSocket` 原生 API建议使用 `URL` 对 token 做编码,不要直接拼接未编码的 token 原生 WebSocket 建议使用 `URL` 自动编码 token
```javascript ```javascript
const wsUrl = new URL('wss://rtc.zhaopinzao8dian.com/signaling') const wsUrl = new URL('wss://rtc.zhaopinzao8dian.com/signaling')
wsUrl.searchParams.set('token', token) wsUrl.searchParams.set('token', rtc.token)
const ws = new WebSocket(wsUrl.toString()) const ws = new WebSocket(wsUrl.toString())
``` ```
加入房间时,发送的业务消息需要包含 `roomId``peerId` 和 token。协议形式如下 连接成功后发送 `join` 请求
```json ```json
{ {
"type": "request", "type": "request",
"id": 1,
"action": "join", "action": "join",
"requestId": "join-001",
"payload": { "payload": {
"roomId": "room-0a4c3fbd8f9e4b70a0e2de99f6b5f2a8", "roomId": "room-0a4c3fbd8f9e4b70a0e2de99f6b3bb",
"peerId": "interviewer-4a78d8f0c4a5468fb0d8d05a8199b3bb" "peerId": "peer-4a78d8f0c4a5468fb0d8d05a8199b3bb"
} }
} }
``` ```
token 通常在建立 WebSocket 连接时通过 SDK 配置或连接参数传入,不能把面试官 token 和候选人 token 互换 `id` 必须是正整数。当前 MyRTC 服务要求使用 `id`,不是 `requestId`
如果使用项目已有 MyRTC SDK前端只需要将以下三个字段传给 SDK ## 7. interviewId 的来源
```javascript `interviewId` 就是业务表 `interview_invitation` 的主键 ID不是 MyRTC 临时生成的 ID。
await rtcClient.connect({
roomId, 候选人端可以从以下接口获得面试邀约 ID
peerId,
token ```http
}) GET /app/interview/list
GET /app/interview/{id}
``` ```
## 5. token 注意事项 返回数据中的:
- token 是一次性的,成功 `join` 后即会被 MyRTC 消费; ```json
- token 默认有效期为 `5m`,过期后不能继续使用; {
- 页面刷新或连接断开后重新连接,不能继续复用已经成功 `join` 的 token需要重新调用后端接口签发 "id": 123
- 面试官和候选人的 token 绑定不同的 `peerId`,不能让两个客户端使用同一组凭据; }
- `roomId``peerId` 和 token 应保存在当前面试页面的内存状态中,不建议写入长期本地存储; ```
- 前端不需要保存或传递 MyRTC 的 `ADMIN_TOKEN`,该令牌只由业务后端访问 MyRTC 控制面时使用。
## 6. 错误响应 其中 `id` 就是获取 RTC Token 时使用的 `interviewId`
企业端也应使用企业端面试列表中的同一条面试邀约记录 ID。
## 8. Token 注意事项
- token 是一次性的,成功 `join` 后即被 MyRTC 消费;
- token 默认有效期为 `5m`
- 页面刷新或连接断开后重新连接,需要重新获取 token
- 每次获取 token 都会生成新的 `peerId`
- 同一个 `interviewId` 在 Redis 中对应同一个 `roomId`,有效期默认 6 小时;
- 前端不需要调用房间详情查询和房间销毁接口;
- 前端不需要保存或传递 MyRTC 的 `ADMIN_TOKEN`
## 9. 错误响应
### 未登录 ### 未登录
@@ -178,9 +202,16 @@ await rtcClient.connect({
} }
``` ```
### MyRTC JWT 密钥未配置或配置不一致 ### 缺少 interviewId
后端可能返回类似以下错误: ```json
{
"code": 400,
"msg": "Required request parameter 'interviewId' for method parameter type Long is not present"
}
```
### MyRTC JWT 密钥未配置
```json ```json
{ {
@@ -189,30 +220,4 @@ await rtcClient.connect({
} }
``` ```
如果后端签发 token 使用的密钥和 MyRTC 服务配置不一致,前端会在连接或 `join` 阶段收到 token 签名错误。此时需要检查业务后端和 MyRTC 的 `JWT_SECRET` 是否完全一致。 如果后端签发 token 使用的密钥和 MyRTC 服务配置不一致,WebSocket 握手会失败,需要检查业务后端和 MyRTC 的 `JWT_SECRET` 是否完全一致。
### MyRTC 服务不可用
```json
{
"code": 500,
"msg": "无法连接 MyRTC"
}
```
## 7. 房间查询和销毁接口
这两个接口不是前端视频通话流程的一部分,前端无需调用:
```text
GET /app/interview/rtc/room/{roomId}
DELETE /app/interview/rtc/room/{roomId}
```
它们仅供后端管理或运维场景使用。正常情况下,前端只需要:
```text
POST /app/interview/rtc/token
```
然后使用响应中的双方凭据加入同一个 `roomId` 即可。

View File

@@ -1,26 +1,20 @@
package com.ruoyi.cms.controller.app; package com.ruoyi.cms.controller.app;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.cms.domain.vo.InterviewRtcSessionVO; import com.ruoyi.cms.domain.vo.InterviewRtcTokenVO;
import com.ruoyi.cms.service.MyRtcService; import com.ruoyi.cms.service.MyRtcService;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.SiteSecurityUtils; import com.ruoyi.common.utils.SiteSecurityUtils;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 面试视频房间接口。 */ /** 面试视频房间接口。 */
@RestController @RestController
@RequestMapping("/app/interview/rtc") @RequestMapping("/app/interview/rtc")
@Api(tags = "移动端:面试视频房间") @Api(tags = "移动端:面试视频房间")
public class AppInterviewRtcController extends BaseController { public class AppInterviewRtcController extends BaseController {
private final MyRtcService myRtcService; private final MyRtcService myRtcService;
@@ -34,19 +28,19 @@ public class AppInterviewRtcController extends BaseController {
*/ */
@ApiOperation("创建面试视频房间并签发双方 token") @ApiOperation("创建面试视频房间并签发双方 token")
@PostMapping("/token") @PostMapping("/token")
public AjaxResult issueToken() { public AjaxResult issueToken(@RequestParam Long interviewId) {
if (!isAuthenticated()) { if (!SiteSecurityUtils.isLogin()) {
return error("未登录"); return error("未登录");
} }
InterviewRtcSessionVO session = myRtcService.issueInterviewSession(); InterviewRtcTokenVO token = myRtcService.issueToken(interviewId);
return AjaxResult.success(session); return AjaxResult.success(token);
} }
/** 查询房间详情。 */ /** 查询房间详情。 */
@ApiOperation("查询面试视频房间") @ApiOperation("查询面试视频房间")
@GetMapping({"/room/{roomId}", "/rooms/{roomId}"}) @GetMapping({"/room/{roomId}", "/rooms/{roomId}"})
public AjaxResult getRoom(@PathVariable String roomId) { public AjaxResult getRoom(@PathVariable String roomId) {
if (!isAuthenticated()) { if (!SiteSecurityUtils.isLogin()) {
return error("未登录"); return error("未登录");
} }
JsonNode room = myRtcService.getRoom(roomId); JsonNode room = myRtcService.getRoom(roomId);
@@ -57,15 +51,12 @@ public class AppInterviewRtcController extends BaseController {
@ApiOperation("销毁面试视频房间") @ApiOperation("销毁面试视频房间")
@DeleteMapping({"/room/{roomId}", "/rooms/{roomId}"}) @DeleteMapping({"/room/{roomId}", "/rooms/{roomId}"})
public AjaxResult destroyRoom(@PathVariable String roomId) { public AjaxResult destroyRoom(@PathVariable String roomId) {
if (!isAuthenticated()) { if (!SiteSecurityUtils.isLogin()) {
return error("未登录"); return error("未登录");
} }
JsonNode result = myRtcService.destroyRoom(roomId); JsonNode result = myRtcService.destroyRoom(roomId);
return AjaxResult.success(result); return AjaxResult.success(result);
} }
private boolean isAuthenticated() {
return Boolean.TRUE.equals(SiteSecurityUtils.isLogin())
|| Boolean.TRUE.equals(SecurityUtils.isLogin());
}
} }

View File

@@ -0,0 +1,62 @@
package com.ruoyi.cms.controller.cms;
import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.cms.domain.vo.InterviewRtcTokenVO;
import com.ruoyi.cms.service.MyRtcService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
/** 面试视频房间接口。 */
@RestController
@RequestMapping("/cms/interview/rtc")
@Api(tags = "后台:面试视频房间")
public class CmsInterviewRtcController extends BaseController {
private final MyRtcService myRtcService;
public CmsInterviewRtcController(MyRtcService myRtcService) {
this.myRtcService = myRtcService;
}
/**
* 创建面试房间并同时签发面试官、候选人的 token。
* roomId 和两个 peerId 均由后端生成,前端不传入也不能覆盖。
*/
@ApiOperation("创建面试视频房间并签发双方 token")
@PostMapping("/token")
public AjaxResult issueToken(@RequestParam Long interviewId) {
if (!SecurityUtils.isLogin()) {
return error("未登录");
}
InterviewRtcTokenVO token = myRtcService.issueToken(interviewId);
return AjaxResult.success(token);
}
/** 查询房间详情。 */
@ApiOperation("查询面试视频房间")
@GetMapping({"/room/{roomId}", "/rooms/{roomId}"})
public AjaxResult getRoom(@PathVariable String roomId) {
if (!SecurityUtils.isLogin()) {
return error("未登录");
}
JsonNode room = myRtcService.getRoom(roomId);
return AjaxResult.success(room);
}
/** 销毁房间并踢出所有成员。 */
@ApiOperation("销毁面试视频房间")
@DeleteMapping({"/room/{roomId}", "/rooms/{roomId}"})
public AjaxResult destroyRoom(@PathVariable String roomId) {
if (!SecurityUtils.isLogin()) {
return error("未登录");
}
JsonNode result = myRtcService.destroyRoom(roomId);
return AjaxResult.success(result);
}
}

View File

@@ -2,9 +2,9 @@ package com.ruoyi.cms.service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.cms.config.MyRtcProperties; import com.ruoyi.cms.config.MyRtcProperties;
import com.ruoyi.cms.domain.vo.InterviewRtcSessionVO; import com.ruoyi.cms.domain.vo.InterviewRtcTokenVO;
import com.ruoyi.cms.domain.vo.RtcPeerTokenVO;
import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
@@ -20,8 +20,10 @@ import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -36,11 +38,15 @@ import java.util.regex.Pattern;
public class MyRtcService { public class MyRtcService {
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z0-9_-]{1,64}$"); private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[A-Za-z0-9_-]{1,64}$");
private static final String ROOM_CACHE_KEY_PREFIX = "interview:rtc:room:";
private static final int ROOM_CACHE_TTL_HOURS = 6;
private final MyRtcProperties properties; private final MyRtcProperties properties;
private final RedisCache redisCache;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
public MyRtcService(MyRtcProperties properties) { public MyRtcService(MyRtcProperties properties, RedisCache redisCache) {
this.properties = properties; this.properties = properties;
this.redisCache = redisCache;
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(Math.max(1000, properties.getConnectTimeoutMillis())); requestFactory.setConnectTimeout(Math.max(1000, properties.getConnectTimeoutMillis()));
requestFactory.setReadTimeout(Math.max(1000, properties.getReadTimeoutMillis())); requestFactory.setReadTimeout(Math.max(1000, properties.getReadTimeoutMillis()));
@@ -48,22 +54,44 @@ public class MyRtcService {
} }
/** 创建一个房间并为面试双方各签发一枚一次性 token。 */ /** 创建一个房间并为面试双方各签发一枚一次性 token。 */
public InterviewRtcSessionVO issueInterviewSession() { public InterviewRtcTokenVO issueToken(Long interviewId) {
ensureEnabled(); ensureEnabled();
String roomId = "room-" + UUID.randomUUID().toString().replace("-", ""); if (interviewId == null || interviewId <= 0) {
String interviewerPeerId = "interviewer-" + UUID.randomUUID().toString().replace("-", ""); throw new ServiceException("interviewId 格式不正确", HttpStatus.BAD_REQUEST);
String candidatePeerId = "candidate-" + UUID.randomUUID().toString().replace("-", ""); }
InterviewRtcSessionVO result = new InterviewRtcSessionVO(); String roomId = getOrCreateRoomId(interviewId);
String peerId = "peer-" + UUID.randomUUID().toString().replace("-", "");
InterviewRtcTokenVO result = new InterviewRtcTokenVO();
result.setRoomId(roomId); result.setRoomId(roomId);
result.setPeerId(peerId);
result.setToken(signRoomToken(roomId, peerId));
result.setExpiresIn(properties.getJwtExpiry()); result.setExpiresIn(properties.getJwtExpiry());
result.setInterviewer(new RtcPeerTokenVO(
"interviewer", interviewerPeerId, signRoomToken(roomId, interviewerPeerId)));
result.setCandidate(new RtcPeerTokenVO(
"candidate", candidatePeerId, signRoomToken(roomId, candidatePeerId)));
return result; return result;
} }
private String getOrCreateRoomId(Long interviewId) {
String cacheKey = ROOM_CACHE_KEY_PREFIX + interviewId;
String roomId = redisCache.getCacheObject(cacheKey);
if (StringUtils.isNotBlank(roomId)) {
return roomId;
}
String generatedRoomId = "room-" + UUID.randomUUID().toString().replace("-", "");
// 使用 Redis 原子写,避免企业端和候选人并发首次请求时生成两个房间。
Boolean created = redisCache.redisTemplate.opsForValue().setIfAbsent(
cacheKey, generatedRoomId, ROOM_CACHE_TTL_HOURS, TimeUnit.HOURS);
if (Boolean.TRUE.equals(created)) {
return generatedRoomId;
}
roomId = redisCache.getCacheObject(cacheKey);
if (StringUtils.isBlank(roomId)) {
throw new ServiceException("创建面试视频房间失败", HttpStatus.ERROR);
}
return roomId;
}
/** 查询 MyRTC 房间详情。 */ /** 查询 MyRTC 房间详情。 */
public JsonNode getRoom(String roomId) { public JsonNode getRoom(String roomId) {
validateRoomId(roomId); validateRoomId(roomId);
@@ -115,13 +143,19 @@ public class MyRtcService {
Date issuedAt = new Date(); Date issuedAt = new Date();
Date expiresAt = new Date(issuedAt.getTime() + parseExpiryMillis(properties.getJwtExpiry())); Date expiresAt = new Date(issuedAt.getTime() + parseExpiryMillis(properties.getJwtExpiry()));
// JJWT 0.9.x 的 signWith(algorithm, String) 会把 String 当作 Base64
// 密钥处理MyRTC Node 服务使用 JWT_SECRET 的原始 UTF-8 字节签名,
// 因此这里必须显式构造 SecretKey保证两端签名算法完全一致。
SecretKeySpec signingKey = new SecretKeySpec(
properties.getJwtSecret().getBytes(StandardCharsets.UTF_8),
SignatureAlgorithm.HS256.getJcaName());
return Jwts.builder() return Jwts.builder()
.claim("roomId", roomId) .claim("roomId", roomId)
.claim("peerId", peerId) .claim("peerId", peerId)
.claim("jti", roomId + ":" + peerId + ":" + UUID.randomUUID()) .claim("jti", roomId + ":" + peerId + ":" + UUID.randomUUID())
.setIssuedAt(issuedAt) .setIssuedAt(issuedAt)
.setExpiration(expiresAt) .setExpiration(expiresAt)
.signWith(SignatureAlgorithm.HS256, properties.getJwtSecret()) .signWith(SignatureAlgorithm.HS256, signingKey)
.compact(); .compact();
} }