From bd7d335c5dc50b17183b79e349756f135a92ab26 Mon Sep 17 00:00:00 2001 From: JeongHun Yu Date: Thu, 20 Jul 2023 14:21:16 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20&=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (#29) 카카오 OAuth를 통해 정보를 받아서 가입하는 기능 구현 * chore: (#29) OAuth API요청에 대한 환경변수 설정 * feat: (#29) Member의 랜덤 닉네임을 지정하기 위한 수 생성기 구현 * chore: (#29) JJWT라이브러리 의존성 추가 * feat: (#29) 로그인한 회원에 대한 정보를 JWT로 반환하는 기능 추가 * chore: (#29) 토큰 관련 환경변수 설정 추가 * refactor: (#29) ResponseDto를 record형식으로 변환 * feat: (#29) 인증정보를 확인하는 AuthenticationFilter구현 * feat: (#29) 멤버가 존재하는지 확인한 후 반환하는 ArgumentResolver구현 * test: (#29) loginWithKakao메서드에 대한 컨트롤러 단위 테스트 작성 * refactor: (#29) Member엔티티 필드명 수정 및 추가 * test: (#29) Member 등록에 대한 검증 추가 * chore: (#29) test를 위한 yaml파일을 추가하여 환경 분리 * refactor: (#29) conflict 해결 * refactor: (#29) Controller Swagger를 위한 어노테이션 추가 * feat: (#29) CORS설정 및 ArgumentResolver등록 * chore: (#29) test환경 환경변수만 존재하도록 수정 * refactor: (#29) 네이밍, 상수화, 위치변경 등의 작업 수행 * fix: (#29) 멤버의 이름에 포함되는 숫자가 고정되는 문제 해결 * refactor: (#29) ObjectMapper Bean으로 등록 * refactor: (#29) 매직넘버 상수화 및 변수, 메서드명 수정 * refactor: (#29) @JsonProperty를 @JsonNaming으로 변경 * chore: (#29) test용 production url 수정 * refactor: (#29) 상수 및 변수명 수정 * test: (#29) 토큰에 대한 검증 추가 * refactor: (#29) 토큰을 파싱할 때 유효성 검사 추가 * refactor: (#29) 로그인 api nickname 필드 추가 * refactor: (#29) 토큰 검증 DisplayName 변경 * refactor: (#29) Swagger tag name 변경 * refactor: (#29) TokenProcessorTest의 필드를 빈을 사용하도록 변경 --- backend/build.gradle | 5 + .../com/votogether/VotogetherApplication.java | 2 + .../java/com/votogether/config/JpaConfig.java | 9 ++ .../com/votogether/config/WebMvcConfig.java | 46 ++++++++ .../auth/controller/AuthController.java | 33 ++++++ .../domain/auth/dto/KakaoMemberResponse.java | 21 ++++ .../domain/auth/dto/LoginResponse.java | 7 ++ .../auth/dto/OAuthAccessTokenResponse.java | 14 +++ .../domain/auth/service/AuthService.java | 31 +++++ .../domain/auth/service/KakaoOAuthClient.java | 52 +++++++++ .../domain/member/entity/Member.java | 14 +++ .../entity/NicknameNumberGenerator.java | 12 ++ .../domain/member/entity/NumberGenerator.java | 7 ++ .../domain/member/entity/SocialType.java | 2 +- .../member/repository/MemberRepository.java | 5 + .../domain/member/service/MemberService.java | 31 +++++ .../post/controller/PostController.java | 6 +- .../java/com/votogether/global/jwt/Auth.java | 11 ++ .../global/jwt/JwtAuthenticationFilter.java | 44 +++++++ .../jwt/JwtAuthorizationArgumentResolver.java | 41 +++++++ .../votogether/global/jwt/TokenPayload.java | 8 ++ .../votogether/global/jwt/TokenProcessor.java | 95 +++++++++++++++ backend/src/main/resources/application.yml | 15 ++- .../auth/controller/AuthControllerTest.java | 64 ++++++++++ .../contorller/CategoryControllerTest.java | 8 ++ .../MemberCategoryRepositoryTest.java | 14 ++- .../member/service/MemberServiceTest.java | 40 +++++++ .../PostControllerIntegratedTest.java | 21 ++++ .../post/controller/PostControllerTest.java | 10 +- .../domain/post/entity/PostTest.java | 4 +- .../post/repository/PostRepositoryTest.java | 7 +- .../vote/repository/VoteRepositoryTest.java | 7 +- .../votogether/fixtures/MemberFixtures.java | 44 +++---- .../global/jwt/TokenProcessorTest.java | 110 ++++++++++++++++++ backend/src/test/resources/application.yml | 17 +++ 35 files changed, 826 insertions(+), 31 deletions(-) create mode 100644 backend/src/main/java/com/votogether/config/WebMvcConfig.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/controller/AuthController.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/dto/KakaoMemberResponse.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/dto/LoginResponse.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/dto/OAuthAccessTokenResponse.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/service/AuthService.java create mode 100644 backend/src/main/java/com/votogether/domain/auth/service/KakaoOAuthClient.java create mode 100644 backend/src/main/java/com/votogether/domain/member/entity/NicknameNumberGenerator.java create mode 100644 backend/src/main/java/com/votogether/domain/member/entity/NumberGenerator.java create mode 100644 backend/src/main/java/com/votogether/domain/member/service/MemberService.java create mode 100644 backend/src/main/java/com/votogether/global/jwt/Auth.java create mode 100644 backend/src/main/java/com/votogether/global/jwt/JwtAuthenticationFilter.java create mode 100644 backend/src/main/java/com/votogether/global/jwt/JwtAuthorizationArgumentResolver.java create mode 100644 backend/src/main/java/com/votogether/global/jwt/TokenPayload.java create mode 100644 backend/src/main/java/com/votogether/global/jwt/TokenProcessor.java create mode 100644 backend/src/test/java/com/votogether/domain/auth/controller/AuthControllerTest.java create mode 100644 backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java create mode 100644 backend/src/test/java/com/votogether/global/jwt/TokenProcessorTest.java create mode 100644 backend/src/test/resources/application.yml diff --git a/backend/build.gradle b/backend/build.gradle index bc67c419e..064a946ef 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -26,7 +26,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' @@ -35,6 +38,8 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured' + testImplementation 'io.rest-assured:spring-mock-mvc' } tasks.named('test') { diff --git a/backend/src/main/java/com/votogether/VotogetherApplication.java b/backend/src/main/java/com/votogether/VotogetherApplication.java index 05ecab8a0..e2aa0e85d 100644 --- a/backend/src/main/java/com/votogether/VotogetherApplication.java +++ b/backend/src/main/java/com/votogether/VotogetherApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +@ConfigurationPropertiesScan @SpringBootApplication public class VotogetherApplication { diff --git a/backend/src/main/java/com/votogether/config/JpaConfig.java b/backend/src/main/java/com/votogether/config/JpaConfig.java index 6053d977f..cd867f3bb 100644 --- a/backend/src/main/java/com/votogether/config/JpaConfig.java +++ b/backend/src/main/java/com/votogether/config/JpaConfig.java @@ -1,9 +1,18 @@ package com.votogether.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration @EnableJpaAuditing public class JpaConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper().registerModule(new JavaTimeModule()); + } + } diff --git a/backend/src/main/java/com/votogether/config/WebMvcConfig.java b/backend/src/main/java/com/votogether/config/WebMvcConfig.java new file mode 100644 index 000000000..28a55ed6a --- /dev/null +++ b/backend/src/main/java/com/votogether/config/WebMvcConfig.java @@ -0,0 +1,46 @@ +package com.votogether.config; + +import com.votogether.global.jwt.JwtAuthorizationArgumentResolver; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; + private final String origins; + + public WebMvcConfig( + final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver, + @Value("${votogether.openapi.prod-url}") final String origins + ) { + this.jwtAuthorizationArgumentResolver = jwtAuthorizationArgumentResolver; + this.origins = origins; + } + + @Override + public void addArgumentResolvers(final List resolvers) { + resolvers.add(jwtAuthorizationArgumentResolver); + } + + @Override + public void addCorsMappings(final CorsRegistry registry) { + registry.addMapping("/**") + .allowedHeaders("*") + .allowedOrigins(origins) + .allowedMethods( + HttpMethod.POST.name(), + HttpMethod.GET.name(), + HttpMethod.PATCH.name(), + HttpMethod.PUT.name(), + HttpMethod.DELETE.name() + ) + .exposedHeaders(HttpHeaders.LOCATION); + } +} diff --git a/backend/src/main/java/com/votogether/domain/auth/controller/AuthController.java b/backend/src/main/java/com/votogether/domain/auth/controller/AuthController.java new file mode 100644 index 000000000..fb805e9cd --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/controller/AuthController.java @@ -0,0 +1,33 @@ +package com.votogether.domain.auth.controller; + +import com.votogether.domain.auth.dto.LoginResponse; +import com.votogether.domain.auth.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "회원 인증", description = "회원 인증 API") +@RequiredArgsConstructor +@RestController +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "카카오 로그인 하기", description = "카카오 계정으로 로그인을 한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "카카오 로그인 성공"), + @ApiResponse(responseCode = "400", description = "올바르지 않은 요청"), + @ApiResponse(responseCode = "401", description = "올바르지 않은 인증코드") + }) + @GetMapping("/auth/kakao/callback") + public ResponseEntity loginByKakao(@RequestParam final String code) { + return ResponseEntity.ok(authService.register(code)); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/auth/dto/KakaoMemberResponse.java b/backend/src/main/java/com/votogether/domain/auth/dto/KakaoMemberResponse.java new file mode 100644 index 000000000..5595dbfde --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/dto/KakaoMemberResponse.java @@ -0,0 +1,21 @@ +package com.votogether.domain.auth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoMemberResponse( + Long id, + KakaoAccount kakaoAccount +) { + + @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) + public record KakaoAccount( + String email, + String ageRange, + String birthday, + String gender + ) { + } + +} diff --git a/backend/src/main/java/com/votogether/domain/auth/dto/LoginResponse.java b/backend/src/main/java/com/votogether/domain/auth/dto/LoginResponse.java new file mode 100644 index 000000000..3ebb28acb --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/dto/LoginResponse.java @@ -0,0 +1,7 @@ +package com.votogether.domain.auth.dto; + +public record LoginResponse( + String accessToken, + String nickname +) { +} diff --git a/backend/src/main/java/com/votogether/domain/auth/dto/OAuthAccessTokenResponse.java b/backend/src/main/java/com/votogether/domain/auth/dto/OAuthAccessTokenResponse.java new file mode 100644 index 000000000..86f020d19 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/dto/OAuthAccessTokenResponse.java @@ -0,0 +1,14 @@ +package com.votogether.domain.auth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +public record OAuthAccessTokenResponse( + String tokenType, + String accessToken, + Integer expiresIn, + String refreshToken, + Integer refreshTokenExpiresIn +) { +} diff --git a/backend/src/main/java/com/votogether/domain/auth/service/AuthService.java b/backend/src/main/java/com/votogether/domain/auth/service/AuthService.java new file mode 100644 index 000000000..b6233af08 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/service/AuthService.java @@ -0,0 +1,31 @@ +package com.votogether.domain.auth.service; + +import com.votogether.domain.auth.dto.KakaoMemberResponse; +import com.votogether.domain.auth.dto.LoginResponse; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.service.MemberService; +import com.votogether.global.jwt.TokenProcessor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class AuthService { + + private final KakaoOAuthClient kakaoOAuthClient; + private final MemberService memberService; + private final TokenProcessor tokenProcessor; + + @Transactional + public LoginResponse register(final String code) { + final String accessToken = kakaoOAuthClient.getAccessToken(code); + final KakaoMemberResponse response = kakaoOAuthClient.getMemberInfo(accessToken); + + final Member member = Member.from(response); + final Member registeredMember = memberService.register(member); + final String token = tokenProcessor.generateToken(registeredMember); + return new LoginResponse(token, member.getNickname()); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/auth/service/KakaoOAuthClient.java b/backend/src/main/java/com/votogether/domain/auth/service/KakaoOAuthClient.java new file mode 100644 index 000000000..7520593a1 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/auth/service/KakaoOAuthClient.java @@ -0,0 +1,52 @@ +package com.votogether.domain.auth.service; + +import com.votogether.domain.auth.dto.KakaoMemberResponse; +import com.votogether.domain.auth.dto.OAuthAccessTokenResponse; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Getter +@ConfigurationProperties(prefix = "oauth.kakao") +@Component +public class KakaoOAuthClient { + + private static final RestTemplate restTemplate = new RestTemplate(); + + private final MultiValueMap info = new LinkedMultiValueMap<>(); + + public String getAccessToken(final String code) { + info.add("code", code); + + final OAuthAccessTokenResponse response = restTemplate.postForObject( + "https://kauth.kakao.com/oauth/token", + info, + OAuthAccessTokenResponse.class + ); + return response.accessToken(); + } + + public KakaoMemberResponse getMemberInfo(final String accessToken) { + final HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + final HttpEntity request = new HttpEntity<>(headers); + + final KakaoMemberResponse response = restTemplate.exchange( + "https://kapi.kakao.com/v2/user/me", + HttpMethod.GET, + request, + KakaoMemberResponse.class + ).getBody(); + return response; + } + +} diff --git a/backend/src/main/java/com/votogether/domain/member/entity/Member.java b/backend/src/main/java/com/votogether/domain/member/entity/Member.java index 38de6b63e..12f83788b 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/Member.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/Member.java @@ -1,5 +1,6 @@ package com.votogether.domain.member.entity; +import com.votogether.domain.auth.dto.KakaoMemberResponse; import com.votogether.domain.common.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -64,6 +65,19 @@ private Member( this.point = point; } + public static Member from(final KakaoMemberResponse response) { + final NicknameNumberGenerator nicknameNumberGenerator = new NicknameNumberGenerator(); + return Member.builder() + .nickname("익명의 손님" + nicknameNumberGenerator.generate()) + .gender(Gender.valueOf(response.kakaoAccount().gender().toUpperCase())) + .ageRange(response.kakaoAccount().ageRange()) + .birthday(response.kakaoAccount().birthday()) + .socialType(SocialType.KAKAO) + .socialId(String.valueOf(response.id())) + .point(0) + .build(); + } + public void plusPoint(final int point) { this.point = this.point + point; } diff --git a/backend/src/main/java/com/votogether/domain/member/entity/NicknameNumberGenerator.java b/backend/src/main/java/com/votogether/domain/member/entity/NicknameNumberGenerator.java new file mode 100644 index 000000000..5346c1d6f --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/member/entity/NicknameNumberGenerator.java @@ -0,0 +1,12 @@ +package com.votogether.domain.member.entity; + +public class NicknameNumberGenerator implements NumberGenerator { + + private static int number = 0; + + @Override + public int generate() { + return ++number; + } + +} diff --git a/backend/src/main/java/com/votogether/domain/member/entity/NumberGenerator.java b/backend/src/main/java/com/votogether/domain/member/entity/NumberGenerator.java new file mode 100644 index 000000000..0ea005406 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/member/entity/NumberGenerator.java @@ -0,0 +1,7 @@ +package com.votogether.domain.member.entity; + +public interface NumberGenerator { + + int generate(); + +} diff --git a/backend/src/main/java/com/votogether/domain/member/entity/SocialType.java b/backend/src/main/java/com/votogether/domain/member/entity/SocialType.java index 3e1fec453..86a25fa5c 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/SocialType.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/SocialType.java @@ -2,7 +2,7 @@ public enum SocialType { - GOOGLE, + KAKAO, ; } diff --git a/backend/src/main/java/com/votogether/domain/member/repository/MemberRepository.java b/backend/src/main/java/com/votogether/domain/member/repository/MemberRepository.java index f8cd1e3fb..d8d5d5608 100644 --- a/backend/src/main/java/com/votogether/domain/member/repository/MemberRepository.java +++ b/backend/src/main/java/com/votogether/domain/member/repository/MemberRepository.java @@ -1,7 +1,12 @@ package com.votogether.domain.member.repository; import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.SocialType; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + + Optional findBySocialIdAndSocialType(final String socialId, final SocialType socialType); + } diff --git a/backend/src/main/java/com/votogether/domain/member/service/MemberService.java b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java new file mode 100644 index 000000000..7d22ff7d0 --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java @@ -0,0 +1,31 @@ +package com.votogether.domain.member.service; + +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.repository.MemberRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class MemberService { + + private final MemberRepository memberRepository; + + @Transactional + public Member register(final Member member) { + final Optional maybeMember = memberRepository.findBySocialIdAndSocialType( + member.getSocialId(), + member.getSocialType() + ); + return maybeMember.orElseGet(() -> memberRepository.save(member)); + } + + @Transactional(readOnly = true) + public Member findById(final Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("해당 Id를 가진 회원은 존재하지 않습니다.")); + } + +} diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostController.java index 134ffd6f5..b90b6a7cd 100644 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostController.java +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostController.java @@ -44,13 +44,17 @@ public ResponseEntity save( ) { // TODO : 일단 돌아가게 하기 위한 member 저장 (실제 어플에선 삭제될 코드) final Member member = Member.builder() + .socialType(SocialType.KAKAO) + .socialId("tjdtls690") .nickname("Abel") .gender(Gender.MALE) .birthday("0718") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("tjdtls690") .point(100) + .ageRange("30~39") + .birthday("0101") .build(); final Long postId = postService.save(request, member, images); diff --git a/backend/src/main/java/com/votogether/global/jwt/Auth.java b/backend/src/main/java/com/votogether/global/jwt/Auth.java new file mode 100644 index 000000000..0e014c0c9 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/jwt/Auth.java @@ -0,0 +1,11 @@ +package com.votogether.global.jwt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} diff --git a/backend/src/main/java/com/votogether/global/jwt/JwtAuthenticationFilter.java b/backend/src/main/java/com/votogether/global/jwt/JwtAuthenticationFilter.java new file mode 100644 index 000000000..3219d9ae8 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,44 @@ +package com.votogether.global.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final List ALLOWED_URIS = List.of( + "/health-check", + "/auth/kakao/callback", + "/categories/guest" + ); + + private final TokenProcessor tokenProcessor; + + @Override + protected void doFilterInternal( + final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain filterChain + ) throws ServletException, IOException { + final String token = request.getHeader(HttpHeaders.AUTHORIZATION); + final String tokenWithoutType = tokenProcessor.resolveToken(token); + tokenProcessor.validateToken(tokenWithoutType); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(final HttpServletRequest request) { + return ALLOWED_URIS.stream() + .anyMatch(url -> request.getRequestURI().contains(url)); + } + +} diff --git a/backend/src/main/java/com/votogether/global/jwt/JwtAuthorizationArgumentResolver.java b/backend/src/main/java/com/votogether/global/jwt/JwtAuthorizationArgumentResolver.java new file mode 100644 index 000000000..7253f1634 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/jwt/JwtAuthorizationArgumentResolver.java @@ -0,0 +1,41 @@ +package com.votogether.global.jwt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Component +public class JwtAuthorizationArgumentResolver implements HandlerMethodArgumentResolver { + + private final TokenProcessor tokenProcessor; + private final MemberService memberService; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + return parameter.withContainingClass(Member.class) + .hasParameterAnnotation(Auth.class); + } + + @Override + public Member resolveArgument( + final MethodParameter parameter, + final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, + final WebDataBinderFactory binderFactory + ) throws JsonProcessingException { + final String token = webRequest.getHeader(HttpHeaders.AUTHORIZATION); + final String tokenWithoutType = tokenProcessor.resolveToken(token); + final TokenPayload tokenPayload = tokenProcessor.parseToken(tokenWithoutType); + return memberService.findById(tokenPayload.memberId()); + } + +} diff --git a/backend/src/main/java/com/votogether/global/jwt/TokenPayload.java b/backend/src/main/java/com/votogether/global/jwt/TokenPayload.java new file mode 100644 index 000000000..c4bf3fd99 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/jwt/TokenPayload.java @@ -0,0 +1,8 @@ +package com.votogether.global.jwt; + +public record TokenPayload( + Long memberId, + Long iat, + Long exp +) { +} diff --git a/backend/src/main/java/com/votogether/global/jwt/TokenProcessor.java b/backend/src/main/java/com/votogether/global/jwt/TokenProcessor.java new file mode 100644 index 000000000..5b3f54876 --- /dev/null +++ b/backend/src/main/java/com/votogether/global/jwt/TokenProcessor.java @@ -0,0 +1,95 @@ +package com.votogether.global.jwt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.votogether.domain.member.entity.Member; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import java.security.Key; +import java.util.Date; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Slf4j +@Component +public class TokenProcessor { + + private static final String TOKEN_DELIMITER = "\\."; + private static final String BEARER_TOKEN_PREFIX = "Bearer "; + private static final String BLANK = " "; + + private final Key key; + private final int tokenExpirationTime; + private final ObjectMapper objectMapper; + + public TokenProcessor( + @Value("${jwt.token.secret-key}") final String secretKey, + @Value("${jwt.token.expiration-time}") final int tokenExpirationTime, + final ObjectMapper objectMapper + ) { + this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + this.tokenExpirationTime = tokenExpirationTime; + this.objectMapper = objectMapper; + } + + public String generateToken(final Member member) { + final Date now = new Date(); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .claim("memberId", member.getId()) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public String resolveToken(final String token) { + if (StringUtils.hasText(token) && token.startsWith(BEARER_TOKEN_PREFIX)) { + return token.split(BLANK)[1]; + } + throw new IllegalArgumentException("올바르지 않은 토큰입니다."); + } + + public TokenPayload parseToken(final String token) throws JsonProcessingException { + validateToken(token); + final String[] chunks = token.split(TOKEN_DELIMITER); + final String payload = new String(Decoders.BASE64.decode(chunks[1])); + return objectMapper.readValue(payload, TokenPayload.class); + } + + public void validateToken(final String token) { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + } catch (final UnsupportedJwtException e) { + log.info("지원하지 않는 JWT입니다."); + throw new IllegalArgumentException("지원하지 않는 JWT입니다."); + } catch (final MalformedJwtException e) { + log.info("잘못된 JWT 서명입니다."); + throw new IllegalArgumentException("잘못된 JWT 서명입니다."); + } catch (final SignatureException e) { + log.info("토큰의 서명 유효성 검사가 실패했습니다."); + throw new IllegalArgumentException("토큰의 서명 유효성 검사가 실패했습니다."); + } catch (final ExpiredJwtException e) { + log.info("토큰의 유효기간이 만료되었습니다."); + throw new IllegalArgumentException("토큰의 유효기간이 만료되었습니다."); + } catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("토큰의 내용이 비어있습니다."); + } catch (final Exception e) { + log.info("알 수 없는 토큰 유효성 문제가 발생했습니다."); + throw new IllegalArgumentException("알 수 없는 토큰 유효성 문제가 발생했습니다."); + } + } + +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9186f8ba1..e2c8397dc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -25,4 +25,17 @@ logging: votogether: openapi: dev-url: http://localhost:8080 - prod-url: http://votogether.com + prod-url: ${PROD_URL} + +oauth: + kakao: + info: + grant_type: ${GRANT_TYPE} + client_id: ${CLIENT_ID} + client_secret: ${CLIENT_SECRET} + redirect_uri: ${REDIRECT_URI} + +jwt: + token: + secret-key: ${SECRET_KEY} + expiration-time: ${EXPIRATION_TIME} diff --git a/backend/src/test/java/com/votogether/domain/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/votogether/domain/auth/controller/AuthControllerTest.java new file mode 100644 index 000000000..e54e68a45 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/auth/controller/AuthControllerTest.java @@ -0,0 +1,64 @@ +package com.votogether.domain.auth.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.votogether.domain.auth.dto.LoginResponse; +import com.votogether.domain.auth.service.AuthService; +import com.votogether.domain.member.service.MemberService; +import com.votogether.global.jwt.TokenProcessor; +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; + +@WebMvcTest(controllers = AuthController.class) +class AuthControllerTest { + + @MockBean + AuthService authService; + + @MockBean + TokenProcessor tokenProcessor; + + @MockBean + MemberService memberService; + + @BeforeEach + void setUp() { + RestAssuredMockMvc.standaloneSetup(new AuthController(authService)); + } + + @Test + @DisplayName("카카오 로그인을 한다.") + void loginByKakao() { + // given + String accessToken = "abcdefg"; + String nickname = "jeomxon"; + LoginResponse response = new LoginResponse(accessToken, nickname); + + given(authService.register(any())).willReturn(response); + + // when + String responseBody = RestAssuredMockMvc + .given().log().all() + .queryParam("code", "abc1234") + .when().get("/auth/kakao/callback") + .then().log().all() + .status(HttpStatus.OK) + .extract() + .asString(); + + // then + assertAll( + () -> assertThat(responseBody).contains("accessToken"), + () -> assertThat(responseBody).contains(accessToken) + ); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/category/contorller/CategoryControllerTest.java b/backend/src/test/java/com/votogether/domain/category/contorller/CategoryControllerTest.java index 81d5226ee..b01d2968b 100644 --- a/backend/src/test/java/com/votogether/domain/category/contorller/CategoryControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/category/contorller/CategoryControllerTest.java @@ -9,6 +9,8 @@ import com.votogether.domain.category.dto.response.CategoryResponse; import com.votogether.domain.category.entity.Category; import com.votogether.domain.category.service.CategoryService; +import com.votogether.domain.member.service.MemberService; +import com.votogether.global.jwt.TokenProcessor; import io.restassured.module.mockmvc.RestAssuredMockMvc; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -24,6 +26,12 @@ class CategoryControllerTest { @MockBean CategoryService categoryService; + @MockBean + TokenProcessor tokenProcessor; + + @MockBean + MemberService memberService; + @BeforeEach void setUp() { RestAssuredMockMvc.standaloneSetup(new CategoryController(categoryService)); diff --git a/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java b/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java index 9a83cd1ab..9f1549dad 100644 --- a/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/member/repository/MemberCategoryRepositoryTest.java @@ -37,12 +37,17 @@ void save() { .build(); Member member = Member.builder() + .gender(Gender.MALE) + .point(0) + .socialType(SocialType.KAKAO) .nickname("user1") .gender(Gender.MALE) .birthday("0718") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") .point(0) .build(); @@ -71,12 +76,17 @@ void findByMemberAndCategory() { .build(); Member member = Member.builder() + .gender(Gender.MALE) + .point(0) + .socialType(SocialType.KAKAO) .nickname("user1") .gender(Gender.MALE) .birthday("0718") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") .point(0) .build(); diff --git a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java new file mode 100644 index 000000000..6b24530a5 --- /dev/null +++ b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java @@ -0,0 +1,40 @@ +package com.votogether.domain.member.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.votogether.ServiceTest; +import com.votogether.domain.member.entity.Gender; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.SocialType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class MemberServiceTest { + + @Autowired + MemberService memberService; + + @Test + @DisplayName("멤버가 존재하지 않으면 저장한다.") + void register() { + // given + Member member = Member.builder() + .nickname("저문") + .gender(Gender.MALE) + .ageRange("20~29") + .birthday("0101") + .socialType(SocialType.KAKAO) + .socialId("123123") + .point(0) + .build(); + + // when + Member registeredMember = memberService.register(member); + + // then + assertThat(registeredMember.getSocialId()).isEqualTo("123123"); + } + +} diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerIntegratedTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerIntegratedTest.java index 051e0da69..33639987a 100644 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerIntegratedTest.java +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerIntegratedTest.java @@ -5,8 +5,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.votogether.domain.member.entity.Gender; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.SocialType; import com.votogether.domain.post.dto.request.PostCreateRequest; import com.votogether.domain.post.integrated.IntegrationTest; +import com.votogether.global.jwt.TokenProcessor; import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.io.File; @@ -16,14 +20,30 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; class PostControllerIntegratedTest extends IntegrationTest { + @Autowired + TokenProcessor tokenProcessor; + @Test @DisplayName("게시글을 등록한다") void save() throws IOException { // given + final Member member = Member.builder() + .gender(Gender.MALE) + .point(0) + .socialType(SocialType.KAKAO) + .nickname("user1") + .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") + .build(); + String token = tokenProcessor.generateToken(member); + final List postOptionRequests = List.of("option1", "option2"); final PostCreateRequest postCreateRequest = PostCreateRequest.builder() @@ -51,6 +71,7 @@ void save() throws IOException { // when, then RestAssured.given().log().all() .contentType(ContentType.MULTIPART) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) .multiPart("request", postCreateRequest, "application/json") .multiPart("images", resultFileName1, new FileInputStream(file1), "image/png") .multiPart("images", resultFileName2, new FileInputStream(file2), "image/png") diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java index 0c6d23723..93394a2b4 100644 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostControllerTest.java @@ -8,10 +8,12 @@ import static org.mockito.BDDMockito.given; import com.fasterxml.jackson.databind.ObjectMapper; +import com.votogether.domain.member.service.MemberService; import com.votogether.domain.post.dto.request.PostCreateRequest; import com.votogether.domain.post.dto.response.VoteCountForAgeGroupResponse; import com.votogether.domain.post.dto.response.VoteOptionStatisticsResponse; import com.votogether.domain.post.service.PostService; +import com.votogether.global.jwt.TokenProcessor; import io.restassured.http.ContentType; import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.restassured.module.mockmvc.response.MockMvcResponse; @@ -31,7 +33,13 @@ class PostControllerTest { @MockBean - private PostService postService; + PostService postService; + + @MockBean + MemberService memberService; + + @MockBean + TokenProcessor tokenProcessor; @BeforeEach void setUp() { diff --git a/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java b/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java index 593498bff..759bbd785 100644 --- a/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java +++ b/backend/src/test/java/com/votogether/domain/post/entity/PostTest.java @@ -41,9 +41,11 @@ void isWriter() { Member member1 = Member.builder() .gender(Gender.MALE) .point(0) - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .nickname("user1") .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") .build(); Member member2 = Member.builder() diff --git a/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java b/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java index 0a8021b3b..de477d830 100644 --- a/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/post/repository/PostRepositoryTest.java @@ -33,12 +33,17 @@ void save() { .build(); final Member member = Member.builder() + .gender(Gender.MALE) + .point(0) + .socialType(SocialType.KAKAO) .nickname("user1") .gender(Gender.MALE) .birthday("0718") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") .point(0) .build(); diff --git a/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java b/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java index 9ba55c82e..36340c0f6 100644 --- a/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java +++ b/backend/src/test/java/com/votogether/domain/vote/repository/VoteRepositoryTest.java @@ -37,12 +37,17 @@ class VoteRepositoryTest { PostOptionRepository postOptionRepository; Member member = Member.builder() + .gender(Gender.MALE) + .point(0) + .socialType(SocialType.KAKAO) .nickname("user1") .gender(Gender.MALE) .birthday("0718") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("kakao@gmail.com") + .ageRange("30~39") + .birthday("0101") .point(0) .build(); diff --git a/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java b/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java index 86bc9571a..e0258d659 100644 --- a/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java +++ b/backend/src/test/java/com/votogether/fixtures/MemberFixtures.java @@ -11,7 +11,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("1~9") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user1@gmail.com") .point(0) .build(); @@ -21,7 +21,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("1~9") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user2@gmail.com") .point(0) .build(); @@ -31,7 +31,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user3@gmail.com") .point(0) .build(); @@ -41,7 +41,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("10~14") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user4@gmail.com") .point(0) .build(); @@ -51,7 +51,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("15~19") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user5@gmail.com") .point(0) .build(); @@ -61,7 +61,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("15~19") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user6@gmail.com") .point(0) .build(); @@ -71,7 +71,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("20~29") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user7@gmail.com") .point(0) .build(); @@ -81,7 +81,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("20~29") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user8@gmail.com") .point(0) .build(); @@ -91,7 +91,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("30~39") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user9@gmail.com") .point(0) .build(); @@ -101,7 +101,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("30~39") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user10@gmail.com") .point(0) .build(); @@ -111,7 +111,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("40~49") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user11@gmail.com") .point(0) .build(); @@ -121,7 +121,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("40~49") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user12@gmail.com") .point(0) .build(); @@ -131,7 +131,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("50~59") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user13@gmail.com") .point(0) .build(); @@ -141,7 +141,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("50~59") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user14@gmail.com") .point(0) .build(); @@ -151,7 +151,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("60~69") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user15@gmail.com") .point(0) .build(); @@ -161,7 +161,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("60~69") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user16@gmail.com") .point(0) .build(); @@ -171,7 +171,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("70~79") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user17@gmail.com") .point(0) .build(); @@ -181,7 +181,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("70~79") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user18@gmail.com") .point(0) .build(); @@ -191,7 +191,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("80~89") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user19@gmail.com") .point(0) .build(); @@ -201,7 +201,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("80~89") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user20@gmail.com") .point(0) .build(); @@ -211,7 +211,7 @@ public class MemberFixtures { .gender(Gender.MALE) .birthday("1225") .ageRange("90~") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user21@gmail.com") .point(0) .build(); @@ -221,7 +221,7 @@ public class MemberFixtures { .gender(Gender.FEMALE) .birthday("1225") .ageRange("90~") - .socialType(SocialType.GOOGLE) + .socialType(SocialType.KAKAO) .socialId("user22@gmail.com") .point(0) .build(); diff --git a/backend/src/test/java/com/votogether/global/jwt/TokenProcessorTest.java b/backend/src/test/java/com/votogether/global/jwt/TokenProcessorTest.java new file mode 100644 index 000000000..382f5f3a9 --- /dev/null +++ b/backend/src/test/java/com/votogether/global/jwt/TokenProcessorTest.java @@ -0,0 +1,110 @@ +package com.votogether.global.jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.votogether.RepositoryTest; +import com.votogether.domain.member.entity.Gender; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.member.entity.SocialType; +import com.votogether.domain.member.repository.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import(TokenProcessor.class) +@RepositoryTest +class TokenProcessorTest { + + @Autowired + MemberRepository memberRepository; + + @Autowired + TokenProcessor tokenProcessor; + + @Test + @DisplayName("토큰을 생성한다.") + void generateToken() throws Exception { + // given + Member member = Member.builder() + .nickname("저문") + .gender(Gender.MALE) + .ageRange("20~29") + .birthday("0101") + .socialType(SocialType.KAKAO) + .socialId("123123") + .point(0) + .build(); + + memberRepository.save(member); + + // when + String token = tokenProcessor.generateToken(member); + + // then + TokenPayload tokenPayload = tokenProcessor.parseToken(token); + assertThat(tokenPayload.memberId()).isEqualTo(member.getId()); + } + + @Nested + @DisplayName("토큰의 prefix를 제외한 값을 추출할 때") + class ResolveToken { + + @Test + @DisplayName("Bearer가 prefix면 성공한다.") + void resolveTokenSuccess() throws JsonProcessingException { + // given + Member member = Member.builder() + .nickname("저문") + .gender(Gender.MALE) + .ageRange("20~29") + .birthday("0101") + .socialType(SocialType.KAKAO) + .socialId("123123") + .point(0) + .build(); + memberRepository.save(member); + + String token = tokenProcessor.generateToken(member); + token = "Bearer " + token; + + // when + String resolvedToken = tokenProcessor.resolveToken(token); + + // then + TokenPayload tokenPayload = tokenProcessor.parseToken(resolvedToken); + assertThat(tokenPayload.memberId()).isEqualTo(member.getId()); + } + + @ParameterizedTest + @ValueSource(strings = {"Bear", "Barrier", "Baerer", "bearer"}) + @DisplayName("Bearer가 아닌 다른 prefix라면 예외를 발생시킨다.") + void resolveTokenFail(String prefix) { + // given + Member member = Member.builder() + .nickname("저문") + .gender(Gender.MALE) + .ageRange("20~29") + .birthday("0101") + .socialType(SocialType.KAKAO) + .socialId("123123") + .point(0) + .build(); + memberRepository.save(member); + + String token = prefix + tokenProcessor.generateToken(member); + + // when, then + assertThatThrownBy(() -> tokenProcessor.resolveToken(token)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바르지 않은 토큰입니다."); + } + + } + +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 000000000..dd3d86540 --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,17 @@ +votogether: + openapi: + dev-url: http://localhost:8080 + prod-url: http://aaaaaaaa.com + +oauth: + kakao: + info: + grant_type: aaaaaaaaaaaaaaaa + client_id: bbbbbbbbbbbbbbbbbbbbbbbbbbbb + client_secret: cccccccccccccccccccccccccccccccccccccc + redirect_uri: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +jwt: + token: + secret-key: abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc + expiration-time: 222222