Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f805455
infra: cicd시 서비스 컨테이너만 재시작하도록 수정
sudhdkso Jun 29, 2025
3de6b77
Merge pull request #139 from dnd-side-project/main
sudhdkso Jul 2, 2025
99647bb
feat: 카카오 소셜 로그인 기능 추가
sudhdkso Jul 8, 2025
7fbdc66
test: 카카오 소셜 로그인 테스트 코드 추가
sudhdkso Jul 8, 2025
e2b43b3
test: 테스트 코드 오류 수정
sudhdkso Jul 8, 2025
b134966
docs: 소셜로그인 문서 추가
sudhdkso Jul 8, 2025
3422555
test: 테스트 쿠키 설정 수정
sudhdkso Jul 8, 2025
480218f
infra: 카카오 api 및 쿠키 설정 업데이트
sudhdkso Jul 8, 2025
6355e3d
feat: 비회원 토큰 발급시 쿠키로 추가로 보낼 수 있도록 수정
sudhdkso Jul 8, 2025
0be580b
test: 로그인 기능에 대한 테스트 코드 추가
sudhdkso Jul 8, 2025
c68fc21
test: 게스트 로그인 쿠키 추가
sudhdkso Jul 8, 2025
44c1850
infra: 카카오 api 테스트용 앱키 설정
sudhdkso Jul 8, 2025
d2232e7
Merge pull request #141 from dnd-side-project/feat/#140-kakao-login
sudhdkso Jul 8, 2025
b0cab98
refactor: Kakao 관련 데이터 KakaoProperties로 분리 및 JsonProperty 어노테이션 적용
sudhdkso Jul 10, 2025
b02294b
refactor: 로그인 플로우 분리 및 인증 메서드 역할 명확화
sudhdkso Jul 10, 2025
4475648
test: User에 추가된 데이터 추가 및 빌더패턴으로 변경
sudhdkso Jul 10, 2025
a3e0a4a
feat: 게스트/카카오 유저 저장 요청 DTO 분리 및 User 기반 JWT 토큰 발급 메서드 추가
sudhdkso Jul 11, 2025
400df17
refactor: 불필요한 property설정 삭제
sudhdkso Jul 11, 2025
27f0fbb
test: 회원 등록 로직 변경에 따른 테스트코드 수정
sudhdkso Jul 11, 2025
ad45cef
refactor: User 도메인 CQRS 패턴 적용 UserCreator/UserReader로 명령과 조회 분리
sudhdkso Jul 11, 2025
ed17bd3
feat: 카카오 + 서비스 로그아웃 기능 추가
sudhdkso Jul 15, 2025
0b6b55d
test: 로그아웃 및 회원 생성,조회에 대한 테스트 코드 추가
sudhdkso Jul 15, 2025
b830b39
infra: cicd 워크 플로우 수정
sudhdkso Jul 15, 2025
db01dd3
refactor: 테스트 코드 메서드 오타 수정 및 로그아웃 리팩토링
sudhdkso Jul 15, 2025
df33cc1
Merge pull request #5 from moddo-kr/feat/#140-kakao-login
sudhdkso Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ jobs:
envs: GITHUB_SHA
script: |
cd dnd-12th-7-backend
sudo docker compose down
sudo docker compose rm -f
sudo docker compose stop app
sudo docker compose rm -f app
sudo docker rmi ${{env.DOCKER_IMAGE_NAME}}:latest
sudo docker compose up -d
sudo docker compose pull app
sudo docker compose up -d app
27 changes: 26 additions & 1 deletion src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,29 @@ include::{snippets}/auth-controller-test/reissue-access-token/http-response.adoc

==== 응답

include::{snippets}/auth-controller-test/reissue-access-token/response-body.adoc[]
include::{snippets}/auth-controller-test/reissue-access-token/response-body.adoc[]

== 카카오톡 소셜 로그인

