본문 바로가기

Spring/Spring Security

[Spring Security] 세션 기반 인증 방식 CSRF 토큰과 XSS 적용하기

개인적으로 제작 중인 프로젝트에 적용시키면서 실전경험 쌓기 (2024.04.05)

https://github.com/GyuminGomin/people-driver 

 

GitHub - GyuminGomin/people-driver: busan IT Academy Final Project

busan IT Academy Final Project. Contribute to GyuminGomin/people-driver development by creating an account on GitHub.

github.com

동기 : Session 기반 인증 방식으로 로그인 시 모든 사용자 정보를 session에 담아 저장하는 프로젝트 구조이며 JSP 기반으로 서버 측 렌더링을 통한 기술을 사용한다. 이때, security를 적용해보며, API 서비스는 csrf 토큰을 사용하지 않았는데, API 서비스가 아닐 경우에서는 어떻게 적용해야하는지 궁금해 글을 작성하게 되었다.

1. 인증과 인가(Autentication, Authorization)

- 인증이란?

접근 주체(Principal)가 누구인지 확인하는 것, 통상 회원가입하고 로그인하는 것을 의미

더보기

In SpringSecurity

사용자가 자신의 신원을 증명하도록 요구하는 메커니즘을 제공

이를 통해 로그인 기능을 구현하고, 사용자 이름과 비밀번호 등의 자격 증명을 검증할 수 있음

- 인가란?

접근 주체(Principal)에 대한 권한을 확인, 허락하는 것

더보기

In SpringSecurity

인증된 사용자가 액세스(접근)할 수 있는 리소스와 기능을 정의

권한을 기반으로 사용자에게 필요한 데이터와 작업을 제한할 수 있음

2. Session 기반 인증 방식과 Token 기반 인증 방식

- Session 기반 인증 방식이란?

HTTP 프로토콜은 요청과 응답을 한 번 주고받으면 바로 연결을 끊어버리는 특성을 갖고 있어 다음 요청을 하기 위해 다시 연결 요청을 보낸다. 이를 HTTP의 비연결성(Connectionless)라고 한다.

 

따라서 HTTP 프로토콜은 이전에 보냈던 request, response를 기억하지 못하는 특성을 갖고 있는데 이것을 HTTP의 비상태성(Stateless)라고 한다.

 

이러한 특징 때문에 오버헤드가 줄고 빠르고 확실하게 데이터를 처리할 수 있는데 한 번 로그인 시 다시 요청을 보낼 필요가 없는 상태유지성(Stateful) 방식이 필요하다.

    - Session의 Simple Understanding!

이것을 위해 인증 방식에 Session 기반 인증 방식이 생겨났는데 사용자가 로그인을 하면, 인증 정보를 서버의 세션 저장소에 저장하고, 사용자에게는 저장된 세션 정보의 식별자인 Session ID를 발급한다. (Ex. Tomcat 서버는 JSessionID) 즉, 실제 인증 정보는 서버에 저장되어 있다.

이렇게 브라우저는 인증 절차를 마친 요청마다 HTTP Cookie 헤더에 Session ID를 함께 서버로 전송하면, Session ID에 해당하는 세션 정보가 세션 저장소에 존재한다면 해당 사용자를 인증된 사용자로 판단한다.

- Token 기반 인증 방식이란?

기존 Session 기반 인증 방식을 사용하다보면 서버의 용량이 많이 필요해지게 되며 수직확장, 수평확장을 해야하는 경우가 생긴다.

    - 수직확장

수직확장을 하게 되면, CPU를 더 좋은 것으로 교체하거나, HDD/SDD 용량을 더 큰 것으로 교체하는 방법으로 서버의 성능과 용량을 향상시킨다. 하지만 경제적인 비용이 커지게 된다.

    - 수평확장

서버를 한 번에 여러대를 운용하는 방법으로 여러 대의 서버를 통해 서버의 성능과 용량을 향상시킨다. 수직확장보다는 경제적인 부담이 덜하며 확장에 유연하다. (즉, 하나의 서버가 죽더라도 다른 서버가 가용성을 유지한다.)

따라서 보통 수평확장으로 서버의 성능을 향상시킨다.

    - Token 기반 인증을 사용하는 이유

Session의 경우, 여러 대의 서버를 사용하면 데이터의 불일치 문제가 발생할 수 있다. (서버마다 고유의 세션 저장소를 갖고 있는데, 각각의 세션 ID를 새롭게 발급하는 경우 생기는 문제) 따라서 모든 서버에서 유저의 세션 ID를 모두 공유해야 하는데 이러한 과정이 많이 번거롭다.

이러한 이유 때문에 토큰 인증을 사용한다.

    - Token 이란?

일반적으로 (JWT)토큰은 아래와 같이 구성되어 있다.

