diff --git a/.gitignore b/.gitignore index 93591d2..33cc3fa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ +.vscode/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index c88604a..cdb6e18 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,24 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //security + implementation 'org.springframework.boot:spring-boot-starter-security' + + + //JWT + implementation 'io.jsonwebtoken:jjwt:0.11.5' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //유효성 검사 라이브러리 + implementation 'commons-validator:commons-validator:1.7' + + // cache memory + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + } tasks.named('test') { diff --git a/src/main/java/com/example/PING/WebConfig.java b/src/main/java/com/example/PING/WebConfig.java index 019de27..96c6ece 100644 --- a/src/main/java/com/example/PING/WebConfig.java +++ b/src/main/java/com/example/PING/WebConfig.java @@ -1,24 +1,42 @@ package com.example.PING; +import com.example.PING.global.annotation.AuthenticationArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration +@EnableScheduling +@RequiredArgsConstructor +@EnableTransactionManagement public class WebConfig implements WebMvcConfigurer { -// private final String ipAddress = "localhost"; // 로컬 테스트용 IP +// private final String ipAddress = "localhost:8080"; // 로컬 테스트용 IP // private final String ipAddress = "43.203.51.237"; // CI/CD 배포를 위한 IP 주소 // private final String frontEndPort = "5173"; // React 앱이 실행되는 포트 + private final AuthenticationArgumentResolver authenticationArgumentResolver; + @Value("${cors-allowed-origins}") + private List corsAllowedOrigins; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowCredentials(true) - .allowedHeaders("*") + .allowedOrigins(corsAllowedOrigins.toArray(new String[0])) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") -// .allowedOrigins("http://" + this.ipAddress + ":" + this.frontEndPort); - .allowedOrigins("http://ping-deploy-bucket.s3-website.ap-northeast-2.amazonaws.com"); // S3 배포로 제공받은 도메인 주소 + .allowedHeaders("*") + .allowCredentials(true); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authenticationArgumentResolver); } } diff --git a/src/main/java/com/example/PING/auth/controller/AuthController.java b/src/main/java/com/example/PING/auth/controller/AuthController.java new file mode 100644 index 0000000..0ff1b2a --- /dev/null +++ b/src/main/java/com/example/PING/auth/controller/AuthController.java @@ -0,0 +1,64 @@ +package com.example.PING.auth.controller; + +import com.example.PING.auth.dto.request.SocialSignUpRequest; +import com.example.PING.auth.dto.response.LoginResponse; +import com.example.PING.auth.dto.response.SocialLoginResponse; +import com.example.PING.auth.dto.response.SocialSignUpResponse; +import com.example.PING.auth.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @Operation( + summary = "소셜 로그인", + description = "소셜 로그인 후 Authorization 토큰과 회원 정보를 발급합니다." + ) + @PostMapping("/oauth/social-login") + public ResponseEntity socialLogin( + @RequestHeader("social_access_token") String accessToken, + @RequestParam("provider") + @Parameter(example = "kakao", description = "OAuth 제공자") + String provider + ) { + LoginResponse response = authService.socialLogin(accessToken, provider); + // Todo 정상토큰이면 로그인 완료 + HttpHeaders headers = new HttpHeaders(); + headers.set("AccessToken", response.accessToken()); + headers.set("RefreshToken", response.refreshToken()); + headers.set("TemporaryToken", response.temporaryToken()); + + return new ResponseEntity<>(SocialLoginResponse.of(response), headers, + HttpStatus.OK); + } + + @Operation( + summary = "소셜 회원가입", + description = "Temporary 토큰을 통해 확인 후, 정식 회원으로 등록합니다." + ) + @PostMapping("/oauth/social-signUp") // 닉네임 받아서 유저 등록 및 회원 가입 완료 + public ResponseEntity socialSignUp( + @RequestHeader("temporary_token") String temporaryToken, + @RequestBody SocialSignUpRequest request // 바디에 닉네임 들어있도록 + ){ + // 받은 닉네임이랑 캐시메모리에 있는 정보 넣어서 정식 user 저장. + // 정상토큰 발급 + LoginResponse response = authService.registerSocialSignUpUser(temporaryToken, request.nickname()); + + HttpHeaders headers = new HttpHeaders(); + headers.set("AccessToken", response.accessToken()); + headers.set("RefreshToken", response.refreshToken()); + + return new ResponseEntity<>(SocialSignUpResponse.of(response), headers, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/auth/dto/request/SocialSignUpRequest.java b/src/main/java/com/example/PING/auth/dto/request/SocialSignUpRequest.java new file mode 100644 index 0000000..8be928a --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/request/SocialSignUpRequest.java @@ -0,0 +1,6 @@ +package com.example.PING.auth.dto.request; + +public record SocialSignUpRequest( + String nickname +) { +} diff --git a/src/main/java/com/example/PING/auth/dto/response/LoginResponse.java b/src/main/java/com/example/PING/auth/dto/response/LoginResponse.java new file mode 100644 index 0000000..1a72bd9 --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/LoginResponse.java @@ -0,0 +1,16 @@ +package com.example.PING.auth.dto.response; + +import com.example.PING.user.entity.User; + +public record LoginResponse( + Long userId, + String nickname, + String email, + String accessToken, + String refreshToken, + String temporaryToken +) { + public static LoginResponse of(User user, TokenSetResponse tokenSetResponse) { + return new LoginResponse(user.getUserId(), user.getNickname(), user.getOauthInfo().getOauthEmail(), tokenSetResponse.accessToken(), tokenSetResponse.refreshToken(), tokenSetResponse.temporaryToken()); + } +} diff --git a/src/main/java/com/example/PING/auth/dto/response/SocialLoginResponse.java b/src/main/java/com/example/PING/auth/dto/response/SocialLoginResponse.java new file mode 100644 index 0000000..bba5b2b --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/SocialLoginResponse.java @@ -0,0 +1,9 @@ +package com.example.PING.auth.dto.response; + +public record SocialLoginResponse(Long userId, + String nickname, + String email) { + public static SocialLoginResponse of(LoginResponse loginResponse) { + return new SocialLoginResponse(loginResponse.userId(), loginResponse.nickname(), loginResponse.email()); + } +} diff --git a/src/main/java/com/example/PING/auth/dto/response/SocialSignUpResponse.java b/src/main/java/com/example/PING/auth/dto/response/SocialSignUpResponse.java new file mode 100644 index 0000000..e66ff4b --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/SocialSignUpResponse.java @@ -0,0 +1,8 @@ +package com.example.PING.auth.dto.response; + +public record SocialSignUpResponse(Long userId, + String nickname) { + public static SocialSignUpResponse of(LoginResponse loginResponse) { + return new SocialSignUpResponse(loginResponse.userId(), loginResponse.nickname()); + } +} diff --git a/src/main/java/com/example/PING/auth/dto/response/TemporaryTokenResponse.java b/src/main/java/com/example/PING/auth/dto/response/TemporaryTokenResponse.java new file mode 100644 index 0000000..75270b9 --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/TemporaryTokenResponse.java @@ -0,0 +1,11 @@ +package com.example.PING.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record TemporaryTokenResponse( + @Schema(description = "임시 토큰", defaultValue = "temporaryToken") String temporaryToken) { + + public static TemporaryTokenResponse of(String temporaryToken) { + return new TemporaryTokenResponse(temporaryToken); + } +} diff --git a/src/main/java/com/example/PING/auth/dto/response/TokenSetResponse.java b/src/main/java/com/example/PING/auth/dto/response/TokenSetResponse.java new file mode 100644 index 0000000..f838a0d --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/TokenSetResponse.java @@ -0,0 +1,13 @@ +package com.example.PING.auth.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record TokenSetResponse( + @Schema(description = "액세스 토큰", defaultValue = "accessToken") String accessToken, + @Schema(description = "리프레시 토큰", defaultValue = "refreshToken") String refreshToken, + @Schema(description = "임시 토큰", defaultValue = "temporaryToken") String temporaryToken) { + + public static TokenSetResponse of(String accessToken, String refreshToken, String temporaryToken) { + return new TokenSetResponse(accessToken, refreshToken, temporaryToken); + } +} diff --git a/src/main/java/com/example/PING/auth/dto/response/auth/KakaoAuthResponse.java b/src/main/java/com/example/PING/auth/dto/response/auth/KakaoAuthResponse.java new file mode 100644 index 0000000..a7402e0 --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/auth/KakaoAuthResponse.java @@ -0,0 +1,13 @@ +package com.example.PING.auth.dto.response.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoAuthResponse( + Long id, + @JsonProperty("kakao_account") KakaoAccountResponse kakaoAccount, + @JsonProperty("properties") PropertiesResponse properties) { + public record KakaoAccountResponse(String email) {} + + public record PropertiesResponse( + String nickname, String profile_image, String thumbnail_image) {} +} diff --git a/src/main/java/com/example/PING/auth/dto/response/auth/OAuthUserInfoResponse.java b/src/main/java/com/example/PING/auth/dto/response/auth/OAuthUserInfoResponse.java new file mode 100644 index 0000000..122f04e --- /dev/null +++ b/src/main/java/com/example/PING/auth/dto/response/auth/OAuthUserInfoResponse.java @@ -0,0 +1,24 @@ +package com.example.PING.auth.dto.response.auth; + +import com.example.PING.auth.service.OAuthProvider; +import com.example.PING.user.entity.OauthInfo; +import lombok.Builder; + +@Builder +public record OAuthUserInfoResponse( // 받은 json 타입을 자바로 변환하기 위한 dto +// 카카오 말고 다른 거에서도 쓸 수 있게 하기 위해 공통 타입으로 변환하려고 이거 dto로 변환하는 거임 + String oauthId, + String email, + String name, + OAuthProvider provider + +) { + public OauthInfo toEntity() { + return OauthInfo.builder() + .oauthId(this.oauthId()) + .oauthEmail(this.email()) + .oauthName(this.name()) + .oauthProvider(this.provider().name()) + .build(); + } +} diff --git a/src/main/java/com/example/PING/auth/entity/RefreshToken.java b/src/main/java/com/example/PING/auth/entity/RefreshToken.java new file mode 100644 index 0000000..1a96e0e --- /dev/null +++ b/src/main/java/com/example/PING/auth/entity/RefreshToken.java @@ -0,0 +1,37 @@ +package com.example.PING.auth.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor +@Getter +public class RefreshToken{ + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name = "update_at") + private LocalDateTime updatedDate; + + @Id + @Column(nullable = false, name = "refresh_token_id") + private Long memberId; + @Column(nullable = false) + private String token; + + @Builder + public RefreshToken(Long memberId, String token) { + this.memberId = memberId; + this.token = token; + } +} diff --git a/src/main/java/com/example/PING/auth/repository/RefreshTokenRepository.java b/src/main/java/com/example/PING/auth/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..42e1759 --- /dev/null +++ b/src/main/java/com/example/PING/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,11 @@ +package com.example.PING.auth.repository; + +import com.example.PING.auth.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { + + boolean existsByMemberId(Long memberId); + + void deleteByMemberId(Long memberId); +} diff --git a/src/main/java/com/example/PING/auth/service/AuthService.java b/src/main/java/com/example/PING/auth/service/AuthService.java new file mode 100644 index 0000000..3482f8e --- /dev/null +++ b/src/main/java/com/example/PING/auth/service/AuthService.java @@ -0,0 +1,62 @@ +package com.example.PING.auth.service; + +import com.example.PING.auth.dto.response.LoginResponse; +import com.example.PING.auth.dto.response.TokenSetResponse; +import com.example.PING.auth.dto.response.auth.OAuthUserInfoResponse; +import com.example.PING.auth.service.kakao.KakaoClient; +import com.example.PING.global.security.utils.JwtTokenGenerator; +import com.example.PING.user.entity.OauthInfo; +import com.example.PING.user.entity.User; +import com.example.PING.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final KakaoClient kakaoClient; + private final UserService userService; + private final JwtTokenGenerator jwtTokenGenerator; + private final TemporalUserCacheService temporalUserCacheService; + + @Transactional + public LoginResponse socialLogin(String socialAccessToken, String provider) { + OAuthUserInfoResponse oauthUserInfoResponse = getOauthUserInfo(socialAccessToken, OAuthProvider.from(provider)); + OauthInfo oauthInfo = oauthUserInfoResponse.toEntity(); + + User user = userService.getUserByOAuthInfo(oauthInfo); + // 만약 소셜로그인으로 새로이 회원가입을 한 사람이라면, 여기서 nickname 필드가 null로 되어 있음. + + if(user.getNickname() == null) { // 임시 토큰을 발급해야 하는 경우 + TokenSetResponse tokenSetResponse = jwtTokenGenerator.generateTemporaryToken(user); + temporalUserCacheService.set(tokenSetResponse.temporaryToken(), user); + return LoginResponse.of(user, tokenSetResponse); + } + + TokenSetResponse tokenSetResponse = jwtTokenGenerator.generateTokenPair(user); + return LoginResponse.of(user, tokenSetResponse); + } + + public LoginResponse registerSocialSignUpUser(String tempToken, String nickname) { + User tempUser = temporalUserCacheService.get(tempToken, User.class); + if (tempUser == null) { + throw new RuntimeException("인증 정보가 만료되었습니다."); + } + + tempUser.setNickname(nickname); // 닉네임 설정 + User savedNewUser = userService.saveTempUser(tempUser); // DB에 새로운 회원으로 저장 + + temporalUserCacheService.delete(tempToken); // 캐시에서 삭제 + // Todo 임시토큰 무효화해야 하나? + return LoginResponse.of(savedNewUser, jwtTokenGenerator.generateTokenPair(savedNewUser)); + } + + private OAuthUserInfoResponse getOauthUserInfo(String socialAccessToken, OAuthProvider provider) { + return switch (provider) { + case APPLE -> null; // 나중에 다른 것도 하면 이런 식으로~^^ + case KAKAO -> kakaoClient.getOauthUserInfo(socialAccessToken); + }; + } +} diff --git a/src/main/java/com/example/PING/auth/service/OAuthProvider.java b/src/main/java/com/example/PING/auth/service/OAuthProvider.java new file mode 100644 index 0000000..874fdda --- /dev/null +++ b/src/main/java/com/example/PING/auth/service/OAuthProvider.java @@ -0,0 +1,21 @@ +package com.example.PING.auth.service; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum OAuthProvider { + KAKAO("KAKAO"), + APPLE("APPLE"), + ; + private final String value; + + public static OAuthProvider from(String provider) { + return switch (provider.toUpperCase()) { + case "APPLE" -> APPLE; + case "KAKAO" -> KAKAO; + default -> throw new IllegalArgumentException("Unknown provider: " + provider); // Todo 에러코드 이넘화 해놓기 + }; + } +} diff --git a/src/main/java/com/example/PING/auth/service/TemporalUserCacheService.java b/src/main/java/com/example/PING/auth/service/TemporalUserCacheService.java new file mode 100644 index 0000000..f578059 --- /dev/null +++ b/src/main/java/com/example/PING/auth/service/TemporalUserCacheService.java @@ -0,0 +1,37 @@ +package com.example.PING.auth.service; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Service +public class TemporalUserCacheService { + + // 데이터 조회 (캐시된 값이 있으면 반환, 없으면 null) + @Cacheable(value = "localCache", key = "#p0", unless = "#result == null") + public T get(String key, Class clazz) { + if (!StringUtils.hasText(key)) { + throw new IllegalArgumentException("Cache key cannot be null or empty"); + } + return null; + } + + // 데이터 저장 (항상 실행되며, 캐시에 저장됨) + @CachePut(value = "localCache", key = "#p0") + public Object set(String key, Object value) { + if (!StringUtils.hasText(key)) { + throw new IllegalArgumentException("Cache key cannot be null or empty"); + } + return value; + } + + // 데이터 삭제 + @CacheEvict(value = "localCache", key = "#p0") + public void delete(String key) { + if (!StringUtils.hasText(key)) { + throw new IllegalArgumentException("Cache key cannot be null or empty"); + } + } +} diff --git a/src/main/java/com/example/PING/auth/service/kakao/KakaoClient.java b/src/main/java/com/example/PING/auth/service/kakao/KakaoClient.java new file mode 100644 index 0000000..5a02bde --- /dev/null +++ b/src/main/java/com/example/PING/auth/service/kakao/KakaoClient.java @@ -0,0 +1,51 @@ +package com.example.PING.auth.service.kakao; + +import com.example.PING.auth.dto.response.auth.KakaoAuthResponse; +import com.example.PING.auth.dto.response.auth.OAuthUserInfoResponse; +import com.example.PING.auth.service.OAuthProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.nio.file.AccessDeniedException; +import java.util.Objects; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KakaoClient { + + private final RestClient restClient; + public static final String KAKAO_USER_ME_URL = "https://kapi.kakao.com/v2/user/me"; + public static final String TOKEN_PREFIX = "Bearer "; + + public OAuthUserInfoResponse getOauthUserInfo(String token) { + KakaoAuthResponse kakaoAuthResponse = + restClient + .get() + .uri(KAKAO_USER_ME_URL) + .header("Authorization", TOKEN_PREFIX + token) + .exchange( + (request, response) -> { + HttpStatusCode statusCode = response.getStatusCode(); + log.info("Received response with status: {}", statusCode); + + if (!response.getStatusCode().is2xxSuccessful()) { + log.error("Kakao API error. Status: {}, Response: {}", + statusCode, response.bodyTo(String.class)); + throw new AccessDeniedException("카카오 AT 인증이 실패했습니다."); + } + return Objects.requireNonNull( + response.bodyTo(KakaoAuthResponse.class)); + }); + + return new OAuthUserInfoResponse( + kakaoAuthResponse.id().toString(), + kakaoAuthResponse.kakaoAccount().email(), + kakaoAuthResponse.properties().nickname(), + OAuthProvider.KAKAO + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/domain/service/DomainService.java b/src/main/java/com/example/PING/domain/service/DomainService.java index 6a2b8db..3bf0d17 100644 --- a/src/main/java/com/example/PING/domain/service/DomainService.java +++ b/src/main/java/com/example/PING/domain/service/DomainService.java @@ -4,7 +4,7 @@ import com.example.PING.domain.dto.response.DomainResponse; import com.example.PING.domain.entity.Domain; import com.example.PING.portfolio.entity.Portfolio; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import com.example.PING.domain.repository.DomainRepository; import com.example.PING.portfolio.repository.PortfolioRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/PING/error/exception/BusinessException.java b/src/main/java/com/example/PING/error/exception/BusinessException.java new file mode 100644 index 0000000..fa37275 --- /dev/null +++ b/src/main/java/com/example/PING/error/exception/BusinessException.java @@ -0,0 +1,19 @@ +package com.example.PING.error.exception; + +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/example/PING/error/exception/ErrorCode.java b/src/main/java/com/example/PING/error/exception/ErrorCode.java new file mode 100644 index 0000000..8379723 --- /dev/null +++ b/src/main/java/com/example/PING/error/exception/ErrorCode.java @@ -0,0 +1,44 @@ +package com.example.PING.error.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + // Common + INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "Test"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"), + ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 엔티티를 찾을 수 없습니다."), + BAD_CREDENTIALS(HttpStatus.UNAUTHORIZED, "잘못된 인증 정보입니다"), + + + //Auth + AUTH_NOT_FOUND(HttpStatus.UNAUTHORIZED, "시큐리티 인증 정보를 찾을 수 없습니다."), + UNKNOWN_ERROR(HttpStatus.UNAUTHORIZED, "알 수 없는 에러"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 Token입니다"), + UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 길이 및 형식이 다른 Token입니다"), + WRONG_TYPE_TOKEN(HttpStatus.UNAUTHORIZED, "서명이 잘못된 토큰입니다."), + ACCESS_DENIED(HttpStatus.UNAUTHORIZED, "토큰이 없습니다"), + TOKEN_SUBJECT_FORMAT_ERROR(HttpStatus.UNAUTHORIZED, "Subject 값에 Long 타입이 아닌 다른 타입이 들어있습니다."), + AT_EXPIRED_AND_RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AT는 만료되었고 RT는 비어있습니다."), + RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "RT가 비어있습니다"), + + //OAuth + KAKAO_TOKEN_CLIENT_FAILED(HttpStatus.UNAUTHORIZED, "카카오 AT 인증이 실패했습니다."), + AUTH_GET_USER_INFO_FAILED(HttpStatus.UNAUTHORIZED, "SocialAccessToken을 통해 사용자 정보를 가져오는 데에 실패했습니다."), + INVALID_PROVIDER_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 provider를 입력하셨습니다."), + PASSWORD_NOT_MATCHES(HttpStatus.BAD_REQUEST, "비밀번호를 잘못 입력하셨습니다."), + + //User + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), + ONBOARD_STATUS_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 온보딩 상태를 찾을 수 없습니다. (잘못된 온보딩 상태를 입력하셨습니다)") + ; + + private final HttpStatus status; + private final String message; + + ErrorCode(HttpStatus status, String message) { + this.status = status; + this.message = message; + } +} diff --git a/src/main/java/com/example/PING/error/exception/JwtInvalidException.java b/src/main/java/com/example/PING/error/exception/JwtInvalidException.java new file mode 100644 index 0000000..ee8d75f --- /dev/null +++ b/src/main/java/com/example/PING/error/exception/JwtInvalidException.java @@ -0,0 +1,14 @@ +package com.example.PING.error.exception; + +import org.springframework.security.core.AuthenticationException; + +public class JwtInvalidException extends AuthenticationException { + + public JwtInvalidException(String msg) { + super(msg); + } + + public JwtInvalidException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/com/example/PING/error/ResourceNotFoundException.java b/src/main/java/com/example/PING/error/exception/ResourceNotFoundException.java similarity index 87% rename from src/main/java/com/example/PING/error/ResourceNotFoundException.java rename to src/main/java/com/example/PING/error/exception/ResourceNotFoundException.java index f0f60a2..a2c2f0a 100644 --- a/src/main/java/com/example/PING/error/ResourceNotFoundException.java +++ b/src/main/java/com/example/PING/error/exception/ResourceNotFoundException.java @@ -1,4 +1,4 @@ -package com.example.PING.error; +package com.example.PING.error.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/com/example/PING/global/annotation/AuthUser.java b/src/main/java/com/example/PING/global/annotation/AuthUser.java new file mode 100644 index 0000000..53c5a01 --- /dev/null +++ b/src/main/java/com/example/PING/global/annotation/AuthUser.java @@ -0,0 +1,15 @@ +package com.example.PING.global.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(hidden = true) +public @interface AuthUser { + +} diff --git a/src/main/java/com/example/PING/global/annotation/AuthenticationArgumentResolver.java b/src/main/java/com/example/PING/global/annotation/AuthenticationArgumentResolver.java new file mode 100644 index 0000000..c6ff579 --- /dev/null +++ b/src/main/java/com/example/PING/global/annotation/AuthenticationArgumentResolver.java @@ -0,0 +1,53 @@ +package com.example.PING.global.annotation; + +import com.example.PING.error.exception.BusinessException; +import com.example.PING.error.exception.ResourceNotFoundException; +import com.example.PING.error.exception.ErrorCode; +import com.example.PING.user.entity.User; +import com.example.PING.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver { + private final UserRepository userRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + final boolean isUserAuthAnnotation = parameter.getParameterAnnotation(AuthUser.class) != null; + final boolean isUserClass = parameter.getParameterType().equals(User.class); + return isUserAuthAnnotation && isUserClass; + } + + @Override + public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return getCurrentUser(); + } + + private User getCurrentUser() { + return userRepository + .findById(getCurrentUSerId()) + .orElseThrow(() -> new ResourceNotFoundException("USER_NOT_FOUND")); + } + + private Long getCurrentUSerId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new BusinessException(ErrorCode.AUTH_NOT_FOUND); + } + + User principal = (User) authentication.getPrincipal(); + return principal.getUserId(); + } + +} diff --git a/src/main/java/com/example/PING/global/config/CacheConfig.java b/src/main/java/com/example/PING/global/config/CacheConfig.java new file mode 100644 index 0000000..1187014 --- /dev/null +++ b/src/main/java/com/example/PING/global/config/CacheConfig.java @@ -0,0 +1,33 @@ +package com.example.PING.global.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public SimpleCacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + + // Caffeine Cache 사용 + CaffeineCache caffeineCache = new CaffeineCache("localCache", + Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .maximumSize(1000) + .build()); + + cacheManager.setCaches(List.of(caffeineCache)); + return cacheManager; + } +} + diff --git a/src/main/java/com/example/PING/global/config/properties/PropertiesConfig.java b/src/main/java/com/example/PING/global/config/properties/PropertiesConfig.java new file mode 100644 index 0000000..e3b546e --- /dev/null +++ b/src/main/java/com/example/PING/global/config/properties/PropertiesConfig.java @@ -0,0 +1,13 @@ +package com.example.PING.global.config.properties; + +import com.example.PING.global.properties.JwtProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@EnableConfigurationProperties({ + JwtProperties.class +}) +@Configuration +public class PropertiesConfig { + +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/global/config/restClient/RestClientConfig.java b/src/main/java/com/example/PING/global/config/restClient/RestClientConfig.java new file mode 100644 index 0000000..a06f29a --- /dev/null +++ b/src/main/java/com/example/PING/global/config/restClient/RestClientConfig.java @@ -0,0 +1,23 @@ +package com.example.PING.global.config.restClient; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +import java.time.Duration; + +@Configuration +public class RestClientConfig { //Todo 채현이가 탐구해야 하는 클래스 + + @Bean + public RestClient restClient() { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(Duration.ofSeconds(10)); + requestFactory.setReadTimeout(Duration.ofSeconds(5)); + + return RestClient.builder() + .requestFactory(requestFactory) + .build(); + } +} diff --git a/src/main/java/com/example/PING/global/config/security/SecurityConfig.java b/src/main/java/com/example/PING/global/config/security/SecurityConfig.java new file mode 100644 index 0000000..2525b9c --- /dev/null +++ b/src/main/java/com/example/PING/global/config/security/SecurityConfig.java @@ -0,0 +1,108 @@ +package com.example.PING.global.config.security; + +import com.example.PING.global.security.filter.JwtAuthenticationFilter; +import com.example.PING.global.security.provider.JwtProvider; +import com.example.PING.global.security.utils.JwtUtil; +import com.example.PING.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { //Todo 채현이가 탐구해야 하는 클래스 + + private final UserService userService; + private final JwtUtil jwtUtil; + + // Todo allowUrls 수정해야 함. 현재 끼니 기준으로 되어 있음. + private String[] allowUrls = {"/", "/favicon.ico", + "/api/v1/auth/oauth/**", "/swagger-ui/**", "/v3/**"}; + + @Value("${cors-allowed-origins}") + private List corsAllowedOrigins; + + @Bean + public JwtProvider jwtTokenProvider() { + return new JwtProvider(jwtUtil, userService); + } + + // 3. AuthenticationManager + @Bean + public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = + http.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder + .authenticationProvider(jwtTokenProvider()); + authenticationManagerBuilder.parentAuthenticationManager(null); + return authenticationManagerBuilder.build(); + } + + // 4. CORS Configuration + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(corsAllowedOrigins); + configuration.addAllowedMethod("*"); + configuration.setAllowedHeaders(List.of("*")); // 허용할 헤더 + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용 + return source; + } + + // 5. Web Security Customizer + @Bean + public WebSecurityCustomizer configure() { + return (web) -> web.ignoring().requestMatchers(allowUrls); + } + + // 6. SecurityFilterChain + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .cors(customizer -> customizer.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(request -> request + .requestMatchers(allowUrls).permitAll() + .anyRequest().authenticated()); + + http + .exceptionHandling(exception -> + exception.authenticationEntryPoint((request, response, authException) -> + response.setStatus(HttpStatus.UNAUTHORIZED.value()))); + + http.authenticationManager(authenticationManager(http)); + + http.addFilterBefore(jwtAuthenticationFilter(authenticationManager(http)), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + // 8. JwtAuthenticationFilter + public JwtAuthenticationFilter jwtAuthenticationFilter(AuthenticationManager authenticationManager) throws Exception { + JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager); + filter.afterPropertiesSet(); + return filter; + } +} diff --git a/src/main/java/com/example/PING/global/config/swagger/SwaggerConfig.java b/src/main/java/com/example/PING/global/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..21cbcab --- /dev/null +++ b/src/main/java/com/example/PING/global/config/swagger/SwaggerConfig.java @@ -0,0 +1,28 @@ +package com.example.PING.global.config.swagger; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("Bearer"); + } + + @Bean + public OpenAPI openAPI(){ + return new OpenAPI().addSecurityItem(new SecurityRequirement().addList("JWT")) + .components(new Components().addSecuritySchemes("JWT", createAPIKeyScheme())) + .info(new Info().title("💚PING API 명세서💚") + .description("PING API 명세서입니다.") + .version("v0.0.1")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/global/properties/JwtProperties.java b/src/main/java/com/example/PING/global/properties/JwtProperties.java new file mode 100644 index 0000000..9c246b0 --- /dev/null +++ b/src/main/java/com/example/PING/global/properties/JwtProperties.java @@ -0,0 +1,25 @@ +package com.example.PING.global.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties( + String accessTokenSecret, + String refreshTokenSecret, + String temporaryTokenSecret, + Long accessTokenExpirationTime, + Long refreshTokenExpirationTime, + Long temporaryTokenExpirationTime, + String issuer +) { + + public Long accessTokenExpirationMilliTime() { + return accessTokenExpirationTime * 1000; + } + + public Long refreshTokenExpirationMilliTime() { + return refreshTokenExpirationTime * 1000; + } + + public Long temporaryTokenExpirationMilliTime() {return temporaryTokenExpirationTime * 1000;} +} diff --git a/src/main/java/com/example/PING/global/security/AuthConstants.java b/src/main/java/com/example/PING/global/security/AuthConstants.java new file mode 100644 index 0000000..c59d9b9 --- /dev/null +++ b/src/main/java/com/example/PING/global/security/AuthConstants.java @@ -0,0 +1,13 @@ +package com.example.PING.global.security; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class AuthConstants { + + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_TYPE = "BEARER"; + public static final String REFRESH_TOKEN_HEADER = "RefreshToken"; + +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/PING/global/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..1fdd940 --- /dev/null +++ b/src/main/java/com/example/PING/global/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,48 @@ +package com.example.PING.global.security.filter; + +import com.example.PING.global.security.token.JwtAuthenticationToken; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Optional; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + public static final String TOKEN_PREFIX = "Bearer "; + + private final AuthenticationManager authenticationManager; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String authorization = request.getHeader("Authorization"); + System.out.println("Authorization: " + authorization); + String accessToken = extractAccessTokenFromHeader(request); + + if (StringUtils.hasText(accessToken)) { + Authentication jwtAuthenticationToken = new JwtAuthenticationToken(accessToken); + Authentication authentication = authenticationManager.authenticate(jwtAuthenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + + private String extractAccessTokenFromHeader(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader("Authorization")) + .filter(header -> header.startsWith(TOKEN_PREFIX)) + .map(header -> header.replace(TOKEN_PREFIX, "")) + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/global/security/provider/JwtProvider.java b/src/main/java/com/example/PING/global/security/provider/JwtProvider.java new file mode 100644 index 0000000..ebbce8a --- /dev/null +++ b/src/main/java/com/example/PING/global/security/provider/JwtProvider.java @@ -0,0 +1,68 @@ +package com.example.PING.global.security.provider; + +import com.example.PING.error.exception.ErrorCode; +import com.example.PING.error.exception.JwtInvalidException; +import com.example.PING.global.security.token.JwtAuthenticationToken; +import com.example.PING.global.security.utils.JwtUtil; +import com.example.PING.user.entity.User; +import com.example.PING.user.service.UserService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.security.SignatureException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; + +@RequiredArgsConstructor +public class JwtProvider implements AuthenticationProvider { + + private final JwtUtil jwtUtil; + private final UserService userService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + Claims claims = getClaims(authentication); + final User user = getMemberById(claims.getSubject()); + + return new JwtAuthenticationToken( + user, + "", + List.of(new SimpleGrantedAuthority("ROLE_USER") // Todo 우리 코드에는 role이 따로 없으므로, 디폴트값으로 "ROLE_USER" 넣어줌 + )); + } + + private Claims getClaims(Authentication authentication) { + Claims claims; + try { + claims = jwtUtil.getAccessTokenClaims(authentication); + } catch (ExpiredJwtException expiredJwtException) { + throw new JwtInvalidException(ErrorCode.EXPIRED_TOKEN.getMessage()); + } catch (SignatureException signatureException) { + throw new JwtInvalidException(ErrorCode.WRONG_TYPE_TOKEN.getMessage()); + } catch (MalformedJwtException malformedJwtException) { + throw new JwtInvalidException(ErrorCode.UNSUPPORTED_TOKEN.getMessage()); + } catch (IllegalArgumentException illegalArgumentException) { + throw new JwtInvalidException(ErrorCode.UNKNOWN_ERROR.getMessage()); + } + return claims; + } + + private User getMemberById(String id) { + try { + return userService.getUserById(Long.parseLong(id)); + } catch (Exception e) { + throw new BadCredentialsException(ErrorCode.BAD_CREDENTIALS.getMessage()); + } + } + + @Override + public boolean supports(Class authentication) { + return JwtAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/global/security/token/JwtAuthenticationToken.java b/src/main/java/com/example/PING/global/security/token/JwtAuthenticationToken.java new file mode 100644 index 0000000..bd3b76d --- /dev/null +++ b/src/main/java/com/example/PING/global/security/token/JwtAuthenticationToken.java @@ -0,0 +1,28 @@ +package com.example.PING.global.security.token; + +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +@Getter +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + private String jsonWebToken; + private Object principal; + private Object credentials; + + public JwtAuthenticationToken(String jsonWebToken) { + super(null); + this.jsonWebToken = jsonWebToken; + this.setAuthenticated(false); + } + + public JwtAuthenticationToken(Object principal, Object credentials, + Collection authorities) { + super(authorities); + this.principal = principal; + this.credentials = credentials; + super.setAuthenticated(true); + } +} diff --git a/src/main/java/com/example/PING/global/security/utils/JwtTokenGenerator.java b/src/main/java/com/example/PING/global/security/utils/JwtTokenGenerator.java new file mode 100644 index 0000000..90e53f5 --- /dev/null +++ b/src/main/java/com/example/PING/global/security/utils/JwtTokenGenerator.java @@ -0,0 +1,61 @@ +package com.example.PING.global.security.utils; + +import com.example.PING.auth.dto.response.TokenSetResponse; +import com.example.PING.auth.dto.response.TemporaryTokenResponse; +import com.example.PING.auth.entity.RefreshToken; +import com.example.PING.auth.repository.RefreshTokenRepository; +import com.example.PING.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JwtTokenGenerator { + + private final JwtUtil jwtUtil; + private final RefreshTokenRepository refreshTokenRepository; + + /** + * 액세스 토큰 & 리프레시 토큰 생성 + */ + public TokenSetResponse generateTokenPair(User user) { // 정상 토큰 발급의 경우 + String accessToken = createAccessToken(user); + String refreshToken = createRefreshToken(user); + String temporaryToken = null; + return TokenSetResponse.of(accessToken, refreshToken, temporaryToken); + } + + private String createAccessToken(User user) { + return jwtUtil.generateAccessToken(user); + } + + private String createRefreshToken(User user) { + String token = jwtUtil.generateRefreshToken(user); + saveRefreshToken(user.getUserId(), token); + return token; + } + + private void saveRefreshToken(Long memberId, String refreshToken) { + if(refreshTokenRepository.existsByMemberId(memberId)) { + refreshTokenRepository.deleteByMemberId(memberId); + } + refreshTokenRepository.save(RefreshToken.builder() + .memberId(memberId) + .token(refreshToken) + .build()); + } + + /** + * 임시 토큰 생성 + */ + public TokenSetResponse generateTemporaryToken(User user) { + String accessToken = null; + String refreshToken = null; + String temporaryToken = createTemporaryToken(user); + return TokenSetResponse.of(accessToken, refreshToken, temporaryToken); + } + + private String createTemporaryToken(User user) { + return jwtUtil.generateTemporaryToken(user); + } +} diff --git a/src/main/java/com/example/PING/global/security/utils/JwtUtil.java b/src/main/java/com/example/PING/global/security/utils/JwtUtil.java new file mode 100644 index 0000000..bb4be34 --- /dev/null +++ b/src/main/java/com/example/PING/global/security/utils/JwtUtil.java @@ -0,0 +1,94 @@ +package com.example.PING.global.security.utils; + +import com.example.PING.global.properties.JwtProperties; +import com.example.PING.global.security.token.JwtAuthenticationToken; +import com.example.PING.user.entity.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.UUID; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtil { + + private final JwtProperties jwtProperties; + + public String generateAccessToken(User user) { + Date issuedAt = new Date(); + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.accessTokenExpirationMilliTime()); + + return Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject(user.getUserId().toString()) // 사용자 ID만 저장 + .claim("tokenType", "access") // 클레임으로 토큰 유형 저장 + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(getAccessTokenKey()) + .compact(); + } + + public String generateRefreshToken(User user) { + Date issuedAt = new Date(); + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.refreshTokenExpirationMilliTime()); + + return Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject(user.getUserId().toString()) // 사용자 ID만 저장 + .claim("tokenType", "refresh") // 클레임으로 토큰 유형 저장 + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(getRefreshTokenKey()) + .compact(); + } + + /** + * 임시 토큰 생성 + */ + public String generateTemporaryToken(User user) { + Date issuedAt = new Date(); + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.temporaryTokenExpirationMilliTime()); + + return Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject(UUID.randomUUID().toString()) // 임시 고유 값 사용 + .claim("tokenType", "temporary") // 클레임으로 토큰 유형 저장 + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(getTemporaryTokenKey()) + .compact(); + + } + + public Claims getAccessTokenClaims(Authentication authentication) { + return Jwts.parserBuilder() + .requireIssuer(jwtProperties.issuer()) + .setSigningKey(getAccessTokenKey()) + .build() + .parseClaimsJws(((JwtAuthenticationToken) authentication).getJsonWebToken()) + .getBody(); + } + + private Key getAccessTokenKey() { + return Keys.hmacShaKeyFor(jwtProperties.accessTokenSecret().getBytes()); + } + + private Key getRefreshTokenKey() { + return Keys.hmacShaKeyFor(jwtProperties.refreshTokenSecret().getBytes()); + } + + /** + * 임시 토큰의 서명 키 (이거는 JwtProperties에서 관리해야 함) + */ + private Key getTemporaryTokenKey() { + return Keys.hmacShaKeyFor(jwtProperties.temporaryTokenSecret().getBytes()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PING/image/S3ImageService.java b/src/main/java/com/example/PING/image/S3ImageService.java index 25f4980..e0d361f 100644 --- a/src/main/java/com/example/PING/image/S3ImageService.java +++ b/src/main/java/com/example/PING/image/S3ImageService.java @@ -7,7 +7,7 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.util.IOUtils; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/PING/project/service/ProjectService.java b/src/main/java/com/example/PING/project/service/ProjectService.java index 773adf8..2ab1a18 100644 --- a/src/main/java/com/example/PING/project/service/ProjectService.java +++ b/src/main/java/com/example/PING/project/service/ProjectService.java @@ -4,7 +4,7 @@ import com.example.PING.project.dto.response.ProjectIdResponse; import com.example.PING.project.dto.response.ProjectResponse; import com.example.PING.project.entity.Project; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import com.example.PING.project.repository.ProjectRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/PING/survey/service/SurveyService.java b/src/main/java/com/example/PING/survey/service/SurveyService.java index ba69b20..3913242 100644 --- a/src/main/java/com/example/PING/survey/service/SurveyService.java +++ b/src/main/java/com/example/PING/survey/service/SurveyService.java @@ -5,7 +5,7 @@ import com.example.PING.survey.dto.response.SurveyResponse; import com.example.PING.project.entity.Project; import com.example.PING.survey.entity.Survey; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import com.example.PING.project.repository.ProjectRepository; import com.example.PING.survey.repository.SurveyRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/PING/template/service/TemplateService.java b/src/main/java/com/example/PING/template/service/TemplateService.java index 6fef997..93be6d2 100644 --- a/src/main/java/com/example/PING/template/service/TemplateService.java +++ b/src/main/java/com/example/PING/template/service/TemplateService.java @@ -3,7 +3,7 @@ import com.example.PING.template.dto.request.TemplateRequest; import com.example.PING.template.dto.response.TemplateResponse; import com.example.PING.template.entity.Template; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import com.example.PING.template.repository.TemplateRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/PING/user/controller/UserController.java b/src/main/java/com/example/PING/user/controller/UserController.java index 9a13f94..c5b3279 100644 --- a/src/main/java/com/example/PING/user/controller/UserController.java +++ b/src/main/java/com/example/PING/user/controller/UserController.java @@ -1,14 +1,11 @@ package com.example.PING.user.controller; -import com.example.PING.user.dto.request.UserLoginRequest; -import com.example.PING.user.dto.request.UserSignUpRequest; import com.example.PING.user.dto.request.UserProfileUpdateRequest; import com.example.PING.user.dto.response.*; import com.example.PING.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.*; import java.util.HashMap; @@ -22,27 +19,9 @@ public class UserController { private final UserService userService; - @PostMapping("/login") - public ResponseEntity login(@RequestBody UserLoginRequest request) { - Object response = userService.login(request); - if (response instanceof UserLoginResponse userLoginResponse) { - return ResponseEntity.ok(userLoginResponse); // HTTP 200 OK - } else if (response instanceof ErrorResponse errorResponse) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); // HTTP 401 Unauthorized - } - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - - @PostMapping// 회원가입 - public ResponseEntity signup(@RequestBody UserSignUpRequest request) { - try { - UserSignUpResponse newUserResponse = userService.signUp(request); - return ResponseEntity.status(HttpStatus.CREATED).body(newUserResponse); // HTTP 201 Created - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("error", e.getMessage())); // HTTP 400 Bad Request - } - } - + @Operation( + summary = "My page 정보 조회" + ) @GetMapping("/{userId}") public ResponseEntity getUserProfile(@PathVariable("userId") Long userId) { // My page 정보 조회 Optional userResponse = userService.getUserProfile(userId); @@ -52,6 +31,10 @@ public ResponseEntity getUserProfile(@PathVariable("userId" .orElse(ResponseEntity.notFound().build()); } + @Operation( + summary = "포트폴리오 id List", + description = "특정 user의 포트폴리오 id 리스트 get" + ) @GetMapping("{user_id}/portfolio") public ResponseEntity getUserPortfolioIdList(@PathVariable("user_id") Long userId) { Optional userResponse = userService.getUserPortfolioIdList(userId); @@ -59,6 +42,9 @@ public ResponseEntity getUserPortfolioIdList(@PathV return userResponse.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()); } + @Operation( + summary = "My page 정보 수정" + ) @PutMapping("/{userId}") public ResponseEntity updateUserProfile( // My page 정보 수정 @PathVariable("userId") Long userId, @@ -68,6 +54,9 @@ public ResponseEntity updateUserProfile( // My page 정보 return ResponseEntity.ok(updatedUser); } + @Operation( + summary = "user 삭제" + ) @DeleteMapping("/{userId}") public ResponseEntity deleteUser(@PathVariable("userId") Long userId) { // User 삭제 userService.deleteUser(userId); diff --git a/src/main/java/com/example/PING/user/dto/request/UserLoginRequest.java b/src/main/java/com/example/PING/user/dto/request/UserLoginRequest.java deleted file mode 100644 index f770562..0000000 --- a/src/main/java/com/example/PING/user/dto/request/UserLoginRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.PING.user.dto.request; - -public record UserLoginRequest( - String email, - String password -) { -} \ No newline at end of file diff --git a/src/main/java/com/example/PING/user/dto/request/UserProfileUpdateRequest.java b/src/main/java/com/example/PING/user/dto/request/UserProfileUpdateRequest.java index 18e3754..b619482 100644 --- a/src/main/java/com/example/PING/user/dto/request/UserProfileUpdateRequest.java +++ b/src/main/java/com/example/PING/user/dto/request/UserProfileUpdateRequest.java @@ -1,9 +1,6 @@ package com.example.PING.user.dto.request; public record UserProfileUpdateRequest( - String name, - String email, - String password, String nickname, String userIcon ) { diff --git a/src/main/java/com/example/PING/user/dto/request/UserSignUpRequest.java b/src/main/java/com/example/PING/user/dto/request/UserSignUpRequest.java deleted file mode 100644 index fc125ad..0000000 --- a/src/main/java/com/example/PING/user/dto/request/UserSignUpRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.PING.user.dto.request; - -public record UserSignUpRequest( - String name, - String email, - String password, - String nickname, - String userIcon -) { -} diff --git a/src/main/java/com/example/PING/user/dto/response/UserLoginResponse.java b/src/main/java/com/example/PING/user/dto/response/UserLoginResponse.java deleted file mode 100644 index e9a137a..0000000 --- a/src/main/java/com/example/PING/user/dto/response/UserLoginResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.PING.user.dto.response; - -public record UserLoginResponse( - Long userId -// String name, -// String email, -// String nickname, -// String userIcon, -// String token -) { -} diff --git a/src/main/java/com/example/PING/user/dto/response/UserProfileResponse.java b/src/main/java/com/example/PING/user/dto/response/UserProfileResponse.java index c4c9208..00c8941 100644 --- a/src/main/java/com/example/PING/user/dto/response/UserProfileResponse.java +++ b/src/main/java/com/example/PING/user/dto/response/UserProfileResponse.java @@ -3,15 +3,11 @@ import com.example.PING.user.entity.User; public record UserProfileResponse( - String name, - String email, String nickname, String userIcon ) { public static UserProfileResponse from(User user) { return new UserProfileResponse( - user.getName(), - user.getEmail(), user.getNickname(), user.getUserIcon() ); diff --git a/src/main/java/com/example/PING/user/dto/response/UserResponse.java b/src/main/java/com/example/PING/user/dto/response/UserResponse.java deleted file mode 100644 index d6c4ccc..0000000 --- a/src/main/java/com/example/PING/user/dto/response/UserResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.PING.user.dto.response; - -import com.example.PING.user.entity.User; - -public record UserResponse( - Long userId, - String name, - String email, - String nickname, - String userIcon -) { - public static UserResponse from(User user) { - return new UserResponse( - user.getUserId(), - user.getName(), - user.getEmail(), - user.getNickname(), - user.getUserIcon() - ); - } -} diff --git a/src/main/java/com/example/PING/user/dto/response/UserSignUpResponse.java b/src/main/java/com/example/PING/user/dto/response/UserSignUpResponse.java deleted file mode 100644 index 1081cb2..0000000 --- a/src/main/java/com/example/PING/user/dto/response/UserSignUpResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.PING.user.dto.response; - -import com.example.PING.user.entity.User; - -import java.time.LocalDateTime; - -public record UserSignUpResponse( - Long userId, - String name, - String email, - String nickname, - String userIcon, - LocalDateTime created_at, - LocalDateTime updated_at -) { - public static UserSignUpResponse from(User user) { - return new UserSignUpResponse( - user.getUserId(), - user.getName(), - user.getEmail(), - user.getNickname(), - user.getUserIcon(), - user.getCreatedAt(), - user.getUpdatedAt() - ); - } -} diff --git a/src/main/java/com/example/PING/user/dto/response/UserUpdateResponse.java b/src/main/java/com/example/PING/user/dto/response/UserUpdateResponse.java index 3e8fdd4..1c03673 100644 --- a/src/main/java/com/example/PING/user/dto/response/UserUpdateResponse.java +++ b/src/main/java/com/example/PING/user/dto/response/UserUpdateResponse.java @@ -6,18 +6,14 @@ public record UserUpdateResponse( Long userId, - String name, String nickname, - String password, String userIcon, LocalDateTime updated_at ) { public static UserUpdateResponse from(User user) { return new UserUpdateResponse( user.getUserId(), - user.getName(), user.getNickname(), - user.getPassword(), user.getUserIcon(), user.getUpdatedAt() ); diff --git a/src/main/java/com/example/PING/user/entity/OauthInfo.java b/src/main/java/com/example/PING/user/entity/OauthInfo.java new file mode 100644 index 0000000..292b371 --- /dev/null +++ b/src/main/java/com/example/PING/user/entity/OauthInfo.java @@ -0,0 +1,26 @@ +package com.example.PING.user.entity; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OauthInfo { + + public String oauthId; + private String oauthProvider; + private String oauthEmail; + private String oauthName; + + @Builder + public OauthInfo(String oauthId, String oauthProvider, String oauthEmail, String oauthName) { + this.oauthId = oauthId; + this.oauthProvider = oauthProvider; + this.oauthEmail = oauthEmail; + this.oauthName = oauthName; + } +} diff --git a/src/main/java/com/example/PING/user/entity/User.java b/src/main/java/com/example/PING/user/entity/User.java index f09f2f1..cb6f73c 100644 --- a/src/main/java/com/example/PING/user/entity/User.java +++ b/src/main/java/com/example/PING/user/entity/User.java @@ -19,24 +19,21 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; + @Embedded //소셜로그인 + private OauthInfo oauthInfo; + @OneToMany(mappedBy = "user") + @ToString.Exclude private List portfolios = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + @ToString.Exclude private List likes = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + @ToString.Exclude private List scraps = new ArrayList<>(); - @Column(nullable = false) - private String password; - - @Column(nullable = false) - private String name; - - @Column(nullable = false, unique = true) - private String email; - @Column(nullable = false) private String nickname; @@ -50,16 +47,23 @@ public class User { @Column(name = "updated_at") @Getter private LocalDateTime updatedAt; - @Builder - public User(String password, String name, String email, String nickname, String userIcon) { - this.password = password; - this.name = name; - this.email = email; + @Builder // Todo 소셜로그인 + public User(String nickname, String userIcon, OauthInfo oauthInfo) { this.nickname = nickname; this.userIcon = userIcon; + this.oauthInfo = oauthInfo; + } + + public static User createTemporalUser(OauthInfo oauthInfo) { // 소셜로그인으로 인해 생성되는 유저 + return User.builder() + .nickname(null) // 사용자가 입력할 닉네임 (초기에는 null) + .userIcon("default") // Todo 기본 프로필 아이콘 (임시) + .oauthInfo(oauthInfo) + .build(); } + public static User objectToUser(Object user) { - if (user == null) new IllegalArgumentException("로그인된 User가 존재하지 않습니다."); + if (user == null) throw new IllegalArgumentException("로그인된 User가 존재하지 않습니다."); return (User) user; } @@ -69,22 +73,12 @@ public List getPortfolioIds() { .toList(); } - public void changeName(String newName){ - this.name = newName; - } - public void changeNickName(String newNickname){ this.nickname = newNickname; } - public void changePassword(String newPassword){ - this.password = newPassword; - } - public void changeUserIcon(String newUserIcon){ this.userIcon = newUserIcon; } - - } diff --git a/src/main/java/com/example/PING/user/repository/UserRepository.java b/src/main/java/com/example/PING/user/repository/UserRepository.java index 73849df..ab45d44 100644 --- a/src/main/java/com/example/PING/user/repository/UserRepository.java +++ b/src/main/java/com/example/PING/user/repository/UserRepository.java @@ -1,5 +1,6 @@ package com.example.PING.user.repository; +import com.example.PING.user.entity.OauthInfo; import com.example.PING.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,6 +10,6 @@ @Repository public interface UserRepository extends JpaRepository { // 추가적인 쿼리 메서드를 정의할 수 있습니다 - Optional findByEmail(String email); + Optional findByOauthInfo(OauthInfo oauthInfo); } diff --git a/src/main/java/com/example/PING/user/service/UserService.java b/src/main/java/com/example/PING/user/service/UserService.java index 0cad365..fe76218 100644 --- a/src/main/java/com/example/PING/user/service/UserService.java +++ b/src/main/java/com/example/PING/user/service/UserService.java @@ -1,10 +1,9 @@ package com.example.PING.user.service; -import com.example.PING.error.ResourceNotFoundException; +import com.example.PING.error.exception.ResourceNotFoundException; import com.example.PING.user.dto.response.*; +import com.example.PING.user.entity.OauthInfo; import com.example.PING.user.repository.UserRepository; -import com.example.PING.user.dto.request.UserLoginRequest; -import com.example.PING.user.dto.request.UserSignUpRequest; import com.example.PING.user.dto.request.UserProfileUpdateRequest; import com.example.PING.user.entity.User; import lombok.RequiredArgsConstructor; @@ -19,21 +18,25 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; -// private final HttpSession httpSession; @Transactional(readOnly = true) - public List getAllUsers() { - return userRepository.findAll().stream() - .map(UserResponse::from) - .collect(Collectors.toList()); + public User getUserById(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); } - @Transactional(readOnly = true) - public UserResponse getUserById(Long userId) { - return UserResponse.from( - userRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)) - ); + public User getUserByOAuthInfo(OauthInfo oauthInfo) { // 소셜로그인 유저 찾기 + return userRepository.findByOauthInfo(oauthInfo) + .orElseGet(() -> createTempUser(oauthInfo)); + } + + public User createTempUser(OauthInfo oauthInfo) { // 임시 회원 생성 + User tempUser = User.createTemporalUser(oauthInfo); + return tempUser; + } + + public User saveTempUser(User user) { + return userRepository.save(user); } @Transactional @@ -43,46 +46,7 @@ public void deleteUser(Long userId) { userRepository.delete(user); } - public UserLoginResponse login(UserLoginRequest request) { - User targetUser = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new ResourceNotFoundException("User not found with Email: " + request.email())); - // 비밀번호 확인 로직 활성화 - if (!targetUser.getPassword().equals(request.password())) { - new IllegalArgumentException("Password does not match. Your input: " + request.password()); - } -// String token = generateToken(targetUser); // JWT 토큰 생성 로직 (모의) -// return new UserResponseDto(targetUser.getUserId(), targetUser.getName(), targetUser.getEmail(), token, targetUser.getProfilePic()); -// httpSession.setAttribute("user", targetUser.getUserId()); -// long loginId = Long.parseLong(httpSession.getAttribute("user").toString()); -// User loginUser = userRepository.findById(loginId) -// .orElseThrow(()-> new IllegalArgumentException("User not found with id: "+ loginId)); - return new UserLoginResponse( - targetUser.getUserId()); -// targetUser.getEmail(), -// targetUser.getName(), -// targetUser.getNickname(), -// targetUser.getProfilePic()); - } - - private String generateToken(User user) { - return "jwt_token_here"; // JWT 토큰 생성 로직 - } - - public UserSignUpResponse signUp(UserSignUpRequest request) { - // 이미 존재하는 이메일인지 확인 - if (userRepository.findByEmail(request.email()).isPresent()) { - throw new IllegalArgumentException("Email already in use"); // 이메일 중복 처리 - } - - // 새 사용자 객체 생성 - User newUser = new User(request.password(), request.name(), request.email(), request.nickname(), request.userIcon()); - - // 사용자 저장 - userRepository.save(newUser); - - return UserSignUpResponse.from(newUser); - } - + @Transactional(readOnly = true) public Optional getUserProfile(Long userId) { //My page 정보 조회 return userRepository.findById(userId) .map(UserProfileResponse :: from); @@ -97,10 +61,8 @@ public UserUpdateResponse updateUserProfile(Long userId, UserProfileUpdateReques User user = userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User not found")); - user.changeName(userProfileUpdateRequest.name()); user.changeNickName(userProfileUpdateRequest.nickname()); user.changeUserIcon(userProfileUpdateRequest.userIcon()); - user.changePassword(userProfileUpdateRequest.password()); user.setUpdatedAt(LocalDateTime.now()); userRepository.save(user); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2901f3c..29ab1cb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -25,4 +25,26 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.transaction.jta.platform=org.hibernate.service.jta.JtaService # Server port configuration for tests -server.port=8080 \ No newline at end of file +server.port=8080 + +# JWT ?? +jwt.access-token-secret=${JWT_ACCESS_TOKEN_SECRET:} +jwt.refresh-token-secret=${JWT_REFRESH_TOKEN_SECRET:} +jwt.temporary-token-secret=${JWT_TEMPORARY_TOKEN_SECRET:} +# ???? ?? 12??, 7?, 2?? +jwt.access-token-expiration-time=${JWT_ACCESS_TOKEN_EXPIRATION_TIME:43200000} +jwt.refresh-token-expiration-time=${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800000} +jwt.temporary-token-expiration-time=${JWT_TEMPORARY_TOKEN_SECRET_TIME:7200000} +jwt.issuer=${JWT_ISSUER:} + +# ??? ??? ?? +kakao.login.client_id=${KAKAO_LOGIN_CLIENT_ID} +kakao.login.client_secret=${KAKAO_LOGIN_CLIENT_SECRET} +kakao.login.redirect_uri=${KAKAO_LOGIN_REDIRECT_URI} + +# our server link will be written... +cors-allowed-origins=http://localhost:8080,http://ping-deploy-bucket.s3-website.ap-northeast-2.amazonaws.com + +# cache memory +spring.cache.type=caffeine +spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=50m