Skip to content

Commit 03ea3ab

Browse files
authored
feat: Spring Security / OAuth2 Cli�ent, Server 구현 (#41)
* feat(application.yml): redis, security(oauth), frontend, logging level 추가 - redis host, port, password 정보 추가 - spring security, oauth(NAVER) 관련 정보 추가 - frontend root url 추가 (임시 - localhost) - security logging level DEBUG로 설정 * feat(RedisConfig): Redis Template, ConnectionFactory 추가 - Redis 연결을 위한 LettuceConnectionFactory 설정 - RedisTemplate 설정 및 key/value 직렬화를 위한 StringRedisSerializer 적용 - RedisStandaloneConfiguration을 사용하여 host, port, password 설정 * feat(Tier): 봉사자 등급 Enum 추가 - RED부터 RAINBOW까지 8개의 등급 정의 * feat(UserRole): 사용자 역할 Enum 추가 - VOLUNTEER, CENTER, ADMIN 3가지 사용자 역할 정의 - 각 역할은 시스템에서의 권한 구분을 위해 사용 * feat(OAuthProvider): OAuth 제공자 Enum 추가 - NAVER 제공자를 위한 Enum 항목 추가 - providerName 필드를 통해 OAuth 제공자(registerId)의 이름 관리 - 문자열을 기반으로 OAuthProvider를 찾는 from 메서드 구현 * feat(main): @EnableJpaAuditing 추가 - BaseEntity 사용을 위해 @EnableJpaAuditing 어노테이션 추가 * feat(Tier): 불필요한 rank 필드 삭제 * feat(Gender): 성별 ENUM 추가 - 남, 여 - 이외의 값은 미지정 * feat(dto): Volunteer 등록을 위한 VolunteerRegisterRequestDto 추가 * feat(VolunteerDetail): VolunteerDetail 엔티티 추가 - 필드: - volunteerId: UUID (BINARY(16)) - gender: EnumType.STRING - birthDate: 문자열 (YYYY-MM-DD 형식) - contactNumber: 문자열 - 정적 팩토리 메서드를 통해 DTO로부터 VolunteerDetail 생성 가능 * feat(Volunteer): Volunteer 엔티티 추가 - BaseEntity 상속 - 필드: - id: UUID (BINARY(16), 고유 식별자) - oauthProvider: OAuth 인증 제공자 (EnumType.STRING) - oauthId: OAuth 제공자 ID - tier: 자원봉사 티어 (EnumType.STRING) - 정적 메서드를 통해 초기 값이 설정된 Volunteer 객체 생성 가능 * feat(VolunteerRepository): VolunteerRepository 인터페이스 추가 - findByOauthId: OAuth ID를 기준으로 Volunteer 조회 * feat(VolunteerDetailRepository): VolunteerDetailRepository 인터페이스 추가 * feat(cookie): 쿠키 설정을 위한 SetCookieUseCase 및 SetCookieService 구현 - SetCookieService 클래스 구현: - SetCookieUseCase를 구현한 쿠키 설정 서비스 - setToken 메서드를 통해 토큰 타입(TokenType)에 따라 쿠키 생성 및 응답 헤더에 추가 - generateCookie 메서드로 HttpOnly, Secure, SameSite 등 안전한 쿠키 생성 로직 포함 - ResponseCookie를 활용 * feat(volunteer): 자원봉사자 등록 UseCase 및 Service 구현 - RegisterVolunteerUseCase 구현체로 자원봉사자(유저) 등록 로직 처리 - Volunteer 엔티티 생성 및 VolunteerRepository에 저장 - VolunteerDetail 엔티티 생성 및 VolunteerDetailRepository에 저장 - createDefault 및 VolunteerDetail.of 메서드를 활용한 객체 생성 * feat(dto): NaverUserProfileResponseDto 구현 - 네이버 사용자 프로필 정보를 처리하기 위한 DTO 클래스 추가 - 필드: - resultcode: 결과 코드 - message: 결과 메시지 - response: 응답 데이터 (중첩 record) - id: 네이버 사용자 일련 번호 - name: 이름 - email: 이메일 - gender: 성별 (F, M, U) - birthday: 생일 (MM-DD) - birthyear: 출생 연도 - mobile: 휴대 전화 번호 - JSON 직렬화를 위한 @JsonNaming - mobile-164 pattern 무시를 위한 @JsonIgnoreProperties - toVolunteerRegisterRequestDto 메서드 추가: - Naver 사용자 데이터를 VolunteerRegisterRequestDto로 변환 - OAuthProvider를 NAVER로 설정하여 Volunteer 등록 로직과 연동 * feat(NaverUser): NaverUser 엔티티 추가 - 네이버 사용자 정보를 저장하기 위한 NaverUser 엔티티 구현 - 정적 팩토리 메서드: OAuth ID를 기반으로 NaverUser 객체를 생성 * feat(NaverUserRepository): NaverUserRepository 인터페이스 추가 * feat(converter): OAuth2User를 NaverUserProfileResponseDto로 변환하는 유틸리티 추가 - OAuth2User 객체의 attributes를 NaverUserProfileResponseDto로 변환 - ObjectMapper를 사용하여 JSON 데이터를 DTO로 매핑 * feat(redirect): 리다이렉트 처리를 위한 config 및 UsaCase, Service 추가 - RedirectConfig: - RedirectStrategy Bean: - DefaultRedirectStrategy를 사용하여 리다이렉트 처리 - RedirectService: - RedirectStrategy를 사용하여 주어진 URL로 클라이언트를 리다이렉트 * feat(CheckNaverUser): 네이버 사용자 존재 여부 확인을 위한 UseCase 및 Service 구현 * feat(ProcessOAuthUser): 네이버 OAuth 사용자 처리 UseCase 및 Service 구현 * feat(RegisterNaverUser): 네이버 OAuth 사용자 등록 UsaCase, Service 추가 * feat(auth): OAuth 실패 처리 핸들러(CustomOAuthFailureHandler) 구현 - SimpleUrlAuthenticationFailureHandler를 상속하여 OAuth 인증 실패 처리, 로그 기록 - 프론트엔드와의 협의 후 추가 처리 로직 구현 예정 (TODO 추가) * feat(NaverOAuth): 네이버 OAuth2 사용자 처리 서비스 추가 - 사용자 정보 처리 로직: - OAuth2User를 NaverUserProfileResponseDto로 변환 - 사용자 존재 여부 확인 (CheckNaverUserUseCase) - 신규 사용자일 경우: - 네이버 사용자 등록 (RegisterNaverUserUseCase) - 자원봉사자 등록 (RegisterVolunteerUseCase) - 기존 사용자일 경우 OAuth2User 반환 * feat(OAuthUser): CustomOAuth2UserService 구현 및 OAuth 사용자 처리 로직 개선 - DefaultOAuth2UserService를 활용하여 OAuth2 사용자 로드 - OAuthProvider에 따라 네이버 사용자 처리 로직 분기: - NaverOAuth2UserInfoService와 연동 - 추후에 추가 가능 * feat(FindVolunteer): OAuth ID로 Volunteer ID 조회 서비스 구현 * feat(OAuthSuccessHandler): OAuth 인증 성공 처리 핸들러 구현 - onAuthenticationSuccess 구현: - OAuthProvider에 따라 사용자 정보를 처리 (현재는 NAVER만 지원) - getOAuthProvider: 인증된 OAuth 제공자 추출 - 지원하지 않는 OAuth 제공자에 대한 예외 처리 추가 - ProcessNaverOAuthUserService를 통해 네이버 사용자 정보 처리 - FindVolunteerIdUseCase로 OAuth ID 기반 Volunteer ID 조회 - GenerateTokensOnLoginUseCase로 RefreshToken 저장 및 AccessToken 생성 - SetCookieUseCase를 사용하여 AccessToken을 클라이언트 쿠키에 저장 - RedirectUseCase로 프론트엔드 URL로 리다이렉션 수행 * chore: Volunteer 엔티티 삭제 (다른 패키지에 있음) * feat(config): Spring Security 설정 추가 및 OAuth2 인증 처리 구성 - Stateless 세션 관리 설정(SessionCreationPolicy.STATELESS) - CSRF, HTTP Basic, Form Login, Logout 기능 비활성화 - SecurityFilterChain 설정: - 공개 API 경로 설정 - 그 외 모든 요청 인증 필요 - OAuth2 인증 처리: - CustomOAuth2UserService로 사용자 정보 처리 - CustomOAuthSuccessHandler로 인증 성공 처리 - CustomOAuthFailureHandler로 인증 실패 처리 - TODO: - JWT 인증 필터 추가 - JWT 예외 필터 추가 * style: unused imports 제거, formatting 수정 * refactor(VolunteerDetail): 불필요한 length 설정 제거 * feat(config): 테스트 환경을 위한 Redis, 프론트엔드, JWT 설정 추가 - 테스트 환경에서 로컬 Redis(호스트: localhost, 포트: 6379) 설정 추가 - 테스트용 프론트엔드 URL(http://localhost:3000) 설정 추가 - JWT 토큰 생성을 위한 별도의 시크릿 키 설정 추가 * feat(Gender): enum 값들을 대문자로 변경 * fix(IntegrationTestSupport): @JpaAuditing 설정을 prod 환경으로 이동 - 테스트 환경과 프로덕션 환경에서 중복된 @JpaAuditing 설정으로 발생한 문제 해결 - @JpaAuditing 활성화를 프로덕션 환경으로 제한하여 충돌 방지 * feat(VolunteerDetail): VolunteerId로 Detail 조회 기능 추가 * style(개행): git 잠재적인 에러 예방 * test(FindVolunteerIdService): 봉사자 ID 조회 서비스 테스트 추가 - 존재하는 OAuth ID로 봉사자 ID를 조회하는 성공 케이스 테스트 - 존재하지 않는 OAuth ID로 조회 시 예외를 던지는 실패 케이스 테스트 * test(RegisterVolunteerService): 봉사자와 상세 정보 저장 서비스 테스트 추가 - 봉사자 등록 시 기본값과 입력값이 올바르게 저장되는지 검증 - Volunteer 엔티티: OAuthProvider, oauthId, nickname 등 기본값 검증 - VolunteerDetail 엔티티: name, email, gender 등 입력값 매핑 검증 * style(개행): git 잠재적인 에러 예방 * feat(OAuthResponseConverter): 유틸 클래스 기본 생성자 private 설정 * fix(VolunteerRepository): 대소문자 오타 수정 * style(개행): git 잠재적인 에러 예방 * feat(SecurityConfig): 모든 요청 permitAll - 빠른 개발을 위해서 모든 요청 허가 * fix(Test): 중복된 테스트 설정 정리 * refactor(OAuthResponseConverter): private 기본 생성자를 어노테이션으로 대체 * feat(Voluntter): updatable = false 삭제 * refactor(ProcessNaverOAuthUserService): @component -> @service ---------
1 parent d801f73 commit 03ea3ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1079
-49
lines changed

src/main/java/com/somemore/SomemoreApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
56

67
@SpringBootApplication
8+
@EnableJpaAuditing
79
public class SomemoreApplication {
810

911
public static void main(String[] args) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.somemore.auth;
2+
3+
public enum UserRole {
4+
VOLUNTEER,
5+
CENTER,
6+
ADMIN
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.somemore.auth.cookie;
2+
3+
import com.somemore.auth.jwt.domain.TokenType;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.http.ResponseCookie;
8+
import org.springframework.stereotype.Service;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
@Slf4j
13+
public class SetCookieService implements SetCookieUseCase {
14+
15+
@Override
16+
public void setToken(HttpServletResponse response, String value, TokenType tokenType) {
17+
ResponseCookie cookie = generateCookie(tokenType.name(), value, tokenType.getPeriodInSeconds());
18+
response.addHeader("Set-Cookie", cookie.toString());
19+
}
20+
21+
private static ResponseCookie generateCookie(String name, String value, int time) {
22+
return ResponseCookie.from(name, value)
23+
.httpOnly(true)
24+
.secure(true)
25+
.path("/")
26+
.maxAge(time)
27+
.sameSite("Lax")
28+
.build();
29+
}
30+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.somemore.auth.cookie;
2+
3+
import com.somemore.auth.jwt.domain.TokenType;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
6+
public interface SetCookieUseCase {
7+
void setToken(HttpServletResponse response, String value, TokenType tokenType);
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.auth.oauth;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum OAuthProvider {
7+
NAVER("naver");
8+
9+
private final String providerName;
10+
11+
OAuthProvider(String providerName) {
12+
this.providerName = providerName;
13+
}
14+
15+
public static OAuthProvider from(String providerName) {
16+
for (OAuthProvider provider : values()) {
17+
if (provider.providerName.equals(providerName)) {
18+
return provider;
19+
}
20+
}
21+
22+
throw new IllegalArgumentException("올바르지 않은 OAuth 제공자: " + providerName);
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.somemore.auth.oauth.handler.failure;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.security.core.AuthenticationException;
8+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
@Slf4j
14+
public class CustomOAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
15+
16+
@Override
17+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
18+
// TODO 프론트엔드와 협의
19+
log.error("안녕 난 말하는 감자야");
20+
}
21+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.somemore.auth.oauth.handler.success;
2+
3+
import com.somemore.auth.cookie.SetCookieUseCase;
4+
import com.somemore.auth.jwt.domain.EncodedToken;
5+
import com.somemore.auth.jwt.domain.TokenType;
6+
import com.somemore.auth.jwt.usecase.command.GenerateTokensOnLoginUseCase;
7+
import com.somemore.auth.oauth.OAuthProvider;
8+
import com.somemore.auth.oauth.naver.service.query.ProcessNaverOAuthUserService;
9+
import com.somemore.auth.redirect.RedirectUseCase;
10+
import com.somemore.volunteer.usecase.query.FindVolunteerIdUseCase;
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import jakarta.servlet.http.HttpServletResponse;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.security.core.Authentication;
17+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
18+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
19+
import org.springframework.stereotype.Component;
20+
21+
import java.io.IOException;
22+
import java.util.UUID;
23+
24+
@Component
25+
@RequiredArgsConstructor
26+
@Slf4j
27+
public class CustomOAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
28+
29+
private final ProcessNaverOAuthUserService processNaverOAuthService;
30+
private final FindVolunteerIdUseCase findVolunteerIdUseCase;
31+
private final GenerateTokensOnLoginUseCase generateTokensOnLoginUseCase;
32+
private final SetCookieUseCase setCookieUseCase;
33+
private final RedirectUseCase redirectUseCase;
34+
35+
@Value("${frontend.url}")
36+
private String frontendRootUrl;
37+
38+
@Override
39+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
40+
String oAuthId;
41+
switch (getOAuthProvider(authentication)) {
42+
case NAVER -> oAuthId = processNaverOAuthService.processOAuthUser(authentication);
43+
default -> {
44+
log.error("지원하지 않는 OAuth 제공자입니다.");
45+
throw new IllegalArgumentException();
46+
}
47+
}
48+
49+
UUID volunteerId = findVolunteerIdUseCase.findVolunteerIdByOAuthId(oAuthId);
50+
EncodedToken accessToken = generateTokensOnLoginUseCase.saveRefreshTokenAndReturnAccessToken(volunteerId);
51+
52+
setCookieUseCase.setToken(response, accessToken.value(), TokenType.ACCESS);
53+
redirectUseCase.redirect(request, response, frontendRootUrl);
54+
}
55+
56+
private static OAuthProvider getOAuthProvider(Authentication authentication) {
57+
if (authentication instanceof OAuth2AuthenticationToken token) {
58+
return OAuthProvider.from(token.getAuthorizedClientRegistrationId());
59+
}
60+
throw new IllegalArgumentException();
61+
}
62+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.somemore.auth.oauth.naver.domain;
2+
3+
import jakarta.persistence.Entity;
4+
import jakarta.persistence.Id;
5+
import jakarta.persistence.Table;
6+
import lombok.*;
7+
8+
@Getter
9+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
10+
@Entity
11+
@Table(name = "naver_user")
12+
public class NaverUser {
13+
@Id
14+
private String oauthId;
15+
16+
private NaverUser(String oauthId) {
17+
this.oauthId = oauthId;
18+
}
19+
20+
public static NaverUser from(String oauthId) {
21+
return new NaverUser(oauthId);
22+
}
23+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.somemore.auth.oauth.naver.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
5+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
6+
import com.somemore.auth.oauth.OAuthProvider;
7+
import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto;
8+
9+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
10+
public record NaverUserProfileResponseDto(
11+
String resultcode, // 결과 코드
12+
String message, // 결과 메시지
13+
Response response // 응답 데이터
14+
) {
15+
@JsonIgnoreProperties(ignoreUnknown = true)
16+
public record Response(
17+
String id, // 일련 번호
18+
String name, // 이름
19+
String email, // 이메일
20+
String gender, // 성별 (F, M, U)
21+
String birthday, // 생일 (MM-DD)
22+
String birthyear, // 출생 연도
23+
String mobile // 휴대 전화 번호
24+
) {}
25+
26+
public VolunteerRegisterRequestDto toVolunteerRegisterRequestDto() {
27+
return new VolunteerRegisterRequestDto(
28+
OAuthProvider.NAVER,
29+
this.response.id(),
30+
this.response.name(),
31+
this.response.email(),
32+
this.response.gender(),
33+
this.response.birthday(),
34+
this.response.birthyear(),
35+
this.response.mobile()
36+
);
37+
}
38+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.somemore.auth.oauth.naver.repository;
2+
3+
import com.somemore.auth.oauth.naver.domain.NaverUser;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
@Repository
8+
public interface NaverUserRepository extends JpaRepository<NaverUser, String> {
9+
}

0 commit comments

Comments
 (0)