Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5c3d831
feat: RedisConfig κ΅¬ν˜„ 및 인메λͺ¨λ¦¬ μΊμ‹œ μ„€μ •
bumstone Jul 16, 2025
ccbf0a2
feat: μœ μ € 인증 κΈ°λ³Έ μ„œλΉ„μŠ€ κ΅¬ν˜„
bumstone Jul 16, 2025
9636027
feat: OAuth2 연동 μ„€μ • 및 webClient 적용, oauth 토큰 μ•”ν˜Έν™” 및 λ³΅ν˜Έν™” 적용
bumstone Jul 16, 2025
aeec789
feat: oauth 토큰 만료 μžλ™ κ°±μ‹ 
bumstone Jul 16, 2025
34b044a
feat: μœ μ € 정보 쑰회 및 νƒˆν‡΄ κΈ°λ³Έ ꡬ성
bumstone Jul 16, 2025
46f9b44
feat: SecurityConfig μ„€μ •
bumstone Jul 16, 2025
883b2f8
feat: QuerydslConfig μ„€μ • 및 μ˜μ‘΄μ„±
bumstone Jul 16, 2025
a651a2e
feat: OAuth2UserDto μˆ˜μ • 및 μ—λŸ¬μ½”λ“œ μ—…λ°μ΄νŠΈ
bumstone Jul 16, 2025
aec935b
feat: JWT 기반 인증/인가 κΈ°λ³Έ κ΅¬ν˜„ + λ¦¬ν”„λ ˆμ‹œ 토큰 및 κ΄€λ ¨ μ„œλΉ„μŠ€ κ΅¬ν˜„
bumstone Jul 16, 2025
2c47614
feat: SecurityConfig κ°œμ„ , JWT λ¬΄νš¨ν™” κΈ°λŠ₯ κ΅¬ν˜„
bumstone Jul 16, 2025
ff6db2c
feat: νšŒμ› κΈ°λ³Έ κΈ°λŠ₯ κ΅¬ν˜„ ꡬ체화
bumstone Jul 18, 2025
3f8761d
refactor: OAuth κ΄€λ ¨ ν•„λ“œ 및 μ„€μ • λ³€κ²½
bumstone Jul 18, 2025
4274787
refactor: 인증관련 κΈ°λŠ₯ κ°œμ„ 
bumstone Jul 18, 2025
7ba0cd7
feat: google -> github 인증을 ν†΅ν•œ 연동
bumstone Jul 19, 2025
1034810
refactor: 쀑볡 κ΄€λ¦¬μž API μ ‘κ·Ό μ œμ–΄ λΆ€λΆ„ 제거
bumstone Jul 19, 2025
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
26 changes: 25 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ repositories {

ext {
mapstructVersion = "1.6.3"
springDocVersion = "2.5.0"
springDocVersion = "2.8.9"
jjwtVersion = "0.12.6"
awsSdkVersion = "2.25.68"
springCloudAwsVersion = "3.1.1"
queryDslVersion = "5.1.0"
}

dependencies {
Expand Down Expand Up @@ -80,6 +81,15 @@ dependencies {
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'

// queryDsl
implementation "com.querydsl:querydsl-core:${queryDslVersion}"
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor (
"com.querydsl:querydsl-apt:${queryDslVersion}:jakarta",
// JPA 메타λͺ¨λΈ μƒμ„±μš©
"jakarta.persistence:jakarta.persistence-api:3.1.0"
)

// db
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand All @@ -90,11 +100,25 @@ dependencies {
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Reactive μ›Ή ν”„λ ˆμž„μ›Œν¬ WebFlux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

/** Q 클래슀 생성 경둜 μ§€μ • **/
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

sourceSets {
main { java.srcDirs += querydslDir }
}

tasks.withType(JavaCompile).configureEach {
options.annotationProcessorGeneratedSourcesDirectory = querydslDir
}

tasks.named('test') {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
volumes:
- redis-data:/data # 데이터 지속성을 μœ„ν•œ λ³Όλ₯¨ μΆ”κ°€
command: redis-server --appendonly yes
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/GitdeunApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;


@EnableJpaAuditing
@EnableScheduling
@SpringBootApplication
public class GitdeunApplication {

Expand Down
35 changes: 35 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/common/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.teamEWSN.gitdeun.common.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.teamEWSN.gitdeun.common.util.CacheType;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.Arrays;

@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();

// 각 μΊμ‹œ νƒ€μž…μ— λŒ€ν•œ μ„€μ • 등둝
Arrays.stream(CacheType.values())
.forEach(cacheType -> {
cacheManager.registerCustomCache(cacheType.getCacheName(),
Caffeine.newBuilder()
.recordStats() // μΊμ‹œ 톡계 기둝
.expireAfterWrite(Duration.ofHours(cacheType.getExpiredAfterWrite())) // ν•­λͺ© 만료 μ‹œκ°„
.maximumSize(cacheType.getMaximumSize()) // μ΅œλŒ€ 크기
.build()
);
});

return cacheManager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.teamEWSN.gitdeun.common.config;

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

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager em;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.teamEWSN.gitdeun.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}

}
114 changes: 114 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.teamEWSN.gitdeun.common.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.teamEWSN.gitdeun.common.jwt.*;
import com.teamEWSN.gitdeun.common.jwt.CustomAccessDeniedHandler;
import com.teamEWSN.gitdeun.common.oauth.handler.CustomOAuth2FailureHandler;
import com.teamEWSN.gitdeun.common.oauth.handler.CustomOAuth2SuccessHandler;
import com.teamEWSN.gitdeun.common.oauth.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.annotation.web.configurers.HeadersConfigurer;
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;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

private final CustomOAuth2UserService customOAuth2UserService;
private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;
private final CustomOAuth2FailureHandler customOAuthFailureHandler;
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

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

http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.headers((headerConfig) -> headerConfig
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));

// oauth2 둜그인 μ„€μ •
http
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService))
// .defaultSuccessUrl("/oauth/success") // 둜그인 μ„±κ³΅μ‹œ 이동할 URL
.successHandler(customOAuth2SuccessHandler)
// .failureUrl("/oauth/fail") // 둜그인 μ‹€νŒ¨μ‹œ 이동할 URL
.failureHandler(customOAuthFailureHandler))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/oauth/logout") // λ‘œκ·Έμ•„μ›ƒ μ„±κ³΅μ‹œ ν•΄λ‹Ή url둜 이동
.clearAuthentication(true) // ν˜„μž¬ μš”μ²­μ˜ SecurityContext μ΄ˆκΈ°ν™”
.deleteCookies("refreshToken") // JWT RefreshToken μΏ ν‚€λ₯Ό ν”„λ‘ νŠΈμ—μ„œ 제거 λͺ…μ‹œ
);