일반적으로 세 파트로 나누어 지며, 각 파트는 점으로 구분하여 순서대로 header, payload, signature로 구성된다.

  • Header는 토큰의 타입과 해시 암호화 알고리즘으로 구성
    • 토큰의 타입(ex. JWT)
    • 해시 암호화 알고리즘(ex. HMAC, SHA256, RSA)
  • Payload는 토큰에 담을 클레임(claim) 정보를 포함
    • Payload에 담는 정보의 한 '조각'을 클레임이라고 부르고, 이는 name/value 의 한 쌍으로 토큰에는 여러개의 클레임을 저장
  • Signature는 secret key를 포함하여 암호화

Sessoin과 달리 Token은 유저의 정보를 서버에 저장하지 않는다. (SessionID를 발급하지 않는다)

대신에 유저가 성공적으로 인증(로그인)을 하면, 서버는 클라이언트에게 토큰을 발급한다.(일반적으로 JWT)

그렇게

클라이언트가 서버로 부터 토큰을 받아 저장하고, HTTP Header에 토큰의 정보 (header.payload.signature)를 함께 실어 전송하면 서버를 Verification(검증)을 하고 유저를 인가 한다.

 

이로 인해 토큰 인증 방식은 각 서버 마다 유저에 대한 정보를 모두 기억하고 있을 필요가 없다.

이로 인해 서버의 확장성에 유연하지만, 클라이언트에 유저의 정보가 저장되므로 노출되기 매우 쉬워 토큰에 민감한 정보를 담아서는 절대 안된다. 또한 토큰의 사이즈는 세션 ID에 비해 크기 때문에 네트워크 트래픽을 많이 차지하게 되어 통신하면서 발생하는 오버헤드를 감안해야 한다.

3. csrf와 Xss 란? (Spring Security로 이해하기)

더보기

csrf와 xss를 하기 전 왜 인증과 인가, 세션과 토큰에 대해 알아보았냐면, 보통 웹 애플리케이션을 제작할 때, 수평확장을 통해 여러개의 서버를 운용하게 되면 보통 REST API 서비스로 운용하게 되는데, stateless한 토큰 기반의 인증을 사용하여 처리하면 csrf 토큰이 필요없습니다.

하지만 제 프로젝트는 하나의 서버로 운용되고 있어, stateful한 세션 기반의 인증방식을 적용했기 때문에 csrf 토큰이 필요해 기본 사항을 이해하기 위해 작성하여 이해를 돕고자 하였습니다.

◈ csrf란? (Cross-site request forgery) 크로스 사이트 요청 위조

csrf 공격은 웹 애플리케이션 취약점 중 하나로 인증된 사용자의 권한을 사용해 웹 애플리케이션의 요청을 위조하는 공격이다. 즉, 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위 (수정, 삭제, 등록 등)을 특정 웹 사이트에 요청하게 만드는 공격이다.

이것을 방지하기 위해서 인증 절차를 위해 csrf_token이라는 것을 발급하고 그것을 비교하여 서버는 현재 자신의 웹 사이트에서 접속하고 있는 경로가 맞는지 인증을 하게 되는 것이다.

- 적용

spring security에서는 인증 상태가 서버에 계속해서 유지되고 있는 상태일 때, CSRF 토큰을 쿠키에 저장하고, 요청할 때마다 해당 토큰을 함께 전송해 검증하고 보안을 강화하는 방식으로 적용하기 위해 아래와 같이 설정한다.

@Configuration
public class SecurityConfig {
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
			.and()
            // 다른 권한 설정들
    }
}

1. csrf() : Spring Security의 CSRF 기능 활성화

2. csrfTokenRepository() : CSRF 토큰의 저장 및 관리 방법 설정하는 메서드

3. CookieCsrfTokenRepository.withHttpOnlyFalse() : CSRF 토큰을 쿠키에 저장해 관리하고, 이 토큰을 HTTPOnly 속성을 false로 설정한다. -> 이 말은 JavaScript가 쿠키에 액세스할 수 있으므로, 클라이언트 측에서 CSRF 토큰을 JavaScript로 읽어올 수 있게 설정하는 것 이다.

 

이렇게 csrf를 활성화 하게 되면

CSRF 활성화

다음과 같이 쿠키에 XSRF-TOKEN이라는 이름의 값이 설정이 된다. (Spring Security가 자동 설정)

 

원리는 다음과 같다.

