이번 강의에서는
제대로 된 CSRF 솔루션을
웹 퍼블리케이션에 구현해 보도록 하겠습니다
마찬가지로 몇 가지 수정 작업을 해야 합니다
이 메소드 안에 SecurityFilterChain을 위한
bean을 생성하려고 합니다
이 메소드는
Spring Security 설정을 하기 전에
먼저 객체를 생성해줍니다
CsrfTokenRequestAttributeHandler라는 이름입니다
이 클래스를 보면
이 클래스의 목적은
CsrfTokenRequestHandler룰 구현하기 위함입니다
이 클래스는 CSRF 토큰가 요청 속성으로써
활성화될 수 있도록 도와주고 헤더로든 변수로든
토큰 값을 해결합니다
이 문장의 의미는
Spring Security 프레임워크가
CSRF 토큰 값을 생성하고
값이 처리되거나
UI 애플리케이션에게
헤더 또는 쿠키의 값을 전달하기 위해서는
이 AttributeHandler 클래스의 도움을 받아야 합니다
그게 이 AttributeHandler를 사용해
같은 이름의 객체를 만드는 이유입니다
이 메소드를 불러오려고 합니다
setCsrfRequestAttributeName입니다
이 메소드에게 AttributeName 을 _csrf 라고 전달하고
이 줄을 언급하지 않아도
기본적으로 handler가 같은 이름을 생성할 것이지만
그래도 가독성을 높이기 위해
이 줄에 코드를 적도록 하겠습니다
이 객체를 정의한 후
CSRF 설정 위치에 가봐야 합니다
지금 보시는 것처럼
csrf라는 메소드를 불러오고
어떠한 method arguments(매게변수)도 수용하지 않고 있습니다
이제 한 가지 더 CSRF 메소드를 불러와야 합니다
마찬가지로 Spring Security 내에 있습니다
먼저 이전 코드를 다 지워주겠습니다
여기 보시면 두 종류의 csrf 메소드가 있습니다
두번째 메소드는
CsrfConfigurer는 수용할 것이고
method input 변수로써의 역할을 가집니다
이 두번째 메소드를 선택합니다
이 메소드에게 모든 CSRF 설정들을 전달해야 합니다
여기에 두 줄의 람다(Lambda)코드를 붙혀 넣겠습니다
붙혀 넣었습니다
이것이 람다 코드입니다
이 CSRFConfigurer 객체를 사용해
우선 CsrfTokenRequestHandler를
불러와야 합니다
이미 requestHandler 객체를 생성했기 때문에
그 위에 같은 객체를 전달하려고 하는 겁니다
CsrfTokenRequestHandler를 언급했으면
이번엔 IgnoringRequestMatchers를 불러옵니다
저번 강의에서 설명드리지 않았습니까?
이 메소드룰 불러오고
공공 API URL들을 언급해야 합니다
그래야 그 URL들에게
CSRF 보호가 작동하지 않을 겁니다
그 다음에는
csrfTokenRepository 라는 메소드를 불러옵니다
이 메소드에게는
CookieCsrfTokenRepository.withHttpOnlyFalse 라는 코드를 전해줍니다
그럼 CookieCsrfTokenRepository 안에는
무엇이 있는 보겠습니다
보시다시피
이 클래스는 CSRF 토큰을
쿠키로 유지하는 역할이고
헤더에서 이 이름으로 찾습니다
AngularJS와 똑같은 규칙을 따릅니다
AngularJS 뿐만 아닙니다
대부분의 UI 프레임워크들 lReactJS 같은 것들도
동일한 쿠키와 헤더 이름을 따릅니다
보시다시피
이 이름이 쿠키에 사용될
이름입니다
그리고 이건 헤더에 사용될 이름이고
UI 애플리케이션으로
보내지게 됩니다
이건 변수 이름인데
전에 설명드렸습니다
이 클래스에서만
Spring Security가 CSRF 토큰의
마법을 보여줄 겁니다
그럼 그런 의문이 생길 것입니다
withHttpOnlyFalse는 왜 불러오는 것입니까?
쿠키들이 생성될 때
주로 백엔드 애플리케이션들이
UI 애플리케이션에서 쿠키 값을
읽어내는 옵션이 있고
아니면 UI 애플리케이션이나
그 안의 JavaScript 코드가 쿠키 값을
읽어내지 못하게 할 수 있습니다
하지만 withHttpOnlyFalse의 도움으로
Spring Security 프레임워크에게
"HttpOnlyFalse를 설정으로
쿠키를 생성해줘 그래야 Angular 앱 안에 있는 JavaScript 코드가
쿠키 값을 읽을 수 있으니까" 라고 말합니다
이 클래스를 확인해 봐도 좋습니다
보시다시피
AngularJS 같은 UI 앱을 사용하는 건
백엔드 서버와 소통하고
HttpOnlyFalse를
사용하기 위해서입니다
이런 것들을 수정하면
프레임워크가 작동할 수 있습니다
CSRF 토큰을 생성할 수 있고 이를
애플리케이션 메모리에 저장할 수 있습니다
그리고 헤더와 쿠키 이름 값들을
UI 애플리케이션로부터 읽어내는 것도 가능합니다
동일하게 검증될 것입니다
마지막으로 남은 단계는
백엔드 앱에서 우선
첫 로그인 후 UI 애플리케이션에게
헤더와 쿠키 값을 보내야 합니다
그렇지 않으면 UI 앱이
CSRF 토큰 값을 알 수가 없습니다
Spring Security 프레임워크가
UI 애플리케이션의 반응으로 생성된
토큰 값을 보내야 하는 이유입니다
CSRF 토큰을 보내려면
UI 애플리케이션으로 보내게 될 모든 응답들에
filter 클래스를 생성해야 합니다
마찬가지로 여기 새로운 패키지를 만들겠습니다
패키지 이름은 com.eazybytes.filter.
이 filter 패키지 안에
새로운 필터를 만들어 줄 것입니다
필터 이름은 CsrfCookieFilter
이 클래스는 우선
이 클래스를 필터로 만들기 위해
Spring Security가 제공한 필터 중 하나를 확장해야 하고
필터 이름은 OncePerRequestFilter 입니다
이 필터를 확장하고 나면
에러가 발생합니다
CsrfCookieFilter 안에 메소드를
덮어써야 한다고 합니다
Implement 메소드를 클릭하고
이 doFilterInternal라는 메소드를
덮어써야 합니다
여기 생성된 메소드는 비어있으며
안에 우리가 직접 로직을 입력해야 합니다
코드 몇 줄을 붙혀넣고
설명해보도록 하겠습니다
여기 클래스들도 다 불러와 보겠습니다
보시다시피
먼저 가능한 CsrfToken을 읽을 수 있게 하려고 합니다
HttpServletRequest 안에서 말입니다
언제든 백엔드에
토큰 값이 생성되면
요청 속성으로써 사용 가능해질 것이며
읽어내서 CSRF 토큰의
객체에게 변환을 해줍니다
CSRF 토큰 객체를 사용해서
우선 객체 안에 헤더 이름이 있는지
확인해 보고
null이 아닐 경우
프레임워크가 CSRF 토큰을 생성했다는 뜻일 수 있습니다
그 다음 이 블록에서
같은 HeaderName을 채우고
이는 응답이 헤더 안에 있는 토큰 값이며
같은 응답이 FilterChain 안에 있는 다음 필터에게
전달될 것입니다
지금 이게 UI 애플리케이션에게
응답을 보내는 것이지만
CSRF 토큰 값은 헤더 안에 있습니다
그럼 또
"쿠키는 어떡하지?" 라는 의문이 생깁니다
헤더만 보내고 쿠키는 보내지 않습니다
여기서 Spring Security의 미학이
눈에 띄게 됩니다
개발자로써 CSRF 토큰 값을 채우면
응답 헤더로써
Spring Security 프레임워크는
CSRF 쿠키를 보내고 동일한 것을
브라우저나 UI 애플리케이션에 응답 보내는 것에 신경써 줍니다
필터를 생성했지만 이제 필터와
Spring Security 프레임워크가 소통할 수 있어야 합니다
그래서 어떻게 하느냐
똑같습니다
ProjectSecurityConfig 클래스에 가서
CSRF 설정을 마친 후에
코드를 여기 정의해야 하는데
보시다시피
그냥 addFilter 메소드를 불러오고
addFilter 메소드에게
우리가 직접 생성한 객체를
전달하는 것입니다
전달해야 할 두번째 인자(argument)는
BasicAuthenticationFilter입니다
BasicAuthenticationFilter Spring Security의 필터로
HTTP Basic Authentication을 사용할 때
중요한 역할을 하게 됩니다
이 필터에 대해서는 잠시 후 더 설명하겠습니다
이 라인에서 우리는
Spring Security 프레임워크에게
"BasicAuthenticationFilter를 먼저 실행하고
CsrfCookieFilter를 실행해줘" 하는 것입니다
BasicAuthenticationFilter 후에만
로그인 동작이 완료될 수 있고
로그인 동작이 완료되고 나면
CSRF 토큰이 생성됩니다
똑같은 CSRF 토큰 값을
응답에 채워넣고 싶습니다
CsrfCookieFilter를 사용해서 말입니다
필터의 자세한 내용은
다가오는 섹션들에서 다룰 것입니다
Spring Security 필터들을 사용 가능한
지정된 섹션들에 대해 배우고
어떻게 우리만의 커스텀(custom) 필터를 만드는지 배우게 됩니다
어쨌든 이러한 수정을 거치고 나면 잘 작동되어야 합니다
이 컴파일 에러부터 해결해봅시다
이것과 메소드는 필요 없어 보입니다
모든 컴파일 에러들이 해결됐습니다
마지막 단계로
설정을 한 가지 더 해야 하는데
CSRF 솔루션과는 연관이 없습니다
일단 설정을 해보겠습니다
그 다음 우리가 왜 이 설정을 하는지
설명하겠습니다
제가 설정하려는 것은
이 cors 메소드 전에 할 것인데
이 cors 메소드는 다음 줄에 오게 합니다
여기에 두 줄의 코드를 붙혀 넣습니다
그럼 보시다시피
여기 제가 붙혀넣은 새로운 설정이 생겼습니다
이 설정의 목적은
전에 브라우저를 통해
API에 접속하려 할 때
백엔드 REST API들과 직접적으로
브라우저를 통해 접속하고 거기에
Spring Security 프레임워크로 만들어진 로그인에
우리의 자격증명을 입력하고
뒤에서는 JSESSIONID가 생성되었습니다
같은 JSESSIONID를 갖고
모든 후속 요청들을 인증 정보 입력 없이
접근하려 하는 것입니다
하지만 지금부터는
분리된 UI 애플리케이션을 사용할 것이기 때문에
로그인해서 REST API들에 접근하려면
Spring Security 프레임워크에게
“내가 만든
sessionManagement를 따라서
JSESSIONID를 만들어줘" 라고 합니다
이 설정은
Spring Security 프레임워크에게
"첫 로그인이 완료되면
항상 JSESSIONID를 생성해줘" 라고 하는 것입니다
동일한 JSESSIONID가
UI 애플리케이션으로 보내지고
UI 애플리케이션은 첫 로그인 후에
만들어지는 후속 요청들을
활용할 수 있게 됩니다
이 두 줄 없이는
매번 보안된 API를 접근할 때마다
Angular 앱에서
자격증명을 입력해야 합니다
그런 이유로
이 두 줄을 잊지 마시기 바랍니다
이 requireExplicitSave를 사용해서
Spring Security 프레임워크에게
"내가 SecurityContextHolder 안에 있는
인증 정보들을 저장하는
역할을 맡지 않을 거야
프레임워크들이 대신 수행하게 해줘" 라고 합니다
기본적으로 이 값은 true입니다
이 기본설정들을 덮어씌움으로써
JSESSIONID를 생성하는 역할을 주는 것입니다
그리고 프레이워크의 SecurityContextHolder 안에
인증 정보를 저장합니다
이렇게 하는 이유는 우리가
분리된 UI 애플리케이션과 백엔드가
소통하는 점을 활용하기 때문입니다
이 클래스 안에서 이루어진 모든 수정 내용을
잘 이해하셨기 바랍니다
그럼 이제 다 끝난 것일까요?
물론 아닙니다
왜냐하면 이러한 변동 사항으로는
오직 백엔드 앱만 CSRF 토큰 값을
UI 애플리케이션에 보낼 수 있게 활성화 됐고
UI 애플리케이션쪽도 CSRF 토큰 값을 받고
나중에 있을 후속 요청에
동일한 CSRF 토큰 값을
보낼 수 있어야 합니다
그럼 이 내용을
UI 애플리케이션 안에 적용해봅시다
마찬가지로 Angular UI 애플리케이션에 가줍니다
열고 싶은 클래스는
login.component.ts입니다
이곳은 로그인 동작이 불러와지는
위치입니다
로그인 동작이 불러와지고 완료되면
모든 응답을을 받게 됩니다
예를 들어 크키라든지 responseData 안에 있는 바디 같은 것들
이 클래스 안에서는
그 쿠키를 읽고
sessionStorage이나 어떠한 형식으로든 저장해야 합니다
그래야 추후의 클라이언트가 생성할 모든 요청에서도
같은 토큰을 몇 번이고 보내줄 수 있기 때문입니다
물론 백엔드로 말입니다
쿠키를 읽기 위해서는
typescript에서 함수를 호출해야 합니다
불러오고 싶은 함수는
getCookie 는
typescript-cookie 라이브러리 안에 있습니다
이를 성공적으로 불러오기 위해서는
package.json 안에
typescript와 관련된 dependencies들이 잘 다운받아져 있어야 합니다
package.json을 열고
밑으로 스크롤 해보면
typescript 라이브러리와 관련된 dependencies들이 있습니다
제가 제공한 코드에
이미 언급되어 있습니다
참고하시라고 다시 강조하는 겁니다
이건 Angular가 아닙니다
이게 getCookie라는 메소드에서
따로 가져오려는 라이브러리입니다
getCookie 메소드를 사용해서
로직을 좀 적어보도록 하겠습니다
두 줄의 코드를 복사하시고
보시다시피
두 줄로 무엇을 하려는 것이냐면
저는 XSRF-TOKEN 이름의 쿠키를 읽으려는 것입니다
왜냐하면 Spring Security가 그 이름의 쿠키를
보낼 것이기 때문입니다
동시에 이 쿠키가 UI 애플리케이션 속 헤더에도
같은 이름으로 있기를
예상합니다
먼저 같은 이름의 쿠키를 읽어야 합니다
예를 들어 이게 쿠키 이름입니다
그래서 getCookie 함수에 쿠키 이름을
전달한 것입니다
그게 변수 안에 담기면
그걸 sessionStorage에 저장하고
속성 이름은 XSRF-TOKEN로 둡니다
이렇게 되면
저는 애플리케이션 내 어디든 접속할 수 있습니다.
이제 UI 코드의 절반을 수행했습니다
다음 다루어 볼 파트는
웹 애플리케이션에서
후속 요청을 생성하고
백엔드로 보낼 때마다
꼭 이 CSRF 토큰을 보내야 합니다
이러한 내용은
interceptor 클래스에서 다뤄야 합니다
interceptor 클래스에서는
UI 애플리케이션으로 가는 모든 요청을 가로채는 것입니다
이 interceptor 코드 안에도 몇 가지 코드를 넣어보겠습니다
보시면 이게 방금 제가
붙혀넣은 코드들입니다
이게 무엇인지 설명드리겠습니다
먼저 XSRF-TOKEN 변수를 자세하게 읽어보겠습니다
sessionStorage에서 입니다
xsrf가 null이 아니라면 무엇을 해야 하냐면
새로운 헤더를 만들어서 httpHeader를 보내고
Angular 코드에 있는 httpHeaders에
추가를 하는 것입니다
헤더 이름은
백엔드에서 쓴 헤더 이름과
꼭 동일한 것을 써야하고 예를 들면 X-XSRF-TOKEN과 같은 겁니다
이 헤더에게 CSRF 토큰 값을 전달합니다
그리고 Angular 클라이언트가 XSRF 토큰을 보낼 겁니다
세션을 볼 때마다 말입니다
하지만 공공 페이지는
로그인을 하지않는 상황에서만 쓰입니다
당연히 XSRF 는 null일 것이고
헤더는 백엔드로 보내지지 않습니다
그리고 이 상황에서는 전혀 문제 없습니다
이 상황에서는 공공 API들이
어떠한 CSRF 토큰도 요구하지 않기 때문입니다
이제 바뀐 내용들을 검증해봅시다
우선 애플리케이션에 로그인해보겠습니다
마찬가지로 이 로그인 버튼을 누릅니다
여기서
우리가 생성한 가장 첫 customer로 로그인해보겠습니다
이 customer_id에 한해서
모든 데이터들이 다른 테이블들에 삽입되었기 때문입니다
이 customer의 이메일은
happy@example.com이고
이 이메일과 비밀번호 12345를 사용해보도록 하겠습니다
로그인 버튼을 누릅니다
웹 애플리케이션에 성공적으로
로그인한 게 보입니다
조금 혼란이 있을 수 있습니다
이 로그인 흐름에 대해서 말입니다
이 로그인 흐름이 어떻게 일어나고 있는지
설명해보겠습니다
이 로그인 흐름 안에서는
자격증명을 입력하려고 할 때
당연히 login.component의 함수가 불러와집니다
바로 validateUser 입니다
이 LoginService에 무슨 일이 일어나고 있는지 확인해보면
여기서 REST API를 불러오려고 합니다
/user 입니다
하지만 ProjectSecurityConfig에서
재수집을 하면
/user가 인증되었다고 설정됩니다
UI 애플리케이션은 보호된 경로로 접근하려고 하기 때문에
Spring Security가 이 자격증명을
받음으로 사용자를 인증하고
우리가 제공한 정보들로
인증 과정을 수행합니다
그 다음엔 사용자를
로그인 컨트롤러 안에 있는
/user로 이동시킵니다
여기서 하는 것은
그저 로그인된 유저의
이름을 알아내고
이 상황에서는 이메일을 알아내는 것입니다 authentication 객체에서 말입니다
authentication 객체는
Spring Security를 통해 자동으로 제공됩니다
기억 날지 모르겠지만
EazyBankUsernamePwdAuthenticationProvider 안에
성공적인 인증 후에
username을 넘겨줍니다
username은 사용자가 입력한 것이고
이 상황에서는 이메일입니다
로그인 컨트롤러 안에 있는 authentication 객체를
username에 접속하려는 것입니다
이메일 정보를 받으면
레포지토리에서 findByEmail를 실행하여
모든 customer 정보를 식별하려는 것입니다
UI 애플리케이션으로 전송하는
동일한 정보입니다
그래서 제 이름 Happy가 보이고
제게 주어진 역할인 admin으로 로그인된 것이 보입니다
이 기본적인 customer 정보들은
백엔드에서 보내진
customer 모델 객체에서 오는 것입니다
여기서 Customer 객체를 보냅니다
같은 정보가 login.component 안에 캐시로 저장됩니다
responseData.body를 보면
모델을 지정하고
이 모델의 type이 User입니다
이 User 안에 같은 종류 열들이 있습니다
name, mobile number, email and password 같은 것들입니다
여기서 또 질문이 생깁니다
Spring Security는 어떻게 엔드 유저가 보낸
자격증명을 읽을 수 있는 것인가
이전에 Spring Security의
로그인 페이지와
username, password와 같은 속성을
UsernamePasswordAuthenticationFilter의 도움으로 읽어냈었습니다
이 상황에서는
엔드 유저의 자격증명을
interceptor 클래스에서 보내는 것이며
Autorization 헤더와 함께 요청을 보냅니다
이 Authorization 헤더 안에는
자격증명들을 넘겨줍니다
간단한 형식의 authentication으로 말입니다
HTTP Basic Authentication 사용할 때면
Spring Security 안에 필터가 하나 더 있는데
굉장히 중요한 역할입니다
BasicAuthenticationFilter 입니다
이 필터 안에
코드를 확인해 볼 수 있습니다
예를 들어 요청을 변환해주는 컨버터
UsernamePasswordAuthenticationToken가 있습니다
여기에 중단점을 주고 다시 로그인을 시도해겠습니다
일단 로그아웃을 하고
다시 로그인해보겠습니다
같은 자격증명 12345입니다
로그인을 누르자
BasicAuthenticationFilter 안에 컨버터가 생겼습니다
HTTP 요청을 변환하고
UsernamePasswordAuthenticationToken 객체 보내줍니다
컨버터 안에 뭐가 있는 확인해보자면
우선 헤더 이름이
AUTHORIZATION인 헤더가 있는지 찾아봅니다
AUTHORIZATION라는 이름의 헤더가 있다면
헤더 값은 "Basic"이고
usernmae과password의 인코딩된 값입니다
이 메소드 안에서 username과 password를
추출해보도록 하겠습니다
이는 username 과 password를 전부 디코딩 할 것입니다
그 다음에 로직과 구분문자 (delimiter)를사용해서
usernamer과 password를 추출합니다
우리가 쓰려는 구분 문자는 콜론 (;)입니다
Angular 앱 안에도
username 과 password 사이에
이 구분 문자를 사용합니다
그러므로 인증 정보를 읽고 변환해서
Spring Security 프레임워크의
authentication 객체 넘겨주는 겁니다
이걸로 우리가 CSRF 토큰을 사용해
구현해본 CSRF 솔루션이
확실히 이해됐으면 좋겠습니다
지금까지 우리의 모든 보호된 API들이 GET API들이 있었습니다
POST API나 PUT API는 하나도 없습니다
다음 강의에서는
CSRF 공격을 주제로
잘 작동하는지 확인해보고
현재 갖고 있는 보호된 API 중에 하나를 변환해서
@PostMapping로 잘 작동되는지 보고
우리가 구현해본 솔루션을
완전히 이해할 수 있도록 하겠습니다
감사합니다 다음 강의 때 뵙겠습니다