일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- css framework
- Unity IAP
- spread 연산자
- MySQL
- --watch
- Google Developer API
- Camera Zoom
- critical rendering path
- Digital Ocean
- OverTheWire
- Spring Boot
- linux
- docker
- Google Refund
- Unity Editor
- express
- screencapture
- unity
- Packet Network
- mongoDB
- java
- server
- rpg server
- nodejs
- draganddrop
- Camera Movement
- react
- SDK upgrade
- springboot
- Git
- Today
- Total
우당탕탕 개발일지
[방치RPG 서버 제작기] 7. backup API 구현하기 본문
액세스 토큰 관리하기(클라이언트)
로그인은 액세스토큰이 없어도 되지만, 로그인 후에 수행하는 대부분의 api는 액세스 토큰이 필요하다. 이를 위해, 클라이언트에서 액세스토큰을 관리해야한다. 서버에서 헤더로 액세스토큰과 리프레시 토큰을 발급하기 때문에, 헤더에서 뽑아낸 후 LocalTokenStore에 저장한다.
private IEnumerator SendGuestLogin(string deviceId, Action<ResponseCode> handler)
{
GuestLoginRequestDto payload = new GuestLoginRequestDto { deviceId = deviceId };
string json = JsonUtility.ToJson(payload);
using (UnityWebRequest request = new UnityWebRequest(guestLoginUrl, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Accept", "application/json"); // ✅ 이거 중요!
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// ✅ 헤더에서 토큰 추출
string accessTokenHeader = request.GetResponseHeader("Authorization");
string refreshTokenHeader = request.GetResponseHeader("X-Refresh-Token");
// 액세스 토큰에서 "Bearer " 제거 (있다면)
string accessToken = accessTokenHeader?.StartsWith("Bearer ") == true
? accessTokenHeader.Substring(7)
: accessTokenHeader;
string refreshToken = refreshTokenHeader;
// ✅ 응답 바디에서 rsaPublicKey, userData 추출
string responseText = request.downloadHandler.text;
LoginResponseDto data = JsonUtility.FromJson<LoginResponseDto>(responseText);
DebugX.Log($"[Token] Access: {accessToken}");
DebugX.Log($"[Token] Refresh: {refreshToken}");
DebugX.Log($"[RSA] PublicKey: {data.rsaPublicKey}");
DebugX.Log($"[UserData] Raw JSON: {data.userData}");
LocalTokenStore.AccessToken = accessToken;
LocalTokenStore.RefreshToken = refreshToken;
LocalTokenStore.RsaPublicKey = data.rsaPublicKey;
}
else
{
Debug.LogError($"Login Failed. url:{guestLoginUrl}, reason : {request.error} ( code {request.responseCode} )");
}
}
}
유저 백업 데이터 전송하기
- 유저들의 데이터를 json화 한다.
- 결과값을 AES 로 암호화한다.
- 서버에서 받아서 AES로 복호화한다
- 데이터를 각 processor 에게 보내 저장한다.
위 순서대로 진행한다. AES 암호/복호화에 사용하는 key는 클라와 서버에서 동일한값을 가지고있다.
public static string encrypt(string plainText)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = Encoding.UTF8.GetBytes(key);
aes.IV = new byte[16];
using var encryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(encryptedBytes);
}
이제 서버에서 데이터를 받을 경우 , 아래 함수를 이용해서 복호화를 진행한다. AESUtil 이라는 클래스를 만들어서 복호화를 진행한다.
주의할점은, 클라이언트의 암호화 방식과 서버의 복호화 방식을 정확하게 맞춰줘야 한다. PaddingMode 와 Mode 를 똑.같.이. 맞춰준다
private static final IvParameterSpec IV = new IvParameterSpec(new byte[16]); // 16바이트 0 초기화
public static String decrypt(String base64Encrypted) throws Exception {
byte[] encrypted = Base64.getDecoder().decode(base64Encrypted);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, IV);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
}
[Error] io.jsonwebtoken.io.DecodingException: Illegal base64url character: ' '
유저 백업 API 호출 시 이런 에러가 발생했다. 처음엔 AES 암호/복호화에서 발생하는 문제인 줄 알았으나, 알고보니 액세스토큰에서 userId를 뽑아오는 부분에서 에러가 발생한거였다....
아래는 BackupController 의 backup 함수이다.
@PostMapping("/backup")
public ResponseEntity<?> backupData(@RequestBody BackupRequestDto request,
@RequestHeader("Authorization") String token) {
try {
String decryptedJson = AESUtil.decrypt(request.getData());
Long userId = jwtProvider.getUserIdFromToken(token);
// 2. JSON → DTO 매핑
UserBackupDto backupDto = objectMapper.readValue(decryptedJson, UserBackupDto.class);
// 3. 유효성 검사
ValidationResult result = BackupDataValidator.validate(backupDto);
if (!result.isValid()) {
return ResponseEntity.badRequest().body("검증 실패: " + String.join(", ", result.getErrorMessages()));
}
backupService.saveBackup(userId, backupDto);
return ResponseEntity.ok(new BackupResponseDto("백업 성공"));
} catch (Exception e) {
e.printStackTrace(); // 실운영에서는 로거로 기록
return ResponseEntity.internalServerError().body(new BackupResponseDto("백업 성공"));
}
}
jwtProvider에서 토큰을 이용해 userId를 가져오는 부분이 있는데, 들어가보면, Bearer 라는 문자열이 토큰에 포함되어있을 경우, 이부분을 제거한 다음 parsing 하는 부분이 빠져있어서 발생한 오류였다. JwtFilter에서는 Bearer 를 잘 제거했는데 , 여기에서 에러가 날 줄은...흑흑 삽질만 2시간 ㅠㅠ
public Long getUserIdFromToken(String token) {
if (token == null || token.isBlank()) {
throw new IllegalArgumentException("토큰이 비어있습니다.");
}
// "Bearer " 접두어가 있으면 제거
if (token.startsWith("Bearer ")) {
token = token.substring(7).trim(); // "Bearer " 제거 후 공백 제거
}
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
[Error] 백업 실패: JSON parse error: Invalid value.
서버에서는 db저장이 정상적으로 되는데, 클라이언트에 에러문이 반환되는 현상이 발생했다. 액세스토큰을 검증해야하는 API의 경우, BackendManager에서 공통적으로 SendRequest<T> 함수를 이용해 통신을 진행한다. 여기서 T는 ResponseDto 객체를 넣어준다. 즉, 서버에서는 무조건 객체형식으로 반환을 해줘야 한다는 의미이다.
public static async Task<T> SendRequest<T>(string path, string method, object body = null)
{
string url = $"{BASE_URL}{path}";
string accessToken = LocalTokenStore.AccessToken;
var request = UnityWebRequest.Put(url, ""); // 초기화용, 나중에 method 오버라이드
request.method = method;
request.SetRequestHeader("Content-Type", "application/json");
if (!string.IsNullOrEmpty(accessToken))
request.SetRequestHeader("Authorization", $"Bearer {accessToken}");
if (body != null)
{
string json = JsonUtility.ToJson(body);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(jsonBytes);
}
request.downloadHandler = new DownloadHandlerBuffer();
// UnityWebRequestAsyncOperation은 Task가 아니므로 수동 대기
var operation = request.SendWebRequest();
while (!operation.isDone)
await Task.Yield();
// AccessToken 만료 시 → refresh 시도
if (request.responseCode == 401)
{
bool refreshSuccess = await BackendTokenHandler.TryRefreshToken();
if (refreshSuccess)
{
// 토큰 갱신 후 재시도
return await SendRequest<T>(path, method, body);
}
else
{
// 로그인 화면 등 처리
// UIManager.ShowLoginScreen();
throw new Exception("AccessToken 만료 및 Refresh 실패");
}
}
if (request.result == UnityWebRequest.Result.Success)
{
return JsonUtility.FromJson<T>(request.downloadHandler.text);
}
else
{
Debug.LogError($"API 요청 실패 [{request.responseCode}]: {request.error}");
throw new Exception(request.error);
}
}
기존에 서버쪽에서는 "백업성공" 과 같이 문자열로 반환을 했는데, 이 반환형이 맞지 않아서 에러가 발생한 것이었다. 따라서 BackupAPI 쪽 반환형을 객체로변경하면 해결된다.
return ResponseEntity.ok(new BackupResponseDto("백업 성공"));//성공시
return ResponseEntity.internalServerError().body(new BackupResponseDto("백업 실패."+e.getMessage())); //실패시
'Server > 방치RPG 서버' 카테고리의 다른 글
[방치RPG 서버 제작기] Docker 내의 db에 접속하기 ( DBeaver ) (0) | 2025.05.06 |
---|---|
[방치RPG 서버 제작기] 8. 서버배포 및 SSL 인증서발급하기 (1) | 2025.04.26 |
[방치RPG 서버 제작기] 6. 전송 암호화, DB테이블 구조 분리하기 (0) | 2025.04.25 |
[방치RPG 서버 제작기] 5. 액세스 토큰 + 리프레시 토큰 관리 및 갱신 (0) | 2025.04.23 |
[방치RPG 서버 제작기] 4. 액세스 토큰 발급 , 기본 데이터 저장하기 (0) | 2025.04.22 |