사용자가 카카오 소셜 로그인을 완료하면, 인가 코드를 통해 카카오 Access Token을 발급받고, 이를 이용해 카카오 사용자 정보를 조회합니다.
조회된 사용자 정보로 서비스의 Access Token을 생성한 후, 해당 토큰은 쿠키를 통해 클라이언트에 전달됩니다.

=== Example

include::{snippets}/auth-controller-test/kakao-login-callback/curl-request.adoc[]

=== HTTP

==== 요청

include::{snippets}/auth-controller-test/kakao-login-callback/http-request.adoc[]

==== 응답

include::{snippets}/auth-controller-test/kakao-login-callback/http-response.adoc[]

=== Body

==== 응답

include::{snippets}/auth-controller-test/kakao-login-callback/response-body.adoc[]
2 changes: 2 additions & 0 deletions src/main/java/com/dnd/moddo/ModdoApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@ConfigurationPropertiesScan
public class ModdoApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,97 @@
package com.dnd.moddo.domain.auth.controller;

import java.util.Collections;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.moddo.domain.auth.service.AuthService;
import com.dnd.moddo.domain.auth.service.RefreshTokenService;
import com.dnd.moddo.global.config.CookieProperties;
import com.dnd.moddo.global.jwt.dto.RefreshResponse;
import com.dnd.moddo.global.jwt.dto.TokenResponse;
import com.dnd.moddo.global.jwt.service.JwtService;

import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/user")
@Validated
@RequestMapping("/api/v1")
public class AuthController {

private final AuthService authService;
private final RefreshTokenService refreshTokenService;
private final AuthService authService;
private final RefreshTokenService refreshTokenService;
private final CookieProperties cookieProperties;
private final JwtService jwtService;

@GetMapping("/user/guest/token")
public ResponseEntity<TokenResponse> getGuestToken() {
TokenResponse tokenResponse = authService.loginWithGuest();

String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString();

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie)
.body(tokenResponse);
}

@PutMapping("/user/reissue/token")
public RefreshResponse reissueAccessToken(@RequestHeader(value = "Authorization") @NotBlank String refreshToken) {
return refreshTokenService.execute(refreshToken);
}

@GetMapping("/login/oauth2/callback")
public ResponseEntity<Void> kakaoLoginCallback(@RequestParam @NotBlank String code) {

TokenResponse tokenResponse = authService.loginOrRegisterWithKakao(code);

String cookie = createCookie("accessToken", tokenResponse.accessToken()).toString();

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie)
.build();
}

@GetMapping("/logout")
public ResponseEntity<?> kakaoLogout(@CookieValue(value = "accessToken") String token) {
String cookie = expireCookie("accessToken").toString();
Long userId = jwtService.getUserId(token);
authService.logout(userId);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie)
.body(Collections.singletonMap("message", "Logout successful"));
}

private ResponseCookie createCookie(String name, String key) {
return ResponseCookie.from(name, key)
.httpOnly(cookieProperties.httpOnly())
.secure(cookieProperties.secure())
.path(cookieProperties.path())
.sameSite(cookieProperties.sameSite())
.maxAge(cookieProperties.maxAge())
.build();

}

@GetMapping("/guest/token")
public ResponseEntity<TokenResponse> getGuestToken() {
return ResponseEntity.ok(authService.createGuestUser());
}
private ResponseCookie expireCookie(String name) {
return ResponseCookie.from(name, null)
.httpOnly(cookieProperties.httpOnly())
.secure(cookieProperties.secure())
.path(cookieProperties.path())
.sameSite(cookieProperties.sameSite())
.maxAge(0L)
.build();
}

