Skip to content

Commit 17b6a5c

Browse files
authored
feat : 로그인 기반 구현 (#5)
* fix : gradle 설정 수정 #3 * feat : BaseResponse 추가 #3 * feat : Exception 추가 #3 * feat : 기타 개발중이던 파일들 추가 #3 * feat : security gradle에 추가 #2 * fix : 에러코드 부분 수정 #2 * feat : cors 설정 #2 * feat : security 설정 #2 * feat : user관련 생성 #2 * feat : Oauth user #2 * feat : jwt 토큰 관련 추가 #2 * feat : spring과 swagger 버전 고려하여 수정 #2 * feat : User role 추가 #2 * feat : User repository 추가 #2 * feat : swagger 설정 추가 #2 * feat : 토큰 제공 코드 수정 및 추가 #2 * feat : 성공 코드 추가 #2 * feat : 에러 코드 추가 #2 * feat : cors 8080열게 수정 #2 * feat : 구글 로그인 구현 #2 * fix : Error 리턴 값 수정 #2 * fix : 엔드포인트 변경 #2 * fix : errorcode 수정 #2 * style : 불필요한 주석제거 #2 * fix : 유저오류 제거 #2 * fix : 메소드 변경 #2
1 parent ee1086e commit 17b6a5c

25 files changed

+853
-16
lines changed

build.gradle

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ dependencies {
2121
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
2222
implementation 'org.springframework.boot:spring-boot-starter-webflux'
2323
testImplementation 'org.springframework.boot:spring-boot-starter-test'
24-
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
25-
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
24+
25+
26+
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
2627

2728
//swagger
28-
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0'
29+
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.8.9'
30+
31+
//jwt
32+
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
33+
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
34+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
35+
36+
//OAuth
37+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
38+
39+
//log
40+
implementation 'org.slf4j:slf4j-api:2.0.9'
41+
implementation 'org.springframework.boot:spring-boot-starter-logging'
42+
2943

3044
testImplementation 'io.projectreactor:reactor-test'
3145
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.backend.crame.domain.google.controller;
2+
3+
import java.util.Map;
4+
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.DeleteMapping;
7+
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.PutMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.bind.annotation.ResponseStatus;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
import com.backend.crame.domain.google.dto.SignUpRequest;
17+
import com.backend.crame.domain.google.service.GoogleOAuthService;
18+
import com.backend.crame.global.exception.BaseException;
19+
import com.backend.crame.global.exception.ErrorCode;
20+
import com.backend.crame.global.response.BaseResponse;
21+
import com.backend.crame.global.response.enums.SuccessCode;
22+
23+
import io.swagger.v3.oas.annotations.tags.Tag;
24+
import lombok.RequiredArgsConstructor;
25+
import reactor.core.publisher.Mono;
26+
27+
@RestController
28+
@RequestMapping("/api/login/google")
29+
@Tag(name = "구글 로그인 API", description = "구글 로그인 관련 API입니다.")
30+
@RequiredArgsConstructor
31+
public class GoogleController {
32+
33+
private final GoogleOAuthService googleOAuthService;
34+
35+
@PostMapping("")
36+
public Mono<ResponseEntity<Map<String, Object>>> loginWithGoogle(@RequestParam("code") String code) {
37+
return googleOAuthService.loginWithGoogle(code)
38+
.map(response -> ResponseEntity.ok().body(response))
39+
.onErrorMap(e -> new IllegalArgumentException(e.getMessage()));
40+
}
41+
42+
@PutMapping("/signup")
43+
public Mono<ResponseEntity<?>> completeSignup(@RequestBody SignUpRequest request) {
44+
return googleOAuthService.completeSignup(request)
45+
.map(data -> BaseResponse.success(SuccessCode.SIGNUP_SUCCESS, data));
46+
}
47+
48+
49+
@DeleteMapping("/logout")
50+
public Mono<ResponseEntity<?>> logout(@RequestParam("userId") String userId){
51+
return googleOAuthService.logOut(userId)
52+
.map(data->BaseResponse.success(SuccessCode.LOGOUT_SUCCESS,data));
53+
}
54+
55+
56+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.backend.crame.domain.google.dto;
2+
3+
import com.backend.crame.domain.terms.Terms;
4+
5+
public record SignUpRequest(String email, String name, String walletUuid, Terms terms) {
6+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.backend.crame.domain.google.entity;
2+
3+
import java.util.Map;
4+
5+
import com.backend.crame.global.token.entity.OAuth2UserInfo;
6+
7+
import lombok.AllArgsConstructor;
8+
9+
@AllArgsConstructor
10+
public class GoogleUserDetails implements OAuth2UserInfo {
11+
12+
private Map<String, Object> attributes;
13+
14+
@Override
15+
public String getProvider() {
16+
return "google";
17+
}
18+
19+
@Override
20+
public String getProviderId() {
21+
return (String) attributes.get("sub");
22+
}
23+
24+
@Override
25+
public String getEmail() {
26+
return (String) attributes.get("email");
27+
}
28+
29+
@Override
30+
public String getName() {
31+
return (String) attributes.get("name");
32+
}
33+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.backend.crame.domain.google.service;
2+
3+
import java.net.URLDecoder;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.core.ParameterizedTypeReference;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.web.reactive.function.client.WebClient;
12+
13+
import com.backend.crame.domain.google.dto.SignUpRequest;
14+
import com.backend.crame.domain.user.entitiy.User;
15+
import com.backend.crame.domain.user.entitiy.UserRole;
16+
import com.backend.crame.domain.user.entitiy.UserStatus;
17+
import com.backend.crame.domain.user.repository.UserRepository;
18+
import com.backend.crame.global.exception.BaseException;
19+
import com.backend.crame.global.exception.ErrorCode;
20+
import com.backend.crame.global.token.entity.RefreshToken;
21+
import com.backend.crame.global.token.repository.RefreshTokenRepository;
22+
import com.backend.crame.global.token.service.JwtTokenProvider;
23+
24+
import lombok.RequiredArgsConstructor;
25+
import lombok.extern.slf4j.Slf4j;
26+
import reactor.core.publisher.Mono;
27+
28+
@Service
29+
@Slf4j
30+
@RequiredArgsConstructor
31+
public class GoogleOAuthService {
32+
33+
private final UserRepository userRepository;
34+
private final RefreshTokenRepository refreshTokenRepository;
35+
private final JwtTokenProvider jwtTokenProvider;
36+
37+
@Value("${spring.security.oauth2.client.registration.google.client-id}")
38+
private String clientId;
39+
40+
@Value("${spring.security.oauth2.client.registration.google.client-secret}")
41+
private String clientSecret;
42+
43+
@Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
44+
private String redirectUri;
45+
46+
47+
private final WebClient webClient = WebClient.create();
48+
49+
public Mono<Map<String, Object>> loginWithGoogle(String code) {
50+
return exchangeCodeForAccessToken(code)
51+
.flatMap(this::fetchUserInfo)
52+
.flatMap(this::handleUserInfo);
53+
}
54+
55+
private Mono<String> exchangeCodeForAccessToken(String code) {
56+
String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8);
57+
58+
return webClient.post()
59+
.uri("https://oauth2.googleapis.com/token")
60+
.bodyValue(Map.of(
61+
"code", decodedCode,
62+
"client_id", clientId,
63+
"client_secret", clientSecret,
64+
"redirect_uri", redirectUri,
65+
"grant_type", "authorization_code"
66+
))
67+
.retrieve()
68+
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
69+
.map(body -> {
70+
if (!body.containsKey("access_token")) {
71+
throw new BaseException(ErrorCode.LOGIN_FAIL);
72+
}
73+
return (String) body.get("access_token");
74+
});
75+
76+
}
77+
78+
private Mono<Map<String, Object>> fetchUserInfo(String accessToken) {
79+
return webClient.get()
80+
.uri("https://www.googleapis.com/oauth2/v3/userinfo")
81+
.headers(headers -> headers.setBearerAuth(accessToken))
82+
.retrieve()
83+
.bodyToMono(new ParameterizedTypeReference<>() {});
84+
}
85+
86+
private Mono<Map<String, Object>> handleUserInfo(Map<String, Object> userInfo) {
87+
String email = (String) userInfo.get("email");
88+
if (email == null) {
89+
return Mono.error(new BaseException(ErrorCode.LOGIN_FAIL));
90+
}
91+
92+
return userRepository.findByEmail(email)
93+
.flatMap(user -> {
94+
if (user.getStatus() == UserStatus.SUCCESS) {
95+
return jwtTokenProvider.createToken(user.getUser_uuid())
96+
.map(tokenResponse ->{
97+
Map<String, Object> result = new HashMap<>();
98+
result.put("accessToken", tokenResponse.accessToken());
99+
result.put("isSignedUp", true);
100+
return result;
101+
});
102+
} else {
103+
Map<String, Object> result = new HashMap<>();
104+
result.put("message", "회원가입이 필요합니다");
105+
result.put("email", email);
106+
result.put("isSignedUp", false);
107+
return Mono.just(result);
108+
}
109+
})
110+
.switchIfEmpty(
111+
userRepository.save(User.builder()
112+
.email(email)
113+
.status(UserStatus.PENDING)
114+
.domain("Google")
115+
.subscribe(false)
116+
.select_model("")
117+
.wallet_uuid("")
118+
.userRole(UserRole.ROLE_USER)
119+
.build())
120+
.thenReturn(Map.of(
121+
"message", "회원가입이 필요합니다",
122+
"email", email,
123+
"isSignedUp", false
124+
))
125+
);
126+
}
127+
128+
129+
// 회원가입 완료 처리
130+
public Mono<Map<String, Object>> completeSignup(SignUpRequest request) {
131+
log.info("회원가입 요청: {}", request.email());
132+
133+
return userRepository.findByEmail(request.email())
134+
.switchIfEmpty(Mono.defer(() -> {
135+
log.warn("해당 이메일 없음: {}", request.email());
136+
return Mono.error(new BaseException(ErrorCode.SIGNUP_ERROR));
137+
}))
138+
.flatMap(user -> {
139+
log.info("유저 발견: {}", user.getEmail());
140+
user.setName(request.name());
141+
user.setWallet_uuid(request.walletUuid());
142+
user.setTerms(request.terms());
143+
user.setStatus(UserStatus.SUCCESS);
144+
log.info("유저 정보 업데이트 완료");
145+
return userRepository.save(user);
146+
})
147+
.flatMap(user -> jwtTokenProvider.createToken(user.getUser_uuid()))
148+
.map(token -> {
149+
log.info("토큰 생성 완료");
150+
return Map.of(
151+
"accessToken", token.accessToken(),
152+
"message", "회원가입 완료"
153+
);
154+
});
155+
}
156+
157+
158+
159+
160+
public Mono<RefreshToken> logOut(String userId){
161+
return refreshTokenRepository.deleteByUserId(userId)
162+
.switchIfEmpty(Mono.error(new BaseException(ErrorCode.LOGOUT_ERROR)));
163+
164+
}
165+
}

src/main/java/com/backend/crame/domain/user/User.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
package com.backend.crame.domain.user;
22

3+
import org.springframework.data.annotation.Id;
34
import org.springframework.data.mongodb.core.mapping.Document;
45
import org.springframework.data.mongodb.core.mapping.Field;
56

67
import com.backend.crame.domain.terms.Terms;
78

8-
import jakarta.persistence.Column;
9-
import jakarta.persistence.Entity;
10-
import jakarta.persistence.GeneratedValue;
11-
import jakarta.persistence.GenerationType;
12-
import jakarta.persistence.Id;
139
import lombok.AccessLevel;
1410
import lombok.AllArgsConstructor;
1511
import lombok.Builder;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.backend.crame.domain.user.entitiy;
2+
3+
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.mongodb.core.mapping.Document;
5+
import org.springframework.data.mongodb.core.mapping.Field;
6+
7+
import com.backend.crame.domain.terms.Terms;
8+
9+
10+
11+
import lombok.AccessLevel;
12+
import lombok.AllArgsConstructor;
13+
import lombok.Builder;
14+
import lombok.Getter;
15+
import lombok.NoArgsConstructor;
16+
import lombok.Setter;
17+
18+
@Document(collection = "user")
19+
@Getter
20+
@Builder
21+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
22+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
23+
public class User {
24+
25+
@Id
26+
private String user_uuid;
27+
28+
@Setter
29+
private String wallet_uuid;
30+
31+
private String email;
32+
33+
@Setter
34+
private String name; //이 이름을 설정하는 부분이 있으면 좋을 것 같긴하다
35+
36+
@Setter
37+
private UserStatus status;
38+
39+
@Field("terms")
40+
@Setter
41+
private Terms terms; //약관에 대한 동의
42+
43+
@Setter
44+
private Boolean subscribe; //구독제 결제 유무
45+
46+
private String select_model;
47+
48+
private String domain; // 어떤 소셜 로그인인지 분기
49+
50+
private UserRole userRole;
51+
52+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.backend.crame.domain.user.entitiy;
2+
3+
public enum UserRole {
4+
ROLE_USER,
5+
ROLE_ADMIN
6+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.backend.crame.domain.user.entitiy;
2+
3+
public enum UserStatus {
4+
//소셜 로그인 중에 대기상태를 두기 위해서 사용하는 부분
5+
PENDING("대기상태"),
6+
SUCCESS("회원가입 성공");
7+
8+
UserStatus(String status){
9+
this.status = status;
10+
}
11+
12+
String status;
13+
}

0 commit comments

Comments
 (0)