Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snackgame.biz): 게임 결과를 서명한다 #179

Merged
merged 6 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.snackgame.server.game.sign.config

import com.snackgame.server.game.sign.domain.PKCS8PEMKey
import com.snackgame.server.game.sign.domain.Signer
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class SignerConfig(
@Value("\${game.signing.private-key-pem}")
privateKeyPem: String
) {
private val privateKey = PKCS8PEMKey(privateKeyPem).toPrivateKey()

@Bean
fun signer() = Signer(privateKey)
}
22 changes: 22 additions & 0 deletions src/main/java/com/snackgame/server/game/sign/domain/PKCS8PEMKey.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.snackgame.server.game.sign.domain

import java.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*

class PKCS8PEMKey(
private val rawPEM: String
) {

fun toPrivateKey(): PrivateKey {
val base64EncodedKey = rawPEM.replace("\n", "")
.removePrefix("-----BEGIN PRIVATE KEY-----")
.removeSuffix("-----END PRIVATE KEY-----")

return Base64.getDecoder().decode(base64EncodedKey).let {
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePrivate(PKCS8EncodedKeySpec(it))
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/snackgame/server/game/sign/domain/Signer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.snackgame.server.game.sign.domain

import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.Jwts
import java.security.PrivateKey

class Signer(private val privateKey: PrivateKey) {

fun sign(content: String): String {
runCatching { ObjectMapper().readTree(content) }
.onFailure { throw Exception("Could not parse JSON") }

return Jwts.builder()
.header().keyId(privateKey.hashCode().toString())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyId를 추가하는 것은 무엇을 위한 작업인가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 스낵게임 Biz 문서 하단을 보시면 이해가 쉬울 것 같은데요.

RSASHA로 서명된 JWT는 공개키인 JWK로 검증이 가능합니다.
이 JWK를 여러개 묶어놓은게 JWKS(JSON Web Key Set)인데요.
그 키들 중에서 해당 JWT에 맞는 공개키를 찾는데 kid를 사용할 수 있습니다.
이렇게 되면 '키 순환'이 가능해진다는 장점도 있습니다.
만일 기존 키페어의 유출/분실 사고가 발생한다면 키셋에서 해당 키를 제거하고 새 키페어를 등록해줄 수 있습니다.
그러면 기존 JWT는 검증에 실패하고, 새 키페어로 서명된 JWT는 검증이 잘 되겠죠?
이런 장점들 때문에 서명에 사용한 keyId를 적고, 키를 관리하는 것이 좋습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 이해했습니다! 자세한 설명 감사드립니다 🫢

.and()
.content(content)
.signWith(privateKey)
.compact()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.snackgame.server.game.sign.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.snackgame.server.game.sign.domain.Signer
import com.snackgame.server.game.sign.service.dto.SignedResponse
import org.springframework.core.MethodParameter
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice

@RestControllerAdvice
class ResponseSigner(
val signer: Signer,
val objectMapper: ObjectMapper
) : ResponseBodyAdvice<Any> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResponseBodyAdvice가 공통된 응답을 위해 쓰인다고 하는데 지금처럼 DTO를 만들어서 하던 방법과 차이점이나 ResponseBodyAdvice를 쓰게 된 이유가 있을까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선요약: 최소한의 접점만으로 문제를 가장 직접적으로 해결하는 방법이다

이것도 풀자면 긴 스토리긴 한데요.
우선 목적은 최대한 기존 코드를 건드리지 않고 반환값을 서명하는 것입니다. (서명에 대한 관심사 분리)
물론 멋있다는 점도 한 몫 합니다 ㅋ.ㅋ

원래 HandlerMethodReturnValueHandler 혹은 HandlerInterceptor를 사용하려고 했습니다. (둘 다 실패)

전자는 @ResponseBody에 대한 ReturnValueHandler가 이미 있기 때문에 제가 새로 추가한 핸들러가 실행되지 않아서 실패했구용

후자는 HttpServletResponse의 body를 조작해야 하는데, 이 body는 이미 String이 된 상태이므로 조작이 조금 복잡해지며 부작용의 우려가 있습니다.
이것보다 더 직접적인 방법(ResponseBodyAdvice)가 있어 이걸 선택했습니다.

ResponseBodyAdvice 인터페이스는 이름처럼 @ResponseBody를 처리하는데 참고자료의 역할을 할 수 있습니다. (Spring API 문서)
말씀해주신 목적으로도 사용하긴 하는 것 같은데,
특별히 '공통된 응답을 만드는데 사용하라' 같은 의도는 없는 것으로 보입니다.
그래서 앞서 말씀드린 목적으로 사용할 수 있다고 생각했구요,

이 친구의 작동방식을 고려해보면 - 실제로 MessageConverter가 바디를 변환하기 직전!에 작동하므로, 변환하고자 하는 값을 직접적으로(타입도 살아있는 상태로) 받고 제어할 수 있게 됩니다.
이 틈에 기존 객체에 서명하고 감쌈으로써 최소한의 접점만으로 원하는 목적을 달성하는 AOP를 할 수 있게 됩니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역시 땡칠... 어노테이션만으로 처리할 수 있게 된게 인상적입니다..
오늘도 하나배워갑니다 ☺️


override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return returnType.hasMethodAnnotation(Signed::class.java)
}

override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
return body?.let {
val jsonContent = objectMapper.writeValueAsString(it)
SignedResponse(it, signer.sign(jsonContent))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.snackgame.server.game.sign.service

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
internal annotation class Signed
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.snackgame.server.game.sign.service.dto

data class SignedResponse(
val original: Any,
val signed: String
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.snackgame.server.game.snackgame.biz.controller

import com.snackgame.server.auth.token.support.Authenticated
import com.snackgame.server.game.sign.service.Signed
import com.snackgame.server.game.snackgame.biz.service.SnackgameBizService
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameEndResponse
import com.snackgame.server.game.snackgame.core.service.dto.SnackgameResponse
Expand Down Expand Up @@ -88,6 +89,7 @@ class SnackgameBizController(
fun resume(@Authenticated member: Member, @PathVariable sessionId: Long): SnackgameResponse =
snackgameBizService.resume(member.id, sessionId)

@Signed
@Operation(summary = "스낵게임 세션 종료", description = "세션을 종료한다")
@PostMapping("/{sessionId}/end")
fun end(@Authenticated member: Member, @PathVariable sessionId: Long): SnackgameEndResponse =
Expand Down
30 changes: 30 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,33 @@ security:
access-expiry-days: 7
refresh-secret-key: snackgamelocalrefreshsecretkey12345
refresh-expiry-days: 30
game.signing:
private-key-pem: |
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC92qcfpwgUADIL
QjRgrYddJYOQrapgEW7UDrrcizJ11di36VdAnAeQhfFKPEnZ+Khq2XGJXl5CKCql
aWJl1XhxkJtFQtBHzVPg/TkrpMb2Zym9ySjHnkTFObllh5GqFU2J573vwWAHm81s
RtADcYje4+l6UMWgD3P+4JVXerheGtq8I4Kdp+ePujg3Myzkjr7mdBvTD3R4VU6s
FBcxDg4OkDc0DDZRvBJ+nzVjt7pALiOvskYeKoqWYTHXpOMzeh+siWoijv7BSlFH
L53qjv2orhOKA1ugwxoxjJ2rw5rfJMy1gyuevXzsC9wXE6Z5pxKskf7V5K6eSgwC
TV1OwRBtAgMBAAECggEAYtkXLqm+0PsREz+TGcr/sYW4rHlfPcmV1J8et3lZ+nQu
9AJvxVn3rk2uxaICKZUHyyoPrSAPH47eNWVbL/4nSamqzjLtUpUbWQc+3vRaCyzH
VOMJcnV7BNz7JFujgdS5nFWmVNBv+UYrfHaaE1DnnHU5uM+ZNa8WC9xOpU9l8shO
uHmrBSFWfs3bhg9MA4PBKyam6I5VrYTu2I0NUKg7/pqiNPjLc0oOFQfTcbFF/bBQ
EFDo7KQuN8kQ6eNSt0ap0c17NP+kzvRbsx8pHJLDGdDpPMhAfR/HAkjYDHi3tGu0
gOdXGEKeeBMvOcuaXnt6lHnO6Sp7Lvn6sBUlw4ixAQKBgQDol6sgeKsGmfLYVSQh
tMI75zBGOeL+M0ElOrAqQtcXxkdtQA9vMRL2MMby1vx3Q/dL+nla6hLrsP7vYs6W
zohmwkPpKFOwjancDuzwAGPW5Oe7hSM1wgS9GmDulrlBrjlTVhwMYMAS9CrQ9s+6
HYNImJH6Qy/9wRixrFiOLWxmDQKBgQDQ9egoxkI4NoXM4T4J65KtaM+12o5aTTjN
Itri0cDtjuSf4c8Ryq+NayKbKGNGWqc/LXMwVbXL+KP9vc33U5pmbeKsZfYHZ1cX
97uK8KqqjJYcIu80EcnfF9yqSSWgSomqKg+wIhzfuOAxhDURLnc9NjGJ9cX1gDpr
HGImzJgb4QKBgQDUwN8kjZwLJsiuv3qUZk6BEuDia9LJqMJ/Nmar0YYEVjdBpKY5
lE7+6PEicipvQbk0Q4G7+n6iHr2i5OOJqJO2Qx5xaJTc20gzZWevxPuONsYNjiP9
6WICQ93EgBQnEyGWrB5t6BXpgHSlvg2W9aX/wqdMnyaH25gl9TrvmnccBQJ/K0K9
zbM+AmVHTiNTtLoQo9YPKpcU4Z0bJV0R8+N5DbWHn5IgTI6pyZ1kjLrP0sDx3yKJ
tngIKzJOwTmKaqnkTVBynT5+sQmz/kJLkXhB3Q0+BpU08JK+OHG4PYjnQUgtOA19
MfneAHNF9vpoAiaciQ+vMeCwGyNw9LvrlhzFYQKBgHflIhIL1OW+hRTM6c6eJAyd
/kWWhBFasIoKIvEM4xpL/z1fHQOcOf3yRXlvGdE3b1N/a5zbFTk1ne1Vvrz38vDI
KKCh/vU8KGYKqgdHosOY8OuDtdoeuOK1mALzGISmOEg6SsmIhieRVD+L8/tyPBGm
xC/YpPH8UAOqabX6ggSu
-----END PRIVATE KEY-----
31 changes: 31 additions & 0 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,34 @@ security:
access-expiry-days: 7
refresh-secret-key: testrefreshsecretkey1234567890
refresh-expiry-days: 30

game.signing:
private-key-pem: |
-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC92qcfpwgUADIL
QjRgrYddJYOQrapgEW7UDrrcizJ11di36VdAnAeQhfFKPEnZ+Khq2XGJXl5CKCql
aWJl1XhxkJtFQtBHzVPg/TkrpMb2Zym9ySjHnkTFObllh5GqFU2J573vwWAHm81s
RtADcYje4+l6UMWgD3P+4JVXerheGtq8I4Kdp+ePujg3Myzkjr7mdBvTD3R4VU6s
FBcxDg4OkDc0DDZRvBJ+nzVjt7pALiOvskYeKoqWYTHXpOMzeh+siWoijv7BSlFH
L53qjv2orhOKA1ugwxoxjJ2rw5rfJMy1gyuevXzsC9wXE6Z5pxKskf7V5K6eSgwC
TV1OwRBtAgMBAAECggEAYtkXLqm+0PsREz+TGcr/sYW4rHlfPcmV1J8et3lZ+nQu
9AJvxVn3rk2uxaICKZUHyyoPrSAPH47eNWVbL/4nSamqzjLtUpUbWQc+3vRaCyzH
VOMJcnV7BNz7JFujgdS5nFWmVNBv+UYrfHaaE1DnnHU5uM+ZNa8WC9xOpU9l8shO
uHmrBSFWfs3bhg9MA4PBKyam6I5VrYTu2I0NUKg7/pqiNPjLc0oOFQfTcbFF/bBQ
EFDo7KQuN8kQ6eNSt0ap0c17NP+kzvRbsx8pHJLDGdDpPMhAfR/HAkjYDHi3tGu0
gOdXGEKeeBMvOcuaXnt6lHnO6Sp7Lvn6sBUlw4ixAQKBgQDol6sgeKsGmfLYVSQh
tMI75zBGOeL+M0ElOrAqQtcXxkdtQA9vMRL2MMby1vx3Q/dL+nla6hLrsP7vYs6W
zohmwkPpKFOwjancDuzwAGPW5Oe7hSM1wgS9GmDulrlBrjlTVhwMYMAS9CrQ9s+6
HYNImJH6Qy/9wRixrFiOLWxmDQKBgQDQ9egoxkI4NoXM4T4J65KtaM+12o5aTTjN
Itri0cDtjuSf4c8Ryq+NayKbKGNGWqc/LXMwVbXL+KP9vc33U5pmbeKsZfYHZ1cX
97uK8KqqjJYcIu80EcnfF9yqSSWgSomqKg+wIhzfuOAxhDURLnc9NjGJ9cX1gDpr
HGImzJgb4QKBgQDUwN8kjZwLJsiuv3qUZk6BEuDia9LJqMJ/Nmar0YYEVjdBpKY5
lE7+6PEicipvQbk0Q4G7+n6iHr2i5OOJqJO2Qx5xaJTc20gzZWevxPuONsYNjiP9
6WICQ93EgBQnEyGWrB5t6BXpgHSlvg2W9aX/wqdMnyaH25gl9TrvmnccBQJ/K0K9
zbM+AmVHTiNTtLoQo9YPKpcU4Z0bJV0R8+N5DbWHn5IgTI6pyZ1kjLrP0sDx3yKJ
tngIKzJOwTmKaqnkTVBynT5+sQmz/kJLkXhB3Q0+BpU08JK+OHG4PYjnQUgtOA19
MfneAHNF9vpoAiaciQ+vMeCwGyNw9LvrlhzFYQKBgHflIhIL1OW+hRTM6c6eJAyd
/kWWhBFasIoKIvEM4xpL/z1fHQOcOf3yRXlvGdE3b1N/a5zbFTk1ne1Vvrz38vDI
KKCh/vU8KGYKqgdHosOY8OuDtdoeuOK1mALzGISmOEg6SsmIhieRVD+L8/tyPBGm
xC/YpPH8UAOqabX6ggSu
-----END PRIVATE KEY-----
1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ spring:
- classpath:secrets/oauth-secrets.yml
- classpath:secrets/aws-secrets.yml
- classpath:secrets/notification-secrets.yml
- classpath:secrets/game-signing-secrets.yml
mvc:
throw-exception-if-no-handler-found: true
web:
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/secrets
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@file:Suppress("NonAsciiCharacters")

package com.snackgame.server.game.sign.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class PKCS8PEMKeyTest {

@Test
fun `PKCS#8 형식의 PEM 키를 프라이빗 키로 변환한다`() {
val pemKey = PKCS8PEMKey(RAW_PKCS8_PEM)

val privateKey = pemKey.toPrivateKey()

assertThat(privateKey.encoded).isEqualTo(KEY_IN_BYTES)
}

companion object {
private const val RAW_PKCS8_PEM = """-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC92qcfpwgUADIL
QjRgrYddJYOQrapgEW7UDrrcizJ11di36VdAnAeQhfFKPEnZ+Khq2XGJXl5CKCql
aWJl1XhxkJtFQtBHzVPg/TkrpMb2Zym9ySjHnkTFObllh5GqFU2J573vwWAHm81s
RtADcYje4+l6UMWgD3P+4JVXerheGtq8I4Kdp+ePujg3Myzkjr7mdBvTD3R4VU6s
FBcxDg4OkDc0DDZRvBJ+nzVjt7pALiOvskYeKoqWYTHXpOMzeh+siWoijv7BSlFH
L53qjv2orhOKA1ugwxoxjJ2rw5rfJMy1gyuevXzsC9wXE6Z5pxKskf7V5K6eSgwC
TV1OwRBtAgMBAAECggEAYtkXLqm+0PsREz+TGcr/sYW4rHlfPcmV1J8et3lZ+nQu
9AJvxVn3rk2uxaICKZUHyyoPrSAPH47eNWVbL/4nSamqzjLtUpUbWQc+3vRaCyzH
VOMJcnV7BNz7JFujgdS5nFWmVNBv+UYrfHaaE1DnnHU5uM+ZNa8WC9xOpU9l8shO
uHmrBSFWfs3bhg9MA4PBKyam6I5VrYTu2I0NUKg7/pqiNPjLc0oOFQfTcbFF/bBQ
EFDo7KQuN8kQ6eNSt0ap0c17NP+kzvRbsx8pHJLDGdDpPMhAfR/HAkjYDHi3tGu0
gOdXGEKeeBMvOcuaXnt6lHnO6Sp7Lvn6sBUlw4ixAQKBgQDol6sgeKsGmfLYVSQh
tMI75zBGOeL+M0ElOrAqQtcXxkdtQA9vMRL2MMby1vx3Q/dL+nla6hLrsP7vYs6W
zohmwkPpKFOwjancDuzwAGPW5Oe7hSM1wgS9GmDulrlBrjlTVhwMYMAS9CrQ9s+6
HYNImJH6Qy/9wRixrFiOLWxmDQKBgQDQ9egoxkI4NoXM4T4J65KtaM+12o5aTTjN
Itri0cDtjuSf4c8Ryq+NayKbKGNGWqc/LXMwVbXL+KP9vc33U5pmbeKsZfYHZ1cX
97uK8KqqjJYcIu80EcnfF9yqSSWgSomqKg+wIhzfuOAxhDURLnc9NjGJ9cX1gDpr
HGImzJgb4QKBgQDUwN8kjZwLJsiuv3qUZk6BEuDia9LJqMJ/Nmar0YYEVjdBpKY5
lE7+6PEicipvQbk0Q4G7+n6iHr2i5OOJqJO2Qx5xaJTc20gzZWevxPuONsYNjiP9
6WICQ93EgBQnEyGWrB5t6BXpgHSlvg2W9aX/wqdMnyaH25gl9TrvmnccBQJ/K0K9
zbM+AmVHTiNTtLoQo9YPKpcU4Z0bJV0R8+N5DbWHn5IgTI6pyZ1kjLrP0sDx3yKJ
tngIKzJOwTmKaqnkTVBynT5+sQmz/kJLkXhB3Q0+BpU08JK+OHG4PYjnQUgtOA19
MfneAHNF9vpoAiaciQ+vMeCwGyNw9LvrlhzFYQKBgHflIhIL1OW+hRTM6c6eJAyd
/kWWhBFasIoKIvEM4xpL/z1fHQOcOf3yRXlvGdE3b1N/a5zbFTk1ne1Vvrz38vDI
KKCh/vU8KGYKqgdHosOY8OuDtdoeuOK1mALzGISmOEg6SsmIhieRVD+L8/tyPBGm
xC/YpPH8UAOqabX6ggSu
-----END PRIVATE KEY-----
"""
val KEY_IN_BYTES = """
308204bb020100300d06092a864886f70d0101010500048204a5308204a1020100
0282010100bddaa71fa7081400320b423460ad875d258390adaa60116ed40ebad
c8b3275d5d8b7e957409c079085f14a3c49d9f8a86ad971895e5e42282aa56962
65d57871909b4542d047cd53e0fd392ba4c6f66729bdc928c79e44c539b96587
91aa154d89e7bdefc160079bcd6c46d0037188dee3e97a50c5a00f73fee09557
7ab85e1adabc23829da7e78fba3837332ce48ebee6741bd30f7478554eac1417
310e0e0e9037340c3651bc127e9f3563b7ba402e23afb2461e2a8a966131d7a4
e3337a1fac896a228efec14a51472f9dea8efda8ae138a035ba0c31a318c9dab
c39adf24ccb5832b9ebd7cec0bdc1713a679a712ac91fed5e4ae9e4a0c024d5d
4ec1106d02030100010282010062d9172ea9bed0fb11133f9319caffb185b8ac
795f3dc995d49f1eb77959fa742ef4026fc559f7ae4daec5a202299507cb2a0f
ad200f1f8ede35655b2ffe2749a9aace32ed52951b59073edef45a0b2cc754e3
0972757b04dcfb245ba381d4b99c55a654d06ff9462b7c769a1350e79c7539b8
cf9935af160bdc4ea54f65f2c84eb879ab0521567ecddb860f4c0383c12b26a6
e88e55ad84eed88d0d50a83bfe9aa234f8cb734a0e1507d371b145fdb0501050
e8eca42e37c910e9e352b746a9d1cd7b34ffa4cef45bb31f291c92c319d0e93c
c8407d1fc70248d80c78b7b46bb480e75718429e78132f39cb9a5e7b7a9479ce
e92a7b2ef9fab01525c388b10102818100e897ab2078ab0699f2d8552421b4c2
3be7304639e2fe3341253ab02a42d717c6476d400f6f3112f630c6f2d6fc7743
f74bfa795aea12ebb0feef62ce96ce8866c243e92853b08da9dc0eecf00063d6
e4e7bb852335c204bd1a60ee96b941ae3953561c0c60c012f42ad0f6cfba1d83
489891fa432ffdc118b1ac588e2d6c660d02818100d0f5e828c642383685cce1
3e09eb92ad68cfb5da8e5a4d38cd22dae2d1c0ed8ee49fe1cf11caaf8d6b229b
2863465aa73f2d733055b5cbf8a3fdbdcdf7539a666de2ac65f607675717f7bb
8af0aaaa8c961c22ef3411c9df17dcaa4925a04a89aa2a0fb0221cdfb8e03184
35112e773d363189f5c5f5803a6b1c6226cc981be102818100d4c0df248d9c0b
26c8aebf7a94664e8112e0e26bd2c9a8c27f3666abd18604563741a4a639944e
fee8f122722a6f41b9344381bbfa7ea21ebda2e4e389a893b6431e716894dcdb
48336567afc4fb8e36c60d8e23fde9620243ddc4801427132196ac1e6de815e9
8074a5be0d96f5a5ffc2a74c9f2687db9825f53aef9a771c05027f2b42bdcdb3
3e0265474e2353b4ba10a3d60f2a9714e19d1b255d11f3e3790db5879f92204c
8ea9c99d648cbacfd2c0f1df2289b678082b324ec1398a6aa9e44d50729d3e7e
b109b3fe424b917841dd0d3e069534f092be3871b83d88e741482d380d7d31f9
de007345f6fa6802269c890faf31e0b01b2370f4bbeb961cc56102818077e522
120bd4e5be8514cce9ce9e240c9dfe459684115ab08a0a22f10ce31a4bff3d5f
1d039c39fdf245796f19d1376f537f6b9cdb1539359ded55bebcf7f2f0c828a0
a1fef53c28660aaa0747a2c398f0eb83b5da1eb8e2b59802f31884a638483a4a
c988862791543f8bf3fb723c11a6c42fd8a4f1fc5003aa69b5fa8204ae"""
.replace("\n", "")
.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
}
75 changes: 75 additions & 0 deletions src/test/java/com/snackgame/server/game/sign/domain/SignerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
@file:Suppress("NonAsciiCharacters")

package com.snackgame.server.game.sign.domain

import com.fasterxml.jackson.databind.ObjectMapper
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test

class SignerTest {

private val signer = Signer(PRIVATE_KEY)

@Test
fun `개인키를 사용해 객체를 JWT로 서명한다`() {
data class TestObject(val key: String)

val testObjectAsJson = TestObject("value")
.let { ObjectMapper().writeValueAsString(it) }

val jwtPayload = signer.sign(testObjectAsJson)
.substringAfter('.')
.substringBeforeLast('.')

assertThat(jwtPayload).asBase64Decoded().asString()
.isEqualTo(testObjectAsJson)
}

@Test
fun `개인키의 hashCode를 keyId로 사용하여 서명한다`() {
val jwtHeader = signer.sign("")
.substringBefore('.')
assertThat(jwtHeader).asBase64Decoded().asString().contains(""""kid":"${PRIVATE_KEY.hashCode()}"""")
}

@Test
fun `서명할 데이터는 JSON이어야한다`() {
assertThatThrownBy { signer.sign("{") }
.hasMessage("Could not parse JSON")
}

companion object {
private val PRIVATE_KEY = PKCS8PEMKey(
"""-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC92qcfpwgUADIL
QjRgrYddJYOQrapgEW7UDrrcizJ11di36VdAnAeQhfFKPEnZ+Khq2XGJXl5CKCql
aWJl1XhxkJtFQtBHzVPg/TkrpMb2Zym9ySjHnkTFObllh5GqFU2J573vwWAHm81s
RtADcYje4+l6UMWgD3P+4JVXerheGtq8I4Kdp+ePujg3Myzkjr7mdBvTD3R4VU6s
FBcxDg4OkDc0DDZRvBJ+nzVjt7pALiOvskYeKoqWYTHXpOMzeh+siWoijv7BSlFH
L53qjv2orhOKA1ugwxoxjJ2rw5rfJMy1gyuevXzsC9wXE6Z5pxKskf7V5K6eSgwC
TV1OwRBtAgMBAAECggEAYtkXLqm+0PsREz+TGcr/sYW4rHlfPcmV1J8et3lZ+nQu
9AJvxVn3rk2uxaICKZUHyyoPrSAPH47eNWVbL/4nSamqzjLtUpUbWQc+3vRaCyzH
VOMJcnV7BNz7JFujgdS5nFWmVNBv+UYrfHaaE1DnnHU5uM+ZNa8WC9xOpU9l8shO
uHmrBSFWfs3bhg9MA4PBKyam6I5VrYTu2I0NUKg7/pqiNPjLc0oOFQfTcbFF/bBQ
EFDo7KQuN8kQ6eNSt0ap0c17NP+kzvRbsx8pHJLDGdDpPMhAfR/HAkjYDHi3tGu0
gOdXGEKeeBMvOcuaXnt6lHnO6Sp7Lvn6sBUlw4ixAQKBgQDol6sgeKsGmfLYVSQh
tMI75zBGOeL+M0ElOrAqQtcXxkdtQA9vMRL2MMby1vx3Q/dL+nla6hLrsP7vYs6W
zohmwkPpKFOwjancDuzwAGPW5Oe7hSM1wgS9GmDulrlBrjlTVhwMYMAS9CrQ9s+6
HYNImJH6Qy/9wRixrFiOLWxmDQKBgQDQ9egoxkI4NoXM4T4J65KtaM+12o5aTTjN
Itri0cDtjuSf4c8Ryq+NayKbKGNGWqc/LXMwVbXL+KP9vc33U5pmbeKsZfYHZ1cX
97uK8KqqjJYcIu80EcnfF9yqSSWgSomqKg+wIhzfuOAxhDURLnc9NjGJ9cX1gDpr
HGImzJgb4QKBgQDUwN8kjZwLJsiuv3qUZk6BEuDia9LJqMJ/Nmar0YYEVjdBpKY5
lE7+6PEicipvQbk0Q4G7+n6iHr2i5OOJqJO2Qx5xaJTc20gzZWevxPuONsYNjiP9
6WICQ93EgBQnEyGWrB5t6BXpgHSlvg2W9aX/wqdMnyaH25gl9TrvmnccBQJ/K0K9
zbM+AmVHTiNTtLoQo9YPKpcU4Z0bJV0R8+N5DbWHn5IgTI6pyZ1kjLrP0sDx3yKJ
tngIKzJOwTmKaqnkTVBynT5+sQmz/kJLkXhB3Q0+BpU08JK+OHG4PYjnQUgtOA19
MfneAHNF9vpoAiaciQ+vMeCwGyNw9LvrlhzFYQKBgHflIhIL1OW+hRTM6c6eJAyd
/kWWhBFasIoKIvEM4xpL/z1fHQOcOf3yRXlvGdE3b1N/a5zbFTk1ne1Vvrz38vDI
KKCh/vU8KGYKqgdHosOY8OuDtdoeuOK1mALzGISmOEg6SsmIhieRVD+L8/tyPBGm
xC/YpPH8UAOqabX6ggSu
-----END PRIVATE KEY-----
"""
).toPrivateKey()
}
}
Loading
Loading