우당탕탕 개발일지

[방치RPG 서버 제작기] 4. 액세스 토큰 발급 , 기본 데이터 저장하기 본문

Server/방치RPG 서버

[방치RPG 서버 제작기] 4. 액세스 토큰 발급 , 기본 데이터 저장하기

devchop 2025. 4. 22. 17:39

Spring Boot로 JWT 기반 방치형 게임 서버 만들기

오늘은 방치형 RPG 게임 서버에서 "게스트 로그인"을 구현해보고, 로그인 시 JWT 토큰을 발급하고, 유저에게 기본 장비를 지급하는 구조를 만들어봤다.


 

1. JWT가 뭐고 왜 쓰는 걸까?

JWT(Json Web Token)는 로그인한 유저를 식별할 수 있는 서버 인증 토큰이야. AccessToken 안에 userId 같은 정보를 암호화해서 담아두고, 이후 요청에선 DB 조회 없이 인증할 수 있게 해주지.


2. application.yml 설정

profile별로 jwt.secret을 다르게 설정해줘야 해.

spring:
  profiles:
    active: dev

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    ...
jwt:
  secret: dev-super-secret-key
  expiration: 7200000

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    ...
jwt:
  secret: prod-super-secret-key
  expiration: 3600000

주의할 점은 jwt:는 spring: 하위에 있으면 안 되고 같은 레벨에 둬야 한다는 것!


3. JwtProvider 클래스 만들기

@Component
public class JwtProvider {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration:7200000}")
    private long expirationTime;

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    public String createToken(Long userId) {
        Date now = new Date();
        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + expirationTime))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public Long getUserIdFromToken(String token) {
        return Long.parseLong(Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject());
    }

    public boolean isTokenValid(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
}


4. UserService에서 게스트 로그인 처리

public class UserService {

    public User guestLogin(String deviceId) {
        Optional<User> optional = userRepository.findByDeviceId(deviceId);
        User user;

        if (optional.isPresent()) {
            user = optional.get();
            user.setNewlyCreated(false);
        } else {
            user = new User(deviceId);
            user.setNewlyCreated(true);
            userRepository.save(user);
        }

        return user;
    }
}


5. UserDataService에서 유저 데이터 조회 + 기본 지급 처리

@Service
@RequiredArgsConstructor
public class UserDataService {

    private final EquipmentRepository equipmentRepository;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public String getAllUserDataAsJson(Long userId) {
        List<Equipment> equipments = equipmentRepository.findAllByUserId(userId);
        List<EquipmentResponse> equipmentResponses = equipments.stream()
                .map(e -> new EquipmentResponse(e.getItemId(), e.getLevel(), e.getCount()))
                .collect(Collectors.toList());

        UserGameData data = UserGameData.builder()
                .equipments(equipmentResponses)
                .build();

        try {
            return objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("직렬화 실패", e);
        }
    }

    public void createInitialData(Long userId) {
        Equipment defaultWeapon = new Equipment(userId, 1, 1, 1);
        equipmentRepository.save(defaultWeapon);
    }
}


6. UserController에서 최종 로그인 응답 처리

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;
    private final UserDataService userDataService;
    private final JwtProvider jwtProvider;

    @PostMapping("/guest-login")
    public ResponseEntity<UserResponse> guestLogin(@RequestBody GuestLoginRequest request) {
        User user = userService.guestLogin(request.getDeviceId());

        if (user.isNewlyCreated()) {
            userDataService.createInitialData(user.getId());
        }

        String token = jwtProvider.createToken(user.getId());
        String userData = userDataService.getAllUserDataAsJson(user.getId());

        UserResponse response = new UserResponse(token, userData);
        HttpStatus status = user.isNewlyCreated() ? HttpStatus.CREATED : HttpStatus.OK;
        return new ResponseEntity<>(response, status);
    }
}


7. 응답 DTO 구조

public class UserResponse {
    private String accessToken;
    private String userData;

    public UserResponse(String accessToken, String userData) {
        this.accessToken = accessToken;
        this.userData = userData;
    }
}

 

주의!

 

각 domain 이 되는 클래스와 Response DTO 클래스 에는 다음처럼 Lombok 어노테이션이 붙어야한다. 그렇지 않으면 Json 직렬화에서 에러가 발생할 수 있다.

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.Builder;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EquipmentResponse {
    private int id;
    private int level;
    private int count;
}


 

 

 

+ 추가)

이미지에서 보면, skills 가 없어서 "null" 이라고 나온다. 이 문자열을 클라이언트에서 받을 경우 파싱 에러가 발생할 수 있으므로, 기본 List를 생성하도록 처리해주자.

import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserGameData {

    @Builder.Default
    private List<EquipmentResponse> equipments = new ArrayList<>(); //기본적으로 List생성

    @Builder.Default
    private List<SkillResponse> skills = new ArrayList<>();

}

성공적으로 동작한다

다음엔 백업기능과 엑세스토큰을 이용한 인증을 만들어보자..!

 

수정완료