ShipJH
1k
2018-07-31 15:17:13
10
3488

API 통신에 JWT를 추가하려는 과정에서 궁금증이 생겼습니다.


현재 서버는 springboot에 api를 만들었고, 클라이언트는 vue.js 로 구성하였습니다. ( 서비스가 아닌 개인적인 공부를 위해 구성하였습니다. ) 정상적으로 통신까지 확인하였고, 문제는 보안적으로 통신마다 토큰을 넣어야 한다는 결론에 이르게 되었습니다.

그리하여.... 저의 계획은 아래와 같습니다 . 시작 1. [클라이언트]사용자 로그인정보를 서버에 요청


2. [서버] 요청받은 DB의 유저정보와 대조 후 이상이 없다면, DB에 시크릿키(사용자마다다르게)를 저장(INSERT or UPDATE) 하고, 간략한 유저정보(페이로드에 쓰일부분)과 사용자 고유의 시크릿키를 응답


3. [클라이언트]클라이언트는 받은 시크릿키와, 유저정보를 (쿠키 나 세션스토리지에) 저장


4. [클라이언트]필요한 API를 매 호출 시, 시크릿키와 받은 유저정보를 가지고 헤더(암호화방식,JWT) ,페이로드(유저정보{서버에서받은것} , 파기시간{클라이언트에서 설정}) 한 값을 시크릿키를 넣어 JWT 생성 후 API 호출


5. [서버]요청받은 HTTP헤더의 JWT를 가지고 검증 이러하도록 만들 예정인데요.. 이때 제일 마지막 JWT를 가지고 검증하는 것중 어떤것으로 검증을 해줘야할지 고민이네요.

JWT를 디코딩해서 다시 DB체크를 해야할지,, 아니면, 그냥 그대로 받아들여서 정보만 갖고 써야할지..
그리고 , 제가 하려고 하는것이 의미가 있는것인지( 예를들면, 보안성이 향상되지 않는 무의미한 짓인지..) 궁금합니다. 또한 저렇게 하는게 문제가 있을지도 궁금해서 글을 남깁니다. 끝까지 읽어주셔서 감사합니다. ( _ _ )꾸벅.

