diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/EnvironmentConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/EnvironmentConstant.java new file mode 100644 index 000000000..3408c7cfd --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/EnvironmentConstant.java @@ -0,0 +1,13 @@ +package com.gdschongik.gdsc.global.common.constant; + +import java.util.List; + +public class EnvironmentConstant { + + private EnvironmentConstant() {} + + public static final String PROD = "prod"; + public static final String DEV = "dev"; + public static final String LOCAL = "local"; + public static final List PROD_AND_DEV = List.of(PROD, DEV); +} diff --git a/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java new file mode 100644 index 000000000..ad3a16918 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/common/constant/UrlConstant.java @@ -0,0 +1,11 @@ +package com.gdschongik.gdsc.global.common.constant; + +public class UrlConstant { + + private UrlConstant() {} + + public static final String PROD_CLIENT_URL = "https://onboarding.gdschongik.com"; + public static final String DEV_CLIENT_URL = "https://dev-onboarding.gdschongik.com"; + public static final String LOCAL_REACT_CLIENT_URL = "http://localhost:3000"; + public static final String LOCAL_VITE_CLIENT_URL = "http://localhost:5173"; +} diff --git a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java index a1e47e596..482c0c4dc 100644 --- a/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java +++ b/src/main/java/com/gdschongik/gdsc/global/config/WebSecurityConfig.java @@ -1,5 +1,7 @@ package com.gdschongik.gdsc.global.config; +import static com.gdschongik.gdsc.global.common.constant.UrlConstant.*; +import static org.springframework.http.HttpHeaders.*; import static org.springframework.security.config.Customizer.*; import com.fasterxml.jackson.databind.ObjectMapper; @@ -10,6 +12,7 @@ import com.gdschongik.gdsc.global.security.JwtExceptionFilter; import com.gdschongik.gdsc.global.security.JwtFilter; import com.gdschongik.gdsc.global.util.CookieUtil; +import com.gdschongik.gdsc.global.util.EnvironmentUtil; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,6 +22,9 @@ 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; @Configuration @EnableWebSecurity @@ -29,24 +35,24 @@ public class WebSecurityConfig { private final JwtService jwtService; private final CookieUtil cookieUtil; private final ObjectMapper objectMapper; + private final EnvironmentUtil environmentUtil; @Bean - public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { - httpSecurity - .httpBasic(AbstractHttpConfigurer::disable) + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .cors(withDefaults()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - httpSecurity.oauth2Login( + http.oauth2Login( oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customUserService(memberRepository))) .successHandler(customSuccessHandler(jwtService, cookieUtil))); - httpSecurity.addFilterBefore(jwtFilter(jwtService, cookieUtil), UsernamePasswordAuthenticationFilter.class); - httpSecurity.addFilterBefore(jwtExceptionFilter(objectMapper), JwtFilter.class); + http.addFilterBefore(jwtFilter(jwtService, cookieUtil), UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(jwtExceptionFilter(objectMapper), JwtFilter.class); - return httpSecurity.build(); + return http.build(); } @Bean @@ -68,4 +74,28 @@ public JwtFilter jwtFilter(JwtService jwtService, CookieUtil cookieUtil) { public JwtExceptionFilter jwtExceptionFilter(ObjectMapper objectMapper) { return new JwtExceptionFilter(objectMapper); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + if (environmentUtil.isProdProfile()) { + configuration.addAllowedOriginPattern(PROD_CLIENT_URL); + } + + if (environmentUtil.isDevProfile()) { + configuration.addAllowedOriginPattern(DEV_CLIENT_URL); + configuration.addAllowedOriginPattern(LOCAL_REACT_CLIENT_URL); + configuration.addAllowedOriginPattern(LOCAL_VITE_CLIENT_URL); + } + + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + configuration.setAllowCredentials(true); + configuration.addExposedHeader(SET_COOKIE); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java index 16ecfb682..d397e284e 100644 --- a/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java +++ b/src/main/java/com/gdschongik/gdsc/global/util/CookieUtil.java @@ -2,16 +2,21 @@ import com.gdschongik.gdsc.global.common.constant.JwtConstant; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.server.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class CookieUtil { - public HttpServletResponse addTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) { - // TODO: Prod profile일 때는 Strict, 아니면 None으로 설정 - String sameSite = "None"; + private final EnvironmentUtil environmentUtil; + + public void addTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) { + + String sameSite = determineSameSitePolicy(); ResponseCookie accessTokenCookie = generateCookie(JwtConstant.ACCESS_TOKEN.getCookieName(), accessToken, sameSite); @@ -21,8 +26,6 @@ public HttpServletResponse addTokenCookies(HttpServletResponse response, String response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); - - return response; } private ResponseCookie generateCookie(String cookieName, String tokenValue, String sameSite) { @@ -33,4 +36,11 @@ private ResponseCookie generateCookie(String cookieName, String tokenValue, Stri .httpOnly(false) .build(); } + + private String determineSameSitePolicy() { + if (environmentUtil.isProdProfile()) { + return Cookie.SameSite.STRICT.attributeValue(); + } + return Cookie.SameSite.NONE.attributeValue(); + } } diff --git a/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java b/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java new file mode 100644 index 000000000..668d2da12 --- /dev/null +++ b/src/main/java/com/gdschongik/gdsc/global/util/EnvironmentUtil.java @@ -0,0 +1,38 @@ +package com.gdschongik.gdsc.global.util; + +import static com.gdschongik.gdsc.global.common.constant.EnvironmentConstant.*; + +import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EnvironmentUtil { + + private final Environment environment; + + public String getCurrentProfile() { + return getActiveProfiles() + .filter(profile -> profile.equals(PROD) || profile.equals(DEV)) + .findFirst() + .orElse(LOCAL); + } + + public Boolean isProdProfile() { + return getActiveProfiles().anyMatch(PROD::equals); + } + + public Boolean isDevProfile() { + return getActiveProfiles().anyMatch(DEV::equals); + } + + public Boolean isProdAndDevProfile() { + return getActiveProfiles().anyMatch(PROD_AND_DEV::contains); + } + + private Stream getActiveProfiles() { + return Stream.of(environment.getActiveProfiles()); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 000000000..ccd90cc64 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,12 @@ +spring: + config: + activate: + on-profile: "dev" + jpa: + hibernate: + ddl-auto: update + +logging: + level: + org.springframework.orm.jpa: DEBUG + org.springframework.transaction: DEBUG diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 000000000..f86a1d3c2 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,19 @@ +spring: + config: + activate: + on-profile: "local" + + jpa: + hibernate: + ddl-auto: update + show-sql: ${SHOW_SQL:true} + properties: + hibernate: + format_sql: ${FORMAT_SQL:true} + defer-datasource-initialization: true + open-in-view: false + +logging: + level: + org.springframework.orm.jpa: DEBUG + org.springframework.transaction: DEBUG diff --git a/src/main/resources/application-redis.yml b/src/main/resources/application-redis.yml new file mode 100644 index 000000000..d7f084933 --- /dev/null +++ b/src/main/resources/application-redis.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: "redis" + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml new file mode 100644 index 000000000..ff8ba5e14 --- /dev/null +++ b/src/main/resources/application-security.yml @@ -0,0 +1,22 @@ +spring: + config: + activate: + on-profile: "security" + + security: + oauth2: + client: + registration: + github: + client-id: ${GITHUB_CLIENT_ID:default} + client-secret: ${GITHUB_CLIENT_SECRET:default} + +jwt: + token: + ACCESS_TOKEN: + secret: ${JWT_ACCESS_TOKEN_SECRET:} + expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200} + REFRESH_TOKEN: + secret: ${JWT_REFRESH_TOKEN_SECRET:} + expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800} + issuer: ${JWT_ISSUER:} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c02ebe320..19c2b7fac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,28 +1,14 @@ spring: profiles: group: - local: "datasource" - dev: "datasource" - test: "test" - security: - oauth2: - client: - registration: - github: - client-id: ${GITHUB_CLIENT_ID:default} - client-secret: ${GITHUB_CLIENT_SECRET:default} - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} + test: "test" + local: "local, datasource" + dev: "dev, datasource" + include: + - redis + - storage + - security -jwt: - token: - ACCESS_TOKEN: - secret: ${JWT_ACCESS_TOKEN_SECRET:} - expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:7200} - REFRESH_TOKEN: - secret: ${JWT_REFRESH_TOKEN_SECRET:} - expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800} - issuer: ${JWT_ISSUER:} +logging: + level: + com.gdschongik.gdsc.domain.*.api.*: debug