Skip to content

Commit a1f2c7a

Browse files
authored
Merge pull request #335 from GTable/feature/#334-app-kakao-login
feat : 앱 전용 카카오 로그인 구현
2 parents 2947a18 + 7bb68e1 commit a1f2c7a

File tree

18 files changed

+358
-172
lines changed

18 files changed

+358
-172
lines changed

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.stream.Collectors;
56

67
import org.springframework.stereotype.Service;
78
import org.springframework.transaction.annotation.Transactional;
@@ -156,7 +157,7 @@ public String updateMenuSortOrder(List<MenuSortUpdateRequest> requests, MemberDe
156157
}
157158

158159
Map<Long, Long> idToSort = requests.stream()
159-
.collect(java.util.stream.Collectors.toMap(MenuSortUpdateRequest::getMenuId,
160+
.collect(Collectors.toMap(MenuSortUpdateRequest::getMenuId,
160161
MenuSortUpdateRequest::getSortOrder));
161162

162163
menus.forEach(m -> m.updateSortOrder(idToSort.get(m.getId())));

nowait-app-user-api/src/main/java/com/nowait/applicationuser/config/security/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1515
import org.springframework.web.cors.CorsConfigurationSource;
1616

17-
import com.nowait.applicationuser.oauth.oauth2.CustomOAuth2UserService;
17+
import com.nowait.applicationuser.oauth.service.CustomOAuth2UserService;
1818
import com.nowait.applicationuser.oauth.oauth2.OAuth2LoginSuccessHandler;
1919
import com.nowait.applicationuser.security.jwt.JwtAuthorizationFilter;
2020
import com.nowait.applicationuser.security.jwt.JwtUtil;
@@ -58,6 +58,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5858
"/oauth2/authorization/kakao", // 카카오 로그인 요청
5959
"/login/oauth2/code/**", // 카카오 인증 콜백
6060
"/api/refresh-token", // refresh token (토큰 갱신)
61+
"/v2/app/oauth/kakao/login", // 카카오 앱 로그인
6162
"/v1/menus/**", // 모든 메뉴 조회
6263
"/v1/store-payments/**", // 결제 관련 API
6364
"/orders/**",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.nowait.applicationuser.oauth.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestBody;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.nowait.applicationuser.oauth.dto.KakaoAppLoginRequest;
11+
import com.nowait.applicationuser.oauth.dto.KakaoAppLoginResponse;
12+
import com.nowait.applicationuser.oauth.service.KakaoAppLoginService;
13+
import com.nowait.common.api.ApiUtils;
14+
15+
import lombok.RequiredArgsConstructor;
16+
17+
@RestController
18+
@RequestMapping("/v2/app/oauth/kakao")
19+
@RequiredArgsConstructor
20+
public class KakaoAppAuthController {
21+
22+
private final KakaoAppLoginService kakaoAppLoginService;
23+
24+
@PostMapping("/login")
25+
public ResponseEntity<?> kakaoAppLogin(@RequestBody KakaoAppLoginRequest request) {
26+
27+
KakaoAppLoginResponse response = kakaoAppLoginService.login(request);
28+
29+
return ResponseEntity
30+
.status(HttpStatus.OK)
31+
.body(
32+
ApiUtils.success(response)
33+
);
34+
}
35+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.nowait.applicationuser.oauth.dto;
2+
3+
import lombok.AccessLevel;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
10+
@AllArgsConstructor
11+
public class KakaoAppLoginRequest {
12+
private String kakaoAccessToken;
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.nowait.applicationuser.oauth.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
10+
@AllArgsConstructor
11+
@Builder
12+
public class KakaoAppLoginResponse{
13+
private String accessToken;
14+
private String refreshToken;
15+
private Long userId;
16+
private String email;
17+
private String nickName;
18+
private String profileImage;
19+
private boolean phoneEntered;
20+
private boolean marketingAgree;
21+
private boolean isNewUser;
22+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.nowait.applicationuser.oauth.dto;
2+
3+
import com.nowait.domaincorerdb.user.entity.User;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public class OAuthUserResult {
11+
private final User user;
12+
private final boolean newUser;
13+
}
Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package com.nowait.applicationuser.oauth.oauth2;
22

33
import java.io.IOException;
4-
import java.time.LocalDateTime;
5-
import java.util.Optional;
64

75
import org.springframework.http.ResponseCookie;
86
import org.springframework.security.core.Authentication;
97
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
108
import org.springframework.stereotype.Component;
119
import org.springframework.transaction.annotation.Transactional;
1210

13-
import com.nowait.applicationuser.security.jwt.JwtUtil;
14-
import com.nowait.domaincorerdb.token.entity.Token;
15-
import com.nowait.domaincorerdb.token.repository.TokenRepository;
11+
import com.nowait.applicationuser.token.dto.AuthenticationResponse;
12+
import com.nowait.applicationuser.token.service.AuthTokenService;
1613
import com.nowait.domaincorerdb.user.entity.User;
1714
import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User;
1815

@@ -30,8 +27,7 @@
3027
@RequiredArgsConstructor
3128
@Slf4j
3229
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
33-
private final JwtUtil jwtUtil;
34-
private final TokenRepository tokenRepository;
30+
private final AuthTokenService authTokenService;
3531

3632
@Override
3733
@Transactional
@@ -40,26 +36,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
4036

4137
CustomOAuth2User customUserDetails = (CustomOAuth2User)authentication.getPrincipal();
4238
User user = customUserDetails.getUser();
43-
Long userId = customUserDetails.getUserId();
44-
String role = authentication.getAuthorities().iterator().next().getAuthority();
4539

46-
// JWT 발급
47-
String accessToken = jwtUtil.createAccessToken("accessToken", userId, role,
48-
Boolean.TRUE.equals(user.getPhoneEntered()), Boolean.TRUE.equals(user.getIsMarketingAgree()),60 * 60 * 1000L); // 1시간
49-
String refreshToken = jwtUtil.createRefreshToken("refreshToken", userId, 30L * 24 * 60 * 60 * 1000L); // 30일
50-
51-
// 1. refreshToken을 DB에 저장 or update
52-
Optional<Token> tokenOptional = tokenRepository.findByUserId(user.getId());
53-
if (tokenOptional.isPresent()) {
54-
Token token = tokenOptional.get();
55-
token.updateRefreshToken(refreshToken, LocalDateTime.now().plusDays(30));
56-
} else {
57-
Token token = Token.toEntity(user, refreshToken, LocalDateTime.now().plusDays(30));
58-
tokenRepository.save(token);
59-
}
40+
AuthenticationResponse authenticationResponse = authTokenService.issueTokens(user);
6041

6142
// 2. refreshToken을 HttpOnly 쿠키로 설정 (ResponseCookie로)
62-
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken)
43+
ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", authenticationResponse.getRefreshToken())
6344
.httpOnly(true)
6445
.secure(false) // 운영환경에서는 true
6546
.path("/")
@@ -71,7 +52,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
7152
response.setHeader("Set-Cookie", refreshTokenCookie.toString());
7253

7354
// 3. 프론트엔드로 리다이렉트 (accessToken만 쿼리로 전달)
74-
String targetUrl = "https://app.nowait.co.kr/login/success?accessToken=" + accessToken;
55+
String targetUrl = "https://app.nowait.co.kr/login/success?accessToken=" + authenticationResponse.getAccessToken();
7556
response.sendRedirect(targetUrl);
7657
}
7758
}

nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/oauth2/CustomOAuth2UserService.java renamed to nowait-app-user-api/src/main/java/com/nowait/applicationuser/oauth/service/CustomOAuth2UserService.java

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
package com.nowait.applicationuser.oauth.oauth2;
2-
3-
import java.time.LocalDateTime;
4-
import java.util.Optional;
1+
package com.nowait.applicationuser.oauth.service;
52

63
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
74
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
@@ -11,10 +8,8 @@
118

129
import com.nowait.applicationuser.oauth.dto.KaKaoResponse;
1310
import com.nowait.applicationuser.oauth.dto.OAuth2Response;
14-
import com.nowait.common.enums.Role;
15-
import com.nowait.common.enums.SocialType;
11+
import com.nowait.applicationuser.oauth.dto.OAuthUserResult;
1612
import com.nowait.domaincorerdb.user.entity.User;
17-
import com.nowait.domaincorerdb.user.repository.UserRepository;
1813
import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User;
1914

2015
import lombok.RequiredArgsConstructor;
@@ -25,12 +20,13 @@
2520
@RequiredArgsConstructor
2621
@Slf4j
2722
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
28-
private final UserRepository userRepository;
23+
private final OAuthUserService oAuthUserService;
2924

3025
@Override
3126
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
3227
OAuth2User oAuth2User = super.loadUser(userRequest);
3328

29+
// TODO : 해당 로그 필요한지 추후 확인 필요
3430
log.info("CustomOAuth2UserService :: {}", oAuth2User);
3531
log.info("oAuthUser.getAttributes :: {}", oAuth2User.getAttributes());
3632

@@ -44,34 +40,10 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
4440
}
4541

4642
// DB에 유저가 있는지 판단
47-
Optional<User> foundUser = userRepository.findByEmail(oAuth2Response.getEmail());
48-
49-
// DB에 유저 없으면 - 회원가입
50-
if (foundUser.isEmpty()) {
51-
52-
User user = User.builder()
53-
.email(oAuth2Response.getEmail())
54-
.phoneNumber("")
55-
.nickname(oAuth2Response.getNickName())
56-
.profileImage(oAuth2Response.getProfileImage())
57-
.socialType(SocialType.KAKAO)
58-
.role(Role.USER) // 일반 유저 설정
59-
.storeId(0L)
60-
.phoneEntered(false)
61-
.isMarketingAgree(false)
62-
.createdAt(LocalDateTime.now())
63-
.updatedAt(LocalDateTime.now())
64-
.build();
65-
66-
userRepository.save(user);
43+
OAuthUserResult result = oAuthUserService.loadOrCreateUser(oAuth2Response);
44+
User user = result.getUser();
45+
boolean newUser = result.isNewUser();
6746

68-
return new CustomOAuth2User(user);
69-
} else {
70-
// DB에 유저 존재하면 - 로그인 진행 (이때 로그인 처리는 안하고, OAuth2LoginSuccessHandler에서 담당함)
71-
User user = foundUser.get();
72-
73-
return new CustomOAuth2User(user);
74-
}
47+
return new CustomOAuth2User(user, newUser);
7548
}
76-
7749
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.nowait.applicationuser.oauth.service;
2+
3+
import java.time.Instant;
4+
5+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
6+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
7+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
8+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
9+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
10+
import org.springframework.security.oauth2.core.user.OAuth2User;
11+
import org.springframework.stereotype.Service;
12+
13+
import com.nowait.applicationuser.oauth.dto.KakaoAppLoginRequest;
14+
import com.nowait.applicationuser.oauth.dto.KakaoAppLoginResponse;
15+
import com.nowait.applicationuser.token.dto.AuthenticationResponse;
16+
import com.nowait.applicationuser.token.service.AuthTokenService;
17+
import com.nowait.domaincorerdb.user.entity.User;
18+
import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User;
19+
20+
import lombok.RequiredArgsConstructor;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
public class KakaoAppLoginService {
25+
26+
private final CustomOAuth2UserService customOAuth2UserService;
27+
private final ClientRegistrationRepository clientRegistrationRepository;
28+
private final AuthTokenService authTokenService;
29+
30+
public KakaoAppLoginResponse login(KakaoAppLoginRequest request) {
31+
String kakaoAccessTokenValue = request.getKakaoAccessToken();
32+
33+
if (kakaoAccessTokenValue == null || kakaoAccessTokenValue.isEmpty()) {
34+
throw new OAuth2AuthenticationException("Kakao Access Token is missing.");
35+
}
36+
37+
ClientRegistration kakaoRegistration = clientRegistrationRepository.findByRegistrationId("kakao");
38+
39+
if (kakaoRegistration == null) {
40+
throw new OAuth2AuthenticationException("Kakao Client Registration not found.");
41+
}
42+
43+
OAuth2AccessToken kakaoAccessToken = new OAuth2AccessToken(
44+
OAuth2AccessToken.TokenType.BEARER,
45+
kakaoAccessTokenValue,
46+
Instant.now(),
47+
null
48+
);
49+
50+
OAuth2UserRequest userRequest = new OAuth2UserRequest(
51+
kakaoRegistration,
52+
kakaoAccessToken
53+
);
54+
55+
OAuth2User oAuth2User = customOAuth2UserService.loadUser(userRequest);
56+
CustomOAuth2User customUser = (CustomOAuth2User) oAuth2User;
57+
User user = customUser.getUser();
58+
59+
AuthenticationResponse authenticationResponse = authTokenService.issueTokens(user);
60+
61+
return KakaoAppLoginResponse.builder()
62+
.accessToken(authenticationResponse.getAccessToken())
63+
.refreshToken(authenticationResponse.getRefreshToken())
64+
.userId(user.getId())
65+
.email(user.getEmail())
66+
.nickName(user.getNickname())
67+
.profileImage(user.getProfileImage())
68+
.phoneEntered(user.getPhoneEntered())
69+
.marketingAgree(user.getIsMarketingAgree())
70+
.isNewUser(customUser.isNewUser())
71+
.build();
72+
}
73+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.nowait.applicationuser.oauth.service;
2+
3+
import java.time.LocalDateTime;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import com.nowait.applicationuser.oauth.dto.OAuth2Response;
9+
import com.nowait.applicationuser.oauth.dto.OAuthUserResult;
10+
import com.nowait.common.enums.Role;
11+
import com.nowait.common.enums.SocialType;
12+
import com.nowait.domaincorerdb.user.entity.User;
13+
import com.nowait.domaincorerdb.user.repository.UserRepository;
14+
15+
import lombok.RequiredArgsConstructor;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class OAuthUserService {
20+
private final UserRepository userRepository;
21+
22+
@Transactional
23+
public OAuthUserResult loadOrCreateUser(OAuth2Response oAuth2Response) {
24+
return userRepository.findByEmail(oAuth2Response.getEmail())
25+
.map(user -> new OAuthUserResult(user, false)) // 기존 유저
26+
.orElseGet(() -> {
27+
User created = createUser(oAuth2Response);
28+
return new OAuthUserResult(created, true); // 신규 유저
29+
});
30+
}
31+
32+
private User createUser(OAuth2Response oAuth2Response) {
33+
User user = User.builder()
34+
.email(oAuth2Response.getEmail())
35+
.phoneNumber("")
36+
.nickname(oAuth2Response.getNickName())
37+
.profileImage(oAuth2Response.getProfileImage())
38+
.socialType(SocialType.KAKAO)
39+
.role(Role.USER) // 일반 유저 설정
40+
.storeId(0L)
41+
.phoneEntered(false)
42+
.isMarketingAgree(false)
43+
.createdAt(LocalDateTime.now())
44+
.updatedAt(LocalDateTime.now())
45+
.build();
46+
47+
return userRepository.save(user);
48+
}
49+
}

0 commit comments

Comments
 (0)