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
135 changes: 51 additions & 84 deletions src/main/java/hello/cluebackend/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@
import hello.cluebackend.domain.user.service.CustomOAuth2UserService;
import hello.cluebackend.global.security.jwt.RefreshTokenService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;


@Configuration
@EnableWebSecurity
Expand All @@ -26,34 +22,64 @@ public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
private final RefreshTokenService refreshTokenService;

public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JWTUtil jwtUtil) {
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService,
CustomSuccessHandler customSuccessHandler,
JWTUtil jwtUtil,
RefreshTokenService refreshTokenService) {
this.customOAuth2UserService = customOAuth2UserService;
this.customSuccessHandler = customSuccessHandler;
this.jwtUtil = jwtUtil;
this.refreshTokenService = refreshTokenService;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, RefreshTokenService refreshTokenService) throws Exception {
@Order(0)
public SecurityFilterChain loginChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(a -> a
.requestMatchers("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
.permitAll()
.anyRequest().permitAll()
)
.oauth2Login(o -> o
.userInfoEndpoint(u -> u.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));

return http.build();
}
Comment on lines +38 to +56
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

login 체인에 API 경로 포함 — 인증 우회 위험.

loginChain.securityMatcherauthorizeHttpRequests/api/timetable/**가 포함되어 Order(0) 체인이 먼저 매치되면 JWT가 적용되는 apiChain까지 도달하지 못하고 무조건 permitAll이 됩니다. 의도치 않은 익명 접근/권한 우회가 발생할 수 있습니다.

다음과 같이 /api/timetable/**를 login 체인에서 제거하고, 공개가 필요하다면 api 체인에서 명시적으로 permitAll 하십시오.

-                .securityMatcher("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
+                .securityMatcher("/oauth2/**", "/login/oauth2/**", "/first-register", "/register")
...
-                        .requestMatchers("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
+                        .requestMatchers("/oauth2/**", "/login/oauth2/**", "/first-register", "/register")
                         .permitAll()

그리고 api 체인 쪽에 공개가 맞다면 추가:

-                                "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**"
+                                "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**",
+                                "/api/timetable/**"
📝 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
@Order(0)
public SecurityFilterChain loginChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(a -> a
.requestMatchers("/oauth2/**", "/login/oauth2/**", "/first-register", "/register", "/api/timetable/**")
.permitAll()
.anyRequest().permitAll()
)
.oauth2Login(o -> o
.userInfoEndpoint(u -> u.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
@Order(0)
public SecurityFilterChain loginChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/oauth2/**", "/login/oauth2/**", "/first-register", "/register")
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(a -> a
.requestMatchers("/oauth2/**", "/login/oauth2/**", "/first-register", "/register")
.permitAll()
.anyRequest().permitAll()
)
.oauth2Login(o -> o
.userInfoEndpoint(u -> u.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/SecurityConfig.java around
lines 38 to 56, the loginChain currently includes "/api/timetable/**" in its
securityMatcher and permitAll requestMatchers which causes the Order(0) chain to
match API requests and bypass the JWT-protected apiChain; remove
"/api/timetable/**" from both the securityMatcher and authorizeHttpRequests
permit list in loginChain so that API requests reach the apiChain, and if
"/api/timetable/**" is intended to be public, explicitly add a permitAll matcher
for that path inside the apiChain (or adjust its authorization rules) so JWT
protection is applied correctly to other API endpoints.


@Bean
@Order(1)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.logout(logout -> logout
.logoutUrl("/logout")
.addLogoutHandler((request, response, authentication) -> {
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies == null) return;
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh_token")) {
refreshToken = cookie.getValue();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("refresh_token".equals(cookie.getName())) {
refreshToken = cookie.getValue();
break;
}
}
}
if (refreshToken == null) {
throw new AuthenticationCredentialsNotFoundException("refresh_token 쿠키가 존재하지 않습니다.");
}
refreshTokenService.deleteByRefresh(refreshToken);
String accessToken = "";
response.setHeader("Authorization", "Bearer " + accessToken);

response.setHeader("Authorization", "Bearer ");

Cookie refreshTokenCookie = new Cookie("refresh_token", null);
refreshTokenCookie.setMaxAge(0);
Expand All @@ -63,77 +89,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, RefreshTokenSe
.deleteCookies("JSESSIONID", "refresh_token")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
})

);

http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
return configuration;
}
}));

// csrf disable
http
.csrf(auth->auth.disable());

// Form 로그인 방식 disable
http
.formLogin(auth->auth.disable());

// HTTP Basic 인증 방식 disable
http
.httpBasic(auth->auth.disable());

// JWTFilter 추가
http
// JWT 토큰 만료시 다시 로그인을 할 경우 JWT 토큰이 없어서 거절하게 되어 무한루프에 빠지게 됨
// .addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
.addFilterBefore(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);
// .addFilterAfter(new JWTFilter(jwtUtil),OAuth2LoginAuthenticationFilter.class);

// oauth2
http
// .oauth2Login(Customizer.withDefaults());
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.successHandler(customSuccessHandler));

// 경로별 인가 작업
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/refresh-token", "/register", "/first-register", "/api/timetable/**", "/test",
"/h2-console/**",
"/favicon.ico",
"/error",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**").permitAll()

.anyRequest().authenticated());


http
.securityMatcher("/first-register", "/register","api/timetable/**")
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.logoutSuccessHandler((request, response, authentication) -> response.setStatus(HttpServletResponse.SC_OK))
)
.authorizeHttpRequests(a -> a
.requestMatchers(
"/", "/refresh-token", "/h2-console/**",
"/favicon.ico", "/error",
"/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**"
).permitAll()
.anyRequest().authenticated()
)
.securityMatcher("/**")
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
.addFilterBefore(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
Comment on lines +102 to +103
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

JWT 필터 삽입 위치를 UsernamePasswordAuthenticationFilter 앞로 조정하세요.

apiChain에는 OAuth2LoginAuthenticationFilter가 존재하지 않을 수 있어 상대 위치 기준이 불안정합니다. 일반적으로 JWT는 UsernamePasswordAuthenticationFilter보다 앞에 두는 것이 안전합니다.

아래와 같이 수정하고 import를 추가하세요.

+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
...
-                .addFilterBefore(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class)
+                .addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/SecurityConfig.java around
lines 102-103, move the JWT filter insertion to be before
UsernamePasswordAuthenticationFilter instead of OAuth2LoginAuthenticationFilter
and add the necessary import for
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
update the DSL call to .addFilterBefore(new JWTFilter(jwtUtil),
UsernamePasswordAuthenticationFilter.class) so the JWTFilter runs prior to the
username/password authentication processing.


return http.build();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ spring:

logging:
level:
root: debug
root: debug
Loading