JWT์ ์ด์ฉํด์ ๋ก๊ทธ์ธ, ํ์๊ฐ์
์ ๋ค์ ๊ณต๋ถํ๋ฉด์ ์ํ์ฐธ์กฐ ๋ช๋ฒ ์ผ์ด๋ฌ๋ค
์ด๋ฒ ํฌ์คํ
๊ฒฝ์ฐ, ์ํ์ฐธ์กฐ์ ๋ํด ๊ธ์ ์์ฑํด ๋ณผ๊น ํ๋ค

์ข์ ์ง์ ์ ์ป์ผ๋ ค๋ฉด ๊ฒฝ๋ ฅ์ด ํ์ํ๋ค
ํ์ง๋ง ๊ฒฝ๋ ฅ์ ์๊ธฐ ์ํด์๋ ์ง์
์ด ํ์ํ๋ค
์ ๊ฐ์ ์์๋ฅผ ์๊ฐํ๋ฉด ์ข๋ ์ดํดํ๊ธฐ ์ฌ์ธ ๊ฒ์ด๋ค
์ด๋ฐ ์ํฉ์์๋ Spring์ด ๋น์ ์์ฑํ ์ ์์ผ๋ฉฐ, UnsatisfiedDependencyException ์์ธ๊ฐ ๋ฐ์ํจ
ํ์ ๊ฒฝ์ฐ ํ ํฐ์ ๊ฒ์ฆํ๋ JwtTokeFilter์ SecurityConfig ์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ์๋ค
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final CsrfTokenRepository csrfTokenRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Long userId = jwtUtil.getUserIdFromToken(token).get("userId", Long.class);
User user = userRepository.findById(userId).orElse(null);
CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
String csrfHeader = request.getHeader("X-CSRF-TOKEN");
if (csrfToken == null || csrfHeader == null || !csrfToken.getToken().equals(csrfHeader)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token invalid or missing");
return;
}
if (user != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid token: User not found");
return;
}
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid token");
return;
}
}
filterChain.doFilter(request, response);
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenFilter jwtTokenFilter;
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_OWNER \n ROLE_OWNER > ROLE_USER");
return roleHierarchy;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(csrfTokenRepository()) // CSRF ํ ํฐ ์ฟ ํค ์ ์ฅ
)
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.disable())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/*", "/api/auth/refresh", "/h2-console/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT ํํฐ ์ถ๊ฐ
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public CsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
repository.setCookieHttpOnly(false); // ์ฟ ํค ์ ๊ทผ ๊ฐ๋ฅ
repository.setHeaderName("X-CSRF-TOKEN");
repository.setCookieName("XSRF-TOKEN");
return repository;
}
}
โโโโโโโ
| jwtTokenFilter (field private org.springframework.security.web.csrf.CsrfTokenRepository com.example.login.JWT.JwtTokenFilter.csrfTokenRepository)
โ โ
| securityConfig defined in file
โโโโโโโ
private final CsrfTokenRepository csrfTokenRepository;
JwtTokenFilter์์ CsrfTokenRepository๋ฅผ ์ฃผ์
๋ฐ์
๋ฌธ์ ๋ CsrfTokenRepository๊ฐ SecurityConfig์์ ๋น์ผ๋ก ๋ฑ๋ก๋๋ค๋ ์
SecurityConfig๋ CsrfTokenRepository๋ฅผ ๋น์ผ๋ก ๋ฑ๋ก
์ฆ, SecurityConfig์์ JwtTokenFilter๋ฅผ ํํฐ๋ก ๋ฑ๋กํ ๋ ค๊ณ ํ ๋ JwtTokenFilter๋ CsrfTokenRepository๋ฅผ ์ฃผ์
๋ฐ์์ผ๋ง ์์ฑ์ด ๊ฐ๋ฅํจ
๋ฐ๋ฉด์, CsrfTokenRepository๋ SecurityConfig์์ ๋น์ผ๋ก ๋ฑ๋ก ๋์ด์๋ค
๊ทธ๋์ ์ํ๋ฆฌํฐ๊ฐ ํ ํฐํํฐ ๋ง๋ค๊ณ ์ถ์๋ฐ ํ ํฐํํฐ๋ ์ํ๋ฆฌํฐ์ ๋น์ผ๋ก ๋ฑ๋ก๋ CsrfTokenRepository ์ด๊ฒ ํ์ํด์
SecurityConfig โ JwtTokenFilter โ CsrfTokenRepository
์๋ก๊ฐ ์๋ก๋ฅผ ํ์ํ ์ํฉ์!
JwtTokenFilter์์ CsrfTokenRepository ๋์ ์์ฒญ ๊ฐ์ฒด(HttpServletRequest)์์ CSRF ํ ํฐ์ ๊ฐ์ ธ์ด.JwtTokenFilter ์์ :
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Long userId = jwtUtil.getUserIdFromToken(token).get("userId", Long.class);
User user = userRepository.findById(userId).orElse(null);
// ์์ฒญ์์ ์ง์ CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
String csrfHeader = request.getHeader("X-CSRF-TOKEN");
if (csrfToken == null || csrfHeader == null || !csrfToken.getToken().equals(csrfHeader)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token invalid or missing");
return;
}
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid token");
return;
}
}
filterChain.doFilter(request, response);
}
}
[ ํ๋ฆ ]
์ฐ์ ,CsrfTokenRepository๋ CSRF ํ ํฐ์ ์์ฑ, ์ ์ฅ, ๋ก๋ํ๋ ์ธํฐํ์ด์ค
CSRF ํ ํฐ์HttpServletRequest์ ์์ฑ(attribute)์ ์ ์ฅ
์ดํ ์์ฒญ์ด ๋ค์ด์ฌ ๋ JwtTokenFilter๋ httpServletRequest์์ CSRF ํ ํฐ์ ๊บผ๋ด ๊ฒ์ฆ
์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ์ ๋ ์ฝ๋ ๊ฒฝ์ฐ,
ํํฐ์์ ์ง์ CsrfTokenRepository ์ด์ฉํด์ ํํฐ์์ ์ฃผ์
๋ฐ์ ์์ฒญ์ด ๋ค์ด์ฌ ๋๋ง๋ค csrfTokenRepository.loadToken(request)๋ก CSRF ํ ํฐ์ ๊ฐ์ ธ์ ๊ฒ์ฆํ์๋ค
CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
ํ์ง๋ง ์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ์๊ณ ..
[ ํด๊ฒฐ๋ฐฉ๋ฒ ]
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
Spring Security๋CsrfFilter์์ CSRF ํ ํฐ์ ์์ฑํ๊ณ
HttpServletRequest์ ์์ฑ์ ์ ์ฅํจ
ํํฐ์์ ์ง์ CsrfTokenRepository๋ฅผ ํธ์ถํ์ง ์๊ณ ,
=>SecurityConfig์ JwtTokenFilter ๊ฐ์ ์์กด์ฑ์ด ์ฌ๋ผ์ง๋ฉด์ ์ํ ์ฐธ์กฐ๊ฐ ํด๊ฒฐ๋จ
๐บ : ํ์ ๊ฒฝ์ฐ ๊ณต๋ถ๋ฅผ ํ๋ฉด์ CsrfTokenRepository๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ํ์ฐธ์กฐ ๋ฐ์ํ๋ค๊น์ง๋ ์๊ฒ ๋๋ฐ ๊ทธ๋ผ ์ ๋ฐ์ํ์๊น๋ผ๋ ์๋ฌธ์ด ๋ค์๋ค
CsrfTokenRepository๊ฒฝ์ฐ๋ CSRFํ ํฐ์ ์์ฑํ๊ณ ์ ์ฅํ๋ค๊ณ ํ๋๋ฐ ๊ทธ๊ฑธ HttpServletRequest์ ์ ์ฅํ๋ค๊น์ง๋ ์๊ฒ ๋ค..! ๊ทผ๋ฐ ์์ง ์ค๊น? ๋ผ๋ ์๊ฐ์ด๋ค์ด ํ๋ฆ์ ๋ค์ ์ดํดํ ๋ ค๊ณ ํ๋ค
์ฆ, ์ผ๋จ ์ํฉ์ ๋ค์ ์ค๋ช ํด๋ณด๋ฉด
๊ณผ๊ฑฐ ๋ฐฉ์
- ํํฐ๊ฐ ์ง์ CsrfTokenRepository๋ฅผ ํธ์ถํด ํ ํฐ์ ๋ถ๋ฌ์ด
- ๋งค๋ฒ ์๋ก ์์ฑํ๊ฑฐ๋ ์ฟ ํค์์ ์กฐํ
- ์์ฒญ์ด ๋ค์ด์ฌ ๋๋ง๋ค CsrfTokenRepository๊ฐ ํ ํฐ์ ์์ฑํ๊ณ ๊ฒ์ฆ
์ ๋ฐฉ์- ์์ฒญ(HttpServletRequest)์์ ํ ํฐ์ ๋ฐ๋ก ๊บผ๋
- CsrfFilter๊ฐ ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ ๋ฃ์ด๋ ๊ฒ์ ๊บผ๋ด๊ธฐ๋ง ํจ
- ํ ๋ฒ ์์ฑ๋ ํ ํฐ์ ์์ฒญ ๊ฐ์ฒด(HttpServletRequest)์ ๋ณด๊ด
ํ์๋ ์ฌ๊ธฐ์ ํผ๋์ด ์๋๊ฒ ๊ฐ๋ค ์ด์งธ๋ ๋๋ค ํ ํฐ์ ๋ถ๋ฌ์์ ๊ฒ์ฆํ๋ ๊ฑด๋ฐ ์ ์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ๋๊ฑด์ง, ์ CsrfTokenRepository์์๋ง ์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ๋์ง ์๋ฌธ์ด ๊ณ์ ๋ค์๋ค
๊ฒฐ๋ก ์ ๋ค์๊ณผ ๊ฐ๋ค
HttpServletRequest ๊ฒฝ์ฐ CSRF ํ ํฐ์ ์ต์ด ์์ฒญ์์ ํ ๋ฒ๋ง ์์ฑ
์ด๊ฒ ํต์ฌ ํฌ์ธํธ์ธ๊ฒ ๊ฐ๋ค
๊ทธ๋ฌ๋ฉด, ์ด ๋ฐฉ์๋ ๊ฒฐ๊ตญ ํ ํฐ์ ์์ฑํด์ ๊ฒ์ฆํด์ผํ๋ ๊ฑฐ๋ฉด ์๋ก๊ฐ ํ์ํด์ ์ํ์ฐธ์กฐ ์ผ์ด๋ ์ ์์ง์์๊น? ๋ผ๋ ์๊ฐ์ด ๋ค์ ์๋ค
์ด๊ธฐ์ ์์ฑ์ ์ํ์ฐธ์กฐ๊ฐ ๋ฐ์ํ์ง ์์!
-> CsrfFilter๋ SecurityConfig์ ๋
๋ฆฝ์ ์ผ๋ก ๋์
-> ์ ํ๋ฆฌ์ผ์ด์
์ด๊ธฐํ ์(๊ตฌ๋) Spring Security๊ฐ ์๋์ผ๋ก ๋์ํด CSRF ํ ํฐ์ ์์ฑ
๊ฒฐ๊ตญ, ์คํ์คํฌ๋ CSRFํ ํฐ์ด ๋ฐ๊ธ๋์ด, ์ดํ ์์ฒญ์์๋ JwtTokenFilter๊ฐ HttpServletRequest์์ ์กฐํ๋ง ํ๋ฉด ๋์ด์ ์ํ์ฐธ์กฐ๋ฅผ ๋ฐฉ์งํ ์ ์๋ค!