diff --git a/build.gradle b/build.gradle index 5fc3f0ab..da239f06 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,14 @@ dependencies { //Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + // JWT + implementation group: "io.jsonwebtoken", name: "jjwt-api", version: "0.11.2" + implementation group: "io.jsonwebtoken", name: "jjwt-impl", version: "0.11.2" + implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2" + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } dependencyManagement { diff --git a/src/main/java/org/moonshot/server/domain/keyResult/controller/KeyResultController.java b/src/main/java/org/moonshot/server/domain/keyresult/controller/KeyResultController.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/controller/KeyResultController.java rename to src/main/java/org/moonshot/server/domain/keyresult/controller/KeyResultController.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultCreateRequestDto.java b/src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultCreateRequestDto.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultCreateRequestDto.java rename to src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultCreateRequestDto.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultCreateRequestInfoDto.java b/src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultCreateRequestInfoDto.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultCreateRequestInfoDto.java rename to src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultCreateRequestInfoDto.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultDeleteRequestDto.java b/src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultDeleteRequestDto.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultDeleteRequestDto.java rename to src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultDeleteRequestDto.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultModifyRequestDto.java b/src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultModifyRequestDto.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/dto/request/KeyResultModifyRequestDto.java rename to src/main/java/org/moonshot/server/domain/keyresult/dto/request/KeyResultModifyRequestDto.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultInvalidPositionException.java b/src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultInvalidPositionException.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultInvalidPositionException.java rename to src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultInvalidPositionException.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultNotFoundException.java b/src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultNotFoundException.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultNotFoundException.java rename to src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultNotFoundException.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultNumberExceededException.java b/src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultNumberExceededException.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/exception/KeyResultNumberExceededException.java rename to src/main/java/org/moonshot/server/domain/keyresult/exception/KeyResultNumberExceededException.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/model/KRState.java b/src/main/java/org/moonshot/server/domain/keyresult/model/KRState.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/model/KRState.java rename to src/main/java/org/moonshot/server/domain/keyresult/model/KRState.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/model/KeyResult.java b/src/main/java/org/moonshot/server/domain/keyresult/model/KeyResult.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/model/KeyResult.java rename to src/main/java/org/moonshot/server/domain/keyresult/model/KeyResult.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/repository/KeyResultRepository.java b/src/main/java/org/moonshot/server/domain/keyresult/repository/KeyResultRepository.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/repository/KeyResultRepository.java rename to src/main/java/org/moonshot/server/domain/keyresult/repository/KeyResultRepository.java diff --git a/src/main/java/org/moonshot/server/domain/keyResult/service/KeyResultService.java b/src/main/java/org/moonshot/server/domain/keyresult/service/KeyResultService.java similarity index 100% rename from src/main/java/org/moonshot/server/domain/keyResult/service/KeyResultService.java rename to src/main/java/org/moonshot/server/domain/keyresult/service/KeyResultService.java diff --git a/src/main/java/org/moonshot/server/domain/user/controller/UserController.java b/src/main/java/org/moonshot/server/domain/user/controller/UserController.java new file mode 100644 index 00000000..ebca2673 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/controller/UserController.java @@ -0,0 +1,54 @@ +package org.moonshot.server.domain.user.controller; + +import lombok.RequiredArgsConstructor; +import org.moonshot.server.domain.user.dto.request.SocialLoginRequest; +import org.moonshot.server.domain.user.dto.response.SocialLoginResponse; +import org.moonshot.server.domain.user.service.UserService; +import org.moonshot.server.global.auth.jwt.JwtTokenProvider; +import org.moonshot.server.global.auth.jwt.TokenResponse; +import org.moonshot.server.global.common.response.ApiResponse; +import org.moonshot.server.global.common.response.SuccessType; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.security.Principal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/user") +public class UserController { + + private final UserService userService; + + @PostMapping("/login") + public ApiResponse login(@RequestHeader("Authorization") String authorization, + @RequestBody SocialLoginRequest socialLoginRequest) throws IOException { + return ApiResponse.success(SuccessType.POST_LOGIN_SUCCESS, userService.login(SocialLoginRequest.of(socialLoginRequest.socialPlatform(), authorization))); + } + + @PostMapping("/reissue") + public ApiResponse reissue(@RequestHeader("Authorization") String refreshToken) { + return ApiResponse.success(SuccessType.POST_REISSUE_SUCCESS, userService.reissue(refreshToken)); + } + + @PostMapping("/log-out") + public ApiResponse logout(Principal principal) { + userService.logout(JwtTokenProvider.getUserIdFromPrincipal(principal)); + return ApiResponse.success(SuccessType.POST_LOGOUT_SUCCESS); + } + + @DeleteMapping("/withdrawal") + public ApiResponse withdrawal(Principal principal) { + userService.withdrawal(JwtTokenProvider.getUserIdFromPrincipal(principal)); + return ApiResponse.success(SuccessType.DELETE_USER_SUCCESS); + } + +// @GetMapping("/login/oauth2/code/kakao") +// public String kakaoSuccess(@RequestParam String code) { +// return code; +// } +// +// @GetMapping("/login/oauth2/code/google") +// public String googleSuccess(@RequestParam String code, @RequestParam String scope, @RequestParam String prompt) { return code; } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/dto/dto b/src/main/java/org/moonshot/server/domain/user/dto/dto deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/org/moonshot/server/domain/user/dto/request/SocialLoginRequest.java b/src/main/java/org/moonshot/server/domain/user/dto/request/SocialLoginRequest.java new file mode 100644 index 00000000..51af0c38 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/request/SocialLoginRequest.java @@ -0,0 +1,12 @@ +package org.moonshot.server.domain.user.dto.request; + +import org.moonshot.server.domain.user.model.SocialPlatform; + +public record SocialLoginRequest( + SocialPlatform socialPlatform, + String code +) { + public static SocialLoginRequest of(SocialPlatform socialPlatform, String code) { + return new SocialLoginRequest(socialPlatform, code); + } +} diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/SocialLoginResponse.java b/src/main/java/org/moonshot/server/domain/user/dto/response/SocialLoginResponse.java new file mode 100644 index 00000000..de428ce4 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/SocialLoginResponse.java @@ -0,0 +1,13 @@ +package org.moonshot.server.domain.user.dto.response; + +import org.moonshot.server.global.auth.jwt.TokenResponse; + +public record SocialLoginResponse( + Long userId, + String userName, + TokenResponse token +) { + public static SocialLoginResponse of(Long userId, String userName, TokenResponse token) { + return new SocialLoginResponse(userId, userName, token); + } +} diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleInfoResponse.java b/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleInfoResponse.java new file mode 100644 index 00000000..47857594 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleInfoResponse.java @@ -0,0 +1,20 @@ +package org.moonshot.server.domain.user.dto.response.google; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record GoogleInfoResponse( + String sub, + String name, + String givenName, + String familyName, + String picture, + String email, + Boolean emailVerified, + String locale +) { + public static GoogleInfoResponse of(String sub, String name, String givenName, String familyName, String picture, String email, Boolean emailVerified, String locale) { + return new GoogleInfoResponse(sub, name, givenName, familyName, picture, email, emailVerified, locale); + } +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleTokenResponse.java b/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleTokenResponse.java new file mode 100644 index 00000000..ce31120b --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/google/GoogleTokenResponse.java @@ -0,0 +1,14 @@ +package org.moonshot.server.domain.user.dto.response.google; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record GoogleTokenResponse( + String accessToken, + String refreshToken +) { + public static GoogleTokenResponse of(String accessToken, String refreshToken) { + return new GoogleTokenResponse(accessToken, refreshToken); + } +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoAccount.java b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoAccount.java new file mode 100644 index 00000000..c76d1605 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoAccount.java @@ -0,0 +1,10 @@ +package org.moonshot.server.domain.user.dto.response.kakao; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccount( + KakaoUserProfile profile +) { +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoTokenResponse.java b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoTokenResponse.java new file mode 100644 index 00000000..a265b7da --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoTokenResponse.java @@ -0,0 +1,14 @@ +package org.moonshot.server.domain.user.dto.response.kakao; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoTokenResponse( + String accessToken, + String refreshToken +) { + public static KakaoTokenResponse of(String accessToken, String refreshToken) { + return new KakaoTokenResponse(accessToken, refreshToken); + } +} diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserProfile.java b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserProfile.java new file mode 100644 index 00000000..cc4d864b --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserProfile.java @@ -0,0 +1,11 @@ +package org.moonshot.server.domain.user.dto.response.kakao; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserProfile( + String nickname, + String profileImageUrl +) { +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserResponse.java b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserResponse.java new file mode 100644 index 00000000..75fec108 --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/dto/response/kakao/KakaoUserResponse.java @@ -0,0 +1,11 @@ +package org.moonshot.server.domain.user.dto.response.kakao; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserResponse( + String id, + KakaoAccount kakaoAccount +) { +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/model/SocialPlatform.java b/src/main/java/org/moonshot/server/domain/user/model/SocialPlatform.java index 9c572208..48bfcfc0 100644 --- a/src/main/java/org/moonshot/server/domain/user/model/SocialPlatform.java +++ b/src/main/java/org/moonshot/server/domain/user/model/SocialPlatform.java @@ -9,7 +9,8 @@ public enum SocialPlatform { KAKAO("kakao"), - GOOGLE("google"); + GOOGLE("google"), + WITHDRAWAL("withdrawal"); private final String value; diff --git a/src/main/java/org/moonshot/server/domain/user/model/User.java b/src/main/java/org/moonshot/server/domain/user/model/User.java index 53341388..f875fecd 100644 --- a/src/main/java/org/moonshot/server/domain/user/model/User.java +++ b/src/main/java/org/moonshot/server/domain/user/model/User.java @@ -36,4 +36,19 @@ public class User { private String description; + @Builder(builderMethodName = "builderWithSignIn") + public static User of(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email) { + return User.builder() + .socialId(socialId) + .socialPlatform(socialPlatform) + .name(name) + .profileImage(profileImage) + .email(email) + .build(); + } + + public void modifySocialPlatform(SocialPlatform socialPlatform) { + this.socialPlatform = socialPlatform; + } + } diff --git a/src/main/java/org/moonshot/server/domain/user/repository/UserRepository.java b/src/main/java/org/moonshot/server/domain/user/repository/UserRepository.java index 1b0e10da..20351278 100644 --- a/src/main/java/org/moonshot/server/domain/user/repository/UserRepository.java +++ b/src/main/java/org/moonshot/server/domain/user/repository/UserRepository.java @@ -1,11 +1,14 @@ package org.moonshot.server.domain.user.repository; -import java.util.Optional; import org.moonshot.server.domain.user.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + Optional findUserBySocialId(String socialId); Optional findUserByNickname(String nickname); } + diff --git a/src/main/java/org/moonshot/server/domain/user/service/UserService.java b/src/main/java/org/moonshot/server/domain/user/service/UserService.java new file mode 100644 index 00000000..a85f4d7c --- /dev/null +++ b/src/main/java/org/moonshot/server/domain/user/service/UserService.java @@ -0,0 +1,153 @@ +package org.moonshot.server.domain.user.service; + +import lombok.RequiredArgsConstructor; +import org.moonshot.server.domain.user.dto.request.SocialLoginRequest; +import org.moonshot.server.domain.user.dto.response.SocialLoginResponse; +import org.moonshot.server.domain.user.dto.response.google.GoogleInfoResponse; +import org.moonshot.server.domain.user.dto.response.google.GoogleTokenResponse; +import org.moonshot.server.domain.user.dto.response.kakao.KakaoTokenResponse; +import org.moonshot.server.domain.user.dto.response.kakao.KakaoUserResponse; +import org.moonshot.server.domain.user.exception.UserNotFoundException; +import org.moonshot.server.domain.user.model.SocialPlatform; +import org.moonshot.server.domain.user.model.User; +import org.moonshot.server.domain.user.repository.UserRepository; +import org.moonshot.server.global.auth.feign.google.GoogleApiClient; +import org.moonshot.server.global.auth.feign.google.GoogleAuthApiClient; +import org.moonshot.server.global.auth.feign.kakao.KakaoApiClient; +import org.moonshot.server.global.auth.feign.kakao.KakaoAuthApiClient; +import org.moonshot.server.global.auth.jwt.JwtTokenProvider; +import org.moonshot.server.global.auth.jwt.TokenResponse; +import org.moonshot.server.global.auth.security.UserAuthentication; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + @Value("${google.client-id}") + private String googleClientId; + + @Value("${google.client-secret}") + private String googleClientSecret; + + @Value("${google.redirect-url}") + private String googleRedirectUrl; + + @Value("${kakao.client-id}") + private String kakaoClientId; + + @Value("${kakao.redirect-url}") + private String kakaoRedirectUrl; + + private final UserRepository userRepository; + private final GoogleAuthApiClient googleAuthApiClient; + private final GoogleApiClient googleApiClient; + private final KakaoAuthApiClient kakaoAuthApiClient; + private final KakaoApiClient kakaoApiClient; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public SocialLoginResponse login(SocialLoginRequest request) throws IOException { + switch (request.socialPlatform().getValue()){ + case "google": + return gooleLogin(request); + case "kakao": + return kakaoLogin(request); + } + return null; + } + + @Transactional + public SocialLoginResponse gooleLogin(SocialLoginRequest request) throws IOException { + GoogleTokenResponse tokenResponse = googleAuthApiClient.googleAuth( + request.code(), + googleClientId, + googleClientSecret, + googleRedirectUrl, + "authorization_code" + ); + GoogleInfoResponse userResponse = googleApiClient.googleInfo("Bearer " + tokenResponse.accessToken()); + Optional findUser = userRepository.findUserBySocialId(userResponse.sub()); + User user; + if (findUser.isEmpty()) { + User newUser = userRepository.save(User.builderWithSignIn() + .socialId(userResponse.sub()) + .socialPlatform(request.socialPlatform()) + .name(userResponse.name()) + .profileImage(userResponse.picture()) + .email(userResponse.email()) + .build()); + + user = newUser; + } else { + user = findUser.get(); + if (user.getSocialPlatform().equals(SocialPlatform.WITHDRAWAL)) { + user.modifySocialPlatform(SocialPlatform.GOOGLE); + } + } + UserAuthentication userAuthentication = new UserAuthentication(user.getId(), null, null); + TokenResponse token = new TokenResponse(jwtTokenProvider.generateAccessToken(userAuthentication), jwtTokenProvider.generateRefreshToken(userAuthentication)); + return SocialLoginResponse.of(user.getId(), user.getName(), token); + } + + @Transactional + public SocialLoginResponse kakaoLogin(SocialLoginRequest request) throws IOException { + KakaoTokenResponse tokenResponse = kakaoAuthApiClient.getOAuth2AccessToken( + "authorization_code", + kakaoClientId, + kakaoRedirectUrl, + request.code() + ); + KakaoUserResponse userResponse = kakaoApiClient.getUserInformation( + "Bearer " + tokenResponse.accessToken()); + Optional findUser = userRepository.findUserBySocialId(userResponse.id()); + User user; + if (findUser.isEmpty()) { + User newUser = userRepository.save(User.builderWithSignIn() + .socialId(userResponse.id()) + .socialPlatform(request.socialPlatform()) + .name(userResponse.kakaoAccount().profile().nickname()) + .profileImage(userResponse.kakaoAccount().profile().profileImageUrl()) + .email("") + .build()); + + user = newUser; + } else { + user = findUser.get(); + if (user.getSocialPlatform().equals(SocialPlatform.WITHDRAWAL)) { + user.modifySocialPlatform(SocialPlatform.KAKAO); + } + } + UserAuthentication userAuthentication = new UserAuthentication(user.getId(), null, null); + TokenResponse token = new TokenResponse(jwtTokenProvider.generateAccessToken(userAuthentication), jwtTokenProvider.generateRefreshToken(userAuthentication)); + return SocialLoginResponse.of(user.getId(), user.getName(), token); + } + + @Transactional + public TokenResponse reissue(String refreshToken) { + String token = refreshToken.substring("Bearer ".length()); + Long userId = jwtTokenProvider.validateRefreshToken(token); + jwtTokenProvider.deleteRefreshToken(userId); + UserAuthentication userAuthentication = new UserAuthentication(userId, null, null); + return jwtTokenProvider.reissuedToken(userAuthentication); + } + + @Transactional + public void logout(Long userId) { + jwtTokenProvider.deleteRefreshToken(userId); + } + + @Transactional + public void withdrawal(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + user.modifySocialPlatform(SocialPlatform.WITHDRAWAL); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/domain/user/service/service b/src/main/java/org/moonshot/server/domain/user/service/service deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/org/moonshot/server/global/auth/exception/ExpiredTokenException.java b/src/main/java/org/moonshot/server/global/auth/exception/ExpiredTokenException.java new file mode 100644 index 00000000..64e9e714 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/exception/ExpiredTokenException.java @@ -0,0 +1,12 @@ +package org.moonshot.server.global.auth.exception; + +import org.moonshot.server.global.common.exception.MoonshotException; +import org.moonshot.server.global.common.response.ErrorType; + +public class ExpiredTokenException extends MoonshotException { + + public ExpiredTokenException() { + super(ErrorType.EXPIRED_TOKEN_ERROR); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/exception/InvalidAuthException.java b/src/main/java/org/moonshot/server/global/auth/exception/InvalidAuthException.java new file mode 100644 index 00000000..24f46bb3 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/exception/InvalidAuthException.java @@ -0,0 +1,12 @@ +package org.moonshot.server.global.auth.exception; + +import org.moonshot.server.global.common.exception.MoonshotException; +import org.moonshot.server.global.common.response.ErrorType; + +public class InvalidAuthException extends MoonshotException { + + public InvalidAuthException() { + super(ErrorType.INVALID_AUTH_ERROR); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/exception/InvalidRefreshTokenException.java b/src/main/java/org/moonshot/server/global/auth/exception/InvalidRefreshTokenException.java new file mode 100644 index 00000000..a5e8c7ab --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/exception/InvalidRefreshTokenException.java @@ -0,0 +1,10 @@ +package org.moonshot.server.global.auth.exception; + +import org.moonshot.server.global.common.exception.MoonshotException; +import org.moonshot.server.global.common.response.ErrorType; + +public class InvalidRefreshTokenException extends MoonshotException { + public InvalidRefreshTokenException() { + super(ErrorType.INVALID_REFRESHTOKEN_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleApiClient.java b/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleApiClient.java new file mode 100644 index 00000000..26abd295 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleApiClient.java @@ -0,0 +1,16 @@ +package org.moonshot.server.global.auth.feign.google; + +import org.moonshot.server.domain.user.dto.response.google.GoogleInfoResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "GoogleApiClient", url = "https://www.googleapis.com") +public interface GoogleApiClient { + + @GetMapping("/oauth2/v3/userinfo") + GoogleInfoResponse googleInfo( + @RequestHeader("Authorization") String token + ); + +} diff --git a/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleAuthApiClient.java b/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleAuthApiClient.java new file mode 100644 index 00000000..848ee52a --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/feign/google/GoogleAuthApiClient.java @@ -0,0 +1,19 @@ +package org.moonshot.server.global.auth.feign.google; + +import org.moonshot.server.domain.user.dto.response.google.GoogleTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "GoogleAuthApiClient", url = "https://oauth2.googleapis.com/token") +public interface GoogleAuthApiClient { + + @PostMapping + GoogleTokenResponse googleAuth( + @RequestParam(name = "code") String code, + @RequestParam(name = "clientId") String clientId, + @RequestParam(name = "clientSecret") String clientSecret, + @RequestParam(name = "redirectUri") String redirectUri, + @RequestParam(name = "grantType") String grantType); + +} diff --git a/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoApiClient.java b/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoApiClient.java new file mode 100644 index 00000000..265b9a39 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoApiClient.java @@ -0,0 +1,15 @@ +package org.moonshot.server.global.auth.feign.kakao; + +import org.moonshot.server.domain.user.dto.response.kakao.KakaoUserResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") +public interface KakaoApiClient { + + @GetMapping(value = "/v2/user/me") + KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken); + +} diff --git a/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoAuthApiClient.java b/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoAuthApiClient.java new file mode 100644 index 00000000..d299ec2d --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/feign/kakao/KakaoAuthApiClient.java @@ -0,0 +1,20 @@ +package org.moonshot.server.global.auth.feign.kakao; + +import org.moonshot.server.domain.user.dto.response.kakao.KakaoTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") +public interface KakaoAuthApiClient { + + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoTokenResponse getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code + ); + +} diff --git a/src/main/java/org/moonshot/server/global/auth/jwt/JwtTokenProvider.java b/src/main/java/org/moonshot/server/global/auth/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..262f36be --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/jwt/JwtTokenProvider.java @@ -0,0 +1,152 @@ +package org.moonshot.server.global.auth.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.moonshot.server.global.auth.exception.InvalidAuthException; +import org.moonshot.server.global.auth.exception.InvalidRefreshTokenException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Base64; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.isNull; +import static org.moonshot.server.global.constants.JWTConstants.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final RedisTemplate redisTemplate; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + @PostConstruct + protected void init() { + JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + } + + public TokenResponse reissuedToken(Authentication authentication) { + return TokenResponse.of( + generateAccessToken(authentication), + generateRefreshToken(authentication)); + } + + public String generateAccessToken(Authentication authentication) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION_TIME)); + + claims.put(USER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + public String generateRefreshToken(Authentication authentication) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME)); + + claims.put(USER_ID, authentication.getPrincipal()); + + String refreshToken = Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + + redisTemplate.opsForValue().set( + authentication.getName(), + refreshToken, + REFRESH_TOKEN_EXPIRATION_TIME, + TimeUnit.MILLISECONDS + ); + return refreshToken; + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateAccessToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + public Long validateRefreshToken(String refreshToken) { + Long userId = getUserFromJwt(refreshToken); + if (redisTemplate.hasKey(String.valueOf(userId))) { + return userId; + } else { + throw new InvalidRefreshTokenException(); + } + } + + public void deleteRefreshToken(Long userId) { + if (redisTemplate.hasKey(String.valueOf(userId))) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + String refreshToken = valueOperations.get(String.valueOf(userId)); + redisTemplate.delete(refreshToken); + } else { + throw new InvalidRefreshTokenException(); + } + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(JWT_SECRET).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.parseLong(claims.get(USER_ID).toString()); + } + + public static Long getUserIdFromPrincipal(Principal principal) { + if (isNull(principal)) { + throw new InvalidAuthException(); + } + return Long.valueOf(principal.getName()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/jwt/JwtValidationType.java b/src/main/java/org/moonshot/server/global/auth/jwt/JwtValidationType.java new file mode 100644 index 00000000..4cbe5f96 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package org.moonshot.server.global.auth.jwt; + +public enum JwtValidationType { + VALID_JWT, + INVALID_JWT_SIGNATURE, + INVALID_JWT_TOKEN, + EXPIRED_JWT_TOKEN, + UNSUPPORTED_JWT_TOKEN, + EMPTY_JWT +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/jwt/TokenResponse.java b/src/main/java/org/moonshot/server/global/auth/jwt/TokenResponse.java new file mode 100644 index 00000000..1bc1d007 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/jwt/TokenResponse.java @@ -0,0 +1,10 @@ +package org.moonshot.server.global.auth.jwt; + +public record TokenResponse( + String accessToken, + String refreshToken +) { + public static TokenResponse of(String accessToken, String refreshToken) { + return new TokenResponse(accessToken, refreshToken); + } +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/redis/RefreshToken.java b/src/main/java/org/moonshot/server/global/auth/redis/RefreshToken.java new file mode 100644 index 00000000..85b05a57 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/redis/RefreshToken.java @@ -0,0 +1,30 @@ +package org.moonshot.server.global.auth.redis; + +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.moonshot.server.global.constants.JWTConstants; +import org.springframework.data.redis.core.RedisHash; + +@RedisHash(value = "refreshToken", timeToLive = 60 * 1000L * 60 * 24 * 7 * 2) +@AllArgsConstructor +@Getter +@Builder +public class RefreshToken { + + @Id + private Long id; + private String refreshToken; + + public static RefreshToken of( + final Long id, + final String refreshToken + ) { + return RefreshToken.builder() + .id(id) + .refreshToken(refreshToken) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/security/CustomAccessDeniedHandler.java b/src/main/java/org/moonshot/server/global/auth/security/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..cb1b3e21 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/security/CustomAccessDeniedHandler.java @@ -0,0 +1,24 @@ +package org.moonshot.server.global.auth.security; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/security/CustomJwtAuthenticationEntryPoint.java b/src/main/java/org/moonshot/server/global/auth/security/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..516a8216 --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/security/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,21 @@ +package org.moonshot.server.global.auth.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/security/JwtAuthenticationFilter.java b/src/main/java/org/moonshot/server/global/auth/security/JwtAuthenticationFilter.java new file mode 100644 index 00000000..a06fa70a --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/security/JwtAuthenticationFilter.java @@ -0,0 +1,57 @@ +package org.moonshot.server.global.auth.security; + + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.moonshot.server.global.auth.exception.ExpiredTokenException; +import org.moonshot.server.global.auth.jwt.JwtTokenProvider; +import org.moonshot.server.global.auth.jwt.JwtValidationType; +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 org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws IOException, ServletException, IOException { + try { + final String token = getJwtFromRequest(request); + + if (jwtTokenProvider.validateAccessToken(token) == JwtValidationType.VALID_JWT) { + Long userId = jwtTokenProvider.getUserFromJwt(token); + UserAuthentication authentication = new UserAuthentication(userId, null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } else if (jwtTokenProvider.validateAccessToken(token) == JwtValidationType.EXPIRED_JWT_TOKEN){ + throw new ExpiredTokenException(); + } + } catch (Exception exception) { + log.info("Error Occured: ", exception); + } + + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/auth/security/UserAuthentication.java b/src/main/java/org/moonshot/server/global/auth/security/UserAuthentication.java new file mode 100644 index 00000000..bd458cfd --- /dev/null +++ b/src/main/java/org/moonshot/server/global/auth/security/UserAuthentication.java @@ -0,0 +1,14 @@ +package org.moonshot.server.global.auth.security; + +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); + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/common/exception/MoonshotControllerAdvice.java b/src/main/java/org/moonshot/server/global/common/exception/MoonshotControllerAdvice.java index 31e1ab2f..ad3ceb85 100644 --- a/src/main/java/org/moonshot/server/global/common/exception/MoonshotControllerAdvice.java +++ b/src/main/java/org/moonshot/server/global/common/exception/MoonshotControllerAdvice.java @@ -1,5 +1,6 @@ package org.moonshot.server.global.common.exception; +import feign.FeignException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintDefinitionException; import jakarta.validation.UnexpectedTypeException; @@ -31,7 +32,7 @@ public class MoonshotControllerAdvice { */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - protected ApiResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + public ApiResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { Errors errors = e.getBindingResult(); Map validateDetails = new HashMap<>(); @@ -46,7 +47,7 @@ protected ApiResponse handleMethodArgumentNotValidException(final MethodArgument @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(UnexpectedTypeException.class) - protected ApiResponse handleUnexpectedTypeException(final UnexpectedTypeException e) { + public ApiResponse handleUnexpectedTypeException(final UnexpectedTypeException e) { log.error(e.getMessage(), e); return ApiResponse.error(ErrorType.INVALID_TYPE); } @@ -59,21 +60,21 @@ public ApiResponse handlerMethodArgumentTypeMismatchException(final MethodArg @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MissingRequestHeaderException.class) - protected ApiResponse handlerMissingRequestHeaderException(final MissingRequestHeaderException e) { + public ApiResponse handlerMissingRequestHeaderException(final MissingRequestHeaderException e) { log.error(e.getMessage(), e); return ApiResponse.error(ErrorType.INVALID_MISSING_HEADER); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(HttpMessageNotReadableException.class) - protected ApiResponse handlerHttpMessageNotReadableException(final HttpMessageNotReadableException e) { + public ApiResponse handlerHttpMessageNotReadableException(final HttpMessageNotReadableException e) { log.error(e.getMessage(), e); return ApiResponse.error(ErrorType.INVALID_HTTP_REQUEST); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - protected ApiResponse handlerHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) { + public ApiResponse handlerHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e) { log.error(e.getMessage(), e); return ApiResponse.error(ErrorType.INVALID_HTTP_METHOD); } @@ -84,12 +85,22 @@ protected ApiResponse handlerConstraintDefinitionException(final ConstraintDe return ApiResponse.error(ErrorType.INVALID_HTTP_REQUEST, e.toString()); } + /** + * 401 UNAUTHROZIED + */ + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(FeignException.class) + public ApiResponse handlerFeignException(final FeignException e) { + log.error(e.getMessage(), e); + return ApiResponse.error(ErrorType.INVALID_AUTHORIZATION_ERROR); + } + /** * 500 INTERNEL_SERVER */ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) - protected ApiResponse handleException(final Exception e, final HttpServletRequest request) throws IOException { + public ApiResponse handleException(final Exception e, final HttpServletRequest request) throws IOException { log.error(e.getMessage(), e); return ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR); } @@ -119,8 +130,9 @@ public ApiResponse handlerRuntimeException(final RuntimeException e, final Ht * CUSTOM_ERROR */ @ExceptionHandler(MoonshotException.class) - protected ApiResponse handleCustomException(MoonshotException e) { + public ApiResponse handleCustomException(MoonshotException e) { log.error(e.getMessage(), e); return ApiResponse.error(e.getErrorType()); } + } diff --git a/src/main/java/org/moonshot/server/global/common/response/ErrorType.java b/src/main/java/org/moonshot/server/global/common/response/ErrorType.java index 3a4f13d7..600e4b54 100644 --- a/src/main/java/org/moonshot/server/global/common/response/ErrorType.java +++ b/src/main/java/org/moonshot/server/global/common/response/ErrorType.java @@ -12,7 +12,6 @@ public enum ErrorType { /* 400 BAD REQUEST */ - REQUEST_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"), INVALID_TYPE(HttpStatus.BAD_REQUEST, "잘못된 타입이 입력되었습니다."), INVALID_MISSING_HEADER(HttpStatus.BAD_REQUEST, "요청에 필요한 헤더값이 존재하지 않습니다."), @@ -25,6 +24,14 @@ public enum ErrorType { INVALID_KEY_RESULT_ORDER(HttpStatus.BAD_REQUEST, "정상적이지 않은 KeyResult 위치입니다."), INVALID_TASK_ORDER(HttpStatus.BAD_REQUEST, "정상적이지 않은 Task 위치입니다."), + /** + * 401 UNAUTHROZIED + */ + INVALID_AUTHORIZATION_ERROR(HttpStatus.UNAUTHORIZED, "유효하지 않은 인증 코드입니다."), + INVALID_REFRESHTOKEN_ERROR(HttpStatus.UNAUTHORIZED, "유효하지 않은 RefreshToken입니다."), + INVALID_AUTH_ERROR(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."), + EXPIRED_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "만료된 TOKEN입니다."), + /** * 404 NOT FOUND */ @@ -45,4 +52,5 @@ public enum ErrorType { public int getHttpStatusCode() { return httpStatus.value(); } + } diff --git a/src/main/java/org/moonshot/server/global/common/response/SuccessType.java b/src/main/java/org/moonshot/server/global/common/response/SuccessType.java index 5390853e..174b3af2 100644 --- a/src/main/java/org/moonshot/server/global/common/response/SuccessType.java +++ b/src/main/java/org/moonshot/server/global/common/response/SuccessType.java @@ -14,6 +14,9 @@ public enum SuccessType { */ OK(HttpStatus.OK, "성공"), GET_PRESIGNED_URL_SUCCESS(HttpStatus.OK, "Presigned Url 조회에 성공하였습니다."), + POST_LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공하였습니다."), + POST_REISSUE_SUCCESS(HttpStatus.OK, "엑세스 토큰 재발급에 성공하였습니다."), + POST_LOGOUT_SUCCESS(HttpStatus.OK, "로그아웃에 성공하였습니다."), /** * 201 CREATED @@ -28,7 +31,8 @@ public enum SuccessType { */ PATCH_KEY_RESULT_SUCCESS(HttpStatus.NO_CONTENT, "KeyResult 수정을 성공하였습니다."), DELETE_KEY_RESULT_SUCCESS(HttpStatus.NO_CONTENT, "KeyResult 삭제를 성공하였습니다."), - DELETE_OBJECTIVE_SUCCESS(HttpStatus.NO_CONTENT, "Objective 삭제를 성공하였습니다."); + DELETE_OBJECTIVE_SUCCESS(HttpStatus.NO_CONTENT, "Objective 삭제를 성공하였습니다."), + DELETE_USER_SUCCESS(HttpStatus.NO_CONTENT, "회원 탈퇴에 성공하였습니다." ); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/org/moonshot/server/global/config/FeignConfig.java b/src/main/java/org/moonshot/server/global/config/FeignConfig.java new file mode 100644 index 00000000..f6cd317d --- /dev/null +++ b/src/main/java/org/moonshot/server/global/config/FeignConfig.java @@ -0,0 +1,10 @@ +package org.moonshot.server.global.config; + +import org.moonshot.server.ServerApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackageClasses = ServerApplication.class) +public class FeignConfig { +} diff --git a/src/main/java/org/moonshot/server/global/config/RedisConfig.java b/src/main/java/org/moonshot/server/global/config/RedisConfig.java new file mode 100644 index 00000000..40f6ee7e --- /dev/null +++ b/src/main/java/org/moonshot/server/global/config/RedisConfig.java @@ -0,0 +1,36 @@ +package org.moonshot.server.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${redis.host}") + private String host; + + @Value("${redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } + +} \ No newline at end of file diff --git a/src/main/java/org/moonshot/server/global/config/SecurityConfig.java b/src/main/java/org/moonshot/server/global/config/SecurityConfig.java index 6d5b7900..09a6c6f5 100644 --- a/src/main/java/org/moonshot/server/global/config/SecurityConfig.java +++ b/src/main/java/org/moonshot/server/global/config/SecurityConfig.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.moonshot.server.global.auth.filter.MoonshotExceptionHandler; +import org.moonshot.server.global.auth.security.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,6 +11,7 @@ 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; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -23,6 +25,7 @@ public class SecurityConfig { "/login/**", "/", "/actuator/health", + "/v1/user/**", "/v1/image", "/v1/objective", "/v1/task", @@ -33,6 +36,7 @@ public class SecurityConfig { "/api-docs/**" }; + private final JwtAuthenticationFilter jwtAuthenticationFilter; private final MoonshotExceptionHandler moonshotExceptionHandler; @Value("${server.ip}") private String serverIp; @@ -56,6 +60,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorizationManagerRequestMatcherRegistry.requestMatchers(WHITELIST).permitAll()) .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry.anyRequest().authenticated()) + .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)) .build(); } @@ -64,9 +70,11 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:8080"); - config.addAllowedOrigin("http://localhost:3000"); + config.addAllowedOrigin("http://localhost:5173"); config.addAllowedOrigin("https://kauth.kakao.com"); config.addAllowedOrigin("https://kapi.kakao.com"); + config.addAllowedOrigin("http://www.googleapis.com"); + config.addAllowedOrigin("https://www.googleapis.com"); config.addAllowedOrigin(serverIp); config.addAllowedOrigin(serverDomain); config.addAllowedHeader("*"); diff --git a/src/main/java/org/moonshot/server/global/constants/AWSConstants.java b/src/main/java/org/moonshot/server/global/constants/AWSConstants.java index ee71e078..668a0199 100644 --- a/src/main/java/org/moonshot/server/global/constants/AWSConstants.java +++ b/src/main/java/org/moonshot/server/global/constants/AWSConstants.java @@ -2,4 +2,5 @@ public class AWSConstants { public static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 1L; + } diff --git a/src/main/java/org/moonshot/server/global/constants/JWTConstants.java b/src/main/java/org/moonshot/server/global/constants/JWTConstants.java new file mode 100644 index 00000000..59509edb --- /dev/null +++ b/src/main/java/org/moonshot/server/global/constants/JWTConstants.java @@ -0,0 +1,9 @@ +package org.moonshot.server.global.constants; + +public class JWTConstants { + + public static final String USER_ID = "userId"; + public static final Long ACCESS_TOKEN_EXPIRATION_TIME = 60 * 1000L * 20; // 액세스 토큰 만료 시간: 20분으로 지정 + public static final Long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 1000L * 60 * 24 * 7 * 2; // 리프레시 토큰 만료 시간: 2주로 지정 + +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 8986f3f6..3f8c6b93 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -17,9 +17,22 @@ spring: activate: on-profile: dev +google: + client-id: ${GOOGLE_CLIENT_ID} + redirect-url: ${GOOGLE_URL} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: email, profile + kakao: - client-id: ${CLIENT_ID} - url: ${KAKAO_URL} + client-id: ${KAKAO_CLIENT_ID} + redirect-url: ${KAKAO_URL} + +jwt: + secret: ${JWT_SECRET} + +redis: + host: localhost + port: 6379 server: ip: ${SERVER_IP} @@ -37,3 +50,19 @@ logging: config: classpath:logback-dev.xml level: org.springframework.security: DEBUG + +springdoc: + packages-to-scan: org.moonshot.server + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + tags-sorter: alpha + operations-sorter: alpha + api-docs: + path: /api-docs/json + groups: + enabled: true + cache: + disabled: true + show-login-endpoint: true + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f271419f..8f70e26d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,3 @@ - - spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -21,9 +19,22 @@ spring: activate: on-profile: local +google: + client-id: ${GOOGLE_CLIENT_ID} + redirect-url: ${GOOGLE_URL} + client-secret: ${GOOGLE_CLIENT_SECRET} + scope: email, profile + kakao: - client-id: ${CLIENT_ID} - url: ${KAKAO_URL} + client-id: ${KAKAO_CLIENT_ID} + redirect-url: ${KAKAO_URL} + +jwt: + secret: ${JWT_SECRET} + +redis: + host: localhost + port: 6379 server: ip: ${SERVER_IP}