사용자가 브라우저로 가동되고 있는 웹 애플리케이션 서버로 접속하면, 첫 CSRF 토큰이 발급된다.
이 CSRF 토큰을 통해 모든 POST 요청을 할 때마다 비교를 하게 되는데,
각각의 브라우저 마다 다른 CSRF 토큰이 발급되기 때문에 다른 브라우저에서 post 요청을 통해 해킹을 시도하려고 하면, 다른 토큰이므로 인증이 실패되게 되어 서버는 접근 불가 가항력을 가할 수 있게 된다.
이 CSRF 토큰을 잘 활용하면 해킹을 시도하려는 대상의 IP 추적을 통해 범인을 추려낼 수 있고 해커를 잡을 수 도 있다.

간단하게 생각하면, 인터셉터가 가로채서 토큰을 비교하고 토큰이 다르면 '인증실패' 라는 명령을 내릴 수 있는 것이다.

- 작동 검증

작동 검증을 확인하기 위해서는 일단 회원가입과 로그인을 먼저 Security로 적용해 확인을 해보아야 한다.

추후에 [Interceptor 처리 -> Spring Security로 전환] 글을 작성할 예정인데, 우선 간단히 살펴보도록 하겠다.

※ Ajax, EL태그를 모르면 이해하기 어려울 수 도 있습니다. (각자의 프로젝트에 적용시키기 위해서는 아래 정리를 봐주세요.)

EL 태그를 사용해 ajax요청으로 ${_csrf.parameterName} : '${_csrf.token}'을 보내 회원가입을 시켰다.

let Tdata = { // 이미지 파일을 뺀 나머지 txt 데이터
        name : u_name.val(),
        email : u_id.val(),
        password : u_pass.val(),
        birth : u_birth.val(),
        phoneNo : u_phone_f.val(),
        pc5 : postcode5.val(),
        address : address.val(),
        detail : detail.val(),
        role : role.val(),
        gender : gender.val(),
        ${_csrf.parameterName} : '${_csrf.token}' // ---------------> 각자의 프로젝트에 맞게 프론트에 추가
        // (EL 태그를 통해 Session에 저장되어 있는 XSRF-TOKEN 값을 서버로 보내줍니다.)
};
// 나머지 데이터도 FormData에 추가
Object.keys(Tdata).forEach(key => {
    formData.append(key, Tdata[key]);
});
$.ajax ({
    type : "POST",
    url : "/user/register",
    data : formData,
    contentType : false,
    processData : false,
    dataType : "text",
    success : function(result) {
        if (result === "ID Duplicate Retry!") {
            alert("아이디 중복입니다.");
            u_id.val('');
            u_id.focus();
        } else {
            // 로그인 화면으로 이동
            alert("회원가입 성공");
            window.location.href= "/user/login";	
        }
    },
    error : function(res) {
        alert(res.responseText);
    }
});

 

이제 로그인도 같이 

$.ajax({
    type : "POST",
    url : "/user/login",
    data :{
        email : $("#email").val(),
        password :  $("#pass").val(),
        checked : $("#loginSession").is(":checked"),
        ${_csrf.parameterName} : '${_csrf.token}' // 회원가입과 똑같이 토큰을 담아서 서버로 보냄
    },
    dataType : "text",
    success : function(result) {
        alert(" 회원 로그인 성공");
        location.href="/";
    },
    error : function(res) {
        if (res.responseText === 'Not Exist') {
            alert("아이디 또는 비밀번호가 일치하지 않습니다.");
            $("#email").val('');
            $("#pass").val('');
            email.focus();
        } else if (res.responseText === 'csrf') {
            alert("잘못된 접근방식 입니다.");
        } else {
            alert("알 수 없는 이유로 로그인에 실패하였습니다. 관리자에게 문의해 주세요.");
        }
    }
});

_csrf.token을 담아서 다음과 같이 서버로 보내게 되면 정상적으로 작동하는 것을 확인할 수 있다.

 

정상적인 작동

- 추가적인 설명

기본적으로 Spring Security를 통해 CSRF를 활성화하게 된다면, 모든 POST 요청에 csrf 토큰을 담아서 서버로 전송하지 않으면 Spring Security에서 AccessDeniedException 예외를 발생시킨다. 따라서 이 예외를 다뤄 사용자 편의를 위한 알림을 구현하고 싶을 때는 

@Configuration
public class SecurityConfig {

	@Autowired
	private CsrfAccessDeniedHandler csrfAccessDeniedHandler;
    
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
			.and()
            .exceptionHandling()
				.accessDeniedHandler(csrfAccessDeniedHandler)
				.and()
            // 다른 권한 설정들
    }
}

 

@Configuration
public class CsrfAccessDeniedHandler implements AccessDeniedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		
		String errorMessage = "csrf";
		
		// 응답 데이터 설정
		response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 
	    response.setContentType("text/html;charset=utf-8");
	    response.getWriter().write(errorMessage);
	}
}

다음과 같이 AccessDeniedHandler를 구현한 csrfAccessDeniedHandler를 따로 만들어 로직을 처리해 주면 된다.

 

