Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복된 테스트 의존성을 발견했습니다.

31번 라인과 46번 라인에 spring-boot-starter-test 의존성이 중복으로 선언되어 있습니다.

다음과 같이 중복을 제거해주시기 바랍니다:

// Web MVC
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
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'
// ... (다른 의존성들)
// SPRING SECURITY
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
🤖 Prompt for AI Agents
In build.gradle at line 31, the dependency 'spring-boot-starter-test' is
declared twice, once at line 31 and again at line 46. Remove the duplicate
declaration at line 31 to eliminate redundancy and keep only one instance of
this test dependency.

// 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); // 쿠키 포함 허용
Comment on lines +17 to +22
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 설정이 잘 구성되었으나 중복 코드와 보안 고려사항이 있습니다.

코드에서 발견된 개선점들:

  1. 17번 라인과 22번 라인(주석 처리됨)에 setAllowCredentials(true) 호출이 중복되어 있습니다
  2. 현재 설정은 개발 환경에 적합하나, 운영 환경에서는 보안 검토가 필요합니다

다음과 같은 개선을 제안드립니다:

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); // 쿠키 포함 허용
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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); // 쿠키 포함 허용
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")); // 클라이언트(브라우저)가 접근할 수 있는 헤더 지정
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/config/CorsConfig.java around lines
17 to 22, remove the duplicated call to setAllowCredentials(true) by deleting
the commented-out line 22 to avoid redundancy. Additionally, ensure that the
allowed origins list is configured dynamically or restricted appropriately for
production environments to enhance security, rather than hardcoding
"http://localhost:3000".


UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); //** 뜻은 모든 URL 경로에 적용한다는 의미
return source;
}
}
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;

@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();
Comment on lines +27 to +61
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

CORS 설정 빈을 주입해두고 실제로 적용하지 않았습니다
corsConfigurationSource를 주입하셨지만 http.cors().configurationSource(corsConfigurationSource) 구문이 없어 CORS 정책이 활성화되지 않습니다. 프런트엔드 호출 시 403(CORS) 오류가 발생할 수 있으니 꼭 추가해주세요.

-        http
+        http
+            // CORS 설정 적용
+            .cors(cors -> cors.configurationSource(corsConfigurationSource))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final CorsConfigurationSource corsConfigurationSource;
@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();
private final CorsConfigurationSource corsConfigurationSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CORS 설정 적용
.cors(cors -> cors.configurationSource(corsConfigurationSource))
// 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() // 그외 요청은 허가된 사람만 인가
)
// JWTFilter
.addFilterBefore(
new JwtAuthorizationFilter(jwtUtil),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
🤖 Prompt for AI Agents
In src/main/java/com/example/gtable/global/config/SecurityConfig.java between
lines 27 and 61, the injected corsConfigurationSource is not applied to the
HttpSecurity configuration, causing CORS policies to be inactive and potential
403 errors on frontend requests. Fix this by adding the call
http.cors().configurationSource(corsConfigurationSource) in the filterChain
method before building the http object to enable CORS with the provided
configuration source.

}

}
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