@PutMapping("/reissue/token")
public RefreshResponse reissueAccessToken(@RequestHeader(value = "Authorization") @NotBlank String refreshToken) {
return refreshTokenService.execute(refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.dnd.moddo.domain.auth.dto;

public record KakaoLogoutResponse(Long id) {
}
25 changes: 25 additions & 0 deletions src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dnd.moddo.domain.auth.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoProfile(
Long id,
@JsonProperty("kakao_account") KakaoAccount kakaoAccount,
Properties properties
) {
public record Properties(
String nickname
) {
}

public record KakaoAccount(
String email,
Profile profile
) {
}

public record Profile(
String nickname
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.moddo.domain.auth.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoTokenResponse(
@JsonProperty("access_token") String accessToken,
@JsonProperty("expires_in") int expiresIn
) {
}
68 changes: 51 additions & 17 deletions src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,77 @@
package com.dnd.moddo.domain.auth.service;

import java.time.LocalDateTime;
import java.util.UUID;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.moddo.domain.auth.dto.KakaoLogoutResponse;
import com.dnd.moddo.domain.auth.dto.KakaoProfile;
import com.dnd.moddo.domain.auth.dto.KakaoTokenResponse;
import com.dnd.moddo.domain.user.dto.request.GuestUserSaveRequest;
import com.dnd.moddo.domain.user.dto.request.UserSaveRequest;
import com.dnd.moddo.domain.user.entity.User;
import com.dnd.moddo.domain.user.entity.type.Authority;
import com.dnd.moddo.domain.user.repository.UserRepository;
import com.dnd.moddo.domain.user.service.CommandUserService;
import com.dnd.moddo.domain.user.service.QueryUserService;
import com.dnd.moddo.global.exception.ModdoException;
import com.dnd.moddo.global.jwt.dto.TokenResponse;
import com.dnd.moddo.global.jwt.utill.JwtProvider;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RequiredArgsConstructor
@Service
@Slf4j
public class AuthService {

private final UserRepository userRepository;
private final CommandUserService commandUserService;
private final QueryUserService queryUserService;
private final JwtProvider jwtProvider;
private final KakaoClient kakaoClient;

@Transactional
public TokenResponse createGuestUser() {
public TokenResponse loginWithGuest() {
String guestEmail = "guest-" + UUID.randomUUID() + "@guest.com";
GuestUserSaveRequest request = new GuestUserSaveRequest(guestEmail, "Guest");

User guestUser = User.builder()
.email(guestEmail)
.name("Guest")
.profile(null)
.createdAt(LocalDateTime.now())
.expiredAt(LocalDateTime.now().plusMonths(1))
.authority(Authority.USER)
.isMember(false)
.build();
User user = commandUserService.createGuestUser(request);

userRepository.save(guestUser);
return jwtProvider.generateToken(user);
}

@Transactional
public TokenResponse loginOrRegisterWithKakao(String code) {
KakaoTokenResponse tokenResponse = kakaoClient.join(code);
KakaoProfile kakaoProfile = kakaoClient.getKakaoProfile(tokenResponse.accessToken());

String email = kakaoProfile.kakaoAccount().email();
String nickname = kakaoProfile.properties().nickname();
Long kakaoId = kakaoProfile.id();

if (email == null || nickname == null || kakaoId == null) {
throw new ModdoException(HttpStatus.BAD_REQUEST, "카카오 프로필 정보가 누락되었습니다.");
}

UserSaveRequest request = new UserSaveRequest(email, nickname, kakaoId);
User user = commandUserService.getOrCreateUser(request);

return jwtProvider.generateToken(guestUser.getId(), guestUser.getEmail(), guestUser.getAuthority().toString(),
guestUser.getIsMember());
log.info("[USER_LOGIN] 로그인 성공 : code = {}, kakaoId = {}, nickname = {}", code, kakaoId, nickname);

return jwtProvider.generateToken(user);
}

public void logout(Long userId) {
queryUserService.findKakaoIdById(userId).ifPresent(kakaoId -> {
KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId);

if (!kakaoId.equals(logoutResponse.id())) {
throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치");
}

log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId);
});
}

}
Loading