From c8db6de5fb4b29c7de2dbbbcc2f8badbf20f850f Mon Sep 17 00:00:00 2001 From: CHO Date: Tue, 19 Nov 2024 05:51:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20openAPI=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve : #164 --- build.gradle | 3 + .../member/controller/MemberController.java | 11 +- .../domain/member/dto/MemberResponseDTO.java | 7 +- .../member}/dto/OAuth2DTO.java | 2 +- .../umc7th/domain/member/enums/Role.java | 5 - .../domain/member/enums/SocialType.java | 5 + .../member/exception/MemberErrorCode.java | 6 +- .../principal/PrincipalDetailsService.java | 1 - .../member/service/command/OAuth2Service.java | 7 + .../service/command/OAuth2ServiceImpl.java | 144 ++++++++++++++++++ .../openAPI/component/OpenApiWebClient.java | 11 ++ .../component/OpenApiWebClientImpl.java | 51 +++++++ .../openAPI/controller/OpenApiController.java | 23 +++ .../openAPI/dto/SearchStayResponseDTO.java | 46 ++++++ .../openAPI/service/OpenApiQueryService.java | 8 + .../service/OpenApiQueryServiceImpl.java | 80 ++++++++++ .../umc7th/global/config/SecurityConfig.java | 13 +- .../umc7th/global/config/WebConfig.java | 37 +++++ .../{ => handler}/JwtAccessDeniedHandler.java | 2 +- .../JwtAuthenticationEntryPoint.java | 2 +- .../oauth/controller/OAuthController.java | 30 ---- .../global/oauth/service/OAuth2Service.java | 7 - .../oauth/service/OAuth2ServiceImpl.java | 115 -------------- src/main/resources/application.yml | 6 +- 24 files changed, 447 insertions(+), 175 deletions(-) rename src/main/java/com/example/umc7th/{global/oauth => domain/member}/dto/OAuth2DTO.java (96%) delete mode 100644 src/main/java/com/example/umc7th/domain/member/enums/Role.java create mode 100644 src/main/java/com/example/umc7th/domain/member/enums/SocialType.java create mode 100644 src/main/java/com/example/umc7th/domain/member/service/command/OAuth2Service.java create mode 100644 src/main/java/com/example/umc7th/domain/member/service/command/OAuth2ServiceImpl.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClient.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/controller/OpenApiController.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryService.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryServiceImpl.java create mode 100644 src/main/java/com/example/umc7th/global/config/WebConfig.java rename src/main/java/com/example/umc7th/global/jwt/{ => handler}/JwtAccessDeniedHandler.java (97%) rename src/main/java/com/example/umc7th/global/jwt/{ => handler}/JwtAuthenticationEntryPoint.java (96%) delete mode 100644 src/main/java/com/example/umc7th/global/oauth/controller/OAuthController.java delete mode 100644 src/main/java/com/example/umc7th/global/oauth/service/OAuth2Service.java delete mode 100644 src/main/java/com/example/umc7th/global/oauth/service/OAuth2ServiceImpl.java diff --git a/build.gradle b/build.gradle index c03a02e5..71bd31bc 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,9 @@ dependencies { // OAuth2 login implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // WebFlux + implementation 'org.springframework.boot:spring-boot-starter-webflux' } tasks.named('test') { diff --git a/src/main/java/com/example/umc7th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc7th/domain/member/controller/MemberController.java index 8ad9eb7a..a9f86384 100644 --- a/src/main/java/com/example/umc7th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc7th/domain/member/controller/MemberController.java @@ -4,17 +4,17 @@ import com.example.umc7th.domain.member.dto.MemberRequestDTO; import com.example.umc7th.domain.member.dto.MemberResponseDTO; import com.example.umc7th.domain.member.service.command.MemberCommandService; +import com.example.umc7th.domain.member.service.command.OAuth2Service; import com.example.umc7th.global.apiPayload.CustomResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor public class MemberController { private final MemberCommandService memberCommandService; + private final OAuth2Service oAuth2Service; @PostMapping("/login") public CustomResponse login(@RequestBody MemberRequestDTO.MemberLoginDTO dto) { @@ -26,5 +26,10 @@ public CustomResponse signup(@RequestBody Memb return CustomResponse.onSuccess(memberCommandService.signUp(dto)); } + // 이건 카카오 서버에서 호출함 + @GetMapping("/oauth2/callback/kakao") + public CustomResponse loginWithKakao(@RequestParam("code") String code) { + return CustomResponse.onSuccess(oAuth2Service.login("kakao", code)); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/member/dto/MemberResponseDTO.java b/src/main/java/com/example/umc7th/domain/member/dto/MemberResponseDTO.java index dce7b7e5..bb39c8c4 100644 --- a/src/main/java/com/example/umc7th/domain/member/dto/MemberResponseDTO.java +++ b/src/main/java/com/example/umc7th/domain/member/dto/MemberResponseDTO.java @@ -13,7 +13,10 @@ public class MemberResponseDTO { @AllArgsConstructor @NoArgsConstructor public static class MemberTokenDTO { - String accessToken; - String refreshToken; + //클라이언트가 두 토큰을 가지고 있다가 전송 시에는 유효시간이 짧은 accessToken만 전송하여 탈취시 피해범위 최소화 + //토큰은 한번 발급하면 유효시간을 수정할 수 없기 때문 + //보통 토큰은 안전한 쿠키에 담아 전송 + String accessToken; // 실제 인증할 때마다 사용하는 토큰, 유효시간을 작게 하여 refreshToken으로 재발급 + String refreshToken;// accessToken이 만료되면 재발급하는 토큰, (로그아웃 시 삭제) } } \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/oauth/dto/OAuth2DTO.java b/src/main/java/com/example/umc7th/domain/member/dto/OAuth2DTO.java similarity index 96% rename from src/main/java/com/example/umc7th/global/oauth/dto/OAuth2DTO.java rename to src/main/java/com/example/umc7th/domain/member/dto/OAuth2DTO.java index 5a1f14f7..fda3ffd1 100644 --- a/src/main/java/com/example/umc7th/global/oauth/dto/OAuth2DTO.java +++ b/src/main/java/com/example/umc7th/domain/member/dto/OAuth2DTO.java @@ -1,4 +1,4 @@ -package com.example.umc7th.global.oauth.dto; +package com.example.umc7th.domain.member.dto; import lombok.Getter; diff --git a/src/main/java/com/example/umc7th/domain/member/enums/Role.java b/src/main/java/com/example/umc7th/domain/member/enums/Role.java deleted file mode 100644 index 845af0ce..00000000 --- a/src/main/java/com/example/umc7th/domain/member/enums/Role.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.umc7th.domain.member.enums; - -public enum Role { - GENERAL, ADMIN -} diff --git a/src/main/java/com/example/umc7th/domain/member/enums/SocialType.java b/src/main/java/com/example/umc7th/domain/member/enums/SocialType.java new file mode 100644 index 00000000..20fe775b --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/member/enums/SocialType.java @@ -0,0 +1,5 @@ +package com.example.umc7th.domain.member.enums; + +public enum SocialType { + KAKAO, GOOGLE, NAVER; +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java b/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java index e63836b0..4c8f7abe 100644 --- a/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/com/example/umc7th/domain/member/exception/MemberErrorCode.java @@ -13,8 +13,10 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."), ALREADY_EXIST(HttpStatus.BAD_REQUEST, "MEMBER400", "이미 존재하는 사용자입니다."), INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401", "비밀번호가 틀립니다."), - OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "비밀번호가 틀립니다."), - OAUTH_USER_INFO_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "비밀번호가 틀립니다."), + + OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "인가코드로 토큰을 가져오는데 실패했습니다."), + OAUTH_USER_INFO_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401", "토큰으로 사용자 정보를 가져오는 데 실패했습니다."), + UNSUPPORTED_OAUTH_TYPE(HttpStatus.BAD_REQUEST, "MEMBER400", "지원하지 않는 소셜 로그인입니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/example/umc7th/domain/member/principal/PrincipalDetailsService.java b/src/main/java/com/example/umc7th/domain/member/principal/PrincipalDetailsService.java index c5359f8e..8799479b 100644 --- a/src/main/java/com/example/umc7th/domain/member/principal/PrincipalDetailsService.java +++ b/src/main/java/com/example/umc7th/domain/member/principal/PrincipalDetailsService.java @@ -23,7 +23,6 @@ public class PrincipalDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(username).orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); - // MemberException과 MemberErrorCode도 한 번 작성해주세요. return new PrincipalDetails(member); } } diff --git a/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2Service.java b/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2Service.java new file mode 100644 index 00000000..ec0046e0 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2Service.java @@ -0,0 +1,7 @@ +package com.example.umc7th.domain.member.service.command; + +import com.example.umc7th.domain.member.dto.MemberResponseDTO; + +public interface OAuth2Service { + MemberResponseDTO.MemberTokenDTO login(String provider, String code); +} diff --git a/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2ServiceImpl.java b/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2ServiceImpl.java new file mode 100644 index 00000000..23398967 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/member/service/command/OAuth2ServiceImpl.java @@ -0,0 +1,144 @@ +package com.example.umc7th.domain.member.service.command; + +import com.example.umc7th.domain.member.dto.MemberResponseDTO; +import com.example.umc7th.domain.member.dto.OAuth2DTO; +import com.example.umc7th.domain.member.entity.Member; +import com.example.umc7th.domain.member.enums.SocialType; +import com.example.umc7th.domain.member.exception.MemberErrorCode; +import com.example.umc7th.domain.member.exception.MemberException; +import com.example.umc7th.domain.member.repository.MemberRepository; +import com.example.umc7th.global.jwt.JwtProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class OAuth2ServiceImpl implements OAuth2Service { + + @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") + private String tokenURI; // Resource Server에 토큰 요청시 사용할 URI + + @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") + private String userInfoURI;// 사용자 정보 가져올 때 사용할 URI + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String clientId;// API KEY + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String redirectURI;// 설정한 Redirect uri + + private final MemberRepository memberRepository; + private final JwtProvider jwtProvider; + + @Override + public MemberResponseDTO.MemberTokenDTO login(String provider, String code) { + if (provider.equalsIgnoreCase(SocialType.KAKAO.name())) { + return loginWithKakao(code); + } else { + throw new MemberException(MemberErrorCode.UNSUPPORTED_OAUTH_TYPE); + } + } + + private MemberResponseDTO.MemberTokenDTO loginWithKakao(String code) { + String token = getAccessTokenFromKakao(code); + OAuth2DTO.KakaoProfile profile = getProfileFromKakao(token); + String email = profile.getKakao_account().getEmail(); + return loginOrSignup(SocialType.KAKAO, email); + } + + private MemberResponseDTO.MemberTokenDTO loginOrSignup(SocialType socialType, String email) { + // SocialType을 Member에 provider라는 필드로 추가해서 저장해도 좋음 + Member member; + Optional optional = memberRepository.findByEmail(email); + + // orElse() 는 즉시 평가 이기 때문에 값이 있던 없던 내부 로직이 실행 되어 비 효율적 임 + // 아래 처럼 로직을 분리 하면 성능을 높일 수 있음 + member = optional.orElseGet(() -> memberRepository.save(Member.builder() + .email(email) + .role("ROLE_USER") + .build())); + + + return MemberResponseDTO.MemberTokenDTO.builder() + .accessToken(jwtProvider.createAccessToken(member)) + .refreshToken(jwtProvider.createRefreshToken(member)) + .build(); + } + + //인가코드로 토큰 가져오기 + private String getAccessTokenFromKakao(String accessCode) { + // 인가코드 토큰 가져오기 + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders httpHeaders = new HttpHeaders(); + + //Content-Type == HTTP 요청 본문의 데이터 형식을 명시 + //application/x-www-form-urlencoded == 데이터가 URL 인코딩 방식으로 전송됨 + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", clientId); + map.add("redirect_uri", redirectURI); + map.add("code", accessCode); + HttpEntity request = new HttpEntity<>(map, httpHeaders); + + ResponseEntity response1 = restTemplate.exchange(//이게 http요청을 서버로 보내고 서버로 받은 응답을 반환하는 메서드임 + tokenURI, // URI + HttpMethod.POST, // Method + request, // Request 내용 + String.class); // 받을 응답 자료형 + + ObjectMapper objectMapper = new ObjectMapper(); + OAuth2DTO.OAuth2TokenDTO oAuth2TokenDTO = null; + + try {//objectMapper.readValue(변환할 json, 바꿀 클래스.class) -> json 데이터를 java 객체로 변경 + oAuth2TokenDTO = objectMapper.readValue(response1.getBody(), OAuth2DTO.OAuth2TokenDTO.class); + return oAuth2TokenDTO.getAccess_token(); + } catch (Exception e) { + throw new MemberException(MemberErrorCode.OAUTH_TOKEN_FAIL); + } + } + + //토큰으로 카카오서버에서 유저정보 가져오기 + private OAuth2DTO.KakaoProfile getProfileFromKakao(String accessToken) { + // 토큰으로 정보 가져오기 + RestTemplate restTemplate = new RestTemplate();// 요청을 보내기 위한 RestTemplate + HttpHeaders httpHeaders = new HttpHeaders();//헤더 선언 + + //Content-Type == HTTP 요청 본문의 데이터 형식을 명시 + //application/x-www-form-urlencoded == 데이터가 URL 인코딩 방식으로 전송됨 + + //Authorization == 인증 관련 정보를 서버에 전달하기 위한 헤더 + httpHeaders.add("Authorization", "Bearer " + accessToken);//Bearer 뒤에 토큰을 추가하면 서버가 요청자의 인증 상태를 확인 가능 + httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + + HttpEntity request1 = new HttpEntity<>(httpHeaders); + + ResponseEntity response2 = restTemplate.exchange( + userInfoURI,// URI + HttpMethod.GET, // Method + request1, // Request 내용 + String.class // 받을 응답 자료형 + ); + + + ObjectMapper om = new ObjectMapper(); + + try { + return om.readValue(response2.getBody(), OAuth2DTO.KakaoProfile.class); + } catch (Exception e) { + throw new MemberException(MemberErrorCode.OAUTH_USER_INFO_FAIL); + } + } +} diff --git a/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClient.java b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClient.java new file mode 100644 index 00000000..daff2e91 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClient.java @@ -0,0 +1,11 @@ +package com.example.umc7th.domain.openAPI.component; + +import org.springframework.web.reactive.function.client.WebClient; + +public interface OpenApiWebClient { + // 한국 관광정보를 가져올 수 있는 WebClient를 반환하는 메소드 정의 + public WebClient getTourWebClient(String language); + // 아래에 메소드를 추가하면서 확장해나갈 수 있겠죠? + +} + diff --git a/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java new file mode 100644 index 00000000..981af532 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java @@ -0,0 +1,51 @@ +package com.example.umc7th.domain.openAPI.component; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +@Component +@Slf4j +public class OpenApiWebClientImpl implements OpenApiWebClient { + + @Override + public WebClient getTourWebClient(String language) { + if (language.equals("korean")) { + return getWebClient("https://apis.data.go.kr/B551011/KorService1"); + } else if (language.equals("english")) { + return getWebClient("https://apis.data.go.kr/B551011/EngService1"); + } else { + throw new OpenApiException(OpenApiErrorCode.UNSUPPORTED_LANGUAGE); + } + } + + // 영문 API를 추가한 경우 + // @Override + // public WebClient getEnglishTourWebClient() { + // return getWebClient("https://apis.data.go.kr/B551011/EngService1"); + // } + + private WebClient getWebClient(String baseUrl) { + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(20000)); + + // Uri를 build하는 factory 생성 (baseUrl을 WebClient 대신 여기에 포함하도록) + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); + // Uri factory에 인코딩 모드를 NONE으로 바꾸어 인코딩하지 않도록해줍니다. + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + + return WebClient.builder() + .uriBuilderFactory(factory) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .filter((request, next) -> { + log.info("Web Client Request: " + request.url()); + return next.exchange(request); + }) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/openAPI/controller/OpenApiController.java b/src/main/java/com/example/umc7th/domain/openAPI/controller/OpenApiController.java new file mode 100644 index 00000000..271372d5 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/controller/OpenApiController.java @@ -0,0 +1,23 @@ +package com.example.umc7th.domain.openAPI.controller; + +import com.example.umc7th.domain.openAPI.dto.OpenApiResponseDTO; +import com.example.umc7th.domain.openAPI.service.OpenApiQueryService; +import com.example.umc7th.global.apiPayload.CustomResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class OpenApiController { + + private final OpenApiQueryService openApiQueryService; + + @GetMapping("/searchStay") + public CustomResponse controller(@RequestParam(name = "arrange", defaultValue = "A") String arrange, + @RequestParam(name = "page", defaultValue = "1") int page, + @RequestParam(name = "offset", defaultValue = "10") int offset) { + return CustomResponse.onSuccess(openApiQueryService.searchStay(arrange, page, offset)); + } +} diff --git a/src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java b/src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java new file mode 100644 index 00000000..3d21c032 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java @@ -0,0 +1,46 @@ +package com.example.umc7th.domain.openAPI.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class OpenApiResponseDTO { + + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Getter + // 해당 어노테이션으로 json 값을 Parsing할 때 필드가 없는 경우 무시하여 에러가 터지는 것을 방지, 한번 없이 돌려보시면 이해가 더 잘 되실겁니다. + @JsonIgnoreProperties(ignoreUnknown = true) + public static class SearchStayResponseDTO { + // 아래 변수는 Api 명세서의 응답을 보고 그대로 받고 싶은 값들만 똑같은 이름으로 만들어줍니다. + private String addr1; + private String title; + private String tel; + private String contentid; + private String contenttypeid; + private String createdtime; + private String firstimage; + private String firstimage2; + private String mapx; + private String mapy; + } + + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Getter + public static class SearchStayResponseListDTO { + private List item; + + public static SearchStayResponseListDTO from(List list) { + return SearchStayResponseListDTO.builder() + .item(list) + .build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryService.java b/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryService.java new file mode 100644 index 00000000..bffe6662 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryService.java @@ -0,0 +1,8 @@ +package com.example.umc7th.domain.openAPI.service; + + +import com.example.umc7th.domain.openAPI.dto.OpenApiResponseDTO; + +public interface OpenApiQueryService { + public OpenApiResponseDTO.SearchStayResponseListDTO searchStay(String arrange, int page, int offset); +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryServiceImpl.java b/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryServiceImpl.java new file mode 100644 index 00000000..e8a1dcdf --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/service/OpenApiQueryServiceImpl.java @@ -0,0 +1,80 @@ +package com.example.umc7th.domain.openAPI.service; + +import com.example.umc7th.domain.openAPI.component.OpenApiWebClient; +import com.example.umc7th.domain.openAPI.dto.OpenApiResponseDTO; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OpenApiQueryServiceImpl implements OpenApiQueryService { + + // WebClient를 가져오기 위한 빈 주입 + private final OpenApiWebClient openApiWebClient; + + @Value("${openapi.tour.serviceKey}") + private String serviceKey; // 인증 키 + + @Override + public OpenApiResponseDTO.SearchStayResponseListDTO searchStay(String arrange, int page, int offset) { + // Web Client 가져오기 + WebClient webClient = openApiWebClient.getTourWebClient("korean"); + Mono mono = webClient.get() // get method 사용 + // UriBuilder를 이용하여 Endpoint와 Query Param 설정 + .uri(uri -> uri + .path("/searchStay1") + .queryParam("numOfRows", offset) + .queryParam("pageNo", page) + .queryParam("MobileOS", "ETC") + .queryParam("MobileApp", "AppTest") + .queryParam("_type", "json") + .queryParam("arrange", arrange) + .queryParam("serviceKey", serviceKey) + .build()) + // 응답을 가져오기 위한 method (.onStatus()를 이용해서 Http 상태코드에 따라 다르게 처리해줄 수 있음) + .retrieve() + // 응답에서 body만 String 타입으로 가져오기 (ResponseEntity 중 Object만 String 형식으로 가져오기) + .bodyToMono(String.class) + // String 값을 메소드로 매핑하여 OpenApiResponseDTO.SearchStayResponseListDTO로 변경하기 + .map(this::toSearchStayResponseListDTO) + // 에러가 발생한 경우 log를 찍도록 + .doOnError(e -> log.error("Open Api 에러 발생: " + e.getMessage())) + // 성공한 경우에도 log를 찍도록 + .doOnSuccess(s -> log.info("관광 정보를 가져오는데 성공했습니다.")); + + // block()을 사용해서 응답을 바로 가져오도록 + return mono.block(); + } + + private OpenApiResponseDTO.SearchStayResponseListDTO toSearchStayResponseListDTO(String response) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + // item으로 담을 list 선언 + List list = new ArrayList<>(); + // JsonNode 형식으로 응답을 읽고 item이 담긴 배열만 읽고 싶기에 item이 있는 배열까지 들어가기 + JsonNode jsonNode = objectMapper.readTree(response).path("response").path("body").path("items").path("item"); + // item 하나씩 처리 + for (JsonNode node : jsonNode) { + // item 하나씩 읽어서 OpenApiResponseDTO.SearchStayResponseDTO로 변경해서 List에 추가 + list.add(objectMapper.convertValue(node, OpenApiResponseDTO.SearchStayResponseDTO.class)); + } + // 응답을 만들어서 반환 + return OpenApiResponseDTO.SearchStayResponseListDTO.from(list); + } catch (Exception e) { + // 에러 처리 + e.fillInStackTrace(); + } + return OpenApiResponseDTO.SearchStayResponseListDTO.from(null); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/config/SecurityConfig.java b/src/main/java/com/example/umc7th/global/config/SecurityConfig.java index 2993cd1c..824291be 100644 --- a/src/main/java/com/example/umc7th/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc7th/global/config/SecurityConfig.java @@ -1,7 +1,7 @@ package com.example.umc7th.global.config; -//import com.example.umc7th.global.jwt.JwtAccessDeniedHandler; -//import com.example.umc7th.global.jwt.JwtAuthenticationEntryPoint; +//import com.example.umc7th.global.jwt.handler.JwtAccessDeniedHandler; +//import com.example.umc7th.global.jwt.handler.JwtAuthenticationEntryPoint; //import com.example.umc7th.global.jwt.JwtFilter; //import lombok.RequiredArgsConstructor; //import org.springframework.context.annotation.Bean; @@ -15,10 +15,10 @@ //import org.springframework.web.filter.Filter; import com.example.umc7th.domain.member.principal.PrincipalDetailsService; -import com.example.umc7th.global.jwt.JwtAccessDeniedHandler; -import com.example.umc7th.global.jwt.JwtAuthenticationEntryPoint; import com.example.umc7th.global.jwt.JwtFilter; import com.example.umc7th.global.jwt.JwtProvider; +import com.example.umc7th.global.jwt.handler.JwtAccessDeniedHandler; +import com.example.umc7th.global.jwt.handler.JwtAuthenticationEntryPoint; import jakarta.servlet.Filter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -63,9 +63,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .anyRequest().authenticated()) // jwtFilter를 UsernamePasswordAuthenticationFilter 앞에 오도록 설정 .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class) - // formLogin 비활성화 + // formLogin 비활성화 (OAuth 사용할거라) (spring security는 기본적으로 로그인 html폼 제공) .formLogin(AbstractHttpConfigurer::disable) - // httpBasic 비활성화 + // httpBasic 비활성화 (OAuth 사용으로 대체) .httpBasic(HttpBasicConfigurer::disable) // OAuth2 Login 설정을 default로 설정 .oauth2Login(Customizer.withDefaults())// ***OAuth2추가**** @@ -89,6 +89,7 @@ public Filter jwtFilter() { return new JwtFilter(jwtProvider, principalDetailsService); } + //PasswordEncoder @Bean public PasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/com/example/umc7th/global/config/WebConfig.java b/src/main/java/com/example/umc7th/global/config/WebConfig.java new file mode 100644 index 00000000..f1df9b18 --- /dev/null +++ b/src/main/java/com/example/umc7th/global/config/WebConfig.java @@ -0,0 +1,37 @@ +package com.example.umc7th.global.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +@Configuration +@Slf4j +public class WebConfig { + + @Bean + public WebClient webClient() { + // 연결 설정 + // TCP 연결 시 응답 시간 초과 값을 설정 + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofMillis(20000)); + + // WebClient 생성 + return WebClient.builder() + // Base URL 설정 + .baseUrl("https://apis.data.go.kr/B551011/KorService1") + // 만들었던 연결 설정 넣어주기 + .clientConnector(new ReactorClientHttpConnector(httpClient)) + // filter를 사용해서 요청을 보낼 때 로그가 찍히도록 + .filter((request, next) -> { + log.info("Web Client Request: " + request.url()); + return next.exchange(request); + }) + // build로 객체 생성 + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/jwt/JwtAccessDeniedHandler.java b/src/main/java/com/example/umc7th/global/jwt/handler/JwtAccessDeniedHandler.java similarity index 97% rename from src/main/java/com/example/umc7th/global/jwt/JwtAccessDeniedHandler.java rename to src/main/java/com/example/umc7th/global/jwt/handler/JwtAccessDeniedHandler.java index 101f2d36..070bd6bc 100644 --- a/src/main/java/com/example/umc7th/global/jwt/JwtAccessDeniedHandler.java +++ b/src/main/java/com/example/umc7th/global/jwt/handler/JwtAccessDeniedHandler.java @@ -1,4 +1,4 @@ -package com.example.umc7th.global.jwt; +package com.example.umc7th.global.jwt.handler; import com.example.umc7th.global.apiPayload.CustomResponse; import com.example.umc7th.global.apiPayload.code.GeneralErrorCode; diff --git a/src/main/java/com/example/umc7th/global/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/umc7th/global/jwt/handler/JwtAuthenticationEntryPoint.java similarity index 96% rename from src/main/java/com/example/umc7th/global/jwt/JwtAuthenticationEntryPoint.java rename to src/main/java/com/example/umc7th/global/jwt/handler/JwtAuthenticationEntryPoint.java index f04923e2..e088eb5f 100644 --- a/src/main/java/com/example/umc7th/global/jwt/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/umc7th/global/jwt/handler/JwtAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package com.example.umc7th.global.jwt; +package com.example.umc7th.global.jwt.handler; import com.example.umc7th.global.apiPayload.CustomResponse; diff --git a/src/main/java/com/example/umc7th/global/oauth/controller/OAuthController.java b/src/main/java/com/example/umc7th/global/oauth/controller/OAuthController.java deleted file mode 100644 index eabb7619..00000000 --- a/src/main/java/com/example/umc7th/global/oauth/controller/OAuthController.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.umc7th.global.oauth.controller; - -import com.example.umc7th.domain.member.dto.MemberResponseDTO; -import com.example.umc7th.global.apiPayload.CustomResponse; -import com.example.umc7th.global.oauth.service.OAuth2Service; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - - -@RestController -@RequiredArgsConstructor -public class OAuthController { - - private final OAuth2Service oAuth2Service; - - @GetMapping("/oauth2/callback/kakao") - // queryParam 형식으로 코드를 받을 예정이니 RequestParam을 설정해줍니다 - // 응답은 저희 서버에 로그인 다 한 뒤에 토큰을 제공할 예정이니 TokenDTO로 설정해줍니다. - public CustomResponse loginWithKakao(@RequestParam("code") String code) { - // 로직 구현 필요 - System.out.println(code); - return null; - // 서비스 생성 이후 - // return CustomResponse.onSuccess(oAuth2Service.login(code)); - - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/global/oauth/service/OAuth2Service.java b/src/main/java/com/example/umc7th/global/oauth/service/OAuth2Service.java deleted file mode 100644 index 8d2c9c4c..00000000 --- a/src/main/java/com/example/umc7th/global/oauth/service/OAuth2Service.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.umc7th.global.oauth.service; - -import com.example.umc7th.domain.member.dto.MemberResponseDTO; - -public interface OAuth2Service { - MemberResponseDTO.MemberTokenDTO login(String code); -} diff --git a/src/main/java/com/example/umc7th/global/oauth/service/OAuth2ServiceImpl.java b/src/main/java/com/example/umc7th/global/oauth/service/OAuth2ServiceImpl.java deleted file mode 100644 index a560977e..00000000 --- a/src/main/java/com/example/umc7th/global/oauth/service/OAuth2ServiceImpl.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.example.umc7th.global.oauth.service; - -import com.example.umc7th.domain.member.dto.MemberResponseDTO; -import com.example.umc7th.domain.member.entity.Member; -import com.example.umc7th.domain.member.exception.MemberErrorCode; -import com.example.umc7th.domain.member.exception.MemberException; -import com.example.umc7th.domain.member.repository.MemberRepository; -import com.example.umc7th.global.jwt.JwtProvider; -import com.example.umc7th.global.oauth.dto.OAuth2DTO; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; - -@Service -@RequiredArgsConstructor -public class OAuth2ServiceImpl implements OAuth2Service { - - @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") - private String tokenURI; // Resource Server에 토큰 요청시 사용할 URI - - @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") - private String userInfoURI; // 사용자 정보 가져올 때 사용할 URI - - @Value("${spring.security.oauth2.client.registration.kakao.client-id}") - private String clientId; // API KEY - - @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") - private String redirectURI; // 설정한 Redirect uri - - private final MemberRepository memberRepository; - private final JwtProvider jwtProvider; - - @Override - public MemberResponseDTO.MemberTokenDTO login(String code) { - // 인가코드 토큰 가져오기 - RestTemplate restTemplate = new RestTemplate(); // 요청을 보내기 위한 RestTemplate - HttpHeaders httpHeaders = new HttpHeaders(); // 헤더 선언 - - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded"); // 헤더 설정 - - MultiValueMap map = new LinkedMultiValueMap<>(); // RequestBody 설정 - map.add("grant_type", "authorization_code"); - map.add("client_id", clientId); - map.add("redirect_uri", redirectURI); - map.add("code", code); - HttpEntity request = new HttpEntity<>(map, httpHeaders); // Header와 Body를 이용하여 요청에 보낼 HttpEntity 생성 - - // 요청을 보내서 응답 받아오기 - ResponseEntity response1 = restTemplate.exchange( - tokenURI, // URI - HttpMethod.POST, // Method - request, // Request 내용 - String.class); // 받을 응답 자료형 - - ObjectMapper objectMapper = new ObjectMapper(); // String을 OAuth2DTO.OAuth2TokenDTO로 변경하기 위해 ObjectMapper 선언 - OAuth2DTO.OAuth2TokenDTO oAuth2TokenDTO = null; - - try { - oAuth2TokenDTO = objectMapper.readValue(response1.getBody(), OAuth2DTO.OAuth2TokenDTO.class); - } catch (Exception e) { - throw new MemberException(MemberErrorCode.OAUTH_TOKEN_FAIL); // 토큰 DTO로 변경하지 못한 경우 Exception 보냄 - } - - // 토큰으로 정보 가져오기 - // 위와 흐름이 동일하여 생략하겠습니다. 아래는 RequestBody가 없어서 추가하지 않은 것을 볼 수 있습니다. - restTemplate = new RestTemplate(); - httpHeaders = new HttpHeaders(); - - httpHeaders.add("Authorization", "Bearer " + oAuth2TokenDTO.getAccess_token()); - httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); - - HttpEntity request1 = new HttpEntity<>(httpHeaders); - - ResponseEntity response2 = restTemplate.exchange( - userInfoURI, - HttpMethod.GET, - request1, - String.class - ); - - OAuth2DTO.KakaoProfile profile = null; - ObjectMapper om = new ObjectMapper(); - - try { - profile = om.readValue(response2.getBody(), OAuth2DTO.KakaoProfile.class); - } catch (Exception e) { - throw new MemberException(MemberErrorCode.OAUTH_USER_INFO_FAIL); // 사용자 정보를 가져오지 못한 경우 Exception 발생 - } - - // 회원가입이 되었으면 사용자 로그인 안되어있으면 회원가입 후 로그인 - String email = profile.getId().toString(); // Kakao에서의 Id를 가지고 Email로 변경 - - // email을 찾고 있으면 member에 넣고 없으면 새로 만들어서 저장하고 넣는다. - Member member = memberRepository.findByEmail(email).orElse( - memberRepository.save(Member.builder() - .email(email) - .role("ROLE_USER") - .build()) - ); - - // TokenDTO로 변경해서 저번 주차에 구현한 JWT 형태로 반환 - return MemberResponseDTO.MemberTokenDTO.builder() - .accessToken(jwtProvider.createAccessToken(member)) - .refreshToken(jwtProvider.createRefreshToken(member)) - .build(); - } -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 503e9e04..e337bba1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,4 +46,8 @@ Jwt: secret: ${JWT_SECRET} token: access-expiration-time: 3600000 # Milliseconds for 1 hour - refresh-expiration-time: 2592000000 # Milliseconds for 30 days \ No newline at end of file + refresh-expiration-time: 2592000000 # Milliseconds for 30 days + +openapi: + tour: + serviceKey: ${SERVICE_KEY} \ No newline at end of file From 689f601b8c2028f36a18c88b69a49cd9642fb888 Mon Sep 17 00:00:00 2001 From: CHO Date: Tue, 19 Nov 2024 06:02:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat=20:=20openAPI=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve : #164 --- .../component/OpenApiWebClientImpl.java | 4 +++- ...esponseDTO.java => OpenApiResponseDTO.java} | 0 .../openAPI/exception/OpenAPIErrorCode.java | 18 ++++++++++++++++++ .../openAPI/exception/OpenAPIException.java | 9 +++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) rename src/main/java/com/example/umc7th/domain/openAPI/dto/{SearchStayResponseDTO.java => OpenApiResponseDTO.java} (100%) create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIErrorCode.java create mode 100644 src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIException.java diff --git a/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java index 981af532..af5d4e93 100644 --- a/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java +++ b/src/main/java/com/example/umc7th/domain/openAPI/component/OpenApiWebClientImpl.java @@ -1,5 +1,7 @@ package com.example.umc7th.domain.openAPI.component; +import com.example.umc7th.domain.openAPI.exception.OpenAPIErrorCode; +import com.example.umc7th.domain.openAPI.exception.OpenAPIException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; @@ -20,7 +22,7 @@ public WebClient getTourWebClient(String language) { } else if (language.equals("english")) { return getWebClient("https://apis.data.go.kr/B551011/EngService1"); } else { - throw new OpenApiException(OpenApiErrorCode.UNSUPPORTED_LANGUAGE); + throw new OpenAPIException(OpenAPIErrorCode.UNSUPPORTED_LANGUAGE); } } diff --git a/src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java b/src/main/java/com/example/umc7th/domain/openAPI/dto/OpenApiResponseDTO.java similarity index 100% rename from src/main/java/com/example/umc7th/domain/openAPI/dto/SearchStayResponseDTO.java rename to src/main/java/com/example/umc7th/domain/openAPI/dto/OpenApiResponseDTO.java diff --git a/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIErrorCode.java b/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIErrorCode.java new file mode 100644 index 00000000..f65cdb63 --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIErrorCode.java @@ -0,0 +1,18 @@ +package com.example.umc7th.domain.openAPI.exception; + +import com.example.umc7th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum OpenAPIErrorCode implements BaseErrorCode { + + UNSUPPORTED_LANGUAGE(HttpStatus.NOT_FOUND, "OpenAPI404", "댓글을 찾지 못했습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIException.java b/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIException.java new file mode 100644 index 00000000..cc63193f --- /dev/null +++ b/src/main/java/com/example/umc7th/domain/openAPI/exception/OpenAPIException.java @@ -0,0 +1,9 @@ +package com.example.umc7th.domain.openAPI.exception; + +import com.example.umc7th.global.apiPayload.exception.CustomException; + +public class OpenAPIException extends CustomException { + public OpenAPIException(OpenAPIErrorCode code) { + super(code); + } +}