Skip to content

refactor: 로그인 티켓 방식으로 변경#18

Merged
yangjunsik merged 1 commit intomainfrom
refactor/#7-login-temp
Aug 21, 2025
Merged

refactor: 로그인 티켓 방식으로 변경#18
yangjunsik merged 1 commit intomainfrom
refactor/#7-login-temp

Conversation

@yangjunsik
Copy link
Copy Markdown
Contributor

@yangjunsik yangjunsik commented Aug 21, 2025

[BE/FE] feat(auth): 소셜 로그인 후 RefreshToken 전달 방식 단순화 (해커톤용)

요약

  • 쿠키 제거 → 헤더/도메인 세팅 없이도 동작 가능하게 변경
  • RefreshToken을 직접 노출하지 않고, 임시 ticket 발급 → 교환 API 방식 적용
  • 프론트는 ticket을 받아 /api/auth/ticket 호출 후 RefreshToken 확보
  • 확보한 RefreshToken으로 기존 /api/auth/refresh API를 호출해 AccessToken 발급

백엔드 변경사항

OAuth2SuccessHandler

  • 로그인 성공 시 RefreshToken 생성
  • RefreshToken을 바로 보내지 않고 UUID ticket 발급 후 인메모리 저장소에 매핑
  • 프론트 리다이렉트 시 ?ticket=UUID 붙여서 전달
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException {
    CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

    String refreshToken = jwtService.generateRefreshToken(userDetails);
    String ticket = UUID.randomUUID().toString();

    TempTokenStore.put(ticket, refreshToken); // 임시 저장

    String base = "dev".equalsIgnoreCase(appEnv) ? redirectDev : redirectLocal;
    response.sendRedirect(base + "?ticket=" + ticket);
}

TempTokenStore

public class TempTokenStore {
    private static final Map<String, String> store = new ConcurrentHashMap<>();

    public static void put(String ticket, String refreshToken) {
        store.put(ticket, refreshToken);
    }

    public static String consume(String ticket) {
        return store.remove(ticket); // 한번 쓰면 바로 삭제
    }
}

AuthController

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @GetMapping("/ticket")
    public ResponseEntity<?> consume(@RequestParam String ticket) {
        String refreshToken = TempTokenStore.consume(ticket);
        if (refreshToken == null) {
            return ResponseEntity.badRequest().body("invalid or expired ticket");
        }
        return ResponseEntity.ok(Map.of("refreshToken", refreshToken));
    }
}

프론트엔드 변경사항

✅ 로그인 Redirect 처리

// 1) 로그인 후 리다이렉트 URL 예시: http://localhost:3000?ticket=UUID
const url = new URL(window.location.href);
const ticket = url.searchParams.get("ticket");

if (ticket) {
  // 2) ticket → refreshToken 교환
  const { refreshToken } = await fetch(`${API_BASE}/api/auth/ticket?ticket=${ticket}`)
    .then(r => r.json());

  // 3) refreshToken → accessToken 발급
  const { accessToken } = await fetch(`${API_BASE}/api/auth/refresh`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken })
  }).then(r => r.json());

  // 4) 로컬 스토리지에 저장
  localStorage.setItem("refreshToken", refreshToken);
  localStorage.setItem("accessToken", accessToken);

  // 5) 이후 요청 시 Authorization 헤더에 accessToken 첨부
}

기대 효과

  • ✅ HTTPS/도메인/Cookie 설정 없이 간단히 소셜로그인 토큰 교환 가능
  • ✅ RefreshToken이 URL에 직접 노출되지 않아 최소한의 보안 확보
  • ✅ 해커톤용으로 빠르게 구현하면서도, 추후 정식 서비스에서는 쿠키 방식으로 전환 가능

##관련이슈
#20

@yangjunsik yangjunsik self-assigned this Aug 21, 2025
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @yangjunsik, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 소셜 로그인 후 RefreshToken 전달 방식을 단순화하기 위해 기존 쿠키 기반 방식에서 임시 티켓 기반 방식으로 변경합니다. 이는 해커톤 환경에서 HTTPS/도메인/쿠키 설정 없이도 소셜 로그인 토큰 교환이 가능하도록 하며, RefreshToken이 URL에 직접 노출되지 않아 최소한의 보안을 확보합니다. 백엔드에서는 OAuth2SuccessHandler가 RefreshToken 대신 티켓을 발급하고, 새로운 TempTokenStore를 통해 티켓과 RefreshToken을 매핑하여 임시 저장합니다. 또한, AuthController에 티켓을 소비하여 RefreshToken을 반환하는 새 엔드포인트가 추가됩니다. 프론트엔드는 이 티켓을 사용하여 RefreshToken을 확보하고 AccessToken을 발급받게 됩니다.

Highlights

  • 백엔드: OAuth2SuccessHandler 변경: 로그인 성공 시 RefreshToken을 쿠키로 직접 전달하는 대신, UUID 기반의 임시 티켓을 발급하고 해당 티켓과 RefreshToken을 TempTokenStore에 저장하도록 변경되었습니다. 이후 프론트엔드는 이 티켓을 URL 쿼리 파라미터로 전달받게 됩니다.
  • 백엔드: TempTokenStore 도입: 티켓과 RefreshToken을 임시로 저장하고 관리하기 위한 새로운 유틸리티 클래스인 TempTokenStore가 추가되었습니다. 이 클래스는 티켓을 통해 RefreshToken을 저장(put)하고, 한 번 사용하면 삭제(consume)하는 기능을 제공합니다.
  • 백엔드: AuthController에 새 엔드포인트 추가: 프론트엔드에서 전달받은 티켓을 사용하여 RefreshToken을 요청할 수 있는 /api/auth/ticket GET 엔드포인트가 AuthController에 추가되었습니다. 이 엔드포인트는 유효한 티켓인 경우 RefreshToken을 반환하고, 유효하지 않거나 만료된 티켓인 경우 오류를 반환합니다.
  • 프론트엔드 워크플로우 변경 (설명 기반): 프론트엔드는 로그인 리다이렉트 URL에서 티켓을 추출하고, 새로 추가된 /api/auth/ticket API를 호출하여 RefreshToken을 교환합니다. 이후 확보한 RefreshToken으로 기존 /api/auth/refresh API를 호출하여 AccessToken을 발급받고 로컬 스토리지에 저장하는 방식으로 변경됩니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

