-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/40 Spring Security / OAuth2 Client, Server 구현 #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
a31f904
cca332c
c0479f8
b77022c
82199e8
a3ef142
d4ee58c
0fd7a67
8416a64
5fa7a74
945f6f9
c3d65d8
a37bf73
345e610
7d26962
f594fe7
565e894
b9d7d6d
bf7ea95
f621689
ed054e0
afa6d2c
70a4833
29a4695
bec0572
af51871
8e4da9b
ace54e1
8d05dc3
2e53b62
d66389a
85b2675
0c6e7f0
35789a5
189c7c9
1b03215
41921e0
d0d31e2
b3fa19f
6e9aecb
f38391e
d1a5daf
5b223a1
67575e2
455fe76
8d0fd8d
f6ab84f
0338697
ffcafa1
3291aa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.somemore.auth; | ||
|
|
||
| public enum UserRole { | ||
| VOLUNTEER, | ||
| CENTER, | ||
| ADMIN | ||
| } |
| 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(); | ||
| } | ||
| } |
| 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); | ||
| } |
| 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 -> { | ||
| 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(); | ||
| } | ||
| } | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. naver_user 테이블 생기는건가요??
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주석 일부러 넣어두신 건가요?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 문제가 되는 부분은 아니지만 NaverOAuth2UserInfoService랑 어노테이션 순서가 달라보여서 댓글 남겨놓겠습니다
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
| @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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 자바 21 문법인가요?
Good 입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다. 자바 17 문법입니다 ㅎㅎ