diff --git a/src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java b/src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java deleted file mode 100644 index 1aca4f0..0000000 --- a/src/main/java/com/pinHouse/server/core/config/SwaggerConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.pinHouse.server.core.config; - -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 { - - @Bean - public OpenAPI openAPI() { - String jwt = "JWT"; - SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); - Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() - .name(jwt) - .type(SecurityScheme.Type.HTTP) - .scheme("Bearer") - .bearerFormat("JWT") - ); - return new OpenAPI() - .components(components) - .info(apiInfo()) - .addSecurityItem(securityRequirement); - } - - private Info apiInfo() { - return new Info() - .version("1.0") - .title("집터 API") - .description("집터 개발 서버의 API 입니다"); - } -} diff --git a/src/main/java/com/pinHouse/server/core/config/swaagger/LocalSwaggerConfig.java b/src/main/java/com/pinHouse/server/core/config/swaagger/LocalSwaggerConfig.java new file mode 100644 index 0000000..9bf2f1d --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/config/swaagger/LocalSwaggerConfig.java @@ -0,0 +1,62 @@ +package com.pinHouse.server.core.config.swaagger; + +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.media.Schema; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.util.Map; +import java.util.TreeMap; + +@Profile("local") +@Configuration +public class LocalSwaggerConfig { + + @Bean + public OpenAPI openAPI() { + String jwt = "JWT"; + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); + Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() + .name(jwt) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT") + ); + return new OpenAPI() + .components(components) + .info(apiInfo()) + .addSecurityItem(securityRequirement); + } + + private Info apiInfo() { + return new Info() + .title("PinHouse Swagger") + .description("핀하우스 로컬 스웨거입니다.") + .version("1.0.0"); + } + + /// 필요없는 스키마 제거 + @Bean + public OpenApiCustomizer removeGenericSchemas() { + return openApi -> { + openApi.getComponents().getSchemas().keySet().removeIf(name -> + name.contains("ApiResponse") || name.contains("SliceResponse") + ); + }; + } + + /// 스키마 이름 기준 오름차순 + @Bean + public OpenApiCustomizer sortSchemasAlphabetically() { + return openApi -> { + Map schemas = openApi.getComponents().getSchemas(); + openApi.getComponents().setSchemas(new TreeMap<>(schemas)); + }; + } +} diff --git a/src/main/java/com/pinHouse/server/core/config/swaagger/SwaggerConfig.java b/src/main/java/com/pinHouse/server/core/config/swaagger/SwaggerConfig.java new file mode 100644 index 0000000..ec146a7 --- /dev/null +++ b/src/main/java/com/pinHouse/server/core/config/swaagger/SwaggerConfig.java @@ -0,0 +1,70 @@ +package com.pinHouse.server.core.config.swaagger; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.servers.Server; +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.media.Schema; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Swagger / OpenAPI 설정 클래스 + * - HTTPS 설정으로 인해 @OpenAPIDefinition 추가 + * - 스웨거 내부 JWT 설정 추가 + */ + +@Configuration +@Profile("dev") +@OpenAPIDefinition(servers = {@Server(url = "https://api.pinhouse.cloud", description = "PinHouse 개발 서버")}) +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + String jwt = "JWT"; + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); + Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() + .name(jwt) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT") + ); + return new OpenAPI() + .components(components) + .info(apiInfo()) + .addSecurityItem(securityRequirement); + } + + private Info apiInfo() { + return new Info() + .title("PinHouse Swagger") + .description("핀하우스 스웨거입니다.") + .version("1.0.0"); + } + + @Bean + public OpenApiCustomizer removeGenericSchemas() { + return openApi -> { + openApi.getComponents().getSchemas().keySet().removeIf(name -> + name.contains("ApiResponse") || name.contains("SliceResponse") + ); + }; + } + /// 스키마 이름 기준 오름차순 + + @Bean + public OpenApiCustomizer sortSchemasAlphabetically() { + return openApi -> { + Map schemas = openApi.getComponents().getSchemas(); + openApi.getComponents().setSchemas(new TreeMap<>(schemas)); + }; + } +} diff --git a/src/main/java/com/pinHouse/server/security/config/CorsConfig.java b/src/main/java/com/pinHouse/server/security/config/CorsConfig.java new file mode 100644 index 0000000..d5f0402 --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/config/CorsConfig.java @@ -0,0 +1,45 @@ +package com.pinHouse.server.security.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + + @Value("${cors.front.local}") + private String front_local; + + @Value("${cors.front.dev}") + private String front_dev; + + @Value("${cors.back.dev}") + private String back_dev; + + /** + * CORS 설정을 진행합니다. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + /// CORS 추가 + configuration.addAllowedOriginPattern(front_local); + configuration.addAllowedOriginPattern(front_dev); + configuration.addAllowedOriginPattern(back_dev); + + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + configuration.setAllowCredentials(true); + + configuration.addExposedHeader("Authorization"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java b/src/main/java/com/pinHouse/server/security/config/RequestMatcherHolder.java similarity index 92% rename from src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java rename to src/main/java/com/pinHouse/server/security/config/RequestMatcherHolder.java index e6c5b3d..f6cc9ff 100644 --- a/src/main/java/com/pinHouse/server/security/jwt/filter/RequestMatcherHolder.java +++ b/src/main/java/com/pinHouse/server/security/config/RequestMatcherHolder.java @@ -1,4 +1,4 @@ -package com.pinHouse.server.security.jwt.filter; +package com.pinHouse.server.security.config; import com.pinHouse.server.platform.domain.user.Role; import io.micrometer.common.lang.Nullable; @@ -38,9 +38,9 @@ public class RequestMatcherHolder { new RequestInfo(GET, "/docs/**", null), new RequestInfo(GET, "/*.ico", null), new RequestInfo(GET, "/resources/**", null), + new RequestInfo(GET, "/style.css", null), new RequestInfo(GET, "/index.html", null), new RequestInfo(GET, "/error", null), - new RequestInfo(GET, "/kikihi.png", null), // Swagger UI 및 API 문서 관련 요청 new RequestInfo(GET, "/v3/api-docs/**", null), @@ -51,10 +51,7 @@ public class RequestMatcherHolder { // 정적 아이콘 요청 new RequestInfo(GET, "/favicon.ico", null), - new RequestInfo(GET, "/apple-touch-icon.png", null), - - // 검색 (임시로 개방) - new RequestInfo(GET, "/api/v1/search", null) + new RequestInfo(GET, "/apple-touch-icon.png", null) ); diff --git a/src/main/java/com/pinHouse/server/security/config/SecurityConfig.java b/src/main/java/com/pinHouse/server/security/config/SecurityConfig.java new file mode 100644 index 0000000..f9a2a6d --- /dev/null +++ b/src/main/java/com/pinHouse/server/security/config/SecurityConfig.java @@ -0,0 +1,73 @@ +package com.pinHouse.server.security.config; + +import com.pinHouse.server.security.jwt.filter.JwtAuthenticationDeniedHandler; +import com.pinHouse.server.security.jwt.filter.JwtAuthenticationFailureHandler; +import com.pinHouse.server.security.jwt.filter.JwtAuthenticationFilter; +import com.pinHouse.server.security.oauth2.handler.OAuth2SuccessHandler; +import com.pinHouse.server.security.oauth2.service.OAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +import static com.pinHouse.server.platform.domain.user.Role.ADMIN; +import static com.pinHouse.server.platform.domain.user.Role.USER; + +/** + * Spring Security 설정 클래스 + * + * 애플리케이션의 인증 및 권한 부여 정책을 정의한다. + */ + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final OAuth2UserService oAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final JwtAuthenticationFilter jwtFilter; + private final JwtAuthenticationFailureHandler jwtFailureHandler; + private final JwtAuthenticationDeniedHandler jwtDeniedHandler; + private final RequestMatcherHolder requestMatcherHolder; + private final CorsConfigurationSource corsConfigurationSource; + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) + .formLogin(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(requestMatcherHolder.getRequestMatchersByMinRole(null)) + .permitAll() + .requestMatchers(requestMatcherHolder.getRequestMatchersByMinRole(USER)) + .hasAnyAuthority(ADMIN.getRole(), USER.getRole()) + .requestMatchers(requestMatcherHolder.getRequestMatchersByMinRole(ADMIN)) + .hasAnyAuthority(ADMIN.getRole()) + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(oAuth2UserService) + ) + .successHandler(oAuth2SuccessHandler) + .failureHandler((request, response, exception) -> { + response.sendRedirect("/login?error"); + }) + ) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exception -> { + exception.authenticationEntryPoint(jwtFailureHandler) + .accessDeniedHandler(jwtDeniedHandler); + }); + + + return http.build(); + } +} diff --git a/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java index cdba280..533ba90 100644 --- a/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/pinHouse/server/security/jwt/filter/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.pinHouse.server.security.jwt.filter; import com.pinHouse.server.core.response.response.ErrorCode; + import com.pinHouse.server.security.config.RequestMatcherHolder; import com.pinHouse.server.security.jwt.exception.JwtAuthenticationException; import com.pinHouse.server.security.jwt.util.JwtTokenExtractor; import jakarta.servlet.FilterChain; @@ -106,11 +107,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse @Override protected boolean shouldNotFilter(HttpServletRequest request) { - /// 상품 조회는 회원/비회원 구분해야되기에 모두 필터를 타도록 설정 - if (request.getRequestURI().startsWith("/api/v1/products")) { - return false; - } - /// null 인 것 해결 return requestMatcherHolder.getRequestMatchersByMinRole(null) .matches(request); diff --git a/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java b/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java index adbb8db..37f4b59 100644 --- a/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/pinHouse/server/security/oauth2/handler/OAuth2SuccessHandler.java @@ -20,7 +20,7 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler private final JwtTokenUseCase tokenService; - @Value("${spring.front.host}") + @Value("${cors.front.dev}") public String REDIRECT_PATH; /* diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index c142bf7..8a3ec49 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -1,5 +1,5 @@ body { - background: linear-gradient(135deg, #f3f7ff 0%, #dceeff 100%); + background: linear-gradient(130deg, #e6f0ff 0%, #c6e0fa 100%); font-family: 'Pretendard', 'Noto Sans KR', sans-serif; margin: 0; padding: 0; @@ -9,54 +9,96 @@ body { align-items: center; } +/* Main container 박스 */ .container { - background: #ffffffee; + background: #fff; padding: 64px 56px; text-align: center; - border-radius: 24px; + border-radius: 34px; max-width: 520px; width: 88%; + position: relative; + overflow: hidden; + /* 깊이감 강조 그림자: 여러 레이어 */ box-shadow: - 0 4px 24px rgba(0, 96, 255, 0.08), - 0 2px 8px rgba(100, 100, 150, 0.08); - border: 1px solid #ddeaff; - transition: all 0.25s ease-in-out; + 0 14px 40px 0 rgba(28,106,255,0.12), /* 진한 메인 그림자 */ + 0 6px 24px 0 rgba(28,106,255,0.08), /* 부드러움 */ + 0 24px 60px 0 rgba(100,180,255,0.17), /* 뒤로 번지는 듯한 파란광 */ + 0 2px 8px rgba(100, 130, 250, 0.08); /* 기존 그림자 */ + border: 1.5px solid #daf0ff; + transition: all 0.31s cubic-bezier(.45,.65,.23,1.02); } +/* 박스 hover 시 입체감 극대화 */ .container:hover { - transform: translateY(-4px); - box-shadow: 0 8px 32px rgba(55, 150, 255, 0.15); + transform: translateY(-7px) scale(1.015); + box-shadow: + 0 20px 54px 0 rgba(28,106,255,0.16), + 0 10px 32px 0 rgba(28,106,255,0.10), + 0 38px 80px 0 rgba(80,150,255,0.21), + 0 3px 14px rgba(100, 130, 250, 0.09); +} + +/* 뒤쪽 퍼지는 "빛 효과" */ +.container::before { + content: ""; + position: absolute; + z-index: 0; + inset: -12px; + border-radius: 42px; + pointer-events: none; + background: radial-gradient( + circle at 70% 15%, rgba(44,130,255,0.12) 0%, transparent 60% + ); + filter: blur(3px); } +/* 나머지 스타일은 기존대로 (로고, h1, p, button 등) */ +.logo { + width: 72px; + margin-bottom: 28px; + display: inline-block; + z-index: 1; + position: relative; +} h1 { - font-size: 2.1rem; - color: #2368b1; - margin-bottom: 24px; + font-size: 2.2rem; + color: #2377ff; /* 로고와 매칭되는 블루 */ + margin-bottom: 22px; font-weight: 700; - letter-spacing: 0.8px; + letter-spacing: 1px; + text-shadow: 0 2px 6px #c8e2ff66; + z-index: 1; + position: relative; } p { - font-size: 1.1rem; - color: #555f6b; + font-size: 1.13rem; + color: #4a5b76; margin-bottom: 36px; - line-height: 1.7; + line-height: 1.75; + z-index: 1; + position: relative; } -/* 버튼 스타일 예시 */ +/* 버튼 */ .button { - padding: 12px 24px; - background-color: #2377ff; + padding: 13px 28px; + background: linear-gradient(90deg, #2377ff 60%, #44bcff 100%); color: white; border: none; - border-radius: 12px; - font-size: 1rem; - font-weight: 600; + border-radius: 14px; + font-size: 1.07rem; + font-weight: 700; cursor: pointer; - box-shadow: 0 2px 8px rgba(0, 72, 255, 0.2); - transition: background-color 0.2s; + box-shadow: 0 3px 12px rgba(44, 128, 255, 0.16); + transition: background 0.25s, transform 0.19s; + z-index: 1; + position: relative; } .button:hover { - background-color: #005be3; + background: linear-gradient(90deg, #1763c6 60%, #2377ff 100%); + transform: translateY(-2px) scale(1.04); } +