Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ jobs:
sudo docker compose stop app
sudo docker compose rm -f app
sudo docker rmi ${{env.DOCKER_IMAGE_NAME}}:latest
sudo docker compose pull app
sudo docker compose up -d app
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,41 +1,43 @@
package com.dnd.moddo.domain.auth.controller;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.dto.KakaoTokenResponse;
import com.dnd.moddo.domain.auth.service.AuthService;
import com.dnd.moddo.domain.auth.service.KakaoClient;
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.properties.CookieProperties;
import com.dnd.moddo.global.jwt.service.JwtService;

import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
@EnableConfigurationProperties(CookieProperties.class)
@Validated
@RequestMapping("/api/v1")
public class AuthController {

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

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

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

Expand All @@ -50,10 +52,9 @@ public RefreshResponse reissueAccessToken(@RequestHeader(value = "Authorization"
}

@GetMapping("/login/oauth2/callback")
public ResponseEntity<Void> kakaoLoginCallback(@RequestParam String code) {
KakaoTokenResponse kakaoTokenResponse = kakaoClient.join(code);
public ResponseEntity<Void> kakaoLoginCallback(@RequestParam @NotBlank String code) {

TokenResponse tokenResponse = authService.getOrCreateKakaoUserToken(kakaoTokenResponse.access_token());
TokenResponse tokenResponse = authService.loginOrRegisterWithKakao(code);

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

Expand All @@ -63,20 +64,20 @@ public ResponseEntity<Void> kakaoLoginCallback(@RequestParam String code) {
}

@GetMapping("/logout")
public ResponseEntity<Void> kakaoLogout() {
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)
.build();
.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())
.domain(cookieProperties.domain())
.sameSite(cookieProperties.sameSite())
.maxAge(cookieProperties.maxAge())
.build();
Expand All @@ -88,9 +89,9 @@ private ResponseCookie expireCookie(String name) {
.httpOnly(cookieProperties.httpOnly())
.secure(cookieProperties.secure())
.path(cookieProperties.path())
.domain(cookieProperties.domain())
.sameSite(cookieProperties.sameSite())
.maxAge(0L)
.build();
}

}
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) {
}
29 changes: 9 additions & 20 deletions src/main/java/com/dnd/moddo/domain/auth/dto/KakaoProfile.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
package com.dnd.moddo.domain.auth.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

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

public record KakaoAccount(
Boolean profile_nickname_needs_agreement,
Boolean profile_image_needs_agreement,
Profile profile,
Boolean has_email,
Boolean email_needs_agreement,
Boolean is_email_valid,
Boolean is_email_verified,
String email
String email,
Profile profile
) {
}

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

import com.fasterxml.jackson.annotation.JsonProperty;

public record KakaoTokenResponse(
String access_token,
String token_type,
String refresh_token,
int expires_in,
String scope,
int refresh_token_expires_in
@JsonProperty("access_token") String accessToken,
@JsonProperty("expires_in") int expiresIn
) {
}
76 changes: 39 additions & 37 deletions src/main/java/com/dnd/moddo/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.dnd.moddo.domain.auth.service;

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

import org.springframework.beans.factory.annotation.Value;
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;

Expand All @@ -22,54 +26,52 @@
@Slf4j
public class AuthService {

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

@Value("${kakao.auth.client_id}")
String client_id;

@Value("${kakao.auth.redirect_uri}")
String redirect_uri;

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

User guestUser = createUser(guestEmail, "Guest", false);
User user = commandUserService.createGuestUser(request);

return jwtProvider.generateToken(guestUser.getId(), guestUser.getEmail(), guestUser.getAuthority().toString(),
guestUser.getIsMember());
}

private User createUser(String email, String name, boolean isMember) {
User user = User.builder()
.email(email)
.name(name)
.profile(null)
.createdAt(LocalDateTime.now())
.expiredAt(LocalDateTime.now().plusMonths(1))
.authority(Authority.USER)
.isMember(isMember)
.build();

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

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

String email = kakaoProfile.kakao_account().email();
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);

log.info("[USER_LOGIN] 로그인 성공 : code = {}, kakaoId = {}, nickname = {}", code, kakaoId, nickname);

return jwtProvider.generateToken(user);
}

User kakaoUser = userRepository.findByEmail(email)
.orElseGet(() -> createUser(email, nickname, true));
public void logout(Long userId) {
queryUserService.findKakaoIdById(userId).ifPresent(kakaoId -> {
KakaoLogoutResponse logoutResponse = kakaoClient.logout(kakaoId);

log.info("[USER_LOGIN] 로그인 성공 : email={}, name={}", kakaoUser.getEmail(), kakaoUser.getName());
if (!kakaoId.equals(logoutResponse.id())) {
throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 로그아웃 실패: id 불일치");
}

return jwtProvider.generateToken(kakaoUser.getId(), kakaoUser.getEmail(), kakaoUser.getAuthority().toString(),
kakaoUser.getIsMember());
log.info("[USER_LOGOUT] 카카오 로그아웃 성공: userId={}, kakaoId={}", userId, kakaoId);
});
}

}
44 changes: 30 additions & 14 deletions src/main/java/com/dnd/moddo/domain/auth/service/KakaoClient.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.dnd.moddo.domain.auth.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand All @@ -10,8 +9,10 @@
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientResponseException;

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.global.config.KakaoProperties;
import com.dnd.moddo.global.exception.ModdoException;

import lombok.RequiredArgsConstructor;
Expand All @@ -21,30 +22,24 @@
@Slf4j
@Component
public class KakaoClient {

@Value("${kakao.auth.client_id}")
String client_id;

@Value("${kakao.auth.redirect_uri}")
String redirect_uri;

private final KakaoProperties kakaoProperties;
private final RestClient.Builder builder;

public KakaoTokenResponse join(String code) {
RestClient restClient = builder.build();

String uri = "https://kauth.kakao.com/oauth/token";
String uri = kakaoProperties.tokenRequestUri();

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", client_id);
params.add("redirect_uri", redirect_uri);
params.add("client_id", kakaoProperties.clientId());
params.add("redirect_uri", kakaoProperties.redirectUri());
params.add("code", code);

try {
return restClient.post()
.uri(uri)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(params)
.retrieve()
.body(KakaoTokenResponse.class);
Expand All @@ -62,13 +57,12 @@ public KakaoTokenResponse join(String code) {
public KakaoProfile getKakaoProfile(String token) {
RestClient restClient = builder.build();

String uri = "https://kapi.kakao.com/v2/user/me";
String uri = kakaoProperties.profileRequestUri();

try {
return restClient.get()
.uri(uri)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
.retrieve()
.body(KakaoProfile.class);

Expand All @@ -81,4 +75,26 @@ public KakaoProfile getKakaoProfile(String token) {
throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패");
}
}

public KakaoLogoutResponse logout(Long kakaoId) {
RestClient restClient = builder.build();
String uri = kakaoProperties.logoutRequestUri();

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("target_id_type", "user_id");
params.add("target_id", kakaoId.toString());

try {
return restClient.post()
.uri(uri)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoProperties.adminKey())
.body(params)
.retrieve()
.body(KakaoLogoutResponse.class);
} catch (Exception e) {
log.error("[KAKAO_CALLBACK_ERROR] 카카오 콜백 처리 실패", e.getMessage());
throw new ModdoException(HttpStatus.INTERNAL_SERVER_ERROR, "카카오 콜백 처리 실패");
}
}
}
Loading
Loading