Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 카카오 로그인 기능을 추가 #123

Merged
merged 13 commits into from
Jul 28, 2023
Merged

Conversation

70825
Copy link
Member

@70825 70825 commented Jul 24, 2023

Issue

✨ 구현한 기능

  • 카카오 로그인을 하면 HttpSession을 통해 세션에 저장하고, 클라이언트에게 세션 ID를 반환하도록 만들었습니다.
  • 테스트 코드 및 기존 코드 수정은 이야기 후에 추가할 예정

📢 논의하고 싶은 내용

  • [ALL] Redirect URI 필요
  • [ALL] 카카오 API를 통해 어떻게 정보를 가져오는지
  • [ALL] 멤버 필드 추가 및 다른 필드 제거
    • [추가] platformId (카카오 회원 번호)
    • [삭제] age, gender, phoneNumber
  • [BE] Session vs JWT
  • [BE] 환경변수 이야기 필요

🎸 기타

RestTemplate vs WebClient

  • 스프링 5.0 이후부터는 WebClient를 권고 (링크)
  • WebClient는 Spring WebFlux 의존성 추가가 필요
  • 성능적으로 보면 RestTemplate는 동기/블로킹 처리, WebClient가 비동기/논블로킹 처리라 WebClient가 좋아보임
  • 하지만 WebFlux는 동시 사용자가 1,000명 이상이어야 유의미한 성능 향상이 있으므로 RestTemplate을 사용해도 문제 없음 (링크 - 링크에서 boot 1은 Spring MVC, boot 2는 Spring WebFlux 입니다)

⏰ 일정

  • 추정 시간 : 12
  • 걸린 시간 : 5일

@70825 70825 force-pushed the feat/issue-68 branch 2 times, most recently from 02f1d28 to d2433b8 Compare July 24, 2023 14:35
@70825 70825 force-pushed the feat/issue-68 branch 2 times, most recently from 5700df6 to a1d89a9 Compare July 25, 2023 17:24
70825

This comment was marked as resolved.

Copy link
Collaborator

@Go-Jaecheol Go-Jaecheol left a comment

Choose a reason for hiding this comment

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

어려운 기능 구현하느라 고생했어요 🥲
몇 가지 작은 수정 사항들이랑 코멘트 남겼습니다!

@github-actions
Copy link

github-actions bot commented Jul 27, 2023

Unit Test Results

50 tests   50 ✔️  7s ⏱️
27 suites    0 💤
27 files      0

Results for commit 1e81060.

♻️ This comment has been updated with latest results.

Copy link
Collaborator

@hanueleee hanueleee left a comment

Choose a reason for hiding this comment

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

너무 멋져요 로건..👍

70825

This comment was marked as off-topic.

70825

This comment was marked as outdated.

@70825 70825 force-pushed the feat/issue-68 branch 2 times, most recently from af8bf68 to b32f4f7 Compare July 28, 2023 03:11
Comment on lines +18 to +65
@WebMvcTest(AuthController.class)
@SuppressWarnings("NonAsciiCharacters")
public class AuthControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private AuthService authService;

