Skip to content
Closed
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
19 changes: 17 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,28 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Web MVC
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// TEST
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// OAUTH2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
// SPRING SECURITY
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/example/gtable/GTableApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@SpringBootApplication
public class GTableApplication {

public static void main(String[] args) {
SpringApplication.run(GTableApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(GTableApplication.class, args);
}

}
22 changes: 0 additions & 22 deletions src/main/java/com/example/gtable/TODO/TodoController.java

This file was deleted.

7 changes: 0 additions & 7 deletions src/main/java/com/example/gtable/TODO/TodoRepository.java

This file was deleted.

18 changes: 0 additions & 18 deletions src/main/java/com/example/gtable/TODO/TodoService.java

This file was deleted.

27 changes: 14 additions & 13 deletions src/main/java/com/example/gtable/global/api/ApiError.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package com.example.gtable.global.api;

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

import lombok.Getter;

@Getter
public class ApiError {
private final String message;
private final int status;
private final String message;
private final int status;

public ApiError(String message, int status) {
this.message = message;
this.status = status;
}
public ApiError(String message, int status) {
this.message = message;
this.status = status;
}

public ApiError(Throwable throwable, HttpStatus status) {
this(throwable.getMessage(), status);
}
public ApiError(Throwable throwable, HttpStatus status) {
this(throwable.getMessage(), status);
}

public ApiError(String message, HttpStatus status) {
this(message, status.value());
}
public ApiError(String message, HttpStatus status) {
this(message, status.value());
}
}
28 changes: 14 additions & 14 deletions src/main/java/com/example/gtable/global/api/ApiResult.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package com.example.gtable.global.api;

public class ApiResult<T> {
private final boolean success;
private final T response;
private final ApiError error;
private final boolean success;
private final T response;
private final ApiError error;

public ApiResult(boolean success, T response, ApiError error) {
this.success = success;
this.response = response;
this.error = error;
}
public ApiResult(boolean success, T response, ApiError error) {
this.success = success;
this.response = response;
this.error = error;
}

public boolean isSuccess() {
return success;
}
public boolean isSuccess() {
return success;
}

public T getResponse() {
return response;
}
public T getResponse() {
return response;
}
}
21 changes: 11 additions & 10 deletions src/main/java/com/example/gtable/global/api/ApiUtils.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.example.gtable.global.api;

import org.springframework.http.HttpStatus;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiUtils {
public static <T> ApiResult<T> success(T response) {
return new ApiResult<>(true, response,null);
}
public static <T> ApiResult<T> success(T response) {
return new ApiResult<>(true, response, null);
}

public static ApiResult<?> error(Throwable throwable, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(throwable, status));
}
public static ApiResult<?> error(Throwable throwable, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(throwable, status));
}

public static ApiResult<?> error(String message,HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(message,status));
}
public static ApiResult<?> error(String message, HttpStatus status) {
return new ApiResult<>(false, null, new ApiError(message, status));
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/example/gtable/global/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.gtable.global.config;

import java.util.List;

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 {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
// config.setAllowCredentials(true); // 쿠키 포함 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미
return source;
}
Comment on lines +11 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

CORS 설정이 잘 구성되어 있지만 몇 가지 개선점이 있습니다

전반적으로 CORS 설정이 적절히 구성되어 있네요. 다만 몇 가지 개선할 점들이 있습니다:

  1. 22번 라인에 주석처리된 중복 코드가 있습니다
  2. 운영 환경에서는 더 제한적인 설정이 필요할 수 있습니다
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();

    config.setAllowCredentials(true); // 쿠키나 인증헤더 자격증명 허용
    config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 출처 설정
    config.setAllowedMethods(List.of("GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS")); // 메서드 허용
    config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더
    config.setExposedHeaders(List.of("Authorization")); //클라이언트(브라우저)가 접근할 수 있는 헤더 지정
-   //  config.setAllowCredentials(true); // 쿠키 포함 허용

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미
    return source;
}

운영 환경을 고려한다면 다음과 같은 개선도 고려해보시면 좋겠어요:

// 환경별로 다른 origin 설정 예시
@Value("${app.cors.allowed-origins:http://localhost:3000}")
private String[] allowedOrigins;

