diff --git a/.deploy/Dockerfile b/.deploy/Dockerfile index 1faad58..1f27dca 100644 --- a/.deploy/Dockerfile +++ b/.deploy/Dockerfile @@ -1,4 +1,28 @@ FROM openjdk:17 + +# 빌드 시 전달받을 ARG 변수 설정 +ARG DATASOURCE_URL_LOCAL +ARG DATASOURCE_USERNAME +ARG DATASOURCE_PASSWORD +ARG JWT_SECRET_KEY +ARG JWT_EXPIRED_IN +ARG REFRESH_TOKEN_EXPIRED_IN +ARG KAKAO_CLIENT_KEY +ARG KAKAO_REDIRECT_URL + +# ARG 값을 ENV로 설정하여 런타임에서도 사용 가능하도록 설정 +ENV DATASOURCE_URL_LOCAL=$DATASOURCE_URL_LOCAL +ENV DATASOURCE_USERNAME=$DATASOURCE_USERNAME +ENV DATASOURCE_PASSWORD=$DATASOURCE_PASSWORD +ENV JWT_SECRET_KEY=$JWT_SECRET_KEY +ENV JWT_EXPIRED_IN=$JWT_EXPIRED_IN +ENV REFRESH_TOKEN_EXPIRED_IN=$REFRESH_TOKEN_EXPIRED_IN +ENV KAKAO_CLIENT_KEY=$KAKAO_CLIENT_KEY +ENV KAKAO_REDIRECT_URL=$KAKAO_REDIRECT_URL + +# JAR 파일 복사 ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file + +# 실행 명령어 +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/.deploy/neighbors-blue-deploy.yml b/.deploy/neighbors-blue-deploy.yml index 19bc094..20905e6 100644 --- a/.deploy/neighbors-blue-deploy.yml +++ b/.deploy/neighbors-blue-deploy.yml @@ -13,9 +13,14 @@ services: - app-network environment: - TZ=Asia/Seoul - - DB_URL=${DATASOURCE_URL_LOCAL} - - DB_USERNAME=${DATASOURCE_USERNAME} - - DB_PASSWORD=${DATASOURCE_PASSWORD} + - DATASOURCE_URL_LOCAL=${DATASOURCE_URL_LOCAL} + - DATASOURCE_USERNAME=${DATASOURCE_USERNAME} + - DATASOURCE_PASSWORD=${DATASOURCE_PASSWORD} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - JWT_EXPIRED_IN=${JWT_EXPIRED_IN} + - REFRESH_TOKEN_EXPIRED_IN=${REFRESH_TOKEN_EXPIRED_IN} + - KAKAO_CLIENT_KEY=${KAKAO_CLIENT_KEY} + - KAKAO_REDIRECT_URL=${KAKAO_REDIRECT_URL} networks: app-network: diff --git a/.deploy/neighbors-green-deploy.yml b/.deploy/neighbors-green-deploy.yml index 54c4260..ed3be75 100644 --- a/.deploy/neighbors-green-deploy.yml +++ b/.deploy/neighbors-green-deploy.yml @@ -13,9 +13,14 @@ services: - app-network environment: - TZ=Asia/Seoul - - DB_URL=${DATASOURCE_URL_LOCAL} - - DB_USERNAME=${DATASOURCE_USERNAME} - - DB_PASSWORD=${DATASOURCE_PASSWORD} + - DATASOURCE_URL_LOCAL=${DATASOURCE_URL_LOCAL} + - DATASOURCE_USERNAME=${DATASOURCE_USERNAME} + - DATASOURCE_PASSWORD=${DATASOURCE_PASSWORD} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - JWT_EXPIRED_IN=${JWT_EXPIRED_IN} + - REFRESH_TOKEN_EXPIRED_IN=${REFRESH_TOKEN_EXPIRED_IN} + - KAKAO_CLIENT_KEY=${KAKAO_CLIENT_KEY} + - KAKAO_REDIRECT_URL=${KAKAO_REDIRECT_URL} networks: app-network: diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 63fd4d3..4161d02 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -9,6 +9,16 @@ jobs: build: runs-on: ubuntu-latest + env: + DATASOURCE_URL_LOCAL: ${{ secrets.DATASOURCE_URL_LOCAL }} + DATASOURCE_USERNAME: ${{ secrets.DATASOURCE_USERNAME }} + DATASOURCE_PASSWORD: ${{ secrets.DATASOURCE_PASSWORD }} + JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} + JWT_EXPIRED_IN: ${{ secrets.JWT_EXPIRED_IN }} + REFRESH_TOKEN_EXPIRED_IN: ${{ secrets.REFRESH_TOKEN_EXPIRED_IN }} + KAKAO_CLIENT_KEY: ${{ secrets.KAKAO_CLIENT_KEY }} + KAKAO_REDIRECT_URL: ${{ secrets.KAKAO_REDIRECT_URL }} + steps: # 레포지토리의 특정 브랜치, 커밋을 가져오는 설정 @@ -43,11 +53,23 @@ jobs: - name: Build with Gradle Wrapper run: ./gradlew build + # docker image를 build 하고 docker hub에 push # docker image를 build 하고 docker hub에 push - name: Docker Build and Push run: | sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - sudo docker build -f ./.deploy/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} . + sudo docker build \ + --build-arg DATASOURCE_URL_LOCAL=${{ secrets.DATASOURCE_URL_LOCAL }} \ + --build-arg DATASOURCE_USERNAME=${{ secrets.DATASOURCE_USERNAME }} \ + --build-arg DATASOURCE_PASSWORD=${{ secrets.DATASOURCE_PASSWORD }} \ + --build-arg JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \ + --build-arg JWT_EXPIRED_IN=${{ secrets.JWT_EXPIRED_IN }} \ + --build-arg REFRESH_TOKEN_EXPIRED_IN=${{ secrets.REFRESH_TOKEN_EXPIRED_IN }} \ + --build-arg KAKAO_CLIENT_KEY=${{ secrets.KAKAO_CLIENT_KEY }} \ + --build-arg KAKAO_REDIRECT_URL=${{ secrets.KAKAO_REDIRECT_URL }} \ + --platform linux/amd64 \ + -f ./.deploy/Dockerfile \ + -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} . sudo docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} deploy: @@ -80,6 +102,11 @@ jobs: echo "DATASOURCE_URL_LOCAL=${{ secrets.DATASOURCE_URL_LOCAL }}" >> ./.env echo "DATASOURCE_USERNAME=${{ secrets.DATASOURCE_USERNAME }}" >> ./.env echo "DATASOURCE_PASSWORD=${{ secrets.DATASOURCE_PASSWORD }}" >> ./.env + echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> ./.env + echo "JWT_EXPIRED_IN=${{ secrets.JWT_EXPIRED_IN }}" >> ./.env + echo "REFRESH_TOKEN_EXPIRED_IN=${{ secrets.REFRESH_TOKEN_EXPIRED_IN }}" >> ./.env + echo "KAKAO_CLIENT_KEY=${{ secrets.KAKAO_CLIENT_KEY }}" >> ./.env + echo "KAKAO_REDIRECT_URL=${{ secrets.KAKAO_REDIRECT_URL }}" >> ./.env chmod +x deploy.sh source deploy.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b532d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM openjdk:17 + +# 빌드 시 전달받을 ARG 변수 설정 +ARG DATASOURCE_URL_LOCAL +ARG DATASOURCE_USERNAME +ARG DATASOURCE_PASSWORD +ARG JWT_SECRET_KEY +ARG JWT_EXPIRED_IN +ARG REFRESH_TOKEN_EXPIRED_IN + +# ARG 값을 ENV로 설정하여 런타임에서도 사용 가능하도록 설정 +ENV DATASOURCE_URL_LOCAL=$DATASOURCE_URL_LOCAL +ENV DATASOURCE_USERNAME=$DATASOURCE_USERNAME +ENV DATASOURCE_PASSWORD=$DATASOURCE_PASSWORD +ENV JWT_SECRET_KEY=$JWT_SECRET_KEY +ENV JWT_EXPIRED_IN=$JWT_EXPIRED_IN +ENV REFRESH_TOKEN_EXPIRED_IN=$REFRESH_TOKEN_EXPIRED_IN + +# JAR 파일 복사 +ARG JAR_FILE=build/libs/*.jar +COPY ${JAR_FILE} app.jar + +# 실행 명령어 +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7b1625a..45a03fc 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + + //Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', 'io.jsonwebtoken:jjwt-jackson:0.11.2' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/neighbors/santa/TestController.java b/src/main/java/com/neighbors/santa/TestController.java deleted file mode 100644 index 61a117b..0000000 --- a/src/main/java/com/neighbors/santa/TestController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.neighbors.santa; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("") -public class TestController { - @GetMapping("/test") - public String test() { - return "hello world"; - } -} diff --git a/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponse.java b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponse.java new file mode 100644 index 0000000..ae506b6 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponse.java @@ -0,0 +1,25 @@ +package com.neighbors.santa.application.baseResponse; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; + +@Data +@JsonPropertyOrder({"code", "message", "data"}) +public class BaseResponse { + private final BaseResponseStatus status; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final T data; + + public BaseResponse(BaseResponseStatus status, String message, T data) { + this.status = status; + this.message = message; + this.data = data; + } + + public BaseResponse(BaseResponseStatus status, String message) { + this(status, message, null); + } +} diff --git a/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseMessage.java b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseMessage.java new file mode 100644 index 0000000..f431154 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseMessage.java @@ -0,0 +1,22 @@ +package com.neighbors.santa.application.baseResponse; + +import lombok.Getter; + +@Getter +public enum BaseResponseMessage { + + //oauth success + 로그인_성공했습니다("로그인 성공했습니다"), + + //jwt error message + JWT_토큰_오류입니다("JWT 토큰 오류입니다"), + 지원하지_않는_토큰_입니다("지원하지 않는 토큰 입니다"), + 토큰이_올바르지_못한_형식입니다("토큰이 올바르지 못한 형식입니다"), + 유효하지_않은_토큰_입니다("유효하지 않은 토큰입니다"); + + private final String message; + + private BaseResponseMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseStatus.java b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseStatus.java new file mode 100644 index 0000000..ba15c01 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/baseResponse/BaseResponseStatus.java @@ -0,0 +1,5 @@ +package com.neighbors.santa.application.baseResponse; + +public enum BaseResponseStatus { + OK, BAD_REQUEST, JWT_ERROR; +} diff --git a/src/main/java/com/neighbors/santa/application/oauth/dto/OAuthLoginResponse.java b/src/main/java/com/neighbors/santa/application/oauth/dto/OAuthLoginResponse.java new file mode 100644 index 0000000..c061373 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/oauth/dto/OAuthLoginResponse.java @@ -0,0 +1,26 @@ +package com.neighbors.santa.application.oauth.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.neighbors.santa.common.jwt.AuthTokens; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class OAuthLoginResponse { + private boolean isMember; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private AuthTokens authTokens; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String email; + + public static OAuthLoginResponse createSuccessObjFrom(AuthTokens authTokens, String email) { + return OAuthLoginResponse.builder() + .authTokens(authTokens) + .email(email) + .isMember(true) + .build(); + } +} diff --git a/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoInfoResponse.java b/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoInfoResponse.java new file mode 100644 index 0000000..48a1a6c --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoInfoResponse.java @@ -0,0 +1,35 @@ +package com.neighbors.santa.application.oauth.dto.kakao; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoInfoResponse{ + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoAccount { + private KakaoProfile profile; + private String email; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoProfile { + private String nickname; + } + + public String getEmail() { + return kakaoAccount.getEmail(); + } + + public String getNickname() { + return kakaoAccount.profile.getNickname(); + } +} + diff --git a/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoTokens.java b/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoTokens.java new file mode 100644 index 0000000..6f8d295 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/oauth/dto/kakao/KakaoTokens.java @@ -0,0 +1,28 @@ +package com.neighbors.santa.application.oauth.dto.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoTokens { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private String expiresIn; + + @JsonProperty("refresh_token_expires_in") + private String refreshTokenExpiresIn; + + @JsonProperty("scope") + private String scope; +} diff --git a/src/main/java/com/neighbors/santa/application/oauth/service/OAuthService.java b/src/main/java/com/neighbors/santa/application/oauth/service/OAuthService.java new file mode 100644 index 0000000..3e8b264 --- /dev/null +++ b/src/main/java/com/neighbors/santa/application/oauth/service/OAuthService.java @@ -0,0 +1,50 @@ +package com.neighbors.santa.application.oauth.service; + +import com.neighbors.santa.application.oauth.dto.kakao.KakaoInfoResponse; +import com.neighbors.santa.application.oauth.dto.kakao.KakaoTokens; +import com.neighbors.santa.application.oauth.dto.OAuthLoginResponse; +import com.neighbors.santa.application.baseResponse.BaseResponse; +import com.neighbors.santa.application.baseResponse.BaseResponseMessage; +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import com.neighbors.santa.common.jwt.AuthTokens; +import com.neighbors.santa.common.jwt.JwtProvider; +import com.neighbors.santa.common.jwt.JwtUserDetails; +import com.neighbors.santa.domain.model.User; +import com.neighbors.santa.domain.service.CreateUser; +import com.neighbors.santa.domain.service.oauth.kakao.RequestKakaoInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +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 OAuthService { + + private final RequestKakaoInfo requestUserInfo; + private final CreateUser createUser; + private final JwtProvider jwtProvider; + + public BaseResponse oAuthKaKaoLoin(String code){ + KakaoInfoResponse kakaoInfoResponse = requestUserInfo.requestKakaoInfo(code); + + User user = User.builder() + .email(kakaoInfoResponse.getEmail()) + .userName(kakaoInfoResponse.getNickname()) + .build(); + + createUser.createUser(user); + + AuthTokens authTokens = jwtProvider.createToken(JwtUserDetails.from(user)); + + return new BaseResponse<>( + BaseResponseStatus.OK, + BaseResponseMessage.로그인_성공했습니다.getMessage(), + OAuthLoginResponse.createSuccessObjFrom(authTokens, kakaoInfoResponse.getEmail()) + ); + } +} diff --git a/src/main/java/com/neighbors/santa/common/ErrorResponseUtil.java b/src/main/java/com/neighbors/santa/common/ErrorResponseUtil.java new file mode 100644 index 0000000..4922914 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/ErrorResponseUtil.java @@ -0,0 +1,23 @@ +package com.neighbors.santa.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.neighbors.santa.application.baseResponse.BaseResponse; +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class ErrorResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void setResponse(HttpServletResponse response, BaseResponseStatus responseStatus) throws IOException { + + BaseResponse errorResponse = new BaseResponse(responseStatus, ""); + + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("application/json"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } + +} diff --git a/src/main/java/com/neighbors/santa/common/config/ClientConfig.java b/src/main/java/com/neighbors/santa/common/config/ClientConfig.java new file mode 100644 index 0000000..9728215 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/config/ClientConfig.java @@ -0,0 +1,14 @@ +package com.neighbors.santa.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ClientConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/neighbors/santa/common/config/SecurityConfig.java b/src/main/java/com/neighbors/santa/common/config/SecurityConfig.java new file mode 100644 index 0000000..edc2a4f --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/config/SecurityConfig.java @@ -0,0 +1,62 @@ +package com.neighbors.santa.common.config; + +import com.neighbors.santa.common.security.CustomAccessDeniedHandler; +import com.neighbors.santa.common.security.CustomJwtAuthenticationEntryPoint; +import com.neighbors.santa.common.security.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers( + "/error", + "/swagger-ui/**", + "/swagger-resources/*", + "/webjars/", + "/v3/api-docs/**", + "/oauth/kakao/callback", + "/auth/refreshToken", + "/address"); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) //csrf 토큰 disable 하기 + .formLogin(AbstractHttpConfigurer::disable) //form login 비활성화 + .httpBasic(AbstractHttpConfigurer::disable)//http 기본 인증 비활성화 + .cors(Customizer.withDefaults()) + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.POST, "/user").permitAll() + .anyRequest().authenticated()) + .sessionManagement(session -> { + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exception -> + { + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); + exception.accessDeniedHandler(customAccessDeniedHandler); + }); + + return http.build(); + } +} diff --git a/src/main/java/com/neighbors/santa/common/enums/Role.java b/src/main/java/com/neighbors/santa/common/enums/Role.java new file mode 100644 index 0000000..2db8f2a --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/enums/Role.java @@ -0,0 +1,15 @@ +package com.neighbors.santa.common.enums; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collection; +import java.util.Collections; + +public enum Role { + ADMIN, USER, GUEST; + + public Collection getAuthority() { + return Collections.singletonList(new SimpleGrantedAuthority(this.name())); + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtBadRequestException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtBadRequestException.java new file mode 100644 index 0000000..2c8f9c0 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtBadRequestException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.bad_request; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtBadRequestException extends RuntimeException { + + private final BaseResponseStatus status; + private final String message; + + public JwtBadRequestException(BaseResponseStatus status, String message) { + super(message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtNoTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtNoTokenException.java new file mode 100644 index 0000000..ce19a89 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtNoTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.bad_request; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtNoTokenException extends JwtBadRequestException { + + private final BaseResponseStatus status; + private final String message; + + public JwtNoTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java new file mode 100644 index 0000000..d793f83 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.bad_request; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtUnsupportedTokenException extends JwtBadRequestException { + + private final BaseResponseStatus status; + private final String message; + + public JwtUnsupportedTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtExpiredTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtExpiredTokenException.java new file mode 100644 index 0000000..7c6b5b1 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtExpiredTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.unauthorized; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtExpiredTokenException extends JwtUnauthorizedTokenException { + + private final BaseResponseStatus status; + private final String message; + + public JwtExpiredTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidRefreshTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidRefreshTokenException.java new file mode 100644 index 0000000..da1bf98 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidRefreshTokenException.java @@ -0,0 +1,18 @@ +package com.neighbors.santa.common.exception.jwt.unauthorized; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtInvalidRefreshTokenException extends JwtUnauthorizedTokenException { + + private final BaseResponseStatus status; + private final String message; + + public JwtInvalidRefreshTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } + +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidTokenException.java new file mode 100644 index 0000000..3335ee5 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtInvalidTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.unauthorized; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtInvalidTokenException extends JwtUnauthorizedTokenException { + + private final BaseResponseStatus status; + private final String message; + + public JwtInvalidTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtMalformedTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtMalformedTokenException.java new file mode 100644 index 0000000..3c8c068 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtMalformedTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.unauthorized; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtMalformedTokenException extends JwtUnauthorizedTokenException { + + private final BaseResponseStatus status; + private final String message; + + public JwtMalformedTokenException(BaseResponseStatus status, String message) { + super(status, message); + this.status = status; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java new file mode 100644 index 0000000..cb5cfee --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java @@ -0,0 +1,17 @@ +package com.neighbors.santa.common.exception.jwt.unauthorized; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import lombok.Getter; + +@Getter +public class JwtUnauthorizedTokenException extends RuntimeException { + + private final BaseResponseStatus status; + private final String message; + + public JwtUnauthorizedTokenException(BaseResponseStatus status, String message) { + super(message); + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/neighbors/santa/common/jwt/AuthTokens.java b/src/main/java/com/neighbors/santa/common/jwt/AuthTokens.java new file mode 100644 index 0000000..85df556 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/jwt/AuthTokens.java @@ -0,0 +1,20 @@ +package com.neighbors.santa.common.jwt; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthTokens { + + private String accessToken; + private String refreshToken; + private Long expiresIn; + private Long refreshTokenExpiresIn; + + public static AuthTokens of(String accessToken, String refreshToken, Long expiresIn, Long refreshTokenExpiresIn) { + return new AuthTokens(accessToken, refreshToken, expiresIn, refreshTokenExpiresIn); + } +} diff --git a/src/main/java/com/neighbors/santa/common/jwt/JwtProvider.java b/src/main/java/com/neighbors/santa/common/jwt/JwtProvider.java new file mode 100644 index 0000000..b07a12d --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/jwt/JwtProvider.java @@ -0,0 +1,127 @@ +package com.neighbors.santa.common.jwt; + +import com.neighbors.santa.application.baseResponse.BaseResponseMessage; +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import com.neighbors.santa.common.enums.Role; +import com.neighbors.santa.common.exception.jwt.bad_request.JwtUnsupportedTokenException; +import com.neighbors.santa.common.exception.jwt.unauthorized.JwtInvalidTokenException; +import com.neighbors.santa.common.exception.jwt.unauthorized.JwtMalformedTokenException; +import io.jsonwebtoken.*; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Slf4j +@Component +public class JwtProvider { + + @Value("${secret.jwt-secret-key}") + private String JWT_SECRET_KEY; + + @Value("${secret.jwt-expired-in.access-token}") + private long JWT_EXPIRED_IN; + + @Value("${secret.jwt-expired-in.refresh-token}") + @Getter + private long REFRESH_TOKEN_EXPIRED_IN; + + public AuthTokens createToken(JwtUserDetails jwtUserDetails) { + log.info("JWT key={}", JWT_SECRET_KEY); + + Claims claims = Jwts.claims() + .setSubject(jwtUserDetails.getEmail()) + .setIssuer("zipkok"); + + claims.put("role", jwtUserDetails.getRole()); + claims.put("id", jwtUserDetails.getUserId()); + + Date now = new Date(); + Date accessTokenExpiredAt = new Date(now.getTime() + JWT_EXPIRED_IN); + Date refreshTokenExpiredAt = new Date(now.getTime() + REFRESH_TOKEN_EXPIRED_IN); + + String accessToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(accessTokenExpiredAt) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY) + .compact(); + + String refreshToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(refreshTokenExpiredAt) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY) + .compact(); + + return AuthTokens.of(accessToken, refreshToken, JWT_EXPIRED_IN, REFRESH_TOKEN_EXPIRED_IN); + } + + public boolean isExpiredToken(String token) throws JwtInvalidTokenException { + log.info("[JwtTokenProvider.isExpiredToken] token={}", token); + try { + Jws claims = Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY).build() + .parseClaimsJws(token); + + return claims.getBody().getExpiration().before(new Date()); + + } catch (ExpiredJwtException e) { + log.error("[ExpiredJwtException]"); + return true; + } catch (UnsupportedJwtException e) { + log.error("[UnsupportedJwtException]"); + throw new JwtUnsupportedTokenException(BaseResponseStatus.JWT_ERROR, BaseResponseMessage.지원하지_않는_토큰_입니다.getMessage()); + } catch (MalformedJwtException e) { + log.error("[MalformedJwtException]"); + throw new JwtMalformedTokenException(BaseResponseStatus.JWT_ERROR, BaseResponseMessage.토큰이_올바르지_못한_형식입니다.getMessage()); + } catch (IllegalArgumentException e) { + log.error("[IllegalArgumentException]"); + throw new JwtInvalidTokenException(BaseResponseStatus.JWT_ERROR, BaseResponseMessage.유효하지_않은_토큰_입니다.getMessage()); + } catch (JwtException e) { + log.error("[JwtTokenProvider.validateAccessToken]", e); + throw e; + } + } + + public String getEmail(String token) { + return Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY).build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + public Long getId(String token) { + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY).build() + .parseClaimsJws(token) + .getBody() + .getId()); + } + + private Claims getBody(String token) { + if (token.contains("Bearer ")) { + token = token.substring("Bearer ".length()); + } + + return Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public JwtUserDetails getJwtUserDetails(String token) { + Claims claims = getBody(token); + + return JwtUserDetails.builder() + .email(String.valueOf(claims.getSubject())) + .userId(Long.valueOf(claims.get("id").toString())) + .role(Role.valueOf(claims.get("role").toString())) + .build(); + + } +} \ No newline at end of file diff --git a/src/main/java/com/neighbors/santa/common/jwt/JwtUserDetails.java b/src/main/java/com/neighbors/santa/common/jwt/JwtUserDetails.java new file mode 100644 index 0000000..654f232 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/jwt/JwtUserDetails.java @@ -0,0 +1,30 @@ +package com.neighbors.santa.common.jwt; + +import com.neighbors.santa.common.enums.Role; +import com.neighbors.santa.domain.model.User; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Builder +public class JwtUserDetails { + private final String email; + private final Long userId; + private final Role role; + + public static JwtUserDetails from(User user) { + return JwtUserDetails.builder() + .email(user.getEmail()) + .userId(user.getUserId()) + .role(user.getRole()) + .build(); + } + + public static JwtUserDetails makeGuestJwtDetails() { + return JwtUserDetails.builder() + .role(Role.GUEST) + .build(); + } +} diff --git a/src/main/java/com/neighbors/santa/common/security/AuthenticationUtil.java b/src/main/java/com/neighbors/santa/common/security/AuthenticationUtil.java new file mode 100644 index 0000000..e8aaa74 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/security/AuthenticationUtil.java @@ -0,0 +1,73 @@ +package com.neighbors.santa.common.security; + +import com.neighbors.santa.common.jwt.JwtProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthenticationUtil { + + private final JwtProvider jwtProvider; + + public void setAuthenticationFromRequest(HttpServletRequest request) { + + log.info("[AuthenticationUtil.setAuthenticationFromRequest]"); + + final String token = getJwtFromRequest(request); + + makeAuthentication(request, token).ifPresent(auth -> { + SecurityContextHolder.getContext().setAuthentication(auth); + }); + } + + private Boolean isRequestAvailableToGuest(HttpServletRequest request) { + return request.getRequestURI().contains("/realEstate") && request.getMethod().equals("GET"); + } + + private Optional makeAuthentication(HttpServletRequest request, String token) { + + log.info("[AuthenticationUtil.makeAuthentication]"); + + UserAuthentication authentication = null; + + if(isTokenValid(token)) { + String role = jwtProvider.getJwtUserDetails(token).getRole().toString(); + log.info("[AuthenticationUtil.makeAuthentication : {} 권한 부여]", role); + authentication = UserAuthentication.from(jwtProvider.getJwtUserDetails(token)); + } else if (isRequestAvailableToGuest(request)) { + log.info("[AuthenticationUtil.makeAuthentication : Guest 권한 부여]"); + authentication = UserAuthentication.makeGuestAuthentication(); + } + + if(authentication != null) { + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + } + + return Optional.ofNullable(authentication); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } else { + return null; + } + + } + + private boolean isTokenValid(String token) { + return token != null && !jwtProvider.isExpiredToken(token); + } + +} diff --git a/src/main/java/com/neighbors/santa/common/security/CustomAccessDeniedHandler.java b/src/main/java/com/neighbors/santa/common/security/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..6d3c25e --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/security/CustomAccessDeniedHandler.java @@ -0,0 +1,24 @@ +package com.neighbors.santa.common.security; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import com.neighbors.santa.common.ErrorResponseUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + log.warn("JwtAuthenticationEntryPoint : enter CustomAccessDeniedHandler "); + + ErrorResponseUtil.setResponse(response, BaseResponseStatus.JWT_ERROR); + } +} diff --git a/src/main/java/com/neighbors/santa/common/security/CustomJwtAuthenticationEntryPoint.java b/src/main/java/com/neighbors/santa/common/security/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..edf1dfa --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/security/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,25 @@ +package com.neighbors.santa.common.security; + +import com.neighbors.santa.application.baseResponse.BaseResponseStatus; +import com.neighbors.santa.common.ErrorResponseUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.warn("[JwtAuthenticationEntryPoint.commence] error message: {}", authException.getMessage()); + + ErrorResponseUtil.setResponse(response, BaseResponseStatus.JWT_ERROR); + } +} diff --git a/src/main/java/com/neighbors/santa/common/security/JwtAuthenticationFilter.java b/src/main/java/com/neighbors/santa/common/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1210f90 --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/security/JwtAuthenticationFilter.java @@ -0,0 +1,31 @@ +package com.neighbors.santa.common.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final AuthenticationUtil authenticationUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + authenticationUtil.setAuthenticationFromRequest(request); + + filterChain.doFilter(request, response); + } + +} diff --git a/src/main/java/com/neighbors/santa/common/security/UserAuthentication.java b/src/main/java/com/neighbors/santa/common/security/UserAuthentication.java new file mode 100644 index 0000000..04f188c --- /dev/null +++ b/src/main/java/com/neighbors/santa/common/security/UserAuthentication.java @@ -0,0 +1,24 @@ +package com.neighbors.santa.common.security; + +import com.neighbors.santa.common.enums.Role; +import com.neighbors.santa.common.jwt.JwtUserDetails; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } + + public static UserAuthentication from(JwtUserDetails jwtUserDetails) { + return new UserAuthentication(jwtUserDetails, null, jwtUserDetails.getRole().getAuthority()); + } + + public static UserAuthentication makeGuestAuthentication() { + return new UserAuthentication(JwtUserDetails.makeGuestJwtDetails(), null, Role.GUEST.getAuthority()); + } + +} diff --git a/src/main/java/com/neighbors/santa/domain/model/User.java b/src/main/java/com/neighbors/santa/domain/model/User.java new file mode 100644 index 0000000..0e950a8 --- /dev/null +++ b/src/main/java/com/neighbors/santa/domain/model/User.java @@ -0,0 +1,14 @@ +package com.neighbors.santa.domain.model; + +import com.neighbors.santa.common.enums.Role; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class User { + private Long userId; + private String userName; + private String email; + private Role role; +} diff --git a/src/main/java/com/neighbors/santa/domain/service/CreateUser.java b/src/main/java/com/neighbors/santa/domain/service/CreateUser.java new file mode 100644 index 0000000..bbff18f --- /dev/null +++ b/src/main/java/com/neighbors/santa/domain/service/CreateUser.java @@ -0,0 +1,11 @@ +package com.neighbors.santa.domain.service; + +import com.neighbors.santa.domain.model.User; +import org.springframework.stereotype.Component; + +@Component +public class CreateUser { + public void createUser(User user) { + + } +} diff --git a/src/main/java/com/neighbors/santa/domain/service/oauth/kakao/RequestKakaoInfo.java b/src/main/java/com/neighbors/santa/domain/service/oauth/kakao/RequestKakaoInfo.java new file mode 100644 index 0000000..aa07190 --- /dev/null +++ b/src/main/java/com/neighbors/santa/domain/service/oauth/kakao/RequestKakaoInfo.java @@ -0,0 +1,68 @@ +package com.neighbors.santa.domain.service.oauth.kakao; + +import com.neighbors.santa.application.oauth.dto.kakao.KakaoInfoResponse; +import com.neighbors.santa.application.oauth.dto.kakao.KakaoTokens; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class RequestKakaoInfo { + + @Value("${oauth.kakao.token-url}") + private String tokenUrl; + + @Value("${oauth.kakao.userInfo-url}") + private String userInfoUrl; + + @Value("${oauth.kakao.client-key}") + private String clientKey; + + @Value("${oauth.kakao.redirect-uri}") + private String redirect_uri; + + private final RestTemplate restTemplate; + + public KakaoInfoResponse requestKakaoInfo(String authorizationCode){ + String kakaoAccessToken = requestToken(authorizationCode); + return requestUserInfo(kakaoAccessToken); + } + + private String requestToken(String code) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("client_id", clientKey); + body.add("redirect_uri", redirect_uri); + body.add("code", code); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + KakaoTokens response = restTemplate.postForObject(tokenUrl, request, KakaoTokens.class); + + assert response != null; + return response.getAccessToken(); + } + + private KakaoInfoResponse requestUserInfo(String accessToken){ + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.set("Authorization", "Bearer " + accessToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]"); + + HttpEntity request = new HttpEntity<>(body, httpHeaders); + + return restTemplate.postForObject(userInfoUrl, request, KakaoInfoResponse.class); + } +} diff --git a/src/main/java/com/neighbors/santa/presentation/OAuthController.java b/src/main/java/com/neighbors/santa/presentation/OAuthController.java new file mode 100644 index 0000000..2a2dc1f --- /dev/null +++ b/src/main/java/com/neighbors/santa/presentation/OAuthController.java @@ -0,0 +1,22 @@ +package com.neighbors.santa.presentation; + +import com.neighbors.santa.application.oauth.dto.OAuthLoginResponse; +import com.neighbors.santa.application.oauth.service.OAuthService; +import com.neighbors.santa.application.baseResponse.BaseResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("") +public class OAuthController { + + private final OAuthService oAuthService; + + @GetMapping("/oauth/kakao/callback") + public ResponseEntity> kakaoLogin(@RequestParam("code") String code) { + return ResponseEntity.ok() + .body(oAuthService.oAuthKaKaoLoin(code)); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 109e3ba..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=santa diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..56caea2 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,39 @@ +spring: + application: + name: santa + + datasource: + url: ${DATASOURCE_URL_LOCAL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + dbcp2: + validation-query: select 1 + sql: + init: + platform: mysql + + jpa: + open-in-view: true + hibernate: + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + default_batch_fetch_size: 100 + +secret: + jwt-secret-key: ${JWT_SECRET_KEY} + jwt-expired-in: + access-token: ${JWT_EXPIRED_IN} + refresh-token: ${REFRESH_TOKEN_EXPIRED_IN} + +oauth: + kakao: + token-url: https://kauth.kakao.com/oauth/token + userInfo-url: https://kapi.kakao.com/v2/user/me + client-key: ${KAKAO_CLIENT_KEY} + redirect-uri: ${KAKAO_REDIRECT_URL} \ No newline at end of file