@Nested
class loginAuthorizeUser_테스트 {

@Test
void 이미_가입된_유저라면_홈_경로로_리다이렉트한다() throws Exception {
// given
final var code = "test";
final var member = new Member("test", "www.test.com", "1");
final var signUserDto = SignUserDto.of(true, member);

// when
when(authService.loginWithKakao(code)).thenReturn(signUserDto);

// then
mockMvc.perform(get("/login/oauth2/code/kakao")
.param("code", code))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"));
}

@Test
void 가입되지_않은_유저라면_프로필_경로로_리다이렉트한다() throws Exception {
// given
final var code = "test";
final var member = new Member("test", "www.test.com", "1");
final var signUserDto = SignUserDto.of(false, member);

// when
when(authService.loginWithKakao(code)).thenReturn(signUserDto);

// then
mockMvc.perform(get("/login/oauth2/code/kakao")
.param("code", code))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/profile"));
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

스크린샷 2023-07-28 오후 3 40 52

카카오 로그인을 하려면 이렇게 이메일과 비밀번호를 입력해야 하는데, 여기서 나오는 인가 코드가 1회용 코드라서 테스트가 불가능해요.
왜냐하면 테스트 수행할 때, 자동적으로 이메일과 비밀번호를 입력해서 인가 코드를 받아야 하는데, 알잘딱깔센하게 이메일과 비밀번호를 입력하게 만드는 방법이 없어보입니다.
그래서 authService.loginWithKakao(code)는 RestAssured로 테스트가 불가능하므로 MockMvc를 사용하여 테스트를 진행했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 이유라면 테스트 작성 안하는 것보다는 MockMvc로 테스트 하는 게 맞다고 생각합니다! 굿굿

Comment on lines +14 to +18
final HttpSession session = request.getSession();
if (session.getAttribute("member") == null) {
throw new IllegalArgumentException("login error");
}
return true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

쉐도우 복싱입니다.

  1. 이미 서버 세션에는 member 정보가 남아있습니다.
  2. 클라이언트는 로그인이 되어있지 않은 사용자가 로그인이 필요한 서비스를 이용하려고 할때,

해당 부분 분기에서 login error 가 뜨지 않고 넘어갈 것 같습니다.

Copy link
Collaborator

@wugawuga wugawuga left a comment

Choose a reason for hiding this comment

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

로그인이 로거인이 됐네요 정말 수고 많으셨어요!!! 🙇‍♂️

Copy link
Collaborator

@hanueleee hanueleee left a comment

Choose a reason for hiding this comment

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

소셜 로그인 테스트.. 멋져요..👍

Copy link
Collaborator

@Go-Jaecheol Go-Jaecheol left a comment

Choose a reason for hiding this comment

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

로그인.. 확실히 복잡하네요..
고생하셨습니다~!

Comment on lines +18 to +65
@WebMvcTest(AuthController.class)
@SuppressWarnings("NonAsciiCharacters")
public class AuthControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private AuthService authService;

@Nested
class loginAuthorizeUser_테스트 {

@Test
void 이미_가입된_유저라면_홈_경로로_리다이렉트한다() throws Exception {
// given
final var code = "test";
final var member = new Member("test", "www.test.com", "1");
final var signUserDto = SignUserDto.of(true, member);

// when
when(authService.loginWithKakao(code)).thenReturn(signUserDto);

// then
mockMvc.perform(get("/login/oauth2/code/kakao")
.param("code", code))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/"));
}

@Test
void 가입되지_않은_유저라면_프로필_경로로_리다이렉트한다() throws Exception {
// given
final var code = "test";
final var member = new Member("test", "www.test.com", "1");
final var signUserDto = SignUserDto.of(false, member);

// when
when(authService.loginWithKakao(code)).thenReturn(signUserDto);

// then
mockMvc.perform(get("/login/oauth2/code/kakao")
.param("code", code))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/profile"));
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 이유라면 테스트 작성 안하는 것보다는 MockMvc로 테스트 하는 게 맞다고 생각합니다! 굿굿

@70825
Copy link
Member Author

70825 commented Jul 28, 2023

세션은 세션 ID - 세션 객체로 이루어져 있습니다.

org.apache.catalina.sessionManagerBase 클래스에서 protected Map<String,Session> sessions = new ConcurrentHashMap<>();에서 쿠키에 저장된 ID를 통해 세션을 가져옵니다.

세션을 가져올 때에는 findSession(Stirng id)를 통해 JSESSIONID에 맞는 세션을 가져오는 것 같아요.

    @Override
    public Session findSession(String id) throws IOException {
        if (id == null) {
            return null;
        }
        return sessions.get(id);
    }

이제 실제로 세션을 가져오는 것은 확인하는 것은 org.apache.catalina.connector.Request에서 확인할 수 있습니다.

    public Session getSessionInternal() {
        return doGetSession(true);
    }
    protected Session doGetSession(boolean create) {

        // There cannot be a session if no context has been assigned yet
        Context context = getContext();
        if (context == null) {
            return null;
        }

        // Return the current session if it exists and is valid
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            return session;
        }

        // Return the requested session if it exists and is valid
        Manager manager = context.getManager();
        if (manager == null) {
            return null; // Sessions are not supported
        }
        if (requestedSessionId != null) {
            try {
                session = manager.findSession(requestedSessionId);
            } catch (IOException e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
                } else {
                    log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
                }
                session = null;
            }
            if ((session != null) && !session.isValid()) {
                session = null;
            }
            if (session != null) {
                session.access();
                return session;
            }
        }

        // Create a new session if requested and the response is not committed
        if (!create) {
            return null;
        }
        boolean trackModesIncludesCookie =
                context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
        if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
            throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
        }

        // Re-use session IDs provided by the client in very limited
        // circumstances.
        String sessionId = getRequestedSessionId();
        if (requestedSessionSSL) {
            // If the session ID has been obtained from the SSL handshake then
            // use it.
        } else if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie())) {
            /*
             * This is the common(ish) use case: using the same session ID with multiple web applications on the same
             * host. Typically this is used by Portlet implementations. It only works if sessions are tracked via
             * cookies. The cookie must have a path of "/" else it won't be provided for requests to all web
             * applications.
             *
             * Any session ID provided by the client should be for a session that already exists somewhere on the host.
             * Check if the context is configured for this to be confirmed.
             */
            if (context.getValidateClientProvidedNewSessionId()) {
                boolean found = false;
                for (Container container : getHost().findChildren()) {
                    Manager m = ((Context) container).getManager();
                    if (m != null) {
                        try {
                            if (m.findSession(sessionId) != null) {
                                found = true;
                                break;
                            }
                        } catch (IOException e) {
                            // Ignore. Problems with this manager will be
                            // handled elsewhere.
                        }
                    }
                }
                if (!found) {
                    sessionId = null;
                }
            }
        } else {
            sessionId = null;
        }
        session = manager.createSession(sessionId);

        // Creating a new session cookie based on that session
        if (session != null && trackModesIncludesCookie) {
            Cookie cookie =
                    ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());

            response.addSessionCookieInternal(cookie);
        }

        if (session == null) {
            return null;
        }

        session.access();
        return session;
    }

이쪽에서 session = manager.findSession(requestedSessionId);를 통해 세션을 가져오고, 그러면 아래에 조건문에서 != null로 인해 return session을 반환합니다.

try - catch문을 벗어나면 이제 새로운 세션을 생성하는 일이에요.

그래서 클라이언트가 쿠키에 저장된 JSESSIONID을 헤더로 보내면 JSESSIONID를 통해 Session 객체를 찾고, Session 객체에 있는 name을 통해 value를 확인하는 과정이 됩니다.

@70825 70825 merged commit b2ddd7e into develop Jul 28, 2023
3 checks passed
@70825 70825 deleted the feat/issue-68 branch July 28, 2023 09:02
@70825 70825 changed the title [BE] 카카오 로그인 기능을 추가 [BE] feat: 카카오 로그인 기능을 추가 Jul 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BE] 카카오 소셜 로그인 기능 추가
4 participants