diff --git a/.gitignore b/.gitignore index 03282e4..770696b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ HELP.md **/.env.* *.env .env +.env.properties !.env.example **/.env.example build/ diff --git a/build.gradle b/build.gradle index 1158cb5..a58d679 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.1' + id 'org.springframework.boot' version '3.3.6' id 'io.spring.dependency-management' version '1.1.7' } @@ -50,7 +50,7 @@ dependencies { implementation 'io.jsonwebtoken:jjwt:0.9.1' // --- AWS (S3) --- - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3' // --- Flyway (DB Migration) --- implementation "org.flywaydb:flyway-core:10.21.0" @@ -69,6 +69,16 @@ dependencies { testImplementation 'org.mockito:mockito-junit-jupiter:5.6.0' testImplementation 'org.assertj:assertj-core:3.24.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' +} + +dependencyManagement { + imports { + mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1" + // ↑ 3.x 대 사용 (프로젝트에 맞는 최신 3.x 가능) + } } /* ===== Tasks ===== */ diff --git a/src/main/java/inha/gdgoc/config/DotenvLoader.java b/src/main/java/inha/gdgoc/config/DotenvLoader.java deleted file mode 100644 index 2194bf6..0000000 --- a/src/main/java/inha/gdgoc/config/DotenvLoader.java +++ /dev/null @@ -1,31 +0,0 @@ -package inha.gdgoc.config; - -import io.github.cdimascio.dotenv.Dotenv; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Profile("!test") -@Component -public class DotenvLoader { - - @PostConstruct - public void loadEnv() { - try { - Dotenv dotenv = Dotenv.load(); - System.setProperty("AWS_ACCESS_KEY_ID", dotenv.get("AWS_ACCESS_KEY_ID")); - System.setProperty("AWS_SECRET_ACCESS_KEY", dotenv.get("AWS_SECRET_ACCESS_KEY")); - System.setProperty("AWS_REGION", dotenv.get("AWS_REGION")); - System.setProperty("AWS_RESOURCE_BUCKET", dotenv.get("AWS_RESOURCE_BUCKET")); - System.setProperty("AWS_TEST_RESOURCE_BUCKET", dotenv.get("AWS_TEST_RESOURCE_BUCKET")); - System.setProperty("GOOGLE_CLIENT_ID", dotenv.get("GOOGLE_CLIENT_ID")); - System.setProperty("GOOGLE_CLIENT_SECRET", dotenv.get("GOOGLE_CLIENT_SECRET")); - System.setProperty("GOOGLE_REDIRECT_URI", dotenv.get("GOOGLE_REDIRECT_URI")); - System.setProperty("GMAIL", dotenv.get("GMAIL")); - System.setProperty("GMAIL_PASSWORD", dotenv.get("GMAIL_PASSWORD")); - } catch (Exception e) { - e.printStackTrace(); // 오류 로그 출력 - throw new RuntimeException("Error loading .env file: " + e.getMessage(), e); - } - } -} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/config/S3Config.java b/src/main/java/inha/gdgoc/config/S3Config.java deleted file mode 100644 index b8d39cb..0000000 --- a/src/main/java/inha/gdgoc/config/S3Config.java +++ /dev/null @@ -1,30 +0,0 @@ -package inha.gdgoc.config; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class S3Config { - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; - - @Value("${cloud.aws.region.static}") - private String region; - - @Bean - public AmazonS3 amazonS3() { - BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); - return AmazonS3ClientBuilder.standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(credentials)) - .build(); - } -} diff --git a/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java b/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java index cf9abc9..4b3f18e 100644 --- a/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.auth.service; -import inha.gdgoc.config.jwt.TokenProvider; +import inha.gdgoc.global.config.jwt.TokenProvider; import inha.gdgoc.domain.auth.dto.request.UserLoginRequest; import inha.gdgoc.domain.auth.dto.response.LoginResponse; import inha.gdgoc.domain.auth.enums.LoginType; @@ -28,7 +28,7 @@ import java.util.Map; import java.util.Optional; -import static inha.gdgoc.util.EncryptUtil.encrypt; +import static inha.gdgoc.global.util.EncryptUtil.encrypt; @Slf4j @Service @@ -36,6 +36,7 @@ public class AuthService { private final RefreshTokenService refreshTokenService; + @Value("${google.client-id}") private String clientId; @@ -63,9 +64,9 @@ public Map processOAuthLogin(String code, HttpServletResponse re HttpEntity> tokenRequest = new HttpEntity<>(params, headers); ResponseEntity tokenResponse = restTemplate.postForEntity( - "https://oauth2.googleapis.com/token", - tokenRequest, - Map.class + "https://oauth2.googleapis.com/token", + tokenRequest, + Map.class ); String googleAccessToken = (String) tokenResponse.getBody().get("access_token"); @@ -76,10 +77,10 @@ public Map processOAuthLogin(String code, HttpServletResponse re HttpEntity userInfoRequest = new HttpEntity<>(userInfoHeaders); ResponseEntity userInfoResponse = restTemplate.exchange( - "https://www.googleapis.com/oauth2/v2/userinfo", - HttpMethod.GET, - userInfoRequest, - Map.class + "https://www.googleapis.com/oauth2/v2/userinfo", + HttpMethod.GET, + userInfoRequest, + Map.class ); // 3. Google에서 가져온 이름, 이메일로 가입된 정보가 없으면 회원가입, 있으면 로그인 @@ -90,9 +91,9 @@ public Map processOAuthLogin(String code, HttpServletResponse re Optional foundUser = userRepository.findByEmail(email); if (foundUser.isEmpty()) { return Map.of( - "exists", false, - "email", email, - "name", name + "exists", false, + "email", email, + "name", name ); } @@ -100,29 +101,30 @@ public Map processOAuthLogin(String code, HttpServletResponse re String jwtAccessToken = tokenProvider.generateGoogleLoginToken(user, Duration.ofHours(1)); String refreshToken = refreshTokenService.getOrCreateRefreshToken(user, Duration.ofDays(1), - LoginType.GOOGLE_LOGIN); + LoginType.GOOGLE_LOGIN); ResponseCookie refreshCookie = ResponseCookie.from("refresh_token", refreshToken) - .httpOnly(true) - .secure(true) - .sameSite("None") - .domain(".gdgocinha.com") - .path("/") - .maxAge(Duration.ofDays(1)) - .build(); + .httpOnly(true) + .secure(true) + .sameSite("None") + .domain(".gdgocinha.com") + .path("/") + .maxAge(Duration.ofDays(1)) + .build(); // Set-Cookie 헤더로 추가 log.info("Response Cookie에 저장된 Refresh Token: {}", refreshCookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); return Map.of( - "exists", true, - "access_token", jwtAccessToken + "exists", true, + "access_token", jwtAccessToken ); } - public LoginResponse loginWithPassword(UserLoginRequest userLoginRequest, HttpServletResponse response) - throws NoSuchAlgorithmException, InvalidKeyException { + public LoginResponse loginWithPassword(UserLoginRequest userLoginRequest, + HttpServletResponse response) + throws NoSuchAlgorithmException, InvalidKeyException { Optional user = userRepository.findByEmail(userLoginRequest.email()); if (user.isEmpty()) { return new LoginResponse(false, null); @@ -135,16 +137,17 @@ public LoginResponse loginWithPassword(UserLoginRequest userLoginRequest, HttpSe } String accessToken = tokenProvider.generateSelfSignupToken(foundUser, Duration.ofHours(1)); - String refreshToken = refreshTokenService.getOrCreateRefreshToken(foundUser, Duration.ofDays(1), - LoginType.SELF_SIGNUP); + String refreshToken = refreshTokenService.getOrCreateRefreshToken(foundUser, + Duration.ofDays(1), + LoginType.SELF_SIGNUP); ResponseCookie refreshCookie = ResponseCookie.from("refresh_token", refreshToken) - .httpOnly(true) - .secure(true) - .sameSite("None") - .path("/") - .maxAge(Duration.ofDays(1)) - .build(); + .httpOnly(true) + .secure(true) + .sameSite("None") + .path("/") + .maxAge(Duration.ofDays(1)) + .build(); log.info("Response Cookie에 저장된 Refresh Token: {}", refreshCookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); diff --git a/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java b/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java index b056d73..df4297d 100644 --- a/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.auth.service; -import inha.gdgoc.config.jwt.TokenProvider; +import inha.gdgoc.global.config.jwt.TokenProvider; import inha.gdgoc.domain.auth.entity.RefreshToken; import inha.gdgoc.domain.auth.enums.LoginType; import inha.gdgoc.domain.auth.repository.RefreshTokenRepository; diff --git a/src/main/java/inha/gdgoc/domain/resource/service/S3Service.java b/src/main/java/inha/gdgoc/domain/resource/service/S3Service.java index 737551f..1546431 100644 --- a/src/main/java/inha/gdgoc/domain/resource/service/S3Service.java +++ b/src/main/java/inha/gdgoc/domain/resource/service/S3Service.java @@ -1,43 +1,43 @@ package inha.gdgoc.domain.resource.service; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.ObjectMetadata; import inha.gdgoc.domain.resource.enums.S3KeyType; +import java.io.IOException; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.UUID; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetUrlRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; @Service @RequiredArgsConstructor public class S3Service { - private final AmazonS3 amazonS3; + private final S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; - public String upload( - Long userId, - S3KeyType s3key, - MultipartFile file - ) throws IOException { + public String upload(Long userId, S3KeyType s3key, MultipartFile file) throws IOException { String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename(); - String prefix = "user/" + userId + "/"; - String uploadFilePath = prefix + s3key.getValue() + "/" + fileName; + String key = "user/%d/%s/%s".formatted(userId, s3key.getValue(), fileName); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(file.getSize()); - metadata.setContentType(file.getContentType()); + PutObjectRequest putReq = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(file.getContentType()) + .build(); - amazonS3.putObject(bucketName, uploadFilePath, file.getInputStream(), metadata); - return uploadFilePath; + s3Client.putObject(putReq, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + return key; } public String getS3FileUrl(String key) { - return amazonS3.getUrl(bucketName, key).toString(); + return s3Client.utilities() + .getUrl(GetUrlRequest.builder().bucket(bucketName).key(key).build()) + .toExternalForm(); } } diff --git a/src/main/java/inha/gdgoc/domain/user/entity/User.java b/src/main/java/inha/gdgoc/domain/user/entity/User.java index 97a8a85..f2424b3 100644 --- a/src/main/java/inha/gdgoc/domain/user/entity/User.java +++ b/src/main/java/inha/gdgoc/domain/user/entity/User.java @@ -4,7 +4,7 @@ import inha.gdgoc.domain.study.entity.StudyAttendee; import inha.gdgoc.domain.user.enums.UserRole; import inha.gdgoc.global.common.BaseEntity; -import inha.gdgoc.util.EncryptUtil; +import inha.gdgoc.global.util.EncryptUtil; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/src/main/java/inha/gdgoc/domain/user/service/UserService.java b/src/main/java/inha/gdgoc/domain/user/service/UserService.java index 233f80c..7fd560e 100644 --- a/src/main/java/inha/gdgoc/domain/user/service/UserService.java +++ b/src/main/java/inha/gdgoc/domain/user/service/UserService.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.user.service; -import static inha.gdgoc.util.EncryptUtil.encrypt; -import static inha.gdgoc.util.EncryptUtil.generateSalt; +import static inha.gdgoc.global.util.EncryptUtil.encrypt; +import static inha.gdgoc.global.util.EncryptUtil.generateSalt; import inha.gdgoc.domain.auth.dto.request.FindIdRequest; import inha.gdgoc.domain.user.dto.request.CheckDuplicatedEmailRequest; @@ -10,7 +10,7 @@ import inha.gdgoc.domain.user.dto.response.CheckDuplicatedEmailResponse; import inha.gdgoc.domain.user.entity.User; import inha.gdgoc.domain.user.repository.UserRepository; -import inha.gdgoc.exception.NotFoundException; +import inha.gdgoc.global.exception.NotFoundException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; diff --git a/src/main/java/inha/gdgoc/global/common/ErrorResponse.java b/src/main/java/inha/gdgoc/global/common/ErrorResponse.java index d3b179a..b1a618b 100644 --- a/src/main/java/inha/gdgoc/global/common/ErrorResponse.java +++ b/src/main/java/inha/gdgoc/global/common/ErrorResponse.java @@ -1,10 +1,10 @@ package inha.gdgoc.global.common; import inha.gdgoc.global.error.ErrorCode; -import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; +@Getter public class ErrorResponse { private final int status; @@ -20,11 +20,4 @@ public ErrorResponse(HttpStatus status, String message) { this.message = message; } - public int getStatus() { - return status; - } - - public String getMessage() { - return message; - } } diff --git a/src/main/java/inha/gdgoc/config/jwt/JwtProperties.java b/src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java similarity index 91% rename from src/main/java/inha/gdgoc/config/jwt/JwtProperties.java rename to src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java index 6a3e2c2..fec214a 100644 --- a/src/main/java/inha/gdgoc/config/jwt/JwtProperties.java +++ b/src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package inha.gdgoc.config.jwt; +package inha.gdgoc.global.config.jwt; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/inha/gdgoc/config/jwt/TokenProvider.java b/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java similarity index 99% rename from src/main/java/inha/gdgoc/config/jwt/TokenProvider.java rename to src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java index 7a81b7d..e41fd91 100644 --- a/src/main/java/inha/gdgoc/config/jwt/TokenProvider.java +++ b/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java @@ -1,4 +1,4 @@ -package inha.gdgoc.config.jwt; +package inha.gdgoc.global.config.jwt; import inha.gdgoc.domain.auth.enums.LoginType; import inha.gdgoc.domain.user.entity.User; diff --git a/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java b/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java new file mode 100644 index 0000000..14ac53b --- /dev/null +++ b/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java @@ -0,0 +1,57 @@ +package inha.gdgoc.global.config.openapi; + +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.servers.Server; +import java.util.List; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public GroupedOpenApi all() { + return GroupedOpenApi.builder() + .group("all") + .pathsToMatch("/**") + .build(); + } + + @Bean + public GroupedOpenApi v1Api() { + return groupedApi("v1", "/api/v1"); + } + + @Bean + public GroupedOpenApi v2Api() { + return groupedApi("v2", "/api/v2"); + } + + private GroupedOpenApi groupedApi(String group, String fullPrefix) { + return GroupedOpenApi.builder() + .group(group) + .pathsToMatch(fullPrefix + "/**") + .addOpenApiCustomizer(stripPrefixAndSetServer(fullPrefix)) + .build(); + } + + private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) { + return openApi -> { + Paths src = openApi.getPaths(); + if (src == null || src.isEmpty()) return; + + Paths dst = new Paths(); + src.forEach((path, item) -> { + String p = path; + if (p.equals(fullPrefix)) p = "/"; + else if (p.startsWith(fullPrefix + "/")) p = p.substring(fullPrefix.length()); + dst.addPathItem(p, item); + }); + + openApi.setPaths(dst); + openApi.setServers(List.of(new Server().url(fullPrefix))); + }; + } +} diff --git a/src/main/java/inha/gdgoc/config/QueryDslConfig.java b/src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java similarity index 91% rename from src/main/java/inha/gdgoc/config/QueryDslConfig.java rename to src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java index db3b0fd..f40600e 100644 --- a/src/main/java/inha/gdgoc/config/QueryDslConfig.java +++ b/src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java @@ -1,4 +1,4 @@ -package inha.gdgoc.config; +package inha.gdgoc.global.config.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; diff --git a/src/main/java/inha/gdgoc/global/config/s3/S3Config.java b/src/main/java/inha/gdgoc/global/config/s3/S3Config.java new file mode 100644 index 0000000..fef0a06 --- /dev/null +++ b/src/main/java/inha/gdgoc/global/config/s3/S3Config.java @@ -0,0 +1,31 @@ +package inha.gdgoc.global.config.s3; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class S3Config { + + @Bean + public Region awsRegion(@Value("${cloud.aws.region.static}") String region) { + return Region.of(region); + } + + @Bean + public AwsCredentialsProvider awsCredentialsProvider() { + return DefaultCredentialsProvider.create(); + } + + @Bean + public S3Client s3Client(Region region, AwsCredentialsProvider provider) { + return S3Client.builder() + .region(region) + .credentialsProvider(provider) + .build(); + } +} diff --git a/src/main/java/inha/gdgoc/exception/NotFoundException.java b/src/main/java/inha/gdgoc/global/exception/NotFoundException.java similarity index 88% rename from src/main/java/inha/gdgoc/exception/NotFoundException.java rename to src/main/java/inha/gdgoc/global/exception/NotFoundException.java index 0b676ce..2a1f83c 100644 --- a/src/main/java/inha/gdgoc/exception/NotFoundException.java +++ b/src/main/java/inha/gdgoc/global/exception/NotFoundException.java @@ -1,4 +1,4 @@ -package inha.gdgoc.exception; +package inha.gdgoc.global.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/inha/gdgoc/config/SecurityConfig.java b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java similarity index 50% rename from src/main/java/inha/gdgoc/config/SecurityConfig.java rename to src/main/java/inha/gdgoc/global/security/SecurityConfig.java index 7aa3550..e2ad123 100644 --- a/src/main/java/inha/gdgoc/config/SecurityConfig.java +++ b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java @@ -1,4 +1,4 @@ -package inha.gdgoc.config; +package inha.gdgoc.global.security; import com.fasterxml.jackson.databind.ObjectMapper; import inha.gdgoc.global.common.ErrorResponse; @@ -30,30 +30,33 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .formLogin(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/**", "/test/**", "/game/**", "/apply/**", "/check/**").permitAll() - .anyRequest().authenticated() - ) - .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(ex -> ex - .authenticationEntryPoint((request, response, authException) -> { - response.setStatus(HttpStatus.FORBIDDEN.value()); - response.setContentType("application/json; charset=UTF-8"); + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", + "/swagger-ui.html", "/auth/**", "/test/**", "/game/**", "/apply/**", + "/check/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType("application/json; charset=UTF-8"); - // ErrorResponse 생성 - ErrorResponse errorResponse = new ErrorResponse(GlobalErrorCode.INVALID_JWT_REQUEST); + // ErrorResponse 생성 + ErrorResponse errorResponse = new ErrorResponse( + GlobalErrorCode.INVALID_JWT_REQUEST); - // JSON 직렬화 후 응답에 쓰기 - ObjectMapper objectMapper = new ObjectMapper(); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); - response.getWriter().flush(); - }) - ); + // JSON 직렬화 후 응답에 쓰기 + ObjectMapper objectMapper = new ObjectMapper(); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().flush(); + }) + ); return http.build(); } @@ -62,15 +65,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:3000", - "http://gdgocinha.com", - "https://gdgocinha.com", - "https://www.gdgocinha.com", - "https://typing-game-alpha-umber.vercel.app" + "http://localhost:3000", + "http://gdgocinha.com", + "https://gdgocinha.com", + "https://www.gdgocinha.com", + "https://typing-game-alpha-umber.vercel.app" )); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // ✅ OPTIONS 포함 - config.setAllowedHeaders(List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization")); - config.setAllowCredentials(true); // ✅ 쿠키 전송 허용 + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders( + List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization")); + config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); diff --git a/src/main/java/inha/gdgoc/config/TokenAuthenticationFilter.java b/src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java similarity index 83% rename from src/main/java/inha/gdgoc/config/TokenAuthenticationFilter.java rename to src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java index 1fdd8bc..fcd05dd 100644 --- a/src/main/java/inha/gdgoc/config/TokenAuthenticationFilter.java +++ b/src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java @@ -1,8 +1,6 @@ -package inha.gdgoc.config; +package inha.gdgoc.global.security; -import inha.gdgoc.config.jwt.TokenProvider; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.GlobalErrorCode; +import inha.gdgoc.global.config.jwt.TokenProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -24,6 +22,20 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { private final TokenProvider tokenProvider; + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String p = request.getRequestURI(); + return p.startsWith("/v3/api-docs") + || p.startsWith("/swagger-ui") + || p.equals("/swagger-ui.html") + || p.startsWith("/auth/") + || p.startsWith("/test/") + || p.startsWith("/game/") + || p.startsWith("/apply/") + || p.startsWith("/check/") + || "OPTIONS".equalsIgnoreCase(request.getMethod()); + } + @Override protected void doFilterInternal( @NotNull HttpServletRequest request, diff --git a/src/main/java/inha/gdgoc/util/CookieUtil.java b/src/main/java/inha/gdgoc/global/util/CookieUtil.java similarity index 97% rename from src/main/java/inha/gdgoc/util/CookieUtil.java rename to src/main/java/inha/gdgoc/global/util/CookieUtil.java index 1b4b4a7..73153af 100644 --- a/src/main/java/inha/gdgoc/util/CookieUtil.java +++ b/src/main/java/inha/gdgoc/global/util/CookieUtil.java @@ -1,4 +1,4 @@ -package inha.gdgoc.util; +package inha.gdgoc.global.util; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/inha/gdgoc/util/EncryptUtil.java b/src/main/java/inha/gdgoc/global/util/EncryptUtil.java similarity index 97% rename from src/main/java/inha/gdgoc/util/EncryptUtil.java rename to src/main/java/inha/gdgoc/global/util/EncryptUtil.java index d08ee84..d4629cd 100644 --- a/src/main/java/inha/gdgoc/util/EncryptUtil.java +++ b/src/main/java/inha/gdgoc/global/util/EncryptUtil.java @@ -1,4 +1,4 @@ -package inha.gdgoc.util; +package inha.gdgoc.global.util; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0242f30..f818194 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -2,6 +2,8 @@ server: forward-headers-strategy: framework spring: + config: + import: optional:file:.env[.properties] jackson: time-zone: Asia/Seoul datasource: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 541db58..60b1cd0 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -2,6 +2,8 @@ server: forward-headers-strategy: framework spring: + config: + import: optional:file:.env[.properties] jackson: time-zone: Asia/Seoul datasource: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e7f0381..ac063cb 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -2,6 +2,8 @@ server: forward-headers-strategy: framework spring: + config: + import: optional:file:.env[.properties] jackson: time-zone: Asia/Seoul datasource: