JWT 기반 다중 디바이스 로그인 구현기

최혜미·2025년 7월 2일
0

Project

목록 보기
19/19
post-thumbnail

요즘 앱/웹 통합 서비스에서는 하나의 계정으로 여러 디바이스에서 로그인하는 다중 로그인 기능이 필수입니다.

하지만 이를 대충 구현하면 보안에 구멍이 생기기 쉽습니다. 제가 구현한 서비스에서는 JWT 기반 구조 안에서 보안까지 고려한 다중 로그인 환경을 설계하고 적용하도록 노력했는데, 그 과정을 공유해보고자 합니다.


🔑 로그인 방식 요약

원래는 보안을 위해 refreshToken을 쿠키에 담아 전달하고 있었습니다.
하지만 모바일 앱 환경에서는 쿠키 사용이 제한된다는 점을 고려해,
지금은 refreshToken을 쿠키가 아닌 응답 바디로 전달하도록 수정하였습니다.

  • refreshToken은 쿠키가 아닌 응답 바디로 전달
  • 유저의 디바이스별로 리프레시 토큰을 저장하여 다중 디바이스 로그인 지원
  • 디바이스를 식별하기 위한 정보UserRefreshToken에 추가 :
    • deviceType (MOBILE/PC)
    • os (iOS, Android, Windows 등)
    • deviceId (mobile: 실제 디바이스 고유값 / web: UUID)
  • ROT (Refresh Token Rotation) 방식을 적용하였습니다.
    • 리프레시 토큰으로 액세스 토큰을 재발급할 때, 리프레시 토큰도 함께 갱신하여 토큰 탈취 위협을 차단합니다.
  • 60일 이상 미접속 디바이스 세션은 자동 로그아웃 처리됩니다.
    • 스케줄러가 주기적으로 오래된 토큰을 정리합니다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
@Builder
public class UserRefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long userId;

    private String token; // 리프레쉬 토큰

    private String deviceId; // 로그인한 디바이스 Id

    @Enumerated(EnumType.STRING)
    private DeviceType deviceType; // 로그인한 디바이스 타입(PC, MOBILE)

    private String os; // 로그인한 os 환경(Window, MacOS...)

    private LocalDateTime lastUsedAt; // 마지막 접속 시각
}

🔑 디바이스 구분 기준

필드명설명
deviceTypeMOBILE, PC (모바일 웹은 MOBILE로 분류합니다)
osiOS, Android, Windows, macOS 등
deviceId고유 디바이스 식별자 (mobile: 디바이스 ID / web: UUID)

❗️ 모바일 브라우저도 deviceType은 MOBILE로 설정하는 것이 정책 분기 및 통계 집계 시 유리합니다!

❗️ 웹 환경에서는 모바일처럼 고유한 deviceId를 직접 추출할 수 없습니다.
따라서 웹에서는 초기 접속 시 UUID를 생성해 localStorage에 저장하고, 이후부터는 이를 deviceId로 활용하도록 처리하였습니다.


🔐 로그인 프로세스

[1] 로그인 시

  • 클라이언트는 로그인 요청 헤더에 다음 정보를 포함해야 합니다:
    • deviceId, deviceType, os
  • 서버는 유저 + 디바이스 조합으로 리프레시 토큰을 저장하 or 갱신합니다.

[2] 토큰 재발급 시

  • 토큰 재발급 요청에는 다음 정보가 포함됩니다:
    • refreshToken
    • deviceId (헤더)
    • deviceType, os (헤더)
  • 버는 유저 + 디바이스 조합으로 기존 리프레시 토큰을 검증한 후, 새로운 액세스 토큰과 리프레시 토큰을 발급합니다. (ROT 방식 적용)
  • 기존 리프레시 토큰은 즉시 폐기되고, 새 리프레시 토큰으로 갱신됩니다.
  • 클라이언트는 새로운 액세스 토큰을 저장하고, 기존 액세스 토큰은 무시하거나 폐기합니다.

[3] 로그아웃 시

  • deviceIdrefreshToken을 기준으로 해당 디바이스 세션만 삭제합니다.
  • 동일 유저의 다른 디바이스 토큰은 유지됩니다. (다른 기기에 로그인은 유지됨)

마무리

로그인 기능은 늘 간단해 보이지만, 막상 구현하려 들면 생각보다 복잡합니다.
서비스 정책에 맞추면서도, 과도한 리소스를 쓰지 않도록 설계하는 데 많은 고민이 필요했고, 그 과정에서 보안까지 신경 쓰느라 쉽지 않았습니다.

돌이켜보면 여전히 부족한 부분들이 눈에 띄지만, 이 부분은 앞으로도 계속 개선해 나갈 예정입니다.
지금까지의 고민과 시행착오가 누군가에게 작은 참고가 되길 바랍니다.

0개의 댓글