각자 프로젝트 마다 구현하는 로직이 다르기 때문에 알아야할 핵심 개념을 정리 한다면

- csrf 정리

기본적으로 
SecurityConfig 설정 파일에

http
  .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  .and()



만 설정하고, javascript 또는 react 에서 post 요청일 때마다 csrf 토큰을 담아서 보내주기만 하면 된다. (session 기반 방식 사용 시 -> 만약 REST API 서버를 운용중이라면 csrf 토큰 적용 x -> .csrf().disable()

하지만 추가적인 유저 편의성을 위해 알림을 주거나 또는 IP 역 추적, 로그 기록을 남기고 싶다면


http
  .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  .and()
  .exceptionHandling()
      .accessDeniedHandler(csrfAccessDeniedHandler)
      .and()


다음과 같이 예외 핸들러를 추가해서




@Configuration
public class CsrfAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 원하는 로직 구현
    }
}

원하는 로직을 AccessDeniedHandler를 구현하면 된다.

◈ xss란? (Cross-site Scripting) 크로스 사이트 스크립팅

xss 공격 또한 웹 애플리케이션 취약점 중 하나로 버튼이나 클릭 이벤트에 악의적인 script를 주입하여 악성코드를 의도치 않게 실행하게 하는 공격이다.

- XSS 방지

1. 사용자 입력의 적절한 검증 및 이스케이프 수행 (path에 의도적인 <script>location.href="악성주소"</script>가 문자 그대로 출력되게 만듬)

<c:url var="escaptPath" value="${path}" />
-> 아래와 같이 변경
<c:out value="${escapedPath}" />

2. Spring Security에 Content Security Policy(CSP) 적용 ( 스크립트가 현재 도메인에서만 로드될 수 있음을 명시 )

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers()
            .contentSecurityPolicy("script-src 'self'")
            // 나머지 로직
}

3. Sping Secuiry에 내장된 XSS 필터 활성화 ( 브라우저의 응답을 검사해 의심스러운 스크립트를 자동 차단 )

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers()
            .xssProtection()
            .block(true)
            // 나머지 로직
}

4. HTTP Only 플래그를 이용한 쿠키 설정

5. 최신 버전의 라이브러리 및 프레임워크 사용

6. 정기적인 보안 감사 및 취약점 점검

 

이러한 방식들을 적용하면 된다.

느낀점

csrf와 xss 자체 security를 통해 적용 시키는 것은 정말로 이지하다. 하지만 가장 중요한 것은 개념!

다음에는 Interceptor와 컨트롤러로 구현한 권한 처리, 로그인, 로그아웃 등등을 Security로 적용시킨 사례를 들고 오겠습니다. 읽어주셔서 감사합니다!

 

많은 피드백 언제나 환영합니다!

Reference

https://hudi.blog/cookie-and-session/

 

쿠키와 세션 (ft. HTTP의 비연결성과 비상태성)

HTTP의 비연결성과 비상태성 HTTP는 요청과 응답을 한번 주고받으면 바로 연결을 끊어버리는 특성을 가지고 있다. 그리고 다음 요청을 하기 위해 다시 연결을 맺어야한다. 이를 HTTP의 비연결성(Conn

hudi.blog

https://hudi.blog/session-based-auth-vs-token-based-auth/

 

세션 기반 인증과 토큰 기반 인증 (feat. 인증과 인가)

인증과 인가 세션기반 인가와 토큰기반 인가에 대해 알아보기 이전에 먼저, 인증과 인가가 무엇인지 부터 알아야할 필요가 있다. 인증과 인가를 같거나 비슷한 개념이라고 생각하는 사람들이

hudi.blog

https://hudi.blog/self-made-jwt/

 

직접 만들어보며 이해하는 JWT

본 포스팅에서는 JWT 의 정의, 구조, 원리 등을 알아보며 실습을 통해 ‘라이브러리 없이’ 직접 JWT 를 생성하고 해독해본다. 1. JWT 에 대하여 1-1. JWT 의 정의 Json Web Token 의 줄임말이다. RFC 7519 에

hudi.blog

https://walking-and-walking.com/entry/Spring-Security%EB%A5%BC-%ED%86%B5%ED%95%9C-XSSCross-Site-Scripting-%EB%B0%A9%EC%96%B4-%EA%B8%B0%EB%B2%95

 

Spring Security를 통한 XSS(Cross-Site Scripting) 방어 기법

Cross-Site Scripting(XSS)은 웹 애플리케이션에서 매우 흔하게 발생하는 보안 취약점 중 하나입니다. 이는 악의적인 스크립트가 웹 페이지에 삽입되어, 사용자의 브라우저에서 실행될 때 발생하는 공

walking-and-walking.com