Skip to content

Commit f66842e

Browse files
author
EpicFn
committed
refactor : signature 서비스 분리
1 parent cc354e0 commit f66842e

File tree

2 files changed

+74
-56
lines changed

2 files changed

+74
-56
lines changed

src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,8 @@ public class DashboardService {
3030
private final DashboardRepository dashboardRepository;
3131
private final MembershipService membershipService;
3232
private final ObjectMapper objectMapper;
33+
private final SignatureService signatureService;
3334

34-
@Value("${liveblocks.secret-key}")
35-
private String liveblocksSecretKey;
36-
37-
// 5분 (밀리초 단위)
38-
private static final long TOLERANCE_IN_MILLIS = 5 * 60 * 1000;
3935

4036

4137
// =========================== Graph 관련 메서드 ===========================
@@ -79,7 +75,7 @@ public void updateGraph(Integer dashboardId, BodyForReactFlow dto) {
7975
*/
8076
public void verifyAndUpdateGraph(Integer dashboardId, String requestBody, String signatureHeader) {
8177
// 1. 서명 검증
82-
if (!isValidSignature(requestBody, signatureHeader)) {
78+
if (!signatureService.isValidSignature(requestBody, signatureHeader)) {
8379
throw new SecurityException("Invalid webhook signature.");
8480
}
8581

@@ -114,56 +110,6 @@ public void verifyAccessPermission(Member member, Integer dashboardId) throws Ac
114110
}
115111

116112

117-
/**
118-
* LiveBlocks Webhook 요청의 유효성을 검증하는 메서드
119-
* @param requestBody 요청 바디
120-
* @param signatureHeader LiveBlocks가 제공하는 서명 헤더
121-
* @return 서명이 유효하면 true, 그렇지 않으면 false
122-
*/
123-
private boolean isValidSignature(String requestBody, String signatureHeader) {
124-
try {
125-
// 1. 헤더 파싱
126-
String[] parts = signatureHeader.split(",");
127-
long timestamp = -1;
128-
String signatureHashFromHeader = null;
129-
130-
for (String part : parts) {
131-
String[] pair = part.split("=", 2);
132-
if (pair.length == 2) {
133-
if ("t".equals(pair[0])) {
134-
timestamp = Long.parseLong(pair[1]);
135-
} else if ("v1".equals(pair[0])) {
136-
signatureHashFromHeader = pair[1];
137-
}
138-
}
139-
}
140-
141-
if (timestamp == -1 || signatureHashFromHeader == null) {
142-
return false; // 헤더 형식이 잘못됨
143-
}
144-
145-
// 2. 리플레이 공격 방지를 위한 타임스탬프 검증 (선택사항)
146-
long now = System.currentTimeMillis();
147-
if (now - timestamp > TOLERANCE_IN_MILLIS) {
148-
return false; // 너무 오래된 요청
149-
}
150-
151-
// 3. 서명 재생성
152-
String payload = timestamp + "." + requestBody;
153-
Mac mac = Mac.getInstance("HmacSHA256");
154-
SecretKeySpec secretKeySpec = new SecretKeySpec(liveblocksSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
155-
mac.init(secretKeySpec);
156-
byte[] expectedHashBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
157-
158-
// 4. 서명 비교 (타이밍 공격 방지를 위해 MessageDigest.isEqual 사용)
159-
byte[] signatureHashBytesFromHeader = Hex.decodeHex(signatureHashFromHeader);
160-
return MessageDigest.isEqual(expectedHashBytes, signatureHashBytesFromHeader);
161-
162-
} catch (Exception e) {
163-
// 파싱 실패, 디코딩 실패 등 모든 예외는 검증 실패로 간주
164-
return false;
165-
}
166-
}
167113

168114

169115
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.tuna.zoopzoop.backend.domain.dashboard.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.apache.commons.codec.binary.Hex;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.stereotype.Service;
7+
8+
import javax.crypto.Mac;
9+
import javax.crypto.spec.SecretKeySpec;
10+
import java.nio.charset.StandardCharsets;
11+
import java.security.MessageDigest;
12+
13+
@Service
14+
@RequiredArgsConstructor
15+
public class SignatureService {
16+
@Value("${liveblocks.secret-key}")
17+
private String liveblocksSecretKey;
18+
19+
// 5분 (밀리초 단위)
20+
private static final long TOLERANCE_IN_MILLIS = 5 * 60 * 1000;
21+
22+
/**
23+
* LiveBlocks Webhook 요청의 유효성을 검증하는 메서드
24+
* @param requestBody 요청 바디
25+
* @param signatureHeader LiveBlocks가 제공하는 서명 헤더
26+
* @return 서명이 유효하면 true, 그렇지 않으면 false
27+
*/
28+
public boolean isValidSignature(String requestBody, String signatureHeader) {
29+
try {
30+
// 1. 헤더 파싱
31+
String[] parts = signatureHeader.split(",");
32+
long timestamp = -1;
33+
String signatureHashFromHeader = null;
34+
35+
for (String part : parts) {
36+
String[] pair = part.split("=", 2);
37+
if (pair.length == 2) {
38+
if ("t".equals(pair[0])) {
39+
timestamp = Long.parseLong(pair[1]);
40+
} else if ("v1".equals(pair[0])) {
41+
signatureHashFromHeader = pair[1];
42+
}
43+
}
44+
}
45+
46+
if (timestamp == -1 || signatureHashFromHeader == null) {
47+
return false; // 헤더 형식이 잘못됨
48+
}
49+
50+
// 2. 리플레이 공격 방지를 위한 타임스탬프 검증 (선택사항)
51+
long now = System.currentTimeMillis();
52+
if (now - timestamp > TOLERANCE_IN_MILLIS) {
53+
return false; // 너무 오래된 요청
54+
}
55+
56+
// 3. 서명 재생성
57+
String payload = timestamp + "." + requestBody;
58+
Mac mac = Mac.getInstance("HmacSHA256");
59+
SecretKeySpec secretKeySpec = new SecretKeySpec(liveblocksSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
60+
mac.init(secretKeySpec);
61+
byte[] expectedHashBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
62+
63+
// 4. 서명 비교 (타이밍 공격 방지를 위해 MessageDigest.isEqual 사용)
64+
byte[] signatureHashBytesFromHeader = Hex.decodeHex(signatureHashFromHeader);
65+
return MessageDigest.isEqual(expectedHashBytes, signatureHashBytesFromHeader);
66+
67+
} catch (Exception e) {
68+
// 파싱 실패, 디코딩 실패 등 모든 예외는 검증 실패로 간주
69+
return false;
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)