우리가 사용하는 웹은 공개된 세상이다. 프로토콜이 공개되어 있고, 자유롭게 접근할 수 있는 데이터들이 있다. 개발자들은 공개된 프로토콜과 데이터를 활용해 이를 공개된 정보를 사용자들에게 제공한다. 웹이 지향하는 이 개방성은 모두에게 방대한 정보를 제공하는 기회를 제공한다. 이것이 최초의 웹이 현재의 웹이 된 이유일 것이다.
웹 세상에서의 통신
하지만 모든 정보가 모두에게 공개될 수 있는 것은 아니다. 특정 정보는 개인의 사적 정보를 담고 있기 때문에 그 사람에게만 제공되어야 한다. 마찬가지로 개인의 사적 정보 역시 안전하게 입력받아야 한다. 특히 사용자가 입력하는 정보는 사용자 개인을 특징지을 수 있는 정보를 포함하기 때문에 무조건 이런 처리를 해야한다.
그럼 웹에서 통신은 어떻게 이뤄질까? 개인 관점에서 정의해보면 아래와 같은 큰 덩어리로 정리될 수 있다고 생각한다. (OSI 7 Layer를 참고하긴 했지만 정확하게 일치하지 않는다. 순전히 개인적인 관점이다. ^^)
각 계층(Layer)가 가지는 의미를 정의해보면 아래와 같다.
- Physical Layer – 실질적인 통신선을 나타낸다. 무선이든 유선이든 일단 선을 깔아야지 통신이 되겠지?
- Transport Layer – 선이 깔린 상태에서 이제 보내는 쪽과 받는 쪽이 일련이 규격을 통해 정보를 주고 받는다.
- Application Layer – 쌍방간에 데이터를 주고 받을 때 어떤 형식(Protocol)을 사용할지 어플리케이션 사이에 정의한다.
이런 환경에서 웹 서버와 클라이언트(웹 브라우저)가 통신을 하고 있다. 당신이 PC에서 이 글을 보고 있다면 아마도 유선 랜으로 연결되어 있을 것이고 스마트폰에서 보고 있다면 무선으로 연결되어 있다.(Physical Layer) 접속한 각 기기는 IP를 가지고 TCP 기반에서 패킷을 통해 이 글을 요청했고, 웹 서버는 이에 응답해서 글을 내려보낸 것이다.(Transport Layer) 마지막으로 서버에서 HTML이라는 프로토콜로 ASCII 데이터를 내려보내면 브라우저가 이를 해석해서 보기 좋은 형태의 텍스트로 표시했다.(Application Layer) 그렇기 때문에 당신은 이 글을 볼 수 있다.
개인적으로 이 글에 내 신상에 관련된 아주 특별한 정보가 있는 것도 아니고 원래 의도가 불특정 다수에게 읽혀지길 원하기 때문에 특별히 보안을 요구하지는 않는다. 다만 내 의지가 아닌 남의 의지에 의해 내가 작성한 글들이 싸그리 날라가는 일만 없길 바랄 뿐이다.
불안함에 대응할려면?
불안함에 대응하기 위한 어지간한 방법들은 이미 널리 알려져있다.
Physical Layer 차원에서의 대응
전용선을 쓰면 된다. 하지만 구리선을 깐다는건 돈이 억수로 든다는 것을 의미한다. 그리고 당신의 프로그램(혹은 시스템)은 특정 고객을 위한 것이어야만 한다는 전제가 깔여 있어야 한다. 전용선을 여기저기 이어준다는 것은 전용선이 전용선이 아니라는 것이니 말이다.
돈이 걱정이라면 VPN(Virtual Private Network)을 이용할 수도 있다. 하지만 이 역시 소프트웨어를 이용한 가상적인 망이라는 것. 그리고 엄청나게까지는 아니겠지만 상당히 느릴 것이라는 것을 감수해야 한다.
Transport Layer 차원의 대응
일반적인 OSI 7 레이어에서 Transport 계층의 상위 계층은 TCP/IP이다. 그냥 쓰면 여러분이 보내는 정보를 그냥 까볼 수 있다. 이게 안되게 할려면 오고 가는 정겨운 메시지들을 암호화하면 된다. 주로 SSL 등이 TCP/IP 계층 위에서 연결된 양 단말 사이에 오고 가는 데이터들을 암호화한다.
이 방식은 대표적인 예가 HTTPS 프로토콜이다. 일반적인 HTTP는 TCP/IP 스택 위에서 암호화를 적용한다. 따라서 만약 이 데이터를 누군가가 중간에 가로채서 데이터를 변경했다고 했을 때 변경됐는지 아닌지 알 도리가 없다. HTTPS는 중간 경로를 통한 위변조를 막기 위해
- “중립 기관(Certificate Authority)”를 통해 서명된 인증서(Certification)를 이용해 데이터를 암호화하고
- 브라우저에 암호화된 데이터와 인증서를 전달한다.
- 브라우저는 인증서에 적용된 서명이 올바른지를 다시 중립 기관을 통해 확인하고
- 실제 데이터를 보낸 서버로부터 받은 경우에만 인증서를 통해 데이터를 복호화하여 실행(표시)한다.
이 과정에서 오류가 발생되면 브라우저는 이를 사용자에게 알리거나 아예 접근을 차단한다. 이것이 우리가 구버전의 IE를 사용하면 안되는 이유이다. IE 구버전은 사용의 편이성(?)을 위해 HTTPS의 증명이 틀리더라도 내려받은 코드(특히 자바스크립트 혹은 ActiveX)를 실행하기 때문이다.
Application Layer 차원의 대응
이정도면 충분한 준비를 갖춘것 아닐까? 기본은 충족을 한 것 같지만 그래도 좀 부족하다.
HTTPS 방식에서 인증 기관을 해커가 사칭한다면 어떻게 될까? 이 상황에서 서버에서 내려오는 정보를 마찬가지로 동일 해커가 위변조시킨다면 영락없이 당할 소중한 정보를 빼앗기거나 사용자 컴퓨터의 이러저러한 정보들이 탈탈 털리는 상황이 나온다. 서버와 클라이언트 사이에 전달되는 정보를 누구나 다 볼 수 있는 쿠키에 암호화되지 않은 형태로 주고받는 환경이라면 더욱 심각한 상황이 되버린다.
이런 상황을 가급적 최소화하기 위해서는 개발자의 힘이 필요하다.
JWT(JSON Web Token)이란?
앞서 이야기한 것처럼 시스템을 통해 정보를 보호하는 건 한계가 있다. 일반적인 보안 시스템이 할 수 있는 수준은 정성들인 해커에게는 대부분 무용지물인 경우가 많다. 쿠키에 많은 정보를 주고받는 것이 그냥 손에 익어버린 경우라면 더욱 더 문제다.
이런 걸 하지 말라는 차원에서 등장한 개념이 암호화된 토큰을 이용하는 방법이다. 우리가 다루는 모든 정보를 암호화할 필요는 없다. 외부에 노출되면 곤란한 개인 정보 혹은 중요 정보들을 그 대상으로 한다. 그리고 이 정보들을 시스템 사이에 주고 받을 때 이놈이 그놈이 맞는지를 확인할 수 있으면 된다.
JWT는 토큰 방식으로 이를 처리하기위해 제시된 방식이다. 이름이 의미하는 것처럼 JSON String 형태이기 때문에 웹 환경에서 쓰기에 좋다. 특히 RFC로 정의된 표준이다. JSON의 특성상 다양한 형태의 정보를 정의할 수 있으며, 정보의 출처를 확인하기 위한 검증 방식을 제공한다.
그림에서 보는 것처럼 JWT은 일련의 암호화된 문자열이며, 점(.)을 기준으로 3가지 영역으로 구분된 구조를 갖는다.
- Header – 해당 토큰이 어떤 암호화 알고리즘을 통해 암호화되었는지를 정의한다. alg 라는 세부 필드로 표현된 알고리즘이 암호화에 적용된 알고리즘을 나타낸다.
- Payload – 실제 전달하고자 하는 메시지 Body를 나타낸다.
- Signature – 전달된 메시지에 대한 Signature 값을 나타낸다. 말 그대로 signature 값이 포함되어 있으며, 해당 값을 인식하기 위해서는 보낸쪽과 받는쪽에서 합의된 키 값을 가지고 Header에서 정의된 암호화 알고리즘을 통해 검증할 수 있다.
구조에서 Header와 Payload에는 암호화가 적용된 것이 아니라 Base64 URL Encoding되었을 뿐이다. 실제 암호화는 Signature에만 적용되어 있으며, 전달된 메시지를 서로 신뢰할 수 있는지를 평가하기 위한 구조로 사용된다. 이 말이 의미하는 바를 풀어 설명하면 이렇다.
전체 데이터에 대한 외부 노출 차단은 HTTPS와 같은 시스템 수준의 암호화를 통해 담보하고, 전달받은 데이터의 신뢰성을 평가를 암호화 토큰을 통해 보장한다. 따라서 암호화 알고리즘과 이에 대한 암호화 키 정의는 어플리케이션 개발자가 시스템의 구조와 토큰의 역할에 의해 결정된다.
그렇기 때문에 개발자의 역할이 중요해진다.
Just do it with code
그럼 이걸 어케 개발자가 프로그램으로 작성할 수 있을까? 앞서 링크한 jwt.io 사이트에서 관련된 이를 지원하는 여러 언어별 라이브러리들이 있다. Javascript를 이용한 예제는 여기를 참조하자. 소개할 예제는 Java를 활용한 예제 코드로 nimbus 라이브러리를 활용했다. nimbus를 사용하기 위해서는 다음의 메이븐 dependency를 설정한다.
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>4.12</version> </dependency>
JWT를 생성하는 방식은 아래 코드를 참조한다.
JWSSigner signer = new MACSigner(key); JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() .claim("cid", "this-is-key") .claim("name", "chidoo") .claim("admin", true) .build(); SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); signedJWT.sign(signer); String jwtString = signedJWT.serialize(); System.out.println(jwtString);
암호화 알고리즘으로 HS256을 사용하기 때문에 key값은 32바이트 이상의 문자열을 사용한다. 출력된 코드가 정상적인지는 간단하게는 jwt.io 사이트에서 Verification할 수 있다. 혹은 다음 코드와 같이 입력된 JWT 값을 동일한 키 값을 통해 확인해볼 수 있다.
JWT signedJWT = (SignedJWT)SignedJWT.parse(jwtString); // check payload and applied algorithm in header System.out.println(signedJWT.getPayload().toJSONObject()); System.out.println(signedJWT.getHeader().getAlgorithm()); // verification JWSVerifier verifier = new MACVerifier(key); assertThat(signedJWT.verify(verifier), is(true));
실제 코드를 통해 이걸 사용하는 방법은 위의 몇 줄에서 보는 바와 같이 쉽다. ^^;
이렇게 활용할 수 있다.
그럼 이걸 어떤 경우에 써먹어야지 제대로 써먹었다고 이야기를 할 수 있을까? 가장 대표적으로 특정 시스템에 접근하기 위한 권한을 이 사용자가 제대로 가지고 있는지를 확인하는 경우이다. 아래 그림의 예제를 통해 살펴보면 다음과 같은 절차로 처리가 이뤄진다.
- 사용자가 로그인을 하면 보통 세션을 생성한다.
- 생성된 세션을 위한 사용자의 Key를 발급하고, 이를 가지고 사용자 식별자(보통 User ID)를 포함한 JWT를 생성해서 이를 Response로 보낸다.
- 이 JWT는 세션이 유지되는 동안 사용자가 쿠키 혹은 내부 변수의 형태로 가지고 있는다.
- 사용자가 민감한 정보를 가지는 시스템에 접근하는 경우 JWT를 그 시스템에 전송한다.
- 해당 시스템은 전달된 JWT 정보를 Decode해서 사용자를 알아낸다.
- 사용자에게 발급된 키를 통해 전달된 JWT 값이 위변조된 값인지 아닌지를 확인한다.
- 정상적인 토큰으로 인식하면 사용자에게 부여된 권한 등등에 부합할 때 데이터에 대한 접근을 허용하면 된다.
위의 경우처럼 일반적인 사용자 인증 체계의 신뢰성을 확보하기 위한 용도가 가장 대표적일 수 있다. 이 이외에도 마이크로서비스 아키텍처 환경에서 각 서비스 시스템간 Collaboration을 위한 권한 관리 용도로도 활용이 가능하다. 마이크로서비스 환경에서 각 서비스 시스템들은 본인들이 제공하는 정보의 규격을 정의한다. 하지만 누구나 정보를 임의대로 허용할 수는 없다. 자신과 협업할 수 있는 시스템들의 제한이 필요하며 이를 위한 신뢰성있는 토큰으로 JWT를 활용할 수 있다.
Service A에 접근하기 위해서는 사전에 시스템간 협의에 의해 key를 발급한다. 연동 서비스에서 Service A에 접근하기 위해서는 본인에게 발급된 키를 가지고 JWT 생성하고 이를 원하는 Operation과 함께 시스템에 전송한다. A 서비스는 전달된 값을 검증해서 허용된 시스템에 대해서만 요청된 기능을 실행한다. 만약 협업이 허용되지 않은 시스템으로부터의 요청에 대해서는 당연히 이를 거부한다.
JWT를 사용하는데 있어서 JWT 값의 Lifetime을 명확하게 관리해야한다. 생성된 JWT가 반복적으로 쓰여도 별 문제가 안된다면 이건 하나마나한 결과를 초래하기 때문이다. 따라서 이 값이 언제 생성된 값인지 그리고 언제까지 유통 가능한지를 JWT 안에 포함시켜 관리하는 것이 보다 안전한 시스템 구성을 유지하는 길이다.
마음만 먹으면 제대로 할 수 있다.
뭔가를 보호하는 일은 솔직히 많이 사람을 귀찮게 한다. 특히나 재미있는 기능을 만들고 싶은 마음이 굴뚝인데, 기능 그 자체와는 동떨어진 일에 얽매여 있다보면 짜증이 날 수도 있다. 하지만 귀중한 정보를 가지고 있다면 그 귀중함을 잘 지켜야 할 의무 또한 존재한다.
방법이 있다면 그 방법을 적용해야 한다.
매번 소잃고 외양간만 고치고 있을 수는 없지 않을까?
– 끝 –