Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a31f904
feat(application.yml): redis, security(oauth), frontend, logging leve…
m-a-king Nov 22, 2024
cca332c
feat(RedisConfig): Redis Template, ConnectionFactory 추가
m-a-king Nov 22, 2024
c0479f8
feat(Tier): 봉사자 등급 Enum 추가
m-a-king Nov 22, 2024
b77022c
feat(UserRole): 사용자 역할 Enum 추가
m-a-king Nov 22, 2024
82199e8
feat(OAuthProvider): OAuth 제공자 Enum 추가
m-a-king Nov 22, 2024
a3ef142
feat(main): @EnableJpaAuditing 추가
m-a-king Nov 22, 2024
d4ee58c
feat(Tier): 불필요한 rank 필드 삭제
m-a-king Nov 23, 2024
0fd7a67
feat(Gender): 성별 ENUM 추가
m-a-king Nov 23, 2024
8416a64
feat(dto): Volunteer 등록을 위한 VolunteerRegisterRequestDto 추가
m-a-king Nov 23, 2024
5fa7a74
feat(VolunteerDetail): VolunteerDetail 엔티티 추가
m-a-king Nov 23, 2024
945f6f9
feat(Volunteer): Volunteer 엔티티 추가
m-a-king Nov 23, 2024
c3d65d8
feat(VolunteerRepository): VolunteerRepository 인터페이스 추가
m-a-king Nov 23, 2024
a37bf73
feat(VolunteerDetailRepository): VolunteerDetailRepository 인터페이스 추가
m-a-king Nov 23, 2024
345e610
feat(cookie): 쿠키 설정을 위한 SetCookieUseCase 및 SetCookieService 구현
m-a-king Nov 23, 2024
7d26962
feat(volunteer): 자원봉사자 등록 UseCase 및 Service 구현
m-a-king Nov 23, 2024
f594fe7
feat(dto): NaverUserProfileResponseDto 구현
m-a-king Nov 23, 2024
565e894
feat(NaverUser): NaverUser 엔티티 추가
m-a-king Nov 23, 2024
b9d7d6d
feat(NaverUserRepository): NaverUserRepository 인터페이스 추가
m-a-king Nov 23, 2024
bf7ea95
feat(converter): OAuth2User를 NaverUserProfileResponseDto로 변환하는 유틸리티 추가
m-a-king Nov 23, 2024
f621689
feat(redirect): 리다이렉트 처리를 위한 config 및 UsaCase, Service 추가
m-a-king Nov 23, 2024
ed054e0
feat(CheckNaverUser): 네이버 사용자 존재 여부 확인을 위한 UseCase 및 Service 구현
m-a-king Nov 23, 2024
afa6d2c
feat(ProcessOAuthUser): 네이버 OAuth 사용자 처리 UseCase 및 Service 구현
m-a-king Nov 23, 2024
70a4833
feat(RegisterNaverUser): 네이버 OAuth 사용자 등록 UsaCase, Service 추가
m-a-king Nov 23, 2024
29a4695
feat(auth): OAuth 실패 처리 핸들러(CustomOAuthFailureHandler) 구현
m-a-king Nov 23, 2024
bec0572
feat(NaverOAuth): 네이버 OAuth2 사용자 처리 서비스 추가
m-a-king Nov 23, 2024
af51871
feat(OAuthUser): CustomOAuth2UserService 구현 및 OAuth 사용자 처리 로직 개선
m-a-king Nov 23, 2024
8e4da9b
feat(FindVolunteer): OAuth ID로 Volunteer ID 조회 서비스 구현
m-a-king Nov 23, 2024
ace54e1
feat(OAuthSuccessHandler): OAuth 인증 성공 처리 핸들러 구현
m-a-king Nov 23, 2024
8d05dc3
chore: Volunteer 엔티티 삭제 (다른 패키지에 있음)
m-a-king Nov 23, 2024
2e53b62
feat(config): Spring Security 설정 추가 및 OAuth2 인증 처리 구성
m-a-king Nov 23, 2024
d66389a
style: unused imports 제거, formatting 수정
m-a-king Nov 23, 2024
85b2675
refactor(VolunteerDetail): 불필요한 length 설정 제거
m-a-king Nov 24, 2024
0c6e7f0
feat(config): 테스트 환경을 위한 Redis, 프론트엔드, JWT 설정 추가
m-a-king Nov 24, 2024
35789a5
feat(Gender): enum 값들을 대문자로 변경
m-a-king Nov 24, 2024
189c7c9
fix(IntegrationTestSupport): @JpaAuditing 설정을 prod 환경으로 이동
m-a-king Nov 24, 2024
1b03215
feat(VolunteerDetail): VolunteerId로 Detail 조회 기능 추가
m-a-king Nov 24, 2024
41921e0
style(개행): git 잠재적인 에러 예방
m-a-king Nov 24, 2024
d0d31e2
test(FindVolunteerIdService): 봉사자 ID 조회 서비스 테스트 추가
m-a-king Nov 24, 2024
b3fa19f
test(RegisterVolunteerService): 봉사자와 상세 정보 저장 서비스 테스트 추가
m-a-king Nov 24, 2024
6e9aecb
style(개행): git 잠재적인 에러 예방
m-a-king Nov 24, 2024
f38391e
feat(OAuthResponseConverter): 유틸 클래스 기본 생성자 private 설정
m-a-king Nov 25, 2024
d1a5daf
fix(VolunteerRepository): 대소문자 오타 수정
m-a-king Nov 25, 2024
5b223a1
style(개행): git 잠재적인 에러 예방
m-a-king Nov 25, 2024
67575e2
feat(SecurityConfig): 모든 요청 permitAll
m-a-king Nov 25, 2024
455fe76
fix(Test): 중복된 테스트 설정 정리
m-a-king Nov 25, 2024
8d0fd8d
refactor(OAuthResponseConverter): private 기본 생성자를 어노테이션으로 대체
m-a-king Nov 25, 2024
f6ab84f
feat(Voluntter): updatable = false 삭제
m-a-king Nov 25, 2024
0338697
Feature/9 기관 프로필 조회 기능 구현 (#32)
7zrv Nov 25, 2024
ffcafa1
refactor(ProcessNaverOAuthUserService): @Component -> @Service
m-a-king Nov 25, 2024
3291aa7
Merge branch 'main' into feature/40-auth-spring-security-oauth
m-a-king Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/com/somemore/SomemoreApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class SomemoreApplication {

public static void main(String[] args) {
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/somemore/auth/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.somemore.auth;

public enum UserRole {
VOLUNTEER,
CENTER,
ADMIN
}
30 changes: 30 additions & 0 deletions src/main/java/com/somemore/auth/cookie/SetCookieService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.somemore.auth.cookie;

import com.somemore.auth.jwt.domain.TokenType;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class SetCookieService implements SetCookieUseCase {

@Override
public void setToken(HttpServletResponse response, String value, TokenType tokenType) {
ResponseCookie cookie = generateCookie(tokenType.name(), value, tokenType.getPeriodInSeconds());
response.addHeader("Set-Cookie", cookie.toString());
}

private static ResponseCookie generateCookie(String name, String value, int time) {
return ResponseCookie.from(name, value)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(time)
.sameSite("Lax")
.build();
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/somemore/auth/cookie/SetCookieUseCase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.somemore.auth.cookie;

import com.somemore.auth.jwt.domain.TokenType;
import jakarta.servlet.http.HttpServletResponse;

public interface SetCookieUseCase {
void setToken(HttpServletResponse response, String value, TokenType tokenType);
}
24 changes: 24 additions & 0 deletions src/main/java/com/somemore/auth/oauth/OAuthProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.somemore.auth.oauth;

import lombok.Getter;

@Getter
public enum OAuthProvider {
NAVER("naver");

private final String providerName;

OAuthProvider(String providerName) {
this.providerName = providerName;
}

public static OAuthProvider from(String providerName) {
for (OAuthProvider provider : values()) {
if (provider.providerName.equals(providerName)) {
return provider;
}
}

throw new IllegalArgumentException("올바르지 않은 OAuth 제공자: " + providerName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.somemore.auth.oauth.handler.failure;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class CustomOAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
// TODO 프론트엔드와 협의
log.error("안녕 난 말하는 감자야");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.somemore.auth.oauth.handler.success;

import com.somemore.auth.cookie.SetCookieUseCase;
import com.somemore.auth.jwt.domain.EncodedToken;
import com.somemore.auth.jwt.domain.TokenType;
import com.somemore.auth.jwt.usecase.command.GenerateTokensOnLoginUseCase;
import com.somemore.auth.oauth.OAuthProvider;
import com.somemore.auth.oauth.naver.service.query.ProcessNaverOAuthUserService;
import com.somemore.auth.redirect.RedirectUseCase;
import com.somemore.volunteer.usecase.query.FindVolunteerIdUseCase;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

@Component
@RequiredArgsConstructor
@Slf4j
public class CustomOAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final ProcessNaverOAuthUserService processNaverOAuthService;
private final FindVolunteerIdUseCase findVolunteerIdUseCase;
private final GenerateTokensOnLoginUseCase generateTokensOnLoginUseCase;
private final SetCookieUseCase setCookieUseCase;
private final RedirectUseCase redirectUseCase;

@Value("${frontend.url}")
private String frontendRootUrl;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
String oAuthId;
switch (getOAuthProvider(authentication)) {
case NAVER -> oAuthId = processNaverOAuthService.processOAuthUser(authentication);
default -> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 자바 21 문법인가요?
Good 입니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다. 자바 17 문법입니다 ㅎㅎ

log.error("지원하지 않는 OAuth 제공자입니다.");
throw new IllegalArgumentException();
}
}

UUID volunteerId = findVolunteerIdUseCase.findVolunteerIdByOAuthId(oAuthId);
EncodedToken accessToken = generateTokensOnLoginUseCase.saveRefreshTokenAndReturnAccessToken(volunteerId);

setCookieUseCase.setToken(response, accessToken.value(), TokenType.ACCESS);
redirectUseCase.redirect(request, response, frontendRootUrl);
}

private static OAuthProvider getOAuthProvider(Authentication authentication) {
if (authentication instanceof OAuth2AuthenticationToken token) {
return OAuthProvider.from(token.getAuthorizedClientRegistrationId());
}
throw new IllegalArgumentException();
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/somemore/auth/oauth/naver/domain/NaverUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.somemore.auth.oauth.naver.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "naver_user")
public class NaverUser {
@Id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naver_user 테이블 생기는건가요??
ERD 상에 없길래 여쭤봅니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 erdcloud에 그려두겠습니다

private String oauthId;

private NaverUser(String oauthId) {
this.oauthId = oauthId;
}

public static NaverUser from(String oauthId) {
return new NaverUser(oauthId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.somemore.auth.oauth.naver.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.auth.oauth.OAuthProvider;
import com.somemore.volunteer.dto.request.VolunteerRegisterRequestDto;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record NaverUserProfileResponseDto(
String resultcode, // 결과 코드
String message, // 결과 메시지
Response response // 응답 데이터
) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Response(
String id, // 일련 번호
String name, // 이름
String email, // 이메일
String gender, // 성별 (F, M, U)
String birthday, // 생일 (MM-DD)
String birthyear, // 출생 연도
String mobile // 휴대 전화 번호
) {}
Comment on lines +11 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 일부러 넣어두신 건가요?
저희 주석 최대한 안 남기기로 했던 거 같아서요

Copy link
Collaborator Author

@m-a-king m-a-king Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 외부 시스템과 연결된 DTO라서 일부러 표기해뒀습니다.


public VolunteerRegisterRequestDto toVolunteerRegisterRequestDto() {
return new VolunteerRegisterRequestDto(
OAuthProvider.NAVER,
this.response.id(),
this.response.name(),
this.response.email(),
this.response.gender(),
this.response.birthday(),
this.response.birthyear(),
this.response.mobile()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.somemore.auth.oauth.naver.repository;

import com.somemore.auth.oauth.naver.domain.NaverUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NaverUserRepository extends JpaRepository<NaverUser, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.somemore.auth.oauth.naver.service.command;

import com.somemore.auth.oauth.naver.dto.response.NaverUserProfileResponseDto;
import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase;
import com.somemore.auth.oauth.naver.usecase.command.RegisterNaverUserUseCase;
import com.somemore.auth.oauth.naver.util.OAuthResponseConverter;
import com.somemore.volunteer.usecase.command.RegisterVolunteerUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
@Transactional
public class NaverOAuth2UserInfoService {

private final CheckNaverUserUseCase checkNaverUserUseCase;
private final RegisterNaverUserUseCase registerNaverUserUseCase;
private final RegisterVolunteerUseCase registerVolunteerUseCase;

public OAuth2User processOAuth2User(OAuth2User oAuth2User) {
NaverUserProfileResponseDto dto = OAuthResponseConverter.convertToNaverUserProfileResponseDto(oAuth2User);
String naverOauthId = dto.response().id();

if (isNewUser(naverOauthId)) {
registerUser(dto);
}

return oAuth2User;
}

private boolean isNewUser(String id) {
return !checkNaverUserUseCase.isNaverUserExists(id);
}

private void registerUser(NaverUserProfileResponseDto dto) {
registerNaverUserUseCase.registerNaverUser(dto.response().id());
registerVolunteerUseCase.registerVolunteer(dto.toVolunteerRegisterRequestDto());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.somemore.auth.oauth.naver.service.command;

import com.somemore.auth.oauth.naver.domain.NaverUser;
import com.somemore.auth.oauth.naver.repository.NaverUserRepository;
import com.somemore.auth.oauth.naver.usecase.command.RegisterNaverUserUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제가 되는 부분은 아니지만 NaverOAuth2UserInfoService랑 어노테이션 순서가 달라보여서 댓글 남겨놓겠습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 맞추려고 노력했는데, 자꾸 섞이네요. 라이브 템플릿으로 관리해보겠습니다!

@Transactional
public class RegisterNaverUserService implements RegisterNaverUserUseCase {

private final NaverUserRepository naverUserRepository;

@Override
public void registerNaverUser(String oAuthId) {
NaverUser naverUser = NaverUser.from(oAuthId);

naverUserRepository.save(naverUser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.somemore.auth.oauth.naver.service.query;

import com.somemore.auth.oauth.naver.repository.NaverUserRepository;
import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CheckNaverUserService implements CheckNaverUserUseCase {

private final NaverUserRepository naverUserRepository;

@Override
public boolean isNaverUserExists(String id) {
return naverUserRepository.existsById(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.somemore.auth.oauth.naver.service.query;

import com.somemore.auth.oauth.OAuthProvider;
import com.somemore.auth.oauth.naver.usecase.query.CheckNaverUserUseCase;
import com.somemore.auth.oauth.usecase.ProcessOAuthUserUseCase;
import com.somemore.auth.oauth.naver.dto.response.NaverUserProfileResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import static com.somemore.auth.oauth.naver.util.OAuthResponseConverter.convertToNaverUserProfileResponseDto;

@Slf4j
@Component
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@service가 아니라 @component로 두신 이유가 있으신가요?
기능적으로 차이가 없는건 알고있지만 궁금해서 여쭤봤습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스는 서비스로 두는 것이 맞았는데, 잘못 작성했습니다. 다른 클래스들에서는 일부러 구분한 것은 맞습니다.
명시적으로 서비스와 그 서비스를 보조하는 작은 컴포넌트로 구분하고 싶었습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이해했습니다 감사합니다!

@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProcessNaverOAuthUserService implements ProcessOAuthUserUseCase {

private final CheckNaverUserUseCase checkNaverUserUseCase;

@Override
public String processOAuthUser(Authentication authentication) {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
return processUserInformation(oAuth2User);
}

private String processUserInformation(OAuth2User oAuth2User) {
NaverUserProfileResponseDto dto = convertToNaverUserProfileResponseDto(oAuth2User);
String oAuthId = dto.response().id();

if (checkNaverUserUseCase.isNaverUserExists(oAuthId)) {
return oAuthId;
}

log.error("유저가 회원 가입을 진행했으나, 존재하지 않는 상태입니다. OAuth Provider: {}, OAuth ID: {}",
OAuthProvider.NAVER,
oAuthId);
throw new IllegalStateException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.somemore.auth.oauth.naver.usecase.command;

public interface RegisterNaverUserUseCase {
void registerNaverUser(String oAuthId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.somemore.auth.oauth.naver.usecase.query;

public interface CheckNaverUserUseCase {
boolean isNaverUserExists(String id);
}
Loading