config.setAllowedOrigins(Arrays.asList(allowedOrigins));
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/config/CorsConfig.java around lines
11 to 27, remove the commented out duplicate line setting allowCredentials on
line 22 to clean up the code. Additionally, refactor the allowed origins to be
configurable per environment by injecting a property (e.g., using @Value to load
allowed origins from application properties) and setting them dynamically in the
config. This will enable more restrictive CORS settings in production while
allowing localhost during development.

}
64 changes: 64 additions & 0 deletions src/main/java/com/example/gtable/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.gtable.global.config;

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.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;

import com.example.gtable.global.security.jwt.JwtAuthorizationFilter;
import com.example.gtable.global.security.jwt.JwtUtil;
import com.example.gtable.global.security.oauth2.CustomOAuth2UserService;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity // security 활성화 어노테이션
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final com.example.gtable.global.security.oauth2.OAuth2LoginSuccessHandler OAuth2LoginSuccessHandler;
private final JwtUtil jwtUtil;

private final CorsConfigurationSource corsConfigurationSource;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

CORS 설정이 주입되었지만 사용되지 않고 있습니다

CorsConfigurationSource가 주입되었지만 실제 HTTP 보안 설정에서 사용되지 않고 있습니다. CORS 정책을 적용하려면 추가 설정이 필요합니다.

다음과 같이 CORS 설정을 추가해주세요:

 		http
+			// CORS 설정 적용
+			.cors(cors -> cors.configurationSource(corsConfigurationSource))
 			// CSRF 방어 기능 비활성화 (jwt 토큰을 사용할 것이기에 필요없음)
 			.csrf(AbstractHttpConfigurer::disable)
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/config/SecurityConfig.java at line
27, the CorsConfigurationSource is injected but not used in the HTTP security
configuration. To fix this, update the security configuration method to apply
the CORS settings by calling cors().configurationSource(corsConfigurationSource)
within the HttpSecurity configuration chain. This ensures the CORS policy is
properly enforced.


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 방어 기능 비활성화 (jwt 토큰을 사용할 것이기에 필요없음)
.csrf(AbstractHttpConfigurer::disable)
// 시큐리티 폼 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable)
// HTTP Basic 인증 비활성화
.httpBasic(AbstractHttpConfigurer::disable)
// oauth2 로그인
// - userInfoEndPoint에서 사용자 정보 불러오고,
// - successHandler에서 로그인 성공 시 JWT 생성 및 반환로직
.oauth2Login(oauth2 ->
oauth2.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.userService(customOAuth2UserService)
).successHandler(OAuth2LoginSuccessHandler)
)
// 세션 사용하지 않음
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/oauth2/authorization/kakao", // 카카오 로그인 요청
"/login/oauth2/code/**", // 카카오 인증 콜백
"/api/refresh-token") // refresh token (토큰 갱신)
.permitAll()
.anyRequest().authenticated() // 그외 요청은 허가된 사람만 인가
)
// JWTFiler
.addFilterBefore(new JwtAuthorizationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.gtable.global.security.exception;

public abstract class BusinessException extends RuntimeException {
private final ErrorMessage errorMessage;

protected BusinessException(ErrorMessage errorMessage) {
super(errorMessage.getMessage());
this.errorMessage = errorMessage;
}

public String getCode() {
return errorMessage.getCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.gtable.global.security.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorMessage {
// global
INVALID_INPUT_VALUE("입력값이 올바르지 않습니다.", "g001"),

// auth
UNAUTHORIZED("권한이 없습니다", "a001"),

// token
REFRESH_TOKEN_NOT_FOUND("기존 리프레시 토큰을 찾을 수 없습니다.", "t001"),
DOES_NOT_MATCH_REFRESH_TOKEN("기존 리프레시 토큰이 일치하지 않습니다.", "t002");

private final String message;
private final String code;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.gtable.global.security.exception;

import java.util.HashMap;
import java.util.Map;

import lombok.Getter;

@Getter
public class ErrorResponse {
private final String message;
private final String code;
private final Map<String, String> errors;

public ErrorResponse(String message, String code) {
this.message = message;
this.code = code;
errors = new HashMap<>();
}

public ErrorResponse(String message, String code, Map<String, String> errors) {
this.message = message;
this.code = code;
this.errors = errors;
}

}
Loading