Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ject.studytrip.global.common.constants;

import java.util.Arrays;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum SwaggerUrlConstants {
SWAGGER_RESOURCES_URL("/swagger-resources/**"),
SWAGGER_UI_URL("/swagger-ui/**"),
SWAGGER_API_DOCS_URL("/v3/api-docs/**"),
;

private final String value;

public static String[] getSwaggerUrls() {
return Arrays.stream(SwaggerUrlConstants.values())
.map(SwaggerUrlConstants::getValue)
.toArray(String[]::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ject.studytrip.global.common.constants;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UrlConstants {
DEV_API_SERVER_URL("https://dev-api-studytrip.duckdns.org"),
LOCAL_API_SERVER_URL("http://localhost:8080"),

// TODO: 개발, 운영 도메인 URL 추가 작업
LOCAL_DOMAIN_URL("http://localhost:3000"),
LOCAL_SECURE_DOMAIN_URL("https://localhost:3000"),
;

private final String value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ject.studytrip.global.common.response;

import com.ject.studytrip.global.exception.response.ErrorResponse;

public record StandardResponse(boolean success, int status, Object data) {

public static StandardResponse success(int status, Object data) {
return new StandardResponse(true, status, data);
}

public static StandardResponse fail(int status, ErrorResponse errorResponse) {
return new StandardResponse(false, status, errorResponse);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/ject/studytrip/global/config/QuerydslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ject.studytrip.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {

private final EntityManager em;

@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/ject/studytrip/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ject.studytrip.global.config;

import com.ject.studytrip.global.config.properties.RedisProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableConfigurationProperties(RedisProperties.class)
@Configuration
@RequiredArgsConstructor
public class RedisConfig {

private final RedisProperties redisProperties;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port());
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);

// 단순 Key-Value 직렬화
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

// 해시 Key-Value 직렬화
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

template.afterPropertiesSet();
return template;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ject.studytrip.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@Configuration
public class SchedulerConfig {}
77 changes: 77 additions & 0 deletions src/main/java/com/ject/studytrip/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.ject.studytrip.global.config;

import com.ject.studytrip.global.config.properties.SwaggerProperties;
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.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.*;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Profile("!prod") // 로컬, 개발환경 활성화
@EnableConfigurationProperties(SwaggerProperties.class)
@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {

private static final String SERVER_NAME = "StudyTrip";
private static final String SERVER_DESCRIPTION = "StudyTrip 서버 URL 입니다.";
private static final String API_TITLE = "StudyTrip 서버 API 문서";
private static final String API_DESCRIPTION = "StudyTrip 서버 API 문서입니다.";
private static final String GITHUB_URL = "https://github.com/JECT-Study/JECT-4-server";

private final SwaggerProperties swaggerProperties;

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.servers(swaggerServer())
.addSecurityItem(securityRequirement())
.components(authComponents())
.info(swaggerInfo());
}

private List<Server> swaggerServer() {
Server server =
new Server().url(swaggerProperties.serverUrl()).description(SERVER_DESCRIPTION);
return List.of(server);
}

private Components authComponents() {
return new Components()
.addSecuritySchemes(
"accessToken",
new SecurityScheme()
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(In.HEADER)
.name("Authorization"));
}

private SecurityRequirement securityRequirement() {
SecurityRequirement securityRequirement = new SecurityRequirement();
securityRequirement.addList("accessToken");
return securityRequirement;
}

private Info swaggerInfo() {
License license = new License();
license.setUrl(GITHUB_URL);
license.setName(SERVER_NAME);

return new Info()
.version("v" + swaggerProperties.version())
.title(API_TITLE)
.description(API_DESCRIPTION)
.license(license);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.ject.studytrip.global.config;

import com.ject.studytrip.global.common.constants.SwaggerUrlConstants;
import com.ject.studytrip.global.security.CustomAccessDeniedHandler;
import com.ject.studytrip.global.security.CustomAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Slf4j
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;

private void defaultFilterChain(HttpSecurity http) throws Exception {
http
// csrf 비활성화
.csrf(AbstractHttpConfigurer::disable)
// http basic 비활성화
.httpBasic(AbstractHttpConfigurer::disable)
// 폼 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable)
// 세션 비활성화
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// cors 설정
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

defaultFilterChain(http);

// 경로 인가 설정
http.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers(SwaggerUrlConstants.getSwaggerUrls())
.permitAll() // Swagger 경로
.requestMatchers("/api/sample/**")
.permitAll() // 샘플 api 경로
.anyRequest()
.authenticated()); // 그 외 요청은 모두 인증 수행

// 예외 핸들링
http.exceptionHandling(
exception ->
exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler));

return http.build();
}

// CORS 설정
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowCredentials(true);
config.addAllowedHeader("*");
config.addAllowedMethod("*");

// TODO: 환경별 CORS 허용 origin 설정 분기 처리
// - dev: LOCAL_DOMAIN, LOCAL_SECURE_DOMAIN, DEV_DOMAIN 허용
// - prod: PROD_DOMAIN 만 허용
// - Spring Active Profile 기반 분기 필요
// - 서비스 도메인, 서버 운영 환경 설정 완료 시 작업

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return source;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ject.studytrip.global.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.data.redis")
public record RedisProperties(int port, String host) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ject.studytrip.global.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "swagger")
public record SwaggerProperties(String version, String serverUrl) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ject.studytrip.global.exception;

import com.ject.studytrip.global.exception.error.CommonErrorCode;
import com.ject.studytrip.global.exception.error.ErrorCode;
import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {

private final ErrorCode errorCode;

public CustomException(CommonErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ject.studytrip.global.exception.error;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum AuthErrorCode implements ErrorCode {
UNAUTHENTICATED(HttpStatus.UNAUTHORIZED, "인증되지 않은 요청입니다."),
ACCESS_DENIED(HttpStatus.FORBIDDEN, "접근 권한이 부족합니다."),
;

private final HttpStatus status;
private final String message;

@Override
public String getName() {
return this.name();
}

@Override
public HttpStatus getStatus() {
return this.status;
}

@Override
public String getMessage() {
return this.message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.ject.studytrip.global.exception.error;

import lombok.*;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메서드 입니다."),
METHOD_ARGUMENT_NOT_VALID(HttpStatus.BAD_REQUEST, "요청 본문(JSON)의 값 유효성 검증에 실패했습니다."),
INVALID_JSON_FORMAT(
HttpStatus.BAD_REQUEST, "요청 본문(JSON) 형식이 잘못되어 파싱할 수 없습니다. (필드 타입 불일치, 필수 필드 누락 등)"),
CONSTRAINT_VIOLATION(HttpStatus.BAD_REQUEST, "요청 파라미터 또는 경로 변수 유효성 검증에 실패했습니다."),
METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "요청한 메서드 파마미터의 타입이 일치하지 않습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러. 관리자에게 문의하세요."),
;

private final HttpStatus status;
private final String message;

@Override
public String getName() {
return this.name();
}

@Override
public HttpStatus getStatus() {
return this.status;
}

@Override
public String getMessage() {
return this.message;
}
}
Loading