일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- express
- SDK upgrade
- Digital Ocean
- unity
- --watch
- Spring Boot
- spread 연산자
- docker
- java
- Google Refund
- Git
- Packet Network
- linux
- mongoDB
- Camera Movement
- draganddrop
- Unity IAP
- screencapture
- springboot
- OverTheWire
- Unity Editor
- critical rendering path
- Camera Zoom
- Google Developer API
- server
- nodejs
- MySQL
- react
- css framework
- rpg server
- Today
- Total
우당탕탕 개발일지
[방치RPG 서버 제작기] 6. 전송 암호화, DB테이블 구조 분리하기 본문
클라에서 암호화 > 서버에서 복호화하기
중요한 데이터는 암호화해서 전송해야한다. backup API에서 암호/복호화를 진행해보자
RSAUtils 작성하기 : 암호/복호화를 하기위한 util 클래스이다. 시작할때 키를 생성하고 암호화/복호화가 있다 ( 우선 여기서는 복호화만 사용할 예정이다). 그리고 클라이언트에게 전달할 publicKey를 리턴하는 함수도 있다.
package com.esaAdventure.dante_server.common.utils;
import java.security.*;
import java.util.Base64;
import javax.crypto.Cipher;
public class RSAUtil {
private static KeyPair keyPair;
static {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
keyPair = generator.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException("RSA 키 생성 실패", e);
}
}
public static String decrypt(String encryptedData) throws Exception {
byte[] bytes = Base64.getDecoder().decode(encryptedData);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] decrypted = cipher.doFinal(bytes);
return new String(decrypted);
}
public static String encrypt(String rawData) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] encrypted = cipher.doFinal(rawData.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
}
// ✅ 공개키를 Base64 문자열로 리턴
public static String getPublicKey() {
return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
}
}
로그인 응답인 LoginResponseDto 에서 rsaPublicKey도 같이 반환한다. 참고로, 이전단계에서 LoginResponseDto 에 accessToken과 refreshToken을 담아서 전달했었는데, 헤더에 두 토큰을 넣는것으로 수정했다.
@Data
public class LoginResponseDto {
private String userData; // JSON string
private String rsaPublicKey;
public LoginResponseDto(String rsaPublicKey, String userData) {
this.rsaPublicKey = rsaPublicKey;
this.userData = userData;
}
}
UserController 의 guest login API 를 다음과 같이 수정했다.
- rsaUtil 에서 publicKey를 받는다.
- accessToken 과 refreshToken을 받아서 응답 헤더에 넣어준다.
@PostMapping("/auth/guest")
public ResponseEntity<LoginResponseDto> guestLogin(@RequestBody GuestLoginRequestDto request) {
User user = userService.guestLogin(request.getDeviceId());
Long userId = user.getId();
HttpStatus status = user.isNewlyCreated() ? HttpStatus.CREATED : HttpStatus.OK;
if (user.isNewlyCreated()) {// 신규 유저라면 기본 데이터 생성
backupService.provideDefaultData(userId);
}
String accessToken = jwtProvider.createToken(userId);
String refreshToken = jwtProvider.createRefreshToken(userId);
UserBackupDto backupDto = backupService.exportBackup(userId);
String json = JsonUtil.toJson(backupDto);
// 📦 서버 DB에 refreshToken 저장 (예: 유저ID별로)
authService.saveRefreshToken(userId, refreshToken);
LoginResponseDto response = new LoginResponseDto(RSAUtil.getPublicKey(),json);
return ResponseEntity.status(status)
.header("Authorization", "Bearer " + accessToken)
.header("X-Refresh-Token", refreshToken)
.body(response); // 또는 body 없이 헤더 기반 처리
}
Processor 를 이용해 테이블별로 분리하기
이제 backup 시에 entity > json 으로 변환하거나 혹은 json> dto 로 변경하기, 수정된 데이터 저장하기 등의 역할을 하는 BackupService를 구현한다. 그런데 테이블이 많아질수록 BackupService가 무거워지기 때문에, 테이블별로 분리하는 작업을 했다
먼저, 테이블 별 작업을 담당하는 Processor 의 인터페이스를 제작한다. 이름은 BackupProcessor.java 이다.
- process : db값과 비교하여 내용이 변경되었을 경우 db에 수정내용을 적용한다.
- extract : db값을 dto형식으로 변환하여 dtoOut 변수에 넣어준다
- provideDefault : 처음 가입 시 기본으로 제공되는 재화를 넣어준다.
public interface BackupProcessor {
void process(Long userId, UserBackupDto backupDto);
void extract(Long userId, UserBackupDto dtoOut);
void provideDefault(Long userId);
}
UserBackupDto 는 유저가 보유하고있는 전체 데이터들을 모아놓은 클래스이다. 리스트 속 요소는 Equipment 가 아니라 EquipmentDto 이다. Equipment는 db에 직접적으로 저장되기 때문에, 반환형으로 직접 가져다 쓰는것은 굉장히 위험하다. 따라서 dto 형식으로 따로 만들어서 전달하는것이 안전하다. Equipment는 userId 과 고유id들까지 있지만 EquipmentDto 는 전달에 필요한 정보 (itemId, level, cnt) 만 저장된다.
@Data
public class UserBackupDto {
private List<EquipmentDto> equipments;
private List<SkillDto> skills;
}
이제 BackupProcessor 를 상속받은 EquipmentBackupProcessor 를 보자. 만약 equipement 테이블 내용이 변경된다면, 이 파일만 수정하면 된다.
package com.esaAdventure.dante_server.backup.processor;
import com.esaAdventure.dante_server.backup.dto.EquipmentDto;
import com.esaAdventure.dante_server.backup.dto.UserBackupDto;
import com.esaAdventure.dante_server.backup.entitiy.Equipment;
import com.esaAdventure.dante_server.backup.repository.EquipmentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class EquipmentBackupProcessor implements BackupProcessor {
private final EquipmentRepository equipmentRepository;
@Override
public void process(Long userId, UserBackupDto dto) {
if(dto.getEquipments() == null) return;
for (EquipmentDto e : dto.getEquipments()) {
equipmentRepository.findByUserIdAndItemId(userId, e.getId())
.ifPresentOrElse(
entity -> {
if (entity.updateFromDto(e)) {
equipmentRepository.save(entity);
}
},
() -> {
equipmentRepository.save(EquipmentMapper.toEntity(userId,e));
}
);
}
}
@Override
public void extract(Long userId, UserBackupDto dtoOut) {
List<Equipment> entities = equipmentRepository.findAllByUserId(userId);
List<EquipmentDto> dtoList = entities.stream()
.map(EquipmentMapper::toDto)
.collect(Collectors.toList());
dtoOut.setEquipments(dtoList);
}
@Override
public void provideDefault(Long userId) {
Equipment defaultWeapon = new Equipment(userId, 1, 1, 1);
equipmentRepository.save(defaultWeapon);
}
public static class EquipmentMapper {
public static EquipmentDto toDto(Equipment e) {
EquipmentDto d = new EquipmentDto();
d.setId(e.getItemId());
d.setLevel(e.getLevel());
d.setCount(e.getCount());
return d;
}
public static Equipment toEntity(Long userId, EquipmentDto d) {
return new Equipment(userId, d.getId(), d.getLevel(), d.getCount());
}
}
}
BackupService에서는 모든 BackupProcessor 을 보유하고있다가, 각 processor 에게 extract 혹은 process 등의 함수를 일괄 호출하는 역할을 담당한다. 명령만 내리기 때문에, 굉장히 간결하다
package com.esaAdventure.dante_server.backup.service;
import com.esaAdventure.dante_server.backup.dto.UserBackupDto;
import com.esaAdventure.dante_server.backup.processor.BackupProcessor;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BackupService {
//스프링이 @Component로 등록된 모든 BackupProcessor 구현체를 리스트로 주입해줌!
private final List<BackupProcessor> processors;
@Transactional
public void saveBackup(Long userId, UserBackupDto dto) {
for (BackupProcessor processor : processors) {
processor.process(userId, dto);
}
}
@Transactional(readOnly = true)
public UserBackupDto exportBackup(Long userId) {
UserBackupDto dto = new UserBackupDto();
for (BackupProcessor processor : processors) {
processor.extract(userId, dto); // 각 Processor가 dto를 채워줌
}
return dto;
}
@Transactional
public void provideDefaultData(Long userId) {
for (BackupProcessor processor : processors) {
processor.provideDefault(userId);
}
}
}
실제 API 호출부인 BackupController를 보자. 5.저장처리에서 명령어 한줄로, 변경된 내용만 알아서 각 processor로 전달된다. 호출부는 간결하니 좋다!
@PostMapping
public ResponseEntity<?> backupData(@RequestBody BackupRequest request,
@RequestHeader("Authorization") String token) {
try {
// 1. 복호화
String decryptedJson = RSAUtil.decrypt(request.getData());
// 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()));
}
// 4. 유저 식별
Long userId = jwtProvider.getUserIdFromToken(token);
// 5. 저장 처리
backupService.saveBackup(userId, backupDto);
return ResponseEntity.ok("백업 성공");
} catch (Exception e) {
e.printStackTrace(); // 실운영에서는 로거로 기록
return ResponseEntity.internalServerError().body("백업 실패: " + e.getMessage());
}
}
내일은 Validator 를 구현해보자.. 지금 게임 데이터가 다 유니티 안에 있어서, 이걸 서버로 어떻게 옮겨야 할지 걱정이 많다. 또 고민해보면 어떻게 되겠지~
'Server > 방치RPG 서버' 카테고리의 다른 글
[방치RPG 서버 제작기] 8. 서버배포 및 SSL 인증서발급하기 (1) | 2025.04.26 |
---|---|
[방치RPG 서버 제작기] 7. backup API 구현하기 (0) | 2025.04.26 |
[방치RPG 서버 제작기] 5. 액세스 토큰 + 리프레시 토큰 관리 및 갱신 (0) | 2025.04.23 |
[방치RPG 서버 제작기] 4. 액세스 토큰 발급 , 기본 데이터 저장하기 (0) | 2025.04.22 |
[방치RPG 서버 제작기] 3. 방치 RPG 유저 데이터 관리 (이론) (0) | 2025.04.21 |