Skip to content

Commit 9e79c15

Browse files
authored
feat: social-unlink (#103)
* feat: security 권한 제어 * feat: 소셜 로그인 저장 도메인 분리 * feat: 애플 소셜 로그인 방식 변경 * test: 애플 소셜 로그인 방식 변경 * test: 애플 client secret 생성 * feat: 카카오 소셜 로그인 방식 변경 * feat: 카카오 소셜 로그인 방식 변경 * feat: 카카오 소셜 로그인 연결 해제 * ci: 테스트 배포 * feat: 애플 refresh token 발급 API 호출 * test: 애플 refresh token 발급 API 호출 * feat: 소셜 서비스로부터 받은 에러 응답을 클라이언트에 전송 * docs: 에러타입 추가 * feat: 탈퇴 시 각 소셜 로그인 연결 해지 요청 결과를 클라이언트에 응답 * docs: 탈퇴 문서 업데이트 * refactor: 제약 조건 추가
1 parent 4c261c5 commit 9e79c15

31 files changed

+1128
-367
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ build/
77

88
**/src/main/resources/application-*.yml
99

10+
*.p8
11+
apple/
12+
secrets/
13+
1014

1115
### STS ###
1216
.apt_generated

capturecat-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
implementation 'org.springframework.boot:spring-boot-starter-security'
1818
implementation 'org.springframework.boot:spring-boot-starter-validation'
1919
implementation 'org.springframework.boot:spring-boot-starter-web'
20+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
2021
implementation 'org.modelmapper:modelmapper:3.2.0'
2122
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
2223
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'

capturecat-core/src/docs/asciidoc/user.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ operation::tutorialComplete[snippets='curl-request,http-request,request-headers,
1414

1515
[[회원-탈퇴]]
1616
=== 회원 탈퇴
17+
소셜 서비스 연결 해제 후 회원 관련 데이터를 삭제 처리 합니다.
18+
소셜 서비스 연결 해제가 모종의 이유로 실패하더라도 삭제 처리는 롤백하지 않습니다. (별도 회원 안내 필요)
19+
1720
==== 성공
1821
operation::withdraw[snippets='curl-request,http-request,request-headers,http-response,response-fields']
1922

capturecat-core/src/main/java/com/capturecat/core/api/auth/Oauth2AuthController.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import org.springframework.http.HttpHeaders;
66
import org.springframework.http.ResponseEntity;
7+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
8+
import org.springframework.web.bind.annotation.DeleteMapping;
79
import org.springframework.web.bind.annotation.PathVariable;
810
import org.springframework.web.bind.annotation.PostMapping;
911
import org.springframework.web.bind.annotation.RequestBody;
@@ -16,9 +18,9 @@
1618
import com.capturecat.core.api.auth.dto.SocialLoginResponse;
1719
import com.capturecat.core.config.jwt.JwtUtil;
1820
import com.capturecat.core.config.jwt.TokenType;
19-
import com.capturecat.core.service.auth.IdTokenVerifierService;
20-
import com.capturecat.core.service.auth.IdTokenVerifierService.OidcUserPayload;
2121
import com.capturecat.core.service.auth.LoginUser;
22+
import com.capturecat.core.service.auth.SocialService;
23+
import com.capturecat.core.service.auth.SocialService.OidcUserPayload;
2224
import com.capturecat.core.service.auth.TokenService;
2325
import com.capturecat.core.service.user.UserService;
2426
import com.capturecat.core.support.response.ApiResponse;
@@ -27,15 +29,15 @@
2729
@RequestMapping("/v1/auth/{provider}")
2830
@RequiredArgsConstructor
2931
public class Oauth2AuthController {
30-
private final IdTokenVerifierService idTokenVerifierService;
32+
private final SocialService socialService;
3133
private final UserService userService;
3234
private final TokenService tokenService;
3335

3436
@PostMapping("/login")
3537
public ResponseEntity<?> socialLogin(@PathVariable String provider, @RequestBody SocialLoginRequest requestDto) {
3638
// 1. provider별 id_token 검증(JWK, iss, aud 등)
37-
OidcUserPayload payload = idTokenVerifierService.verifyAndExtract(provider, requestDto.idToken(),
38-
requestDto.nickname());
39+
OidcUserPayload payload = socialService.verifyAndExtract(provider,
40+
requestDto.idToken(), requestDto.nickname(), requestDto.authToken());
3941

4042
// 2. 유저 정보 추출/회원 처리
4143
LoginUser user = userService.upsertSocialUser(payload);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package com.capturecat.core.api.auth.dto;
22

3-
public record SocialLoginRequest(String idToken, String nickname) {
3+
public record SocialLoginRequest(String idToken, String nickname, String authToken) {
44
}

capturecat-core/src/main/java/com/capturecat/core/api/user/UserController.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ public ApiResponse<?> tutorialCompleted(@AuthenticationPrincipal LoginUser login
3737
return ApiResponse.success();
3838
}
3939

40+
/**
41+
* 탈퇴 API
42+
* 1) 소셜 로그인 연결 해제
43+
* 2) 회원 정보 삭제
44+
*/
4045
@DeleteMapping("/withdraw")
4146
public ApiResponse<?> withdraw(@AuthenticationPrincipal LoginUser loginUser) {
42-
userService.withdraw(loginUser);
43-
return ApiResponse.success();
47+
String resultMessage = userService.withdraw(loginUser);
48+
return ApiResponse.success(resultMessage);
4449
}
4550
}

capturecat-core/src/main/java/com/capturecat/core/config/AppConfig.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.modelmapper.convention.MatchingStrategies;
1010
import org.springframework.context.annotation.Bean;
1111
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.web.reactive.function.client.WebClient;
1213

1314
@Configuration
1415
public class AppConfig {
@@ -24,12 +25,16 @@ public ModelMapper modelMapper() {
2425

2526
// 전역 Converter: LocalDateTime -> String
2627
Converter<LocalDateTime, String> dateTimeToString = ctx -> ctx.getSource() == null ? null
27-
: ctx.getSource().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
28+
: ctx.getSource().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
2829

2930
// 모든 LocalDateTime → String 매핑에 TypeMap 강제 등록
3031
modelMapper.createTypeMap(LocalDateTime.class, String.class).setConverter(dateTimeToString);
3132

3233
return modelMapper;
3334
}
3435

36+
@Bean
37+
public WebClient webClient() {
38+
return WebClient.builder().build();
39+
}
3540
}

capturecat-core/src/main/java/com/capturecat/core/config/SecurityConfig.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
5454
.addFilterAt(new JwtLogoutFilter(tokenService), LogoutFilter.class)
5555
.authorizeHttpRequests(
5656
authorizeRequests -> authorizeRequests
57-
.requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join",
58-
"/v1/**").permitAll()
59-
.anyRequest().hasRole("USER"));
57+
.requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join")
58+
.permitAll()
59+
.anyRequest()
60+
.hasRole("USER"));
6061

6162
return http.build();
6263
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.capturecat.core.config.auth;
2+
3+
import jakarta.validation.Valid;
4+
import jakarta.validation.constraints.NotBlank;
5+
6+
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
import org.springframework.stereotype.Component;
8+
9+
import lombok.Getter;
10+
import lombok.Setter;
11+
12+
@Getter
13+
@Setter
14+
@Valid
15+
@Component
16+
@ConfigurationProperties(prefix = "social.api")
17+
public class SocialApiProperties {
18+
@Valid
19+
private Apple apple;
20+
@Valid
21+
private Kakao kakao;
22+
23+
@Getter
24+
@Setter
25+
public static class Apple {
26+
@NotBlank
27+
private String tokenUrl;
28+
@NotBlank
29+
private String revokeUrl;
30+
@NotBlank
31+
private String teamId; //Apple 개발자 계정의 팀 ID
32+
@NotBlank
33+
private String keyId; //Apple 에서 발급한 키의 ID
34+
@NotBlank
35+
private String privateKeyPath; //Apple 에서 다운로드한 AuthKey_XXX.p8 파일 경로
36+
}
37+
38+
@Getter
39+
@Setter
40+
public static class Kakao {
41+
@NotBlank
42+
private String userinfoUrl;
43+
@NotBlank
44+
private String unlinkUrl;
45+
@NotBlank
46+
private String serviceAppAdminKey;
47+
}
48+
}

capturecat-core/src/main/java/com/capturecat/core/domain/user/User.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.capturecat.core.domain.user;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import jakarta.persistence.CascadeType;
37
import jakarta.persistence.Column;
48
import jakarta.persistence.Entity;
59
import jakarta.persistence.EnumType;
610
import jakarta.persistence.Enumerated;
711
import jakarta.persistence.GeneratedValue;
812
import jakarta.persistence.Id;
13+
import jakarta.persistence.OneToMany;
914
import jakarta.persistence.Table;
1015

1116
import lombok.AccessLevel;
@@ -43,22 +48,19 @@ public class User extends BaseTimeEntity {
4348
@Enumerated(EnumType.STRING)
4449
private UserRole role;
4550

46-
private String provider; // "google", "apple", "kakao" 등
47-
private String socialId; // 소셜 서비스의 "sub" (고유 OIDC ID)
48-
4951
private boolean tutorialCompleted = false;
5052

53+
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
54+
private List<UserSocialAccount> socialAccounts = new ArrayList<>();
55+
5156
@Builder
52-
public User(Long id, String username, String password, String email, String nickname,
53-
UserRole role, String provider, String socialId) {
57+
public User(Long id, String username, String password, String email, String nickname, UserRole role) {
5458
this.id = id;
5559
this.username = username;
5660
this.password = password;
5761
this.email = email;
5862
this.nickname = nickname;
5963
this.role = role;
60-
this.provider = provider;
61-
this.socialId = socialId;
6264
}
6365

6466
public void tutorialComplete() {

0 commit comments

Comments
 (0)