기본적인 JWT 토큰 발행방법
이전 포스팅에서는 JWT HMAC 암호화 방식에 대해 공부를 하였습니다.
지금은 조금 더 어려운 개념은 Spring Security를 이용한 JWT Cors 필터체이닝 방식에 대해서 공부해보고자 합니다.
CORS 필터체이닝 방식을 적용하기에 앞서 CORS란 무엇인지에 대한 이해가 중요하다.
CORS ( Cross-Origin Resource Sharing )
웹 브라우저에서 다른 도메인의 API를 사용할 때, 보안을 위해 설정된 보안 정책
Cross-Origin의 Resource를 공유하는 정책이라고 할 수 있다.
즉, CORS란 도메인이 다른 서버끼리 리소스를 주고 받을 때 보안을 위해 설정된 정책이라고 생각하면 된다.
예를 들어, 웹 사이트 A가 API 서버 B에서 데이터를 가져오려 할 때, API 서버 B에서 CORS 허용 설정이 되어 있지 않으면 웹 브라우저에서 API 접근이 거부될 수 있다.
또한 프론트엔드와 백엔드가 협업하면서 각자 따로 서버를 띄우게 되었을 경우도 마찬가지다. 서로 다른 React 서버(3000 포트)와 Springboot(8080 포트) 서버가 리소스를 주고 받으려 한다면 포트가 달라 서로 다른 출처로 판단되어 CORS 위반 에러가 발생한다.
Origin
서버의 위치를 의미하는 https://google.com 과 같은 URL들은 마치 하나의 문자열 같아 보여도, 사실은 여러 개의 구성 요소로 이루어져있다.
이때 출처는 Protocol과 Host 그리고 위 그림에는 나와있지 않지만 80, :443과 같은 포트 번호까지 모두 합친 것을 의미한다. 즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.
또한 출처 내의 포트 번호는 생략이 가능한데, 이는 각 웹에서 사용하는 HTTP, HTTPS 프로토콜의 기본 포트 번호가 정해져있기 때문이다. (port 80)
그러나 만약 https://google.com:443과 같이 출처에 포트 번호가 명시적으로 포함되어 있다면 이 포트 번호까지 모두 일치해야 같은 출처라고 인정된다.하지만 이 케이스에 대한 명확한 정의가 표준으로 정해진 것은 아니기 때문에, 더 정확히 이야기하자면 어떤 경우에는 같은 출처, 또 어떤 경우에는 다른 출처로 판단될 수도 있다.
출처 : https://velog.io/@effirin/CORS%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
Cross-Orgin의 다른 출처 판단 기준
두 개의 출처가 서로 같다고 판단하는 로직은, 두 URL의 구성 요소 중 Scheme(프로토콜), Host(도메인), Port, 이 3가지만 동일하면 된다.
https://evan-moon.github.io:80라는 출처를 예로 들면 https:// 이라는 스킴에 evan-moon.github.io 호스트를 가지고
:80번 포트를 사용하고 있다는 것만 같다면 나머지는 전부 다르더라도 같은 출처로 인정이 된다는 것이다.
출처 : https://evan-moon.github.io/2020/05/21/about-cors/#access-control-allow-origin-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0
따라서 일반적으로는 same-origin이란 scheme(프로토콜), host(도메인), 포트가 같다는 말이며, 이 3가지 중 하나라도 다르면 cross-origin이다.
그러나 각 브라우저들의 독자적인 출처 비교 로직을 따라가는 경우도 있다.
예) https://evan-moon.github.io 와 https://evan-moon.github.io:8000 -> 브라우저의 구현에 따라 다름
SpringSecurity 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
//시큐리티 타임리프에서 사용
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
//시큐리티 테스트
testImplementation 'org.springframework.security:spring-security-test'
build.gradle에 시큐리티 라이브러리를 추가한다.
추가를 하게되면 이전에 스프링 시큐리티를 사용할 때 확인했던 것 처럼 아래와 같이 고유의 비밀번호를 발급받게 된다.
1. 불필요 메서드 비활성화 작업
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.csrf().disable();
http.formLogin().disable();
http.httpBasic().disable(); // http의 기본 인증 형태도 폐기 Authorization(아이디,비밀번호 형태 폐기)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션방식 비활성화
스프링시큐리티에서 웹 애플리 케이션에 주로 영향을 주는 방식은 ServeltRequest 필터를 사용하는 방식이며, 필터들이 애플리케이션에 대한 모든 요청을 감싸서 처리합니다. 스프링 시큐리티에서 여러개의 필터들은 아래 그림과 같이 체인 형태를 이루면서 동작합니다.
- Spring Security 는 기본적으로 순서가 있는 Security Filter 들을 제공합니다.
- Spring Security가 제공하는 Filter를 구현하면 addFilter()
- Spring Security가 아닌 필터를 사용하려면 addFilterBefore() or addFilterAfter()를 사용합니다.
필터명 | 역할 |
LogoutFilter | 유저의 로그아웃을 진행합니다. 설정된 로그아웃 URL로 오는 요청을 감시하여 , 해당 유저를 로그아웃 처리합니다. |
UsernamePasswordAuthenticationFilter | 설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증을 처리합니다. 인증 실패시, AuthenticationFailureHandler를 실행합니다. |
DefaultLoginPageGenerationgFilter | 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지를 처리합니다. |
BasicAuthenticationFilter | HTTP요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다 |
RememberMeAuthenticationFilter | SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체의 요청이 있을 경우, Remember-Me인증 토큰으로 컨텍스트에 주입합니다. |
AnonymousAuthenticationFilter | SecurityContextHolder에 인증(Authentication)객체가 있는지 확인하고, 필요한 경우 Authentication 객체를 주입합니다. |
SessionManagementFilter | 요청이 시작된 이후 인증된 사용자인지 확인하고, 인증된 사용자일 경우, SessionAuthenticaitonStrategy를 호출하여 세션 고정 보호 메커니즘을 활성화하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다. |
ExceptionTranslationFilter | 필터 체인 내에서 발생(Throw)하는 모든 예외(AccessDeniedException, AuthenticationException)를 처리합니다. |
FilterSecurityInterceptor | HTTP리소스의 보안처리를 수행합니다. |
SecurityFilterChain은 인증을 처리하는 여러 개의 시큐리티 필터를 담는 필터 체인입니다. 또, 필터체인 프록시를 통해 서블릿 필터와 연결되고 어떤 시큐리티 필터를 통해 인증을 수행할지 결정하는 역할을 합니다.
- csrf() 비활성화
rest api를 이용한 서버라면, session 기반 인증과는 다르게 stateless하기 때문에 서버에 인증정보를 보관하지 않는다. rest api에서 client는 권한이 필요한 요청을 하기 위해서는 요청에 필요한 인증 정보를(OAuth2, jwt토큰 등)을 포함시켜야 한다. 따라서 서버에 인증정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다. - formLogin() 비활성화
formLogin 대신 Jwt를 사용하기 때문에 disable로 설정 - basic() 비활성화
httpBasic 방식 대신 Jwt를 사용하기 때문에 disable로 설정 - sessionCreationPolicy() 비활성화
Jwt를 사용하기 때문에 session을 stateless로 설정한다. stateless로 설정 시 Spring Security는 세션을 사용하지 않는다.
2. 크로스 오리진(Cross-Origin) 필터 등록
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//configuration.setAllowedOrigins(Arrays.asList("http://localhost:8181")); //부메랑에서 요청이 deny됨
configuration.setAllowedOrigins(Arrays.asList("*")); //모든요청 주소를 허용 == CrossOrigin
configuration.setAllowedMethods(Arrays.asList("*")); // * : 모든요청 메서드를 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
cors관련 설정을 포함한 필터.
기본적으로 서버 또는 지정된 특정 도메인의 요청만 허용하지만 프런트가 정해져있지 않기 때문에 모든 도메인을 허용하는 방식으로 설정.
● setAllowCredentials : 내 서버가 응답을 할 때 json을 자바스크립트에서 처리할수 있게 할지를 설정
● addAllowedOriginPattern : 허용할 도메인 목록
● addAllowedHeader : 허용할 헤더 목록
● addAllowedMethod : 허용할 메서드(GET, PUT, 등) 목록
● source.registerCorsConfiguration : 지정한 url에 config 적용
3. 크로스 오리진 활성화 및 시큐리티에 필터 추가
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.csrf().disable();
http.formLogin().disable();
http.httpBasic().disable(); // http의 기본 인증 형태도 폐기 Authorization(아이디,비밀번호 형태 폐기)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션방식 비활성화
// 2. 크로스 오리진 활성화
http.cors( Customizer.withDefaults() );
// 3. 시큐리티에 필터 추가
http.addFilter(new FilterOne()); //addFilter에는 시큐리티 타입의 필터만 등록이 된다.
return http.build();
}
http.addFilter
addFilter에는 Filter 객체가 포함되어야 하기 때문에, filter 객체를 사용할 파일을 하나 만들어준다.
FilterOne.Java
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterOne implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("필터1 실행");
chain.doFilter(request, response);
}
}
위와 같이 FilterOne을 만들고, http.addFilter에 Filter타입의 FilterOne 객체를 추가하게 되면 아래와 같은 오류를 만나게 되는데 그 이유는 바로 시큐리티 과정의 순서문제로 인해 만나게 되는 문제이다.
시큐리티의 필터과정은 아래와 같다.
우리가 Spring Security에서 Filter를 처리하는 과정은 Authentication Filter에서 AuthenticationManager로 넘어가면서
동작이 이루어지는데, AuthenticationFilter는 한 개의 필터가 아닌 아래와 같이 엄청나게 많은 필터로 이루어져있다.
그렇기 때문에 UsernamePasswordAuthenticationFilter 보다 먼저 실행시켜줘야 하기 때문에
기존에 http.addFilter(new FilterOne()); 이 부분을 아래와 같이 수정해준다.
http.addFilterBefore(new FilterOne(), UsernamePasswordAuthenticationFilter.class);
Controller 작성
// 필터 동작 확인
@GetMapping("/api/v1/filter")
public String hello() {
return "<h3>hello</h3>";
}
위와 같이 컨트롤러를 간단하게 만들고, Boomerang을 이용하여 실행시켜준다.
'Programming > Spring Boot' 카테고리의 다른 글
[Spring boot] Spring Security static 폴더(CSS,JS) 적용 문제 해결하기 (0) | 2024.06.15 |
---|---|
[Spring Boot] Spring Security를 이용한 JWT CORS Filter 로그인 커스터마이징하기 (0) | 2024.05.30 |
[Spring Boot] JWT HMAC 암호화 (0) | 2024.05.30 |
[Spring Boot] JWT (1) | 2024.05.30 |
[Spring Boot] 어노테이션을 활용해 권한 설정하기 (0) | 2024.05.29 |