@@ -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}
0 commit comments