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
9 changes: 5 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4'
implementation 'com.nimbusds:nimbus-jose-jwt:9.37.2'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // 3.3.x 버전으로 변경 없음
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' // 또는 jjwt-gson 사용 시 'io.jsonwebtoken:jjwt-gson:0.11.2'
compileOnly 'org.projectlombok:lombok:1.18.30' // 최신 버전 확인 후 사용
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.2'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' // Spring Boot 3.3.4에 맞는 2.x 버전
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/mtvs/devlinkbackend/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.mtvs.devlinkbackend.config;

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;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.addAllowedOrigin("http://125.132.216.190:15530");
configuration.addAllowedOrigin("http://localhost:8080");
configuration.addAllowedOrigin("http://localhost:5173"); // 테스트에서 사용되는 도메인 추가

configuration.addAllowedMethod("GET");
configuration.addAllowedMethod("POST");
configuration.addAllowedMethod("PUT");
configuration.addAllowedMethod("PATCH");
configuration.addAllowedMethod("DELETE");

configuration.addAllowedHeader("*"); // 모든 헤더 허용

configuration.setAllowCredentials(true); // 인증 정보 포함 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

@Bean
public CorsFilter corsFilter(CorsConfigurationSource corsConfigurationSource) {
return new CorsFilter(corsConfigurationSource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public JwtAuthenticationFilter(JwtUtil jwtUtil, EpicGamesTokenService epicGamesT
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {

System.out.println("JwtAuthenticationFilter 실행됨: " + request.getRequestURI());

// Authorization 헤더에서 Bearer 토큰 추출
String authorizationHeader = request.getHeader("Authorization");
String token = null;
Expand All @@ -45,29 +47,41 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
// 새로 발급된 액세스 토큰을 Authorization 헤더에 추가
response.setHeader("Authorization", "Bearer " + token);
} else {
System.out.println("refrehToken으로 AccessToken 발급하려다가 refreshToken 없어서 실패");
System.out.println("refrehToken으로 accessToken 발급하려다가 refreshToken 없어서 실패");
return;
}
}

if (token != null) { // refreshToken도 없어 AccessToken이 아예 없는 경우 지나가기
try {
// 토큰 검증 | 검증 성공 시 SecurityContext에 인증 정보 저장
String userPrincipal = jwtUtil.getSubjectFromTokenWithAuth(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userPrincipal, null, null);
try {
// 토큰 검증 | 검증 성공 시 SecurityContext에 인증 정보 저장
String userPrincipal = jwtUtil.getSubjectFromTokenWithAuth(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userPrincipal, null, null);

SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
// 검증 실패 시 401 에러 설정
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
// 검증 실패 시 401 에러 설정
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

// 필터 체인 진행
chain.doFilter(request, response);
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
// Swagger 관련 모든 경로 예외 처리
return path.startsWith("/swagger-ui")
|| path.startsWith("/v3/api-docs")
|| path.equals("/swagger-ui.html")
|| path.startsWith("/swagger-resources")
|| path.startsWith("/webjars")
|| path.startsWith("/login")
|| path.startsWith("/**");
}

// 쿠키에서 리프레시 토큰을 추출하는 메서드
private String getRefreshTokenFromCookies(HttpServletRequest request) {
if (request.getCookies() != null) {
Expand Down
117 changes: 58 additions & 59 deletions src/main/java/com/mtvs/devlinkbackend/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
Expand All @@ -21,53 +22,75 @@
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.web.cors.CorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final UserService userService;
private final OAuth2AuthorizedClientService authorizedClientService;
private final CorsConfigurationSource corsConfigurationSource;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(UserService userService, OAuth2AuthorizedClientService authorizedClientService, JwtAuthenticationFilter jwtAuthenticationFilter) {
public SecurityConfig(UserService userService, CorsConfigurationSource corsConfigurationSource, OAuth2AuthorizedClientService authorizedClientService, JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userService = userService;
this.corsConfigurationSource = corsConfigurationSource;
this.authorizedClientService = authorizedClientService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource)) // CORS 설정
.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("*", "/question/**", "/login").permitAll() // "*"에 대한 설정은 CorsTest를 위함
.requestMatchers(
"/api/auth/epicgames/callback", // 토큰 호출 부분
"/login",
"/v3/api-docs/**", // Swagger API Docs 경로
"/swagger-ui/**", // Swagger UI 정적 리소스 경로
"/swagger-ui.html", // Swagger UI 페이지 경로
"/swagger-resources/**", // Swagger 관련 리소스 경로
"/webjars/**", // Webjars로 제공되는 Swagger 리소스 경로
"/configuration/ui", // 추가 Swagger 설정 경로
"/configuration/security", // 추가 Swagger 설정 경로
"/error" // 오류 페이지 경로 허용
).permitAll() // Swagger 관련 경로 허용
.anyRequest().authenticated()
)

// oauth2Login 설정이 다른 경로에서만 작동하도록 설정
.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/login")
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true")
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(oauth2UserService())
)
.successHandler(oauth2AuthenticationSuccessHandler()) // 성공 핸들러 추가
)
.logout(logout -> logout
.logoutUrl("/logout")
.addLogoutHandler(logoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
.successHandler(oauth2AuthenticationSuccessHandler())
)
// 세션을 생성하지 않도록 설정
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// JWT 필터 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilter(corsFilter());
// .logout(logout -> logout
// .logoutUrl("/logout")
// .addLogoutHandler(logoutHandler())
// .logoutSuccessUrl("/")
// )
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

// 가능성 :
// "/swagger" 경로로 들어온 요청은 필터에서 permitAll로 권한 인증을 통과했지만
// "index.html"로 서블릿 포워딩 되면서 시큐리티 필터를 다시 거치게 되는데 권한 없음으로 403이 내려오는 것이다.
// 요청 한번에 권한 인증이 여러번 되는 이유는 직접 구현한 토큰 인증 필터와 같은 경우 OncePerRequestFilter를 확장해서
// 요청 당 한번만 거치도록 설정하지만 기본적으로 필터는 서블릿에 대한 매요청마다 거치는 것이 기본 전략임

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return new DefaultOAuth2UserService();
Expand All @@ -76,6 +99,7 @@ public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
@Bean
public AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler() {
return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
System.out.println("여기 오냐");
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();

// OAuth2AuthorizedClient를 사용하여 액세스 토큰과 리프레시 토큰 가져오기
Expand All @@ -97,46 +121,21 @@ public AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler() {
};
}

@Bean
public LogoutHandler logoutHandler() {
return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
// 쿠키 삭제
deleteCookie(response, "access_token");
deleteCookie(response, "refresh_token");
};
}

@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
// 로그아웃 성공 후 리디렉트
response.sendRedirect("/login?logout");
};
}

private void deleteCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setPath("/");
cookie.setMaxAge(0); // 쿠키 즉시 삭제
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}

