diff --git a/.gitignore b/.gitignore index e47ee75..dd0b8da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +application-local.yml # Created by https://www.toptal.com/developers/gitignore/api/intellij,java,gradle # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,java,gradle diff --git a/build.gradle b/build.gradle index 939ebcc..167572b 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,17 @@ repositories { mavenCentral() } +ext { + set('springCloudVersion', "2023.0.0") +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + + dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' @@ -38,6 +49,42 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' + //jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //security + implementation 'org.springframework.boot:spring-boot-starter-security' + + //oauth 2.0 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64' + + //kakao access token을 가져오기 위한 Oauth 서버와의 통신을 위한 webclient + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.projectreactor.netty:reactor-netty' + + //redis & cache + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + + //spring-cloud & openfeign + implementation 'org.springframework.cloud:spring-cloud-starter-config' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + + + + + + + + //oauth2-client + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + //Oauth 서버와의 통신을 위한 webclient + implementation 'org.springframework.boot:spring-boot-starter-webflux' + //jwt + implementation 'io.jsonwebtoken:jjwt:0.9.1' } diff --git a/config b/config index 36ae41c..d6baa8f 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 36ae41c65ede1ec8c777af7c3edd98a43da82bc4 +Subproject commit d6baa8f22ccb9fde570118808c313ca134f292ab diff --git a/src/main/java/com/example/tripy/TripyApplication.java b/src/main/java/com/example/tripy/TripyApplication.java index 468741c..82f0430 100644 --- a/src/main/java/com/example/tripy/TripyApplication.java +++ b/src/main/java/com/example/tripy/TripyApplication.java @@ -4,17 +4,17 @@ import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; +import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @OpenAPIDefinition(servers = { @Server(url = "/", description = "Default Server URL") }) + @EnableScheduling @SpringBootApplication +@EnableFeignClients @EnableJpaAuditing public class TripyApplication { @@ -22,19 +22,4 @@ public static void main(String[] args) { SpringApplication.run(TripyApplication.class, args); } - -// @Bean -// public WebMvcConfigurer corsConfigurer() { -// return new WebMvcConfigurer() { -// @Override -// public void addCorsMappings(CorsRegistry registry) { -// registry.addMapping("/**") -// .allowedOrigins("*") -// .allowedMethods("GET", "POST", "PUT", "DELETE") -// .allowedHeaders("*") -// .maxAge(3000); -// } -// }; -// } - } diff --git a/src/main/java/com/example/tripy/domain/auth/AuthController.java b/src/main/java/com/example/tripy/domain/auth/AuthController.java new file mode 100644 index 0000000..0af3e24 --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/AuthController.java @@ -0,0 +1,30 @@ +package com.example.tripy.domain.auth; + + +import com.example.tripy.domain.auth.dto.AuthResponseDto.LoginSimpleInfo; +import com.example.tripy.global.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/auth") +@Tag(name="Auth", description = "인증(로그인, 회원가입) 관련") +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + @Operation(summary = "로그인/회원가입 api", description = "code : Authorization code / 회원가입, 로그인 구분 없이 동일한 API 사용") + public ApiResponse getIdToken(@RequestParam String code){ + String idToken = authService.getOauth2Authentication(code); + return ApiResponse.onSuccess(authService.login(idToken)); + } + + +} diff --git a/src/main/java/com/example/tripy/domain/auth/AuthService.java b/src/main/java/com/example/tripy/domain/auth/AuthService.java new file mode 100644 index 0000000..88e05b5 --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/AuthService.java @@ -0,0 +1,100 @@ +package com.example.tripy.domain.auth; + +import com.example.tripy.domain.auth.dto.AuthResponseDto.KakaoAccessTokenResponse; +import com.example.tripy.domain.auth.dto.AuthResponseDto.LoginSimpleInfo; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse; +import com.example.tripy.domain.member.Member; +import com.example.tripy.domain.member.MemberRepository; +import com.example.tripy.domain.member.enums.SocialType; +import com.example.tripy.global.security.JwtTokenProvider; +import java.util.Optional; +import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@RequiredArgsConstructor +@Service +public class AuthService { + private final KakaoAuthApiClient kakaoAuthApiClient; + private final RedisTemplate redisTemplate; + private final KakaoOauthHelper kakaoOauthHelper; + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + + @Value("${security.oauth2.client.kakao.authorization-grant-type}") + private String grantType; + @Value("${security.oauth2.client.kakao.client-id}") + private String clientId; + @Value("${security.oauth2.client.kakao.redirect-uri}") + private String redirectUri; + + + + + public String getOauth2Authentication(final String authorizationCode){ + KakaoAccessTokenResponse tokenInfo = kakaoAuthApiClient.getOAuth2AccessToken( + grantType, + clientId, + redirectUri, + authorizationCode + ); + return tokenInfo.idToken(); + } + + @Transactional + public LoginSimpleInfo login(String idToken){ + OIDCDecodePayload oidcDecodePayload = kakaoOauthHelper.getOIDCDecodePayload(idToken); + String email = oidcDecodePayload.email(); + String nickName = oidcDecodePayload.nickName(); + String picture = oidcDecodePayload.picture(); + Optional optionalMember = memberRepository.findByEmail(email); + + String accessToken = null; + String refreshToken = null; + Member member = null; + + //회원가입 + if(optionalMember.isEmpty()){ + member = Member.toEntity(email, nickName, picture, SocialType.KAKAO); + memberRepository.save(member); + accessToken = jwtTokenProvider.createAccessToken(member.getPayload()); + refreshToken = jwtTokenProvider.createRefreshToken(member.getId()); + } + if(optionalMember.isPresent()){ + accessToken = jwtTokenProvider.createAccessToken(optionalMember.get().getPayload()); + refreshToken = jwtTokenProvider.createRefreshToken(optionalMember.get().getId()); + member = optionalMember.get(); + + } + + member.updateToken(accessToken, refreshToken); + + return LoginSimpleInfo.toDTO(accessToken, refreshToken); + + + } + + @Cacheable(cacheNames = "KakaoOIDC", cacheManager = "oidcCacheManager") + public OIDCPublicKeysResponse getKakaoOIDCOpenKeys() { + return kakaoAuthApiClient.getKakaoOIDCOpenKeys(); + } + + public void updateOpenKeyTestRedis() { + OIDCPublicKeysResponse oidcPublicKeysResponse = getKakaoOIDCOpenKeys(); + saveOIDCPublicKeysResponse(oidcPublicKeysResponse); + } + public void saveOIDCPublicKeysResponse(OIDCPublicKeysResponse response) { + String key = "oidc:public_keys"; + + redisTemplate.opsForValue().set(key, response); + } + + +} diff --git a/src/main/java/com/example/tripy/domain/auth/KakaoAuthApiClient.java b/src/main/java/com/example/tripy/domain/auth/KakaoAuthApiClient.java new file mode 100644 index 0000000..431e35b --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/KakaoAuthApiClient.java @@ -0,0 +1,33 @@ +package com.example.tripy.domain.auth; + +import com.example.tripy.domain.auth.dto.AuthResponseDto.KakaoAccessTokenResponse; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +@FeignClient(name= "kakaoApiClient", url="https://kauth.kakao.com") +public interface KakaoAuthApiClient { + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoAccessTokenResponse getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code //인가코드 + ); + + + //@Cacheable(cacheNames = "KakaoOIDC", cacheManager = "oidcCacheManager") + @GetMapping("/.well-known/jwks.json") + OIDCPublicKeysResponse getKakaoOIDCOpenKeys(); + + + + + +} + + diff --git a/src/main/java/com/example/tripy/domain/auth/KakaoOauthHelper.java b/src/main/java/com/example/tripy/domain/auth/KakaoOauthHelper.java new file mode 100644 index 0000000..97abfb9 --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/KakaoOauthHelper.java @@ -0,0 +1,30 @@ +package com.example.tripy.domain.auth; + + +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class KakaoOauthHelper { + private final KakaoAuthApiClient kakaoAuthApiClient; + private final OauthOIDCHelper oauthOIDCHelper; + @Value("${security.oauth2.client.kakao.client-id}") + private String appId; + + @Value("${security.oauth2.client.kakao.iss}") + private String iss; + + public OIDCDecodePayload getOIDCDecodePayload(String token){ + OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoAuthApiClient.getKakaoOIDCOpenKeys(); + return oauthOIDCHelper.getPayloadFromIdToken( + token, iss, appId, oidcPublicKeysResponse + ); + } + + + +} diff --git a/src/main/java/com/example/tripy/domain/auth/OauthOIDCHelper.java b/src/main/java/com/example/tripy/domain/auth/OauthOIDCHelper.java new file mode 100644 index 0000000..f126a9e --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/OauthOIDCHelper.java @@ -0,0 +1,35 @@ +package com.example.tripy.domain.auth; + +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKey; +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCPublicKeysResponse; +import com.example.tripy.global.security.JwtOIDCProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + + +@RequiredArgsConstructor +@Component +public class OauthOIDCHelper { + + private final JwtOIDCProvider jwtOIDCProvider; + + //토큰에서 kid 가져온다 -> 가져온 kid는 공개키 결정에 사용 + private String getKidFromUnsignedIdToken(String token, String iss, String aud){ + return jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud); + } + + public OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse){ + String kid = getKidFromUnsignedIdToken(token, iss, aud); + + //같은 Kid인 공개키 불러와서 토큰 검증에 사용 + OIDCPublicKey oidcPublicKey = oidcPublicKeysResponse.keys().stream() + .filter(o-> o.kid().equals(kid)) + .findFirst() + .orElseThrow(); + + //검증 된 토큰에서 바디를 꺼내온다 + return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKey.n(), oidcPublicKey.e()); + } + +} diff --git a/src/main/java/com/example/tripy/domain/auth/dto/AuthResponseDto.java b/src/main/java/com/example/tripy/domain/auth/dto/AuthResponseDto.java new file mode 100644 index 0000000..9b42e92 --- /dev/null +++ b/src/main/java/com/example/tripy/domain/auth/dto/AuthResponseDto.java @@ -0,0 +1,76 @@ +package com.example.tripy.domain.auth.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +public class AuthResponseDto { + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class LoginSimpleInfo{ + private String accessToken; + private String refreshToken; + + public static LoginSimpleInfo toDTO(String accessToken, String refreshToken) { + return LoginSimpleInfo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + } + + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record KakaoAccessTokenResponse( + String idToken + ) { + public static KakaoAccessTokenResponse of( + final String idToken + ) { + return new KakaoAccessTokenResponse( + idToken + ); + } + + + + } + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record OIDCPublicKeysResponse(List keys) { + } + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record OIDCPublicKey( + String kid, + String kty, + String alg, + String use, + String n, + String e + ) { + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record OIDCDecodePayload( + String issuer, + String audience, + String subject, + String email, + String nickName, + String picture + + ){ + } + + + + + +} diff --git a/src/main/java/com/example/tripy/domain/bag/BagController.java b/src/main/java/com/example/tripy/domain/bag/BagController.java index b3adb82..c86278c 100644 --- a/src/main/java/com/example/tripy/domain/bag/BagController.java +++ b/src/main/java/com/example/tripy/domain/bag/BagController.java @@ -10,11 +10,13 @@ import com.example.tripy.domain.countrymaterial.CountryMaterialService; import com.example.tripy.domain.material.dto.MaterialRequestDto.CreateMaterialRequest; import com.example.tripy.domain.material.dto.MaterialResponseDto.MaterialListByCountry; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import com.example.tripy.domain.member.Member; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.ApiResponse; +import com.example.tripy.global.security.CurrentUser; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -36,17 +38,17 @@ public class BagController { private final BagService bagService; private final CountryMaterialService countryMaterialService; - /** - * [GET] 내 여행 가방 모두 불러오기 - */ - @Operation(summary = "내 여행 가방 모두 불러오기 (내 가방 목록 조회)", description = "여행 가방이 존재하는 목록을 불러옵니다.") - @Parameter(name = "page", description = "내 여행 가방 목록 페이지 번호, query string 입니다.") - @Parameter(name = "size", description = "내 여행 가방 목록 페이지 번호, query string 입니다.") - @GetMapping("/members/bags") - public ApiResponse>> getBagsList( - @RequestParam(value = "page") int page, @RequestParam(value = "size") int size) { - return ApiResponse.onSuccess(bagService.getTravelBagExistsList(page, size)); - } + + /** + * [GET] 내 여행 가방 모두 불러오기 + */ + + @GetMapping("/member/bag") + public ApiResponse>> getBagsList( + @CurrentUser Member member, @RequestParam(value = "page") int page, + @RequestParam(value = "size") int size) { + return ApiResponse.onSuccess(bagService.getTravelBagExistsList(page, size, member.getId())); + } /** * [POST] 내 여행 일정 목록에서 해당하는 가방 목록 생성하기 diff --git a/src/main/java/com/example/tripy/domain/bag/BagService.java b/src/main/java/com/example/tripy/domain/bag/BagService.java index 2e63dd5..ac19785 100644 --- a/src/main/java/com/example/tripy/domain/bag/BagService.java +++ b/src/main/java/com/example/tripy/domain/bag/BagService.java @@ -24,9 +24,9 @@ import com.example.tripy.domain.member.MemberRepository; import com.example.tripy.domain.travelplan.TravelPlan; import com.example.tripy.domain.travelplan.TravelPlanRepository; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import jakarta.transaction.Transactional; import java.util.List; import java.util.stream.Collectors; @@ -70,10 +70,10 @@ public List setBagListSimpleInfo(List travelPlans // 내 일정에 맞는 가방 목록 모두 불러오기 - public PageResponseDto> getTravelBagExistsList(int page, int size) { + public PageResponseDto> getTravelBagExistsList(int page, int size, Long id) { // Member 관련 메서드가 추가되면 수정 예정 - Member member = memberRepository.findById(1L) + Member member = memberRepository.findById(id) .orElseThrow(() -> new GeneralException(ErrorStatus._EMPTY_MEMBER)); Pageable pageable = PageRequest.of(page, size); diff --git a/src/main/java/com/example/tripy/domain/comment/CommentController.java b/src/main/java/com/example/tripy/domain/comment/CommentController.java index fb628e2..e27287c 100644 --- a/src/main/java/com/example/tripy/domain/comment/CommentController.java +++ b/src/main/java/com/example/tripy/domain/comment/CommentController.java @@ -2,8 +2,8 @@ import com.example.tripy.domain.comment.dto.CommentRequestDto.CreateCommentRequest; import com.example.tripy.domain.comment.dto.CommentResponseDto.GetCommentInfo; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.ApiResponse; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.ApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/example/tripy/domain/comment/CommentService.java b/src/main/java/com/example/tripy/domain/comment/CommentService.java index 80fa55d..0938381 100644 --- a/src/main/java/com/example/tripy/domain/comment/CommentService.java +++ b/src/main/java/com/example/tripy/domain/comment/CommentService.java @@ -5,9 +5,9 @@ import com.example.tripy.domain.member.MemberRepository; import com.example.tripy.domain.post.Post; import com.example.tripy.domain.post.PostRepository; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import com.example.tripy.global.utils.DateTimeConverter; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/tripy/domain/currency/CurrencyController.java b/src/main/java/com/example/tripy/domain/currency/CurrencyController.java index d934baa..dac5632 100644 --- a/src/main/java/com/example/tripy/domain/currency/CurrencyController.java +++ b/src/main/java/com/example/tripy/domain/currency/CurrencyController.java @@ -1,7 +1,7 @@ package com.example.tripy.domain.currency; import com.example.tripy.domain.currency.dto.CurrencyResponseDto; -import com.example.tripy.global.common.response.ApiResponse; +import com.example.tripy.global.response.ApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/example/tripy/domain/currency/CurrencyService.java b/src/main/java/com/example/tripy/domain/currency/CurrencyService.java index 1851ce1..5967a14 100644 --- a/src/main/java/com/example/tripy/domain/currency/CurrencyService.java +++ b/src/main/java/com/example/tripy/domain/currency/CurrencyService.java @@ -3,8 +3,8 @@ import com.example.tripy.domain.country.Country; import com.example.tripy.domain.country.CountryRepository; import com.example.tripy.domain.currency.dto.CurrencyResponseDto; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/tripy/domain/member/Member.java b/src/main/java/com/example/tripy/domain/member/Member.java index ab1dddb..d1c12db 100644 --- a/src/main/java/com/example/tripy/domain/member/Member.java +++ b/src/main/java/com/example/tripy/domain/member/Member.java @@ -2,15 +2,40 @@ import com.example.tripy.domain.member.enums.SocialType; import com.example.tripy.global.utils.BaseTimeEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Collection; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.cglib.core.Local; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import com.example.tripy.domain.bag.Bag; +import com.example.tripy.domain.friend.Friend; +import com.example.tripy.domain.member.enums.SocialType; +import com.example.tripy.domain.planfriend.PlanFriend; +import com.example.tripy.domain.post.Post; +import com.example.tripy.domain.travelplan.TravelPlan; +import com.example.tripy.global.utils.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @AllArgsConstructor @@ -30,12 +55,36 @@ public class Member extends BaseTimeEntity { @NotNull private String email; - @NotNull - private String password; private String profileImgUrl; + @NotNull private SocialType socialType; + private String accessToken; + private String refreshToken; + + + public String getPayload(){ + return this.getId()+"+tripy"; + } + + public static Member toEntity(String email, String nickName, String profileImgUrl, + SocialType socialType){ + return Member.builder() + .nickName(nickName) + .email(email) + .profileImgUrl(profileImgUrl) + .socialType(socialType) + .build(); + } + + public void updateToken(String accessToken, String refreshToken){ + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + + } \ No newline at end of file diff --git a/src/main/java/com/example/tripy/domain/member/MemberRepository.java b/src/main/java/com/example/tripy/domain/member/MemberRepository.java index 6152126..9bdad4b 100644 --- a/src/main/java/com/example/tripy/domain/member/MemberRepository.java +++ b/src/main/java/com/example/tripy/domain/member/MemberRepository.java @@ -1,6 +1,11 @@ package com.example.tripy.domain.member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); } diff --git a/src/main/java/com/example/tripy/domain/member/enums/SocialType.java b/src/main/java/com/example/tripy/domain/member/enums/SocialType.java index cfb24e9..3fb40db 100644 --- a/src/main/java/com/example/tripy/domain/member/enums/SocialType.java +++ b/src/main/java/com/example/tripy/domain/member/enums/SocialType.java @@ -1,6 +1,6 @@ package com.example.tripy.domain.member.enums; public enum SocialType { - KAKAO, NAVER, GOOGLE, APPLE + KAKAO, NAVER } diff --git a/src/main/java/com/example/tripy/domain/post/PostController.java b/src/main/java/com/example/tripy/domain/post/PostController.java index f088f2d..4daf340 100644 --- a/src/main/java/com/example/tripy/domain/post/PostController.java +++ b/src/main/java/com/example/tripy/domain/post/PostController.java @@ -3,8 +3,8 @@ import com.example.tripy.domain.post.dto.PostRequestDto.CreatePostRequest; import com.example.tripy.domain.post.dto.PostResponseDto.GetPostDetailInfo; import com.example.tripy.domain.post.dto.PostResponseDto.GetPostSimpleInfo; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.ApiResponse; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.ApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; diff --git a/src/main/java/com/example/tripy/domain/post/PostService.java b/src/main/java/com/example/tripy/domain/post/PostService.java index ab1baba..b10b879 100644 --- a/src/main/java/com/example/tripy/domain/post/PostService.java +++ b/src/main/java/com/example/tripy/domain/post/PostService.java @@ -12,9 +12,9 @@ import com.example.tripy.domain.posttag.PostTagService; import com.example.tripy.domain.travelplan.TravelPlan; import com.example.tripy.domain.travelplan.TravelPlanRepository; -import com.example.tripy.global.common.dto.PageResponseDto; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.common.PageResponseDto; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; diff --git a/src/main/java/com/example/tripy/domain/posttag/PostTagService.java b/src/main/java/com/example/tripy/domain/posttag/PostTagService.java index 7de6bbe..d778615 100644 --- a/src/main/java/com/example/tripy/domain/posttag/PostTagService.java +++ b/src/main/java/com/example/tripy/domain/posttag/PostTagService.java @@ -3,8 +3,8 @@ import com.example.tripy.domain.post.Post; import com.example.tripy.domain.tag.Tag; import com.example.tripy.domain.tag.TagRepository; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import jakarta.transaction.Transactional; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/tripy/domain/redis/RedisController.java b/src/main/java/com/example/tripy/domain/redis/RedisController.java new file mode 100644 index 0000000..b0dc4ed --- /dev/null +++ b/src/main/java/com/example/tripy/domain/redis/RedisController.java @@ -0,0 +1,31 @@ +package com.example.tripy.domain.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class RedisController { + + private final RedisService redisService; + + @Autowired + public RedisController(RedisService redisService) { + this.redisService = redisService; + } + + // Redis에 데이터 저장 + @GetMapping("/redis/set") + public void setRedisData(@RequestParam String key, @RequestParam String value) { + redisService.setValue(key, value); + } + + // Redis에서 데이터 검색 + @GetMapping("/redis/get/{key}") + public String getRedisData(@PathVariable String key) { + return redisService.getValue(key); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/tripy/domain/redis/RedisService.java b/src/main/java/com/example/tripy/domain/redis/RedisService.java new file mode 100644 index 0000000..75e0a98 --- /dev/null +++ b/src/main/java/com/example/tripy/domain/redis/RedisService.java @@ -0,0 +1,24 @@ +package com.example.tripy.domain.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class RedisService { + + private final StringRedisTemplate redisTemplate; + + @Autowired + public RedisService(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void setValue(String key, String value) { + redisTemplate.opsForValue().set(key, value); + } + + public String getValue(String key) { + return redisTemplate.opsForValue().get(key); + } +} diff --git a/src/main/java/com/example/tripy/domain/tag/TagController.java b/src/main/java/com/example/tripy/domain/tag/TagController.java index 52a17a0..64d3e96 100644 --- a/src/main/java/com/example/tripy/domain/tag/TagController.java +++ b/src/main/java/com/example/tripy/domain/tag/TagController.java @@ -1,10 +1,10 @@ package com.example.tripy.domain.tag; -import com.example.tripy.global.common.response.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.example.tripy.global.response.ApiResponse; @RequiredArgsConstructor @RestController diff --git a/src/main/java/com/example/tripy/domain/travelplan/TravelPlan.java b/src/main/java/com/example/tripy/domain/travelplan/TravelPlan.java index 765cbb4..ad7a982 100644 --- a/src/main/java/com/example/tripy/domain/travelplan/TravelPlan.java +++ b/src/main/java/com/example/tripy/domain/travelplan/TravelPlan.java @@ -43,4 +43,5 @@ public void updateBagExists() { //가방이 존재하지 않으면 true로 변경, 존재하면 false로 변경 this.bagExists = !this.bagExists; } + } diff --git a/src/main/java/com/example/tripy/domain/wheather/WeatherService.java b/src/main/java/com/example/tripy/domain/wheather/WeatherService.java index 0016336..56142ed 100644 --- a/src/main/java/com/example/tripy/domain/wheather/WeatherService.java +++ b/src/main/java/com/example/tripy/domain/wheather/WeatherService.java @@ -4,12 +4,12 @@ import com.example.tripy.domain.city.CityRepository; import com.example.tripy.domain.wheather.dto.WeatherResponseDto.WhetherResponseInfo; import com.example.tripy.domain.wheather.dto.WeatherResponseDto.WhetherResponseSimpleInfo; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; import jakarta.persistence.Tuple; import java.time.LocalDate; import java.time.LocalTime; import java.util.Comparator; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/tripy/global/common/dto/PageResponseDto.java b/src/main/java/com/example/tripy/global/common/PageResponseDto.java similarity index 86% rename from src/main/java/com/example/tripy/global/common/dto/PageResponseDto.java rename to src/main/java/com/example/tripy/global/common/PageResponseDto.java index 6a779d6..f2b4c83 100644 --- a/src/main/java/com/example/tripy/global/common/dto/PageResponseDto.java +++ b/src/main/java/com/example/tripy/global/common/PageResponseDto.java @@ -1,4 +1,4 @@ -package com.example.tripy.global.common.dto; +package com.example.tripy.global.common; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/example/tripy/global/config/RedisConfig.java b/src/main/java/com/example/tripy/global/config/RedisConfig.java new file mode 100644 index 0000000..40904de --- /dev/null +++ b/src/main/java/com/example/tripy/global/config/RedisConfig.java @@ -0,0 +1,71 @@ +package com.example.tripy.global.config; + + + +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + + template.setValueSerializer(serializer); + template.setKeySerializer(new StringRedisSerializer()); + + return template; + } + + @Bean + public CacheManager oidcCacheManager(RedisConnectionFactory cf){ + RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + // TTL 일주일로 설정 + .entryTtl(Duration.ofDays(7L)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } + + +} + diff --git a/src/main/java/com/example/tripy/global/config/SecurityConfig.java b/src/main/java/com/example/tripy/global/config/SecurityConfig.java new file mode 100644 index 0000000..edfd1ca --- /dev/null +++ b/src/main/java/com/example/tripy/global/config/SecurityConfig.java @@ -0,0 +1,33 @@ +package com.example.tripy.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableMethodSecurity +@RequiredArgsConstructor +@Configuration +public class SecurityConfig { + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(request -> + request.requestMatchers("/api/**").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .anyRequest().permitAll() + ) + .build(); + } +} diff --git a/src/main/java/com/example/tripy/global/config/SwaggerConfig.java b/src/main/java/com/example/tripy/global/config/SwaggerConfig.java index 1346c6f..df7592d 100644 --- a/src/main/java/com/example/tripy/global/config/SwaggerConfig.java +++ b/src/main/java/com/example/tripy/global/config/SwaggerConfig.java @@ -3,22 +3,39 @@ import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SwaggerConfig { + String jwtSchemeName = "JWT TOKEN"; @Bean public OpenAPI openAPI() { return new OpenAPI() .components(new Components()) - .info(apiInfo()); + .info(apiInfo()) + .addSecurityItem(securityRequirement()) + .components(components()); } private Info apiInfo() { return new Info() - .title("Springdoc 테스트") - .description("Springdoc을 사용한 Swagger UI 테스트") + .title("Tripy Swagger") + .description("트리피 Swagger 입니다!") .version("1.0.0"); } + private Components components(){ + return new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); + } + + private SecurityRequirement securityRequirement(){ + return new SecurityRequirement().addList(jwtSchemeName); + } } \ No newline at end of file diff --git a/src/main/java/com/example/tripy/global/config/WebClientConfig.java b/src/main/java/com/example/tripy/global/config/WebClientConfig.java new file mode 100644 index 0000000..84f0760 --- /dev/null +++ b/src/main/java/com/example/tripy/global/config/WebClientConfig.java @@ -0,0 +1,56 @@ +package com.example.tripy.global.config; + +import com.example.tripy.domain.member.MemberRepository; +import com.example.tripy.global.security.CurrentUserArgumentResolver; +import com.example.tripy.global.security.JwtTokenProvider; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import reactor.netty.http.client.HttpClient; +import java.time.Duration; +import java.util.function.Function; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ReactorResourceFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +@RequiredArgsConstructor +public class WebClientConfig implements WebMvcConfigurer { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + @Bean + public ReactorResourceFactory resourceFactory() { + ReactorResourceFactory factory = new ReactorResourceFactory(); + factory.setUseGlobalResources(false); + return factory; + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new CurrentUserArgumentResolver(jwtTokenProvider, memberRepository)); + } + + @Bean + public WebClient webClient() { + Function mapper = client -> client + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) + .doOnConnected(connection -> + connection.addHandlerLast(new ReadTimeoutHandler(10)) + .addHandlerLast(new WriteTimeoutHandler(10))) + .responseTimeout(Duration.ofSeconds(1)); + + ClientHttpConnector connector = + new ReactorClientHttpConnector(resourceFactory(), mapper); + return WebClient.builder().clientConnector(connector).build(); + } + + +} diff --git a/src/main/java/com/example/tripy/global/common/response/ApiResponse.java b/src/main/java/com/example/tripy/global/response/ApiResponse.java similarity index 88% rename from src/main/java/com/example/tripy/global/common/response/ApiResponse.java rename to src/main/java/com/example/tripy/global/response/ApiResponse.java index 32f8922..6c1f42d 100644 --- a/src/main/java/com/example/tripy/global/common/response/ApiResponse.java +++ b/src/main/java/com/example/tripy/global/response/ApiResponse.java @@ -1,6 +1,6 @@ -package com.example.tripy.global.common.response; +package com.example.tripy.global.response; -import com.example.tripy.global.common.response.code.status.SuccessStatus; +import com.example.tripy.global.response.code.status.SuccessStatus; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; diff --git a/src/main/java/com/example/tripy/global/common/response/code/BaseErrorCode.java b/src/main/java/com/example/tripy/global/response/code/BaseErrorCode.java similarity index 60% rename from src/main/java/com/example/tripy/global/common/response/code/BaseErrorCode.java rename to src/main/java/com/example/tripy/global/response/code/BaseErrorCode.java index 30cb88d..3e31480 100644 --- a/src/main/java/com/example/tripy/global/common/response/code/BaseErrorCode.java +++ b/src/main/java/com/example/tripy/global/response/code/BaseErrorCode.java @@ -1,4 +1,4 @@ -package com.example.tripy.global.common.response.code; +package com.example.tripy.global.response.code; public interface BaseErrorCode { public ErrorReasonDto getReasonHttpStatus(); diff --git a/src/main/java/com/example/tripy/global/common/response/code/ErrorReasonDto.java b/src/main/java/com/example/tripy/global/response/code/ErrorReasonDto.java similarity index 83% rename from src/main/java/com/example/tripy/global/common/response/code/ErrorReasonDto.java rename to src/main/java/com/example/tripy/global/response/code/ErrorReasonDto.java index a7bf565..47106eb 100644 --- a/src/main/java/com/example/tripy/global/common/response/code/ErrorReasonDto.java +++ b/src/main/java/com/example/tripy/global/response/code/ErrorReasonDto.java @@ -1,4 +1,4 @@ -package com.example.tripy.global.common.response.code; +package com.example.tripy.global.response.code; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/tripy/global/common/response/code/status/ErrorStatus.java b/src/main/java/com/example/tripy/global/response/code/status/ErrorStatus.java similarity index 84% rename from src/main/java/com/example/tripy/global/common/response/code/status/ErrorStatus.java rename to src/main/java/com/example/tripy/global/response/code/status/ErrorStatus.java index 14db7a3..52d1353 100644 --- a/src/main/java/com/example/tripy/global/common/response/code/status/ErrorStatus.java +++ b/src/main/java/com/example/tripy/global/response/code/status/ErrorStatus.java @@ -1,7 +1,7 @@ -package com.example.tripy.global.common.response.code.status; +package com.example.tripy.global.response.code.status; -import com.example.tripy.global.common.response.code.BaseErrorCode; -import com.example.tripy.global.common.response.code.ErrorReasonDto; +import com.example.tripy.global.response.code.BaseErrorCode; +import com.example.tripy.global.response.code.ErrorReasonDto; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -17,10 +17,13 @@ public enum ErrorStatus implements BaseErrorCode { //멤버 관련 _EMPTY_MEMBER(HttpStatus.CONFLICT, "MEMBER_001", "존재하지 않는 사용자입니다."), + _INVALID_MEMBER(HttpStatus.CONFLICT, "MEMBER_002", "존재하지 않는 사용자입니다."), //인증 관련 _EMPTY_JWT(HttpStatus.UNAUTHORIZED, "AUTH_001", "JWT가 존재하지 않습니다."), _INVALID_JWT(HttpStatus.UNAUTHORIZED, "AUTH_002", "유효하지 않은 JWT입니다."), + _EXPIRED_JWT(HttpStatus.UNAUTHORIZED, "AUTH_003", "만료된 JWT입니다"), + _EXPIRED_AUTHORIZATION_CODE(HttpStatus.UNAUTHORIZED, "AUTH_004", "만료된 authorization code 입니다"), //통화 관련 _EMPTY_CURRENCY(HttpStatus.CONFLICT, "CUR_001", "환율정보가 존재하지 않습니다."), diff --git a/src/main/java/com/example/tripy/global/common/response/code/status/SuccessStatus.java b/src/main/java/com/example/tripy/global/response/code/status/SuccessStatus.java similarity index 85% rename from src/main/java/com/example/tripy/global/common/response/code/status/SuccessStatus.java rename to src/main/java/com/example/tripy/global/response/code/status/SuccessStatus.java index 967a99b..7bbac77 100644 --- a/src/main/java/com/example/tripy/global/common/response/code/status/SuccessStatus.java +++ b/src/main/java/com/example/tripy/global/response/code/status/SuccessStatus.java @@ -1,4 +1,4 @@ -package com.example.tripy.global.common.response.code.status; +package com.example.tripy.global.response.code.status; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/example/tripy/global/common/response/exception/ExceptionAdvice.java b/src/main/java/com/example/tripy/global/response/exception/ExceptionAdvice.java similarity index 95% rename from src/main/java/com/example/tripy/global/common/response/exception/ExceptionAdvice.java rename to src/main/java/com/example/tripy/global/response/exception/ExceptionAdvice.java index 274f874..b421207 100644 --- a/src/main/java/com/example/tripy/global/common/response/exception/ExceptionAdvice.java +++ b/src/main/java/com/example/tripy/global/response/exception/ExceptionAdvice.java @@ -1,8 +1,8 @@ -package com.example.tripy.global.common.response.exception; +package com.example.tripy.global.response.exception; -import com.example.tripy.global.common.response.ApiResponse; -import com.example.tripy.global.common.response.code.ErrorReasonDto; -import com.example.tripy.global.common.response.code.status.ErrorStatus; +import com.example.tripy.global.response.ApiResponse; +import com.example.tripy.global.response.code.ErrorReasonDto; +import com.example.tripy.global.response.code.status.ErrorStatus; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import java.util.LinkedHashMap; diff --git a/src/main/java/com/example/tripy/global/common/response/exception/GeneralException.java b/src/main/java/com/example/tripy/global/response/exception/GeneralException.java similarity index 59% rename from src/main/java/com/example/tripy/global/common/response/exception/GeneralException.java rename to src/main/java/com/example/tripy/global/response/exception/GeneralException.java index 2c5a192..144227d 100644 --- a/src/main/java/com/example/tripy/global/common/response/exception/GeneralException.java +++ b/src/main/java/com/example/tripy/global/response/exception/GeneralException.java @@ -1,7 +1,7 @@ -package com.example.tripy.global.common.response.exception; +package com.example.tripy.global.response.exception; -import com.example.tripy.global.common.response.code.BaseErrorCode; -import com.example.tripy.global.common.response.code.ErrorReasonDto; +import com.example.tripy.global.response.code.BaseErrorCode; +import com.example.tripy.global.response.code.ErrorReasonDto; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/example/tripy/global/s3/S3Controller.java b/src/main/java/com/example/tripy/global/s3/S3Controller.java index 052c0b3..5aedf49 100644 --- a/src/main/java/com/example/tripy/global/s3/S3Controller.java +++ b/src/main/java/com/example/tripy/global/s3/S3Controller.java @@ -1,12 +1,12 @@ package com.example.tripy.global.s3; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; + +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import com.example.tripy.global.s3.dto.S3Result; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/com/example/tripy/global/s3/S3Service.java b/src/main/java/com/example/tripy/global/s3/S3Service.java index 212e185..3546b14 100644 --- a/src/main/java/com/example/tripy/global/s3/S3Service.java +++ b/src/main/java/com/example/tripy/global/s3/S3Service.java @@ -12,8 +12,8 @@ import com.example.tripy.domain.country.Country; import com.example.tripy.domain.country.CountryRepository; import com.example.tripy.domain.language.LanguageRepository; -import com.example.tripy.global.common.response.code.status.ErrorStatus; -import com.example.tripy.global.common.response.exception.GeneralException; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; import com.example.tripy.global.s3.dto.S3Result; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/com/example/tripy/global/security/CurrentUser.java b/src/main/java/com/example/tripy/global/security/CurrentUser.java new file mode 100644 index 0000000..aa7f704 --- /dev/null +++ b/src/main/java/com/example/tripy/global/security/CurrentUser.java @@ -0,0 +1,12 @@ +package com.example.tripy.global.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +@Target({ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CurrentUser { +} diff --git a/src/main/java/com/example/tripy/global/security/CurrentUserArgumentResolver.java b/src/main/java/com/example/tripy/global/security/CurrentUserArgumentResolver.java new file mode 100644 index 0000000..5cde417 --- /dev/null +++ b/src/main/java/com/example/tripy/global/security/CurrentUserArgumentResolver.java @@ -0,0 +1,60 @@ +package com.example.tripy.global.security; + +import com.example.tripy.domain.member.Member; +import com.example.tripy.domain.member.MemberRepository; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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 CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(CurrentUser.class) != null + && parameter.getParameterType().equals(Member.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + String authorizationHeader = webRequest.getHeader("authorization"); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String token = authorizationHeader.substring(7); // "Bearer " 이후 문자열 + return loadUserFromToken(token); + } + throw new GeneralException(ErrorStatus._EMPTY_JWT); + } + + public Member loadUserFromToken(String token) { + try { + Claims claims = Jwts.parser().setSigningKey(jwtTokenProvider.getSecretKey()) + .parseClaimsJws(token).getBody(); + + String targetIndex = "+"; + + String targetSubject = claims.getSubject(); + + int index = targetSubject.indexOf(targetIndex); + String userId = targetSubject.substring(0, index); + + return memberRepository.findById(Long.valueOf(userId)) + .orElseThrow(() -> new GeneralException(ErrorStatus._EMPTY_MEMBER)); + } catch (Exception ex) { + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } +} diff --git a/src/main/java/com/example/tripy/global/security/JwtOIDCProvider.java b/src/main/java/com/example/tripy/global/security/JwtOIDCProvider.java new file mode 100644 index 0000000..0ce75cf --- /dev/null +++ b/src/main/java/com/example/tripy/global/security/JwtOIDCProvider.java @@ -0,0 +1,103 @@ +package com.example.tripy.global.security; + +import com.example.tripy.domain.auth.dto.AuthResponseDto.OIDCDecodePayload; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.Jwts; +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Component +@Configuration +@RequiredArgsConstructor +@Service +@Slf4j +public class JwtOIDCProvider { + + //header, body 받아오는 로직 + private String getUnsignedToken(String token){ + String[] splitToken = token.split("\\."); + if(splitToken.length != 3) + throw new GeneralException(ErrorStatus._INVALID_JWT); + return splitToken[0] + "." + splitToken[1] + "."; + } + + //인증되지 않은 IdToken에서 payload 받아오는 로직 + private Jwt getUnsignedTokenClaims(String token, String iss, String aud) { + try { + return Jwts.parserBuilder() + .requireAudience(aud) //aud(카카오톡 어플리케이션 아이디) 가 같은지 확인 + .requireIssuer(iss)//iss(이슈어)가 카카오인지 확인 + .build() + .parseClaimsJwt(getUnsignedToken(token)); + } catch (ExpiredJwtException e) { //파싱하면서 만료된 토큰인지 확인. + throw new GeneralException(ErrorStatus._EXPIRED_JWT); + } catch (Exception e) { + log.error(e.toString()); + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } + + private final String KID = "kid"; + public String getKidFromUnsignedTokenHeader(String token, String iss, String aud){ + return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID); + } + + //공개키로 토큰 검증 + public Jws getOIDCTokenJws(String token, String modulus, String exponent){ + try{ + return Jwts.parserBuilder() + .setSigningKey(getRSAPublicKey(modulus, exponent)) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e){ + throw new GeneralException(ErrorStatus._EXPIRED_JWT); + } catch (Exception e){ + log.error(e.toString()); + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } + + //OIDCDecodePayload를 가져옴. OIDC 스펙 -> 공통 사용 + public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent){ + Claims body = getOIDCTokenJws(token, modulus, exponent).getBody(); + return new OIDCDecodePayload( + body.getIssuer(), + body.getAudience(), + body.getSubject(), + body.get("email", String.class), + body.get("nickname", String.class), + body.get("picture", String.class) + ); + } + + //n, e 값으로 RSA 퍼블릭 키 연산 + private Key getRSAPublicKey(String modulus, String exponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + byte[] decodeN = Base64.getUrlDecoder().decode(modulus); + byte[] decodeE = Base64.getUrlDecoder().decode(exponent); + BigInteger n = new BigInteger(1, decodeN); + BigInteger e = new BigInteger(1, decodeE); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e); + return keyFactory.generatePublic(keySpec); + } + + +} diff --git a/src/main/java/com/example/tripy/global/security/JwtTokenProvider.java b/src/main/java/com/example/tripy/global/security/JwtTokenProvider.java new file mode 100644 index 0000000..9cfc686 --- /dev/null +++ b/src/main/java/com/example/tripy/global/security/JwtTokenProvider.java @@ -0,0 +1,126 @@ +package com.example.tripy.global.security; + +import com.example.tripy.domain.member.Member; +import com.example.tripy.domain.member.MemberRepository; +import com.example.tripy.global.response.code.status.ErrorStatus; +import com.example.tripy.global.response.exception.GeneralException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Component +@Configuration +@RequiredArgsConstructor +@PropertySource("classpath:application-dev.yml") +@Service +@Slf4j +public class JwtTokenProvider { + @Value("${jwt.access-token.expire-length}") + private long accessTokenValidityInMilliseconds; + @Value("${jwt.refresh-token.expire-length}") + private long refreshTokenValidityInMillseconds; + @Value("${jwt.custom.secretKey}") + private String SECRET_KEY; + + + private SecretKey cachedSecretKey; + private final MemberRepository memberRepository; + + private SecretKey _getSecretKey() { + String keyBase64Encoded = Base64.getEncoder().encodeToString(SECRET_KEY.getBytes()); + return Keys.hmacShaKeyFor(keyBase64Encoded.getBytes()); + } + + + // 시크릿 키를 반환하는 method + public SecretKey getSecretKey() { + if (cachedSecretKey == null) + cachedSecretKey = _getSecretKey(); + + return cachedSecretKey; + } + + public String createAccessToken(String payload) { + return createToken(payload, accessTokenValidityInMilliseconds); + } + + public String createRefreshToken(Long climberId) { + return createToken(climberId.toString(), refreshTokenValidityInMillseconds); + } + + public String refreshAccessToken(String refreshToken) { + if (validateToken(refreshToken)) { + Long userId = Long.parseLong(getPayload(refreshToken)); + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new GeneralException(ErrorStatus._INVALID_MEMBER)); + String payload = member.getPayload(); + return createAccessToken(payload); + + } else { + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } + + + + public String createToken(String payload, long expireLength){ + Claims claims = Jwts.claims().setSubject(payload); + Date now = new Date(); + long nowMillis = now.getTime(); + long expireMillis = nowMillis + (expireLength * 1000); // expireLength를 초 단위로 받았다고 가정 + Date validity = new Date(expireMillis); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, getSecretKey()) + .compact(); + } + + public String getPayload(String token){ + try{ + return Jwts.parser() + .setSigningKey(getSecretKey()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + }catch (ExpiredJwtException e){ + return e.getClaims().getSubject(); + }catch(JwtException e){ + log.error(e.toString()); + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } + public boolean validateToken(String token) { + try { + Jws claimsJws = Jwts.parser().setSigningKey(getSecretKey()).parseClaimsJws(token); + Date now = new Date(); + long nowMillis = now.getTime(); + if (claimsJws.getBody().getExpiration().before(new Date())) { + throw new GeneralException(ErrorStatus._EXPIRED_JWT); + } + return true; + } catch (ExpiredJwtException e) { + log.error(e.toString()); + throw new GeneralException(ErrorStatus._EXPIRED_JWT); + } catch (JwtException | IllegalArgumentException exception) { + throw new GeneralException(ErrorStatus._INVALID_JWT); + } + } + + +}