2
  • 답변 10

  • 아야로
    1k
    2018-07-31 15:43:09

    전제 조건을 좀 확인하고 싶네요.

    CSRF 예방 기능을 구현하고자 하시나요? 혹은 JWT를 통한 URL 접근 권한 관리?

  • ShipJH
    1k
    2018-07-31 16:42:32

    아야로 

    일단 저 방식으로 해도 되는지가 가장 궁금하고.. 

    접근관리는 스프링 시큐리티랑 같이 쓸 생각입니다. 

    필터로 먼저 검증할 생각인데... 아직 구체화하진 않았습니다.


    전제조건이라고 하는건 아직 구체적으로 생각해보진 않았습니다 ㅠㅠ

    가능한 간단하면서도 그나마 보안적인?? 

    형태로 구현하고 싶습니다... 


    답변주셔서 감사합니다.

  • 아야로
    1k
    2018-07-31 17:32:03 작성 2018-07-31 17:35:55 수정됨

    보안적으로 통신마다 토큰을 넣는 대표적인 두 경우가 CSRF 예방을 목표로 할때와 URL 접근 권한을 JWT토큰으로 판단할때 이며, 목적에 따라 질문주신 방식을 써도 되는지 안되는지가 갈리기 때문에 여쭈었습니다.


    스프링 시큐리티를 쓴다 하셨으니 두가지 상황으로 가정하겠습니다.

    1. 유저의 식별 목적으로 JWT를 사용할 경우

    - 이는 세션 관리 없이 토큰만으로 사용자를 식별하겠다는 의미가 됩니다.(jsessionid를 안쓰겠다는 의미)

    - 스크립트를 통한 토큰 탈취가 가능하기에 JWT토큰을 로컬 리포지토리에 담아선 안됩니다.

    - 쿠키에 담는 경우 서버에서 HTTP-ONLY 옵션을 부여한 쿠키를 줘야 합니다.

    - HTTP-ONLY 쿠키는 자바스크립트로 접근이 불가합니다. 따라서 XSS 예방효과가 있습니다.

    - 쿠키는 Form요청이든 Ajax요청이든 자동으로 오가기에 클라이언트에서의 별도 처리는 불필요합니다.

    - 모든 요청에 대해 쿠키속에서 JWT 토큰을 찾아내 유저를 식별하도록 필터를 추가해야 합니다.

    - 스프링 시큐리티에선 SecurityContextRepository에서 세션쿠키를 찾는 동작을 JWT토큰을 찾아 검증하는 동작으로 교체하시면 됩니다.

    - 이때 JWT 토큰의 위조 여부는 Jwts라이브러리의 parser() 메소드를 통해 간단히 확인할 수 있습니다.

    try{
    	claims = Jwts.parser()
    	.setSigningKey(secret.getBytes("UTF-8"))
    	.parseClaimsJws(token).getBody();
    	// OK, we can trust this JWT
    	
    }catch(ExpiredJwtException eje){
    //JWT를 생성할 때 지정한 유효기간 초과할 때.
    	LOGGER.debug("JWT 유효기간 초과");
    	throw new RuntimeException("JWT 유효기간 초과");
    }catch(UnsupportedJwtException uje){
    //예상하는 형식과 일치하지 않는 특정 형식이나 구성의 JWT일 때
    	LOGGER.debug("JWT 형식 불일치");
    	throw new RuntimeException("JWT 형식 불일치");
    }catch(MalformedJwtException mje){
    //JWT가 올바르게 구성되지 않았을 때
    	LOGGER.debug("잘못된 JWT 구성");
    	throw new RuntimeException("잘못된 JWT 구성");
    }catch(SignatureException se){
    //JWT의 기존 서명을 확인하지 못했을 때
    	LOGGER.debug("JWT 서명 확인 불가");
    	throw new RuntimeException("JWT 서명 확인 불가");
    }catch(IllegalArgumentException iae){
    	LOGGER.debug("JWT IllegalArgumentException");
    	throw new RuntimeException("JWT IllegalArgumentException");
    }catch (Exception e) {
    	LOGGER.debug("JWT 검증 중, 알 수 없는 오류 발생 {}", e);
    	throw new RuntimeException("JWT 검증 중, 알 수 없는 오류 발생 {}", e);
    }


    2. CSRF 예방 목적으로 JWT를 사용할 경우

    - 모든 페이지에 박힌 토큰의 유무로 서버에서 가짜 페이지의 요청을 구분하고자 하는데 목표가 있습니다.

    - 모든 응답마다 JWT형식의 랜덤 스트링을 만들어 페이지 상단 meta태그와 Form안쪽 hidden input 태그에 붙여둡니다. 이를 CSRF토큰이라 부릅니다.

    <head>
    	<meta name="_csrf_header" content="X-XSRF-TOKEN" />
    	<meta name="_csrf" content="b8eb4f09-1c0b-4f31-89a0-b986de188374" />
    </head>
    <body>
    	<form>
    		<input type="hidden" name="_csrf" value="b8eb4f09-1c0b-4f31-89a0-b986de188374" />
    	</form>
    </body>

    - 서버에선 이렇게 발급한 토큰을 DB/세션/HTTP-ONLY쿠키 중 한곳에 보관해둔 후 나중에 비교하는 용도로 사용해야 합니다.

    - 이렇게 고유한 토큰이 박힌 페이지에서의 Form 요청은 hidden input의 토큰이 자동으로 전송됩니다.

    - Ajax요청일 경우 말씀하신대로 헤더에 토큰을 담는 별도의 스크립트 처리가 필요합니다.

    var header = $("meta[name='_csrf_header']").attr("content");
    var token = $("meta[name='_csrf']").attr("content");
    
    $.ajax({
    	type: "POST",
    	url: "/~~",
    	beforeSend: function(xhr){
    		xhr.setRequestHeader(header, token);	// 헤더의 csrf meta태그를 읽어 CSRF 토큰 함께 전송
    	},
    	...
    });

    - 서버에선 모든 요청에 대해 헤더 또는 파라미터에서 CSRF토큰을 읽어들이는 필터와 모든 응답에 대해 새로운 CSRF토큰을 발급하는 두 개의 필터가 추가되어야 합니다.

    - 스프링 시큐리티 사용시 이 모든게 이미 구현되어 있어 간편히 사용할 수 있습니다.

    - 스프링 시큐리티의 CSRF 필터는 JWT 방식을 사용하지 않습니다. 따라서 스프링 시큐리티를 사용하는 순간 이 항목이 무의미 해집니다.



    종합

    스프링 시큐리티를 쓰는 순간 JWT 사용이 불필요해집니다.

    그럼에도 JWT를 쓰는 경우는 위에 설명드린대로 성능 개선을 목적으로 커스텀하기 위함인데 명시해둔 각 경우의 필요조건들에 유의하여 사용하시면 되겠습니다.

  • 아야로
    1k
    2018-07-31 17:44:31

    너무 장황해서 짧게 다시 적겠습니다.

    아래 경우는 스프링 시큐리티를 안쓰거나, 스프링 시큐리티가 세션을 사용하지 않도록 커스텀 했을때 유효합니다.

    1. [클라이언트]사용자 로그인정보를 서버에 요청

    2. [서버] 요청받은 DB의 유저정보와 대조 후 이상이 없다면, DB에 토큰을 저장하고, 간략한 유저정보를 담아 JWT로 변환한 HTTP-ONLY 쿠키를 응답

    3. [클라이언트]응답을 받으면 브라우저에 의해 자동적으로 쿠키에 토큰이 저장.

    4. [클라이언트]필요한 API를 호출 시, 쿠키에 담긴 토큰이 브라우저에 의해 자동 전송

    5. [서버]요청받은 쿠키의 JWT 토큰을 찾아내 위조 여부 검증

  • ShipJH
    1k
    2018-07-31 18:17:59

    아야로 허거거걱.... 상세하게 적어주시다니 정말로 감사합니다. !

  • 파이리v
    269
    2020-01-13 17:05:29

    아야로 

    안녕하세요. JWT 검색하다가 좋은 댓글 보고 궁금증이 있어서 남겨봅니다!

    스프링 시큐리티를 사용하고 JWT 로그인을 구현하기 위해(serverless) 세션을 비활성화 했습니다.

    JWT 토큰을 로컬스토리지가 아닌 httpOnly 쿠키에 저장한 후 클라이언트로 응답을 했습니다.

    근데 httpOnly 쿠키의 경우 CSRF 공격에 취약한 단점이 있는걸로 알고 있습니다. (sameSite 플래그가 이를 방지해주지만 아직 모든 브라우저가 지원은 안하는걸로 알고있습니다.)

    그렇다면 httpOnly 쿠키를 사용할 때도 CSRF 방어를 구현해야 되지 않나요?

    구글에 보면 많은 JWT 인증 예제가 시큐리티 설정에서 csrf를 비활성화 해놓습니다. 그래서 혼란이 있는데..

    httpOnly 쿠키를 사용해도 시큐리티의 csrf를 활성화 시켜놓아야 될까요?

  • 아야로
    1k
    2020-01-13 20:53:50

    파이리v

    httpOnly쿠키를 사용하든말든 CSRF 방어 로직은 반드시 구현해야 합니다.

    생각하신게 옳습니다.


    다만 스프링 시큐리티의 기본 CSRF필터가 세션을 사용하고 있기에,

    로그인 방식으로 JWT쿠키를 사용하고자 세션을 비활성화 하였다면,

    CSRF필터 역시 세션을 사용하지 않는 방식으로 재구현 해야겠지요.


    추측입니다만, 이 두가지를 동시에 소개하지 않기 위해 언급한 예제들에서 CSRF필터를 비활성화 해놓는게 아닐까 합니다

    SESSION없이 CSRF Filter구현하기가 아니라, JWT 인증 예제니까요.

  • 파이리v
    269
    2020-01-14 00:14:46

    아야로

    CSRF 방어는 반드시 구현해야 되는게 맞군요ㅎㅎ 답변 감사합니다.

    추가로 한가지 더 궁금한게 있습니다.

    스프링 시큐리티의 기존 CSRF는 JWT 인증에 사용하기엔 적합하지 않으므로 자체적으로 CSRF 구현을 해야 된다는 것 까진 이해를 했습니다.

    열심히 구글링을 해보니 참조할 만한 링크를 찾았는데

    http://rapasoft.eu/blog/posts/csrf-protection-implementation-in-spring-rest-jquery.html

    여기서는 클라이언트단에서 csrf 토큰을 생성해서 쿠키에 설정하고 동시에 헤더에도 설정(이중 쿠키 제출?) 한 뒤 서버를 호출하더라구요.

    csrf 토큰은 서버든 클라이언트든 어디에서 생성해도 상관이 없을까요?

    또, httpOnly 쿠키가 xss를 완화시켜준다고는 하지만 그래도 xss 방어 또한 구현을 해야 될까요?

    현업에서 jwt 인증을 구현해본 적이 없어서 궁금한 점이 많네요..ㅠㅠ

    답변 감사합니다!

  • 아야로
    1k
    2020-01-14 19:03:23 작성 2020-01-14 19:06:05 수정됨

    파이리v

    좋은 글이 있네요.

    Double Submit Cookie Pattern

    --------------------------------

    추가로 httpOnly는 수많은 xss공격중 하나를 막기위한 수단일 뿐이니, xss필터 역시 추가 구현하셔야 합니다.

    생각하신 대로입니다.

  • 파이리v
    269
    2020-01-15 10:09:56

    아야로

    답변 감사합니다. 정말 많은 도움 되었습니다^^

  • 로그인을 하시면 답변을 등록할 수 있습니다.