@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:5173"); // 테스트에서 사용되는 도메인 추가
configuration.addAllowedMethod("GET");
configuration.addAllowedMethod("POST");
configuration.addAllowedMethod("PUT");
configuration.addAllowedMethod("PATCH");
configuration.addAllowedMethod("DELETE");
configuration.addAllowedHeader("*"); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 포함 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 CORS 설정 적용
return new CorsFilter(source);
}
// @Bean
// public LogoutHandler logoutHandler() {
// return (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
// // 쿠키 삭제
// deleteCookie(response, "access_token");
// deleteCookie(response, "refresh_token");
// };
// }
//
// private void deleteCookie(HttpServletResponse response, String cookieName) {
// Cookie cookie = new Cookie(cookieName, null);
// cookie.setPath("/");
// cookie.setMaxAge(0); // 쿠키 즉시 삭제
// cookie.setHttpOnly(true);
// cookie.setSecure(true);
// response.addCookie(cookie);
// }
}
18 changes: 18 additions & 0 deletions src/main/java/com/mtvs/devlinkbackend/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.mtvs.devlinkbackend.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("DevLink API Documentation")
.version("1.0.0")
.description("API documentation for DevLink Backend"));
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/mtvs/devlinkbackend/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mtvs.devlinkbackend.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springdoc-openapi-ui/")
.setCachePeriod(3600);
}
}
Loading