Skip to content

Commit d758e7f

Browse files
authored
[TNT-83] feat: 소셜 로그인 API 구현 (#12)
* [TNT-83] feat: oauth login api endpoint 구현 * [TNT-83] feat: oauth login request 구현 * [TNT-83] feat: oauth login response 구현 * [TNT-83] refactor: 클래스 이름 수정 * [TNT-83] feat: 동의 여부 컬럼 추가 * [TNT-83] refactor: 변수명 수정 * [TNT-83] feat: oauthLogin 메서드 구현 중 * [TNT-83] refactor: 가입 여부 추가 * [TNT-83] refactor: 개행 추가 * [TNT-83] refactor: sessionValue 클래스 삭제 * [TNT-83] chore: jwt 라이브러리 설치 * [TNT-83] refactor: 소셜 로그인 api tag, operation 추가 * [TNT-83] refactor: 동의 관련 컬럼명 수정 * [TNT-83] refactor: 폴더 위치 수정 * [TNT-83] refactor: schema 설정 및 필드 수정 * [TNT-83] refactor: sessionValue 삭제 * [TNT-83] feat: oauth, not found 예외 추가 * [TNT-83] refactor: error message 객체 구현 * [TNT-83] feat: 소셜 유저 정보 객체 구현 * [TNT-83] feat: findBySocialIdAndSocialType 추가 * [TNT-83] refactor: 폴더 위치 수정 * [TNT-83] refactor: 예외 처리 로직 제거 * [TNT-83] feat: servlet 예외 필터 구현 * [TNT-83] refactor: base time entity 경로 수정 * [TNT-83] feat: 서블릿 예외 필터 구현 * [TNT-83] refactor: 갱신 로직 추가, 세션 밸류 제거 * [TNT-83] feat: 에러 메시지 추가 * [TNT-83] refactor: enum 대입 * [TNT-83] refactor: 에러 메시지 enum 대입, 세션 밸류 삭제 * [TNT-83] feat: jacoco 예외 dto 폴더 추가 * [TNT-83] feat: 서블릿 필터 테스트 구현 * [TNT-83] feat: SocialType 테스트 구현 * [TNT-83] refactor: 메서드명 수정 * [TNT-83] refactor: 에러 메시지 수정 * [TNT-83] refactor: 허용 url 수정 * [TNT-83] refactor: 에러 메시지 enum 클래스로 이동 * [TNT-83] chore: mockwebserver 추가 * [TNT-83] feat: LoginControllerTest 구현 * [TNT-83] feat: oauth 로그인 구현 * [TNT-83] refactor: 로그 stack trace 추가 * [TNT-83] feat: OAuthServiceTest 구현 * [TNT-83] chore: jacoco 예외에 AppleEcdsaKeyProvider 추가 * [TNT-83] refactor: OAuthService 필드 @Autowired 추가 * [TNT-83] refactor: OAuthService 필드 @Autowired 삭제 * [TNT-83] refactor: OAuthService 필드 @Injectmocks 추가 * [TNT-83] refactor: OAuthService 필드 @mock 추가 * [TNT-83] refactor: @component 추가 * Update config submodule reference * [TNT-83] refactor: @mock 제거 * [TNT-83] refactor: 허용 url 로그 제거 * [TNT-83] refactor: 디렉토리 위치 변경 * [TNT-83] refactor: 디렉토리 위치 변경 * [TNT-83] refactor: 파라미터 Schema 수정 * [TNT-83] refactor: socialId, socialType length 지정 * [TNT-83] refactor: 불필요한 테스트 제거 * [TNT-83] refactor: fetchUserInfoWithoutApple 메서드명, 일부 메서드 접근 제어자 수정 * [TNT-83] refactor: socialType @Schema 수정 * [TNT-83] refactor: age, gender 메서드 삭제 * [TNT-83] refactor: extractOAuthUserInfo 메서드 삭제 * [TNT-83] feat: oauth attribute 추출자 구현 * [TNT-83] refactor: 불필요한 클래스 제거 * [TNT-83] refactor: 불필요한 클래스 제거 * [TNT-83] refactor: 불필요한 요소 제거 * [TNT-83] refactor: 자주쓰는 값 상수화 * [TNT-83] refactor: 불필요한 테스트 삭제 * [TNT-83] refactor: createData 메서드의 불필요한 파라미터 수정 * [TNT-83] feat: 중복 로그인 상태 검증 로직 추가, 기타 중복 로직 수정 * [TNT-83] refactor: createData 메서드의 불필요한 파라미터 수정 * [TNT-83] refactor: checkSessionAndAuthentication 수정 * [TNT-83] feat: 광고성 알림 동의 여부 추가 * [TNT-83] feat: socialId, isSignUp 추가 * [TNT-83] refactor: 신규 회원이면 소셜 id, 가입 여부 리턴하도록 수정 * [TNT-83] refactor: 회원 객체 from 메서드로 생성하도록 수정 * [TNT-83] refactor: 회원 객체 from 메서드로 생성하도록 수정 * [TNT-83] refactor: from 메서드 제거 * [TNT-83] refactor: error log 추가 * [TNT-83] refactor: 컬럼 설명 DATETIME 으로 수정 * [TNT-83] refactor: final 제거 * [TNT-83] refactor: HttpStatus를 static import로 수정 * [TNT-83] refactor: @component, @requiredargsconstructor 제거, HttpServletResponse를 static import로 수정 * [TNT-83] refactor: SecurityContext에 저장하는 username 파라미터를 memberId로 수정 * [TNT-83] refactor: createOrUpdateSession, removeSession로 수정 * [TNT-83] refactor: 불필요한 뎁스 삭제 * [TNT-83] refactor: 클래스 @RequestMapping 제거, swagger 내용 수정 * [TNT-83] refactor: OAuthLoginResponse from 메서드 제거 * [TNT-83] refactor: findBySocialIdAndSocialType 조건에 AndDeletedAt 추가 * [TNT-83] refactor: 신규 회원 테스트명 수정 * [TNT-83] refactor: 회원 생성 방법 빌더 패턴으로 수정 * [TNT-83] refactor: 회원 생성 시 값 추가 * [TNT-83] refactor: HttpStatus 추가 * [TNT-83] refactor: AuthenticationController로 수정 * [TNT-83] refactor: CLIENT_BAD_REQUEST로 수정 * [TNT-83] refactor: HttpStatus static import 적용 * [TNT-83] refactor: 생성자 빌더로 수정 * [TNT-83] refactor: 생성자 빌더로 수정 * [TNT-83] refactor: findBySocialIdAndSocialTypeAndDeletedAt로 수정 * [TNT-83] refactor: CLIENT_BAD_REQUEST로 수정
1 parent 9d4e746 commit d758e7f

33 files changed

+1489
-220
lines changed

build.gradle

+7-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ jacocoTestCoverageVerification {
106106
excludes = [
107107
'*.*Application',
108108
'*.*Config',
109-
'*.error.*'
109+
'*.error.*',
110+
'*.dto.*',
111+
'*.*AppleEcdsaKeyProvider'
110112
]
111113
}
112114
}
@@ -130,6 +132,7 @@ dependencies {
130132
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
131133

132134
// 애플 로그인 관련 라이브러리
135+
implementation 'com.auth0:java-jwt:4.4.0'
133136
implementation 'com.auth0:jwks-rsa:0.22.1'
134137
implementation 'org.json:json:20231013'
135138
implementation 'org.bouncycastle:bcprov-jdk18on:1.79'
@@ -154,4 +157,7 @@ dependencies {
154157
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
155158

156159
testImplementation 'org.springframework.boot:spring-boot-starter-test'
160+
161+
// MockWebServer
162+
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
157163
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package com.tnt.application.auth;
22

3+
import static com.tnt.global.error.model.ErrorMessage.*;
34
import static io.micrometer.common.util.StringUtils.*;
4-
import static java.util.Objects.*;
55

6-
import java.time.LocalDateTime;
76
import java.util.concurrent.TimeUnit;
87

9-
import org.springframework.data.redis.core.RedisTemplate;
8+
import org.springframework.data.redis.core.StringRedisTemplate;
109
import org.springframework.stereotype.Service;
1110

12-
import com.tnt.domain.auth.SessionValue;
1311
import com.tnt.global.error.exception.UnauthorizedException;
1412

1513
import jakarta.servlet.http.HttpServletRequest;
@@ -24,40 +22,49 @@ public class SessionService {
2422
static final long SESSION_DURATION = 2L * 24 * 60 * 60; // 48시간
2523
private static final String AUTHORIZATION_HEADER = "Authorization";
2624
private static final String SESSION_ID_PREFIX = "SESSION-ID ";
27-
private final RedisTemplate<String, SessionValue> redisTemplate;
25+
26+
private final StringRedisTemplate redisTemplate;
2827

2928
public String authenticate(HttpServletRequest request) {
3029
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
3130

3231
if (isBlank(authHeader) || !authHeader.startsWith(SESSION_ID_PREFIX)) {
33-
log.error("Authorization 헤더가 존재하지 않거나 올바르지 않은 형식입니다.");
32+
log.error(AUTHORIZATION_HEADER_ERROR.getMessage());
3433

35-
throw new UnauthorizedException("인가 세션이 존재하지 않습니다.");
34+
throw new UnauthorizedException(AUTHORIZATION_HEADER_ERROR);
3635
}
3736

3837
String sessionId = authHeader.substring(SESSION_ID_PREFIX.length());
38+
String sessionValue = redisTemplate.opsForValue().get(sessionId);
39+
40+
if (sessionValue == null) {
41+
log.error(NO_EXIST_SESSION_IN_STORAGE.getMessage());
3942

40-
requireNonNull(redisTemplate.opsForValue().get(sessionId), "세션 스토리지에 세션이 존재하지 않습니다.");
43+
throw new UnauthorizedException(NO_EXIST_SESSION_IN_STORAGE);
44+
}
4145

42-
return sessionId;
46+
createOrUpdateSession(sessionId, "");
47+
48+
return sessionValue;
4349
}
4450

45-
public void createSession(String memberId, HttpServletRequest request) {
46-
SessionValue sessionValue = SessionValue.builder()
47-
.lastAccessTime(LocalDateTime.now())
48-
.userAgent(request.getHeader("User-Agent"))
49-
.clientIp(request.getRemoteAddr())
50-
.build();
51-
52-
redisTemplate.opsForValue().set(
53-
memberId,
54-
sessionValue,
55-
SESSION_DURATION,
56-
TimeUnit.SECONDS
57-
);
51+
public void createOrUpdateSession(String sessionId, String memberId) {
52+
if (isBlank(memberId)) { // 세션 갱신
53+
redisTemplate.expire(sessionId, SESSION_DURATION, TimeUnit.SECONDS);
54+
redisTemplate.expire(memberId, SESSION_DURATION, TimeUnit.SECONDS);
55+
} else { // 로그인 시 기존 로그인 상태 제거하고 새로운 세션 생성
56+
String existingSessionId = redisTemplate.opsForValue().get(memberId);
57+
58+
if (existingSessionId != null) {
59+
removeSession(sessionId);
60+
removeSession(memberId);
61+
}
62+
redisTemplate.opsForValue().set(sessionId, memberId, SESSION_DURATION, TimeUnit.SECONDS);
63+
redisTemplate.opsForValue().set(memberId, sessionId, SESSION_DURATION, TimeUnit.SECONDS);
64+
}
5865
}
5966

60-
public void removeSession(String sessionId) {
61-
redisTemplate.delete(sessionId);
67+
public void removeSession(String dataKey) {
68+
redisTemplate.delete(dataKey);
6269
}
6370
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.tnt.application.member;
2+
3+
import static com.tnt.global.error.model.ErrorMessage.*;
4+
5+
import java.security.KeyFactory;
6+
import java.security.interfaces.ECPrivateKey;
7+
import java.security.interfaces.ECPublicKey;
8+
import java.security.spec.PKCS8EncodedKeySpec;
9+
import java.util.Base64;
10+
11+
import org.springframework.stereotype.Component;
12+
13+
import com.auth0.jwt.interfaces.ECDSAKeyProvider;
14+
import com.tnt.global.error.exception.OAuthException;
15+
16+
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
18+
19+
@Slf4j
20+
@Component
21+
@RequiredArgsConstructor
22+
public class AppleEcdsaKeyProvider implements ECDSAKeyProvider {
23+
24+
private String privateKey;
25+
private String keyId;
26+
27+
public AppleEcdsaKeyProvider(String privateKey, String keyId) {
28+
this.privateKey = privateKey;
29+
this.keyId = keyId;
30+
}
31+
32+
@Override
33+
public ECPublicKey getPublicKeyById(String keyId) {
34+
return null; // 클라이언트 시크릿 생성에는 불필요
35+
}
36+
37+
@Override
38+
public ECPrivateKey getPrivateKey() {
39+
try {
40+
byte[] pkcs8EncodedBytes = Base64.getDecoder().decode(privateKey);
41+
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
42+
KeyFactory kf = KeyFactory.getInstance("EC");
43+
return (ECPrivateKey)kf.generatePrivate(keySpec);
44+
} catch (Exception e) {
45+
log.error(FAILED_TO_FETCH_PRIVATE_KEY.getMessage(), e);
46+
47+
throw new OAuthException(FAILED_TO_FETCH_PRIVATE_KEY);
48+
}
49+
}
50+
51+
@Override
52+
public String getPrivateKeyId() {
53+
return keyId;
54+
}
55+
}

0 commit comments

Comments
 (0)