// κ²½λ‘œλ³„ 인가 μž‘μ—…
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN")
.requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN")
.requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll()
.anyRequest().permitAll()
// .anyRequest().authenticated()
);

// μ˜ˆμ™Έ 처리
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(customAuthenticationEntryPoint) // 인증 μ‹€νŒ¨ 처리
.accessDeniedHandler(customAccessDeniedHandler)); // 인가 μ‹€νŒ¨ 처리

// JwtFilter μΆ”κ°€
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

// CORS 섀정을 μœ„ν•œ Bean 등둝
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = getCorsConfiguration();
configuration.setAllowedHeaders(java.util.List.of("Authorization", "Content-Type"));
configuration.setExposedHeaders(java.util.List.of("Authorization"));
configuration.setAllowCredentials(true); // 인증 정보 ν—ˆμš© (μΏ ν‚€ λ“±)

org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new org.springframework.web.cors.UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // λͺ¨λ“  κ²½λ‘œμ— λŒ€ν•΄ 적용
return source;
}


private static CorsConfiguration getCorsConfiguration() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000"); // 개발 ν™˜κ²½
configuration.addAllowedOrigin("https://gitdeun.netlify.app");
configuration.addAllowedOrigin("https://gitdeun.site"); // ν˜œνƒμ˜¨ 도메인
configuration.addAllowedOrigin("https://www.gitdeun.site");
configuration.addAllowedMethod("*"); // λͺ¨λ“  HTTP λ©”μ„œλ“œ ν—ˆμš©
configuration.addAllowedHeader("*"); // λͺ¨λ“  헀더 ν—ˆμš©
return configuration;
}

}
27 changes: 27 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/common/config/SecurityPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.teamEWSN.gitdeun.common.config;


public class SecurityPath {

// permitAll
public static final String[] PUBLIC_ENDPOINTS = {
"/api/signup",
"/api/login",
"/api/token/refresh",
"/api/users/check-duplicate",
"/"
};

// hasRole("USER")
public static final String[] USER_ENDPOINTS = {
"/api/users/me",
"/api/users/me/**",
"/api/logout"
};

// hasRole("ADMIN")
public static final String[] ADMIN_ENDPOINTS = {
"/api/admin/**"
};
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.teamEWSN.gitdeun.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.teamEWSN.gitdeun.common.config.redis;

import org.springframework.beans.factory.annotation.Value;
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.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String redisHost;

@Value("${spring.data.redis.port}")
private int redisPort;

@Value("${spring.data.redis.password}")
private String redisPassword;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(redisHost);
redisConfig.setPort(redisPort);

if (!redisPassword.isEmpty()) {
redisConfig.setPassword(RedisPassword.of(redisPassword));
}

return new LettuceConnectionFactory(redisConfig);
}

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

// Redis에 μ €μž₯λ˜λŠ” λ°μ΄ν„°μ˜ 직렬화 방식을 μ§€μ •
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return redisTemplate;
}
}
Loading