안녕하세요. 로그인 방식을 쿠키에서 임시 티켓 방식으로 변경하는 리팩토링 작업을 잘 보았습니다. 해커톤 환경에서 배포 복잡성을 줄이기 위한 좋은 결정이라고 생각합니다.

전반적인 로직은 명확하게 구현되었으나, 몇 가지 개선점을 제안드립니다.

  • TempTokenStore의 메모리 관리: 현재 구현은 사용되지 않은 티켓이 메모리에 계속 남아 메모리 누수를 일으킬 수 있습니다. 티켓에 만료 시간을 설정하여 이 문제를 해결하는 것이 중요합니다.
  • API 문서 및 오류 메시지: AuthController의 API 문서(@Operation)를 더 명확하게 개선하고, 오류 메시지를 일관성 있게 수정하면 API를 사용하는 클라이언트 개발자에게 더 도움이 될 것입니다.

자세한 내용은 각 파일의 코드 리뷰 댓글을 참고해주세요. 감사합니다.

Comment on lines +1 to +16
package com.busan.config.security.oauth;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class TempTokenStore {
private static final Map<String, String> store = new ConcurrentHashMap<>();

public static void put(String ticket, String refreshToken) {
store.put(ticket, refreshToken);
}

public static String consume(String ticket) {
return store.remove(ticket); // 한번 쓰면 바로 삭제
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

현재 TempTokenStore 구현에 몇 가지 개선점을 제안합니다.

  1. 유틸리티 클래스 설계 (Medium): 이 클래스는 static 메서드만 제공하므로 final로 선언하고 private 생성자를 추가하여 인스턴스화를 방지하는 것이 좋습니다.
  2. 메모리 누수 위험 (High): 현재 구현은 만료 정책이 없는 ConcurrentHashMap을 사용합니다. 만약 티켓이 발급된 후 사용되지 않으면, 해당 데이터는 메모리에 영원히 남아 메모리 누수(memory leak)를 유발할 수 있습니다. 해커톤 기간에는 문제가 되지 않을 수 있지만, 장기적으로는 서버 안정성에 심각한 영향을 줄 수 있습니다.

이 문제들을 해결하기 위해 티켓에 유효 시간(TTL)을 설정하는 것이 중요합니다. Caffeine과 같은 캐시 라이브러리를 사용하면 이 문제를 쉽게 해결할 수 있습니다.

제안:

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public final class TempTokenStore {
    private TempTokenStore() {
        // 유틸리티 클래스 인스턴스화 방지
    }

    private static final Cache<String, String> store = Caffeine.newBuilder()
        .expireAfterWrite(5, TimeUnit.MINUTES) // 5분 후 만료
        .build();

    public static void put(String ticket, String refreshToken) {
        store.put(ticket, refreshToken);
    }

    public static String consume(String ticket) {
        // asMap().remove()는 원자적으로 값을 가져오고 삭제합니다.
        return store.asMap().remove(ticket);
    }
}

이 방식을 적용하려면 build.gradle 또는 pom.xml에 Caffeine 의존성을 추가해야 합니다. 이 방법은 코드를 더 안전하고 견고하게 만들어줍니다.

Comment on lines +36 to +39
description = "헤커톤을 위한 최소한의 보안절차로 프론트는 url에서 ticket을 추출하고 이걸로" +
"refreshToken을 교환한다." +
"그후 access 발급 api를 호출하면 된다" +
"물론 추후에는 당연히 쿠키방식으로 변경하는게 맞으나 헤커톤을 위한 최소한의 보안장치이다"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

API 문서의 가독성을 높이기 위해 description 내용을 좀 더 명확하고 구조적으로 작성하는 것이 좋습니다. 현재 문자열이 여러 줄로 나뉘어 있고 내용이 장황하여 가독성이 떨어집니다.

            description = "소셜 로그인 후 발급된 임시 티켓(ticket)을 사용하여 Refresh Token으로 교환합니다. " +
                    "이 API는 해커톤을 위해 쿠키 설정의 복잡성을 피하고자 구현된 임시 방식입니다. " +
                    "클라이언트는 이 API로 Refresh Token을 받은 후, '/api/auth/refresh'를 호출하여 Access Token을 발급받아야 합니다."

public ResponseEntity<?> consume(@RequestParam String ticket) {
String refreshToken = TempTokenStore.consume(ticket);
if (refreshToken == null) {
return ResponseEntity.badRequest().body("invalid ticket");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

PR 설명에는 "invalid or expired ticket"이라고 되어 있는데, 실제 코드에서는 "invalid ticket"만 반환하고 있습니다. 티켓이 존재하지 않거나 만료된 경우를 모두 포함하는 더 명확한 메시지로 수정하는 것이 좋습니다.

            return ResponseEntity.badRequest().body("invalid or expired ticket");

@yangjunsik yangjunsik merged commit a35c8bd into main Aug 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant