반응형

1. 개요

최근 Node.js로 백엔드 개발을 할 일이 있었다. Node.js 경험은 거의 없는 상태로 설계 단계에서 Node.js에 대해 공부하는 와중에 REST 뽕을 거하게 맞았다.(프론트엔드 경험이 없었기에 프론트엔드와 백엔드를 완벽하게 분리할 수 있다는 점이 좋았다...) 이로 인해 일반적인 세션 대신 비연결성의 토큰을 이용한 로그인 / 로그아웃에 관심이 생겼다. 구체적으로 구현하는 방법이나 코드에 대해서는 다른 글에 많이 있으니 JWT에 대한 이야기만 하려고 한다.


2. JSON Web Token, JWT

2.1 JWT?

RFC 7519에 정의된 JSON 형태의 요청을 주고 받는 형식이다. 간단하게 설명하자면 JSON 문자열을 BASE64 인코딩(암호화가 아니다!)하고 뒷부분에 문자열을 서버의 비밀 키로 Hashing해서 덧붙여 전송한다.

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

위의 JWT 예시는 https://jwt.io/ 에서 가져왔다. 언뜻 보기에는 암호화된 것 같지만 실제로는 BASE64 인코딩된 것이므로 공개해도 되는 정보만 넣어야 한다. 토큰 내용을 암호화해서 숨기고 싶다면 JSON Web Encryption(RFC7516) 구현체를 알아봐야 할 것 같다.

JWT는 세 개의 문자열 사이에 점 두 개가 있는 형태다. 각 문자열 별로 역할이 다르다. 각 문자열이 어떤 값을 지니고 있는 지에 대해서 알아보자.

2.1.1 헤더

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

위의 예제에서 주황색 문자열은 아래의 JSON을 BASE64로 인코딩한 문자열이다.
{
"alg": "HS256",
"typ": "JWT"
}


alg는 어떤 방식으로 Hashing할 지를 정한다. 구현체에 따라 지원하는 알고리즘의 종류가 다르지만, RFC 상에는 HS256과 none(Hashing 없음)을 반드시 구현해야 한다고 적혀있다. Node.js의 구현체 jsonwebtoken 패키지의 경우 [HMAC using SHA-*]HS256, HS384, HS512, [RSA using SHA-*]RS256, RS384, RS512, [ECDSA using P-* curve and SHA-*]ES256, ES384, ES512 모두를 지원한다.

기본적으로는 HS256을 이용한다.

typ는 이 것이 JWT임을 나타낸다. 반드시 있어야 하는 필드는 아니지만 RFC 상 넣기를 권장하는 내용이다.

특별한 일이 없다면 헤더에는 alg, typ 필드 두 개에 대한 내용만 포함되어 BASE64 인코딩된다.

2.1.2 Payload

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

위의 토큰 예제에서 분홍색 문자열은 Payload라고 부르며, 전달하고자 하는 JSON형태의 내용이 BASE64 인코딩되어 들어간다. 필드(JWT에서는 Claim이라고 부른다) 명과 데이터는 자기 마음이지만, 몇 가지 필드 이름은 정해진 역할을 한다. 다만 모두 RFC 상 선택적인 항목이라, 구현체에 따라 동작할 수도, 하지 않을 수도 있다.

jsonwebtoken의 경우 aud, exp 필드에 대한 검사는 구현되어 있지만, iss, sub, nbf, iat, jti 필드에 대해서는 구현되어 있지 않은 것으로 나타난다. 실제로 구현할 때는 exp 필드 정도만 유용하게 쓴 것 같다.

exp 필드에 Math.floor(Date.now() / 1000) 에 초 단위로 시간을 더해서 직접 입력해도 된다. Node.js의 jsonwebtoken 패키지는 sign 과정에서 expiresIn 옵션을 이용해서 조절할 수도 있다.

jwt.sign({
    exp: Math.floor(Date.now() / 1000) + 3600,
    name: "John Doe"
}, "private_key");

jwt.sign({
    name: "John Doe"
}, "private_key", {expiresIn: "1h"});


미리 정의된 Claim (참고: https://tools.ietf.org/html/rfc7519#section-4.1)

 iss

 issuer. 토큰 발급자. 문자열이나 URI로 대소문자를 구별한다.

 sub

 subject. 토큰의 제목, 주제. issuer 단위로 유일하거나 토큰 전체에서 유일해야 한다. 문자열이나 URI로 대소문자를 구별한다.

 aud

 토큰 대상자. 일반적으로, aud 값은 대소문자를 구별하는 문자열/URI 값의 배열이다. 토큰 대상이 하나인 경우 aud 값은 대소문자를 구별하는 문자열/URI 값이다. 사용자 아이디같은 걸 넣어주면 될 것 같다.

 exp

 토큰 만료 시간(숫자). 만료시간을 넘긴 토큰은 반드시 거부되어야 한다. 반드시 현재 시간보다 미래의 시간이어야 한다.

 nbf

 토큰 유효화 시작 시간(숫자). 지정된 시간보다 이전인 토큰은 반드시 거부되어야 한다. 반드시 현재 시간과 같거나 이전의 시간이어야 한다.

 iat

 토큰 발급 시간(숫자).

 jti

 JWT ID. JWT에 대한 유일한 식별자. 대소문자를 구별하는 문자열이다. 중복 처리 방지를 위해 사용할 수 있다. 식별자는 충돌 날 가능성이 거의 없도록 할당되어야 한다. 어플리케이션이 여러 발급자가 발급한 토큰을 이용하는 경우 서로다른 발급자 사이의 jti가 절대로 충돌해서는 안 된다.


2.1.3 서명

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 

2.1.1의 BASE64 인코딩된 헤더와 점, BASE64 인코딩된 2.1.2의 Payload를 서버의 비밀키로 Hashing해서 점과 함께 뒤에 붙인다. 위 예제에서는 파란 색 문자열이 서명 부분. 클라이언트(프론트 엔드)에서는 서명이 유효한지 아닌지 확인할 수 없고, 비밀키를 가지고 있는 서버에서만 확인할 수 있다.


2.2 언어별 구현

주요 언어는 대부분 JWT 구현체가 존재하며, 구현체에 따라 지원하는 Hashing 알고리즘, 검사하는 필드에 차이가 있을 수 있다. 이러한 정보는 다음 링크에서 확인할 수 있다.

https://jwt.io/#libraries-io

Node.js에서는 jsonwebtoken 1건이 검색된다. 다른 구현체가 아마도 있겠지만 일단은 jswonwebtoken 패키지만으로도 충분히 구현할 수 있었다.

jsonwebtoken 패키지의 경우 RFC에 정의된 Hashing 알고리즘 전부 및 aud(수신자), exp(토큰 유효시간) 체크는 구현되어 있으나, 나머지의 경우 구현되어 있지 않는 것으로 표시된다. 다만 jsonwebtoken 페이지의 레퍼런스를 확인해보면 iss, nbf, iat(max age)는 지원하는 것으로 보인다.


장점

간편함

Node.js로 개발하던 내 경우에는, jsonwebtoken을 가져다가 자체적인 검사 로직을 추가해 decode, verify, signing 을 구현한 후 따로 로컬 패키지로 만들고, 자체적으로 만든 미들웨어에서 이 패키지를 가져다 써서 로그인 및 권한 체크를 하는 식으로 사용했다. 미들웨어에서 권한 체크 후 문제가 있으면 에러 핸들러로 에러를 던지기 때문에 비즈니스 로직에 무언가 추가할 필요가 (거의) 없었다.

데이터베이스 접근 최소화

권한 유효성을 검사할 때 데이터베이스까지 접근하지 않아도 된다. 로그인한 사용자인지 확인할 때, DB에 쿼리를 날릴필요 없이 토큰이 유효한지만 확인하면 된다.


단점

프론트 엔드에서 토큰 관리

큰 단점은 아니라고 생각하고, 또한 전혀 신경쓰지 않아도 되긴 하다. 토큰이 만료되었는지 검사하지 않고 무조건 서버로 보내면 된다. 정상적으로 구현했다면 서버에서 유효하지 않은 토큰에 대해 요청을 거부하는 응답을 보낼 것이다.

다만 토큰을 주기적으로 확인해(주로 exp 필드) 토큰이 만료된 상태인 경우 토큰을 직접 삭제하고 '로그아웃되었습니다'와 같은 안내문구를 띄워주는 등의 처리를 하는 것이 서버로의 요청 수를 줄이고 더 나은 사용자 경험을 제공하는 데 도움이 될 것 같다. javascript로 된 jwt 구현체도 여럿 있어서 크게 어렵진 않을 것 같다.

보안 문제

JWT를 이용하면서 생길 수 있는 가장 큰 문제다.

https 연결을 이용하고 XSS같은 것도 다 막았지만 컴퓨터 해킹 등의 이유로 토큰이 탈취당했다고 생각해보자. 탈취한 토큰을 이용해 서버로 요청을 전송하는 경우 서버는 토큰을 무효화할 방법이 없다.

가장 간단하게는 토큰 발급 시 접속 IP를 같이 기록한 후 API 호출 시 요청 IP와 토큰에 기록된 IP를 비교하는 방법이 있는 것 같다. 토큰에 필드를 하나 추가하고 검사할 때 IP를 검사하는 로직만 추가하면 된다. 다만 완벽한 해결책은 아니다. 또한 모바일에서 LTE, 3G, 와이파이 전환이 자주 일어나며 IP가 자주 변경되는 경우 사용자가 자꾸 다시 로그인 해야 하는 불편함을 경험할 수 있다.

또는 만료시간이 긴 토큰 하나, 실제로 서버에 무언가 요청할 때 보내기 위해 쓰는 토큰을 나누어서 관리하는 방법이 있는 것 같다. 실제 요청 때 쓰는 토큰은 만료시간을 짧게 주고, 토큰이 만료되면 만료시간이 긴 토큰으로 서버에 요청하는 식으로 관리하는 것 같다. 이런 경우 토큰이 탈취된 상황에서도 다른 경우에 비해 상대적으로 짧은 시간 내에 토큰이 자동으로 무효화된다. 그리고 만료시간이 긴 토큰에 대해서만 잘 관리하면 성능과 보안 사이에서 균형있는 서비스가 가능할 것 같다.

완벽하게 토큰을 무효화하려면, 데이터베이스에서 탈취된 토큰 유효시간동안은 토큰 정보를 저장하고, 모든 요청을 받을 때 토큰 정보가 데이터베이스에 있는지 여부를 검사해야만 하는 듯 하다. Redis 등 메모리 DB를 이용할 순 있지만, 결국 DB 조회가 들어가면서 세션 기반 처리와 큰 차이가 없게 되는 점이 아쉬운 부분.


결론

Stateless하도록 하기 위해서라면 JWT는 괜찮은 선택이라고 생각한다. 실제로 JWT를 이용해 로그인/로그아웃 서비스를 만들어보니 생각보다 만족스러웠다. 다만 공개 서비스가 아니라 기술 검증 수준의 단계에서 개발하다보니 어느 정도 느슨하게(특히 보안과 관련된 부분) 고려해서 선택한 만큼, 공개 서비스를 개발한다면 여러 가지(특히 보안)를 충분히 고려해서 선택하는 것이 좋을 것 같다.


참고자료

https://blog.outsider.ne.kr/1160

https://velopert.com/2389

https://jwt.io

https://www.npmjs.com/package/jsonwebtoken

반응형
반응형

얼마 전 코딩 테스트에서 정렬 문제를 시간이 부족해 아쉽게 못 푼 가슴아픈 기억때문에 정렬 문제 위주로 요즘 풀고 있다. 금방 풀었던 어떤 문제의 코드를 개선할 방법을 고민하다가 unique의 레퍼런스를 제대로 읽지 않고 대충 써서 조금 헤맸기에 레퍼런스를 보고 간략히 정리해본다.

*std::list를 사용한다면 멤버 함수 unique를 사용하면 된다.

헤더

#include <algorithm>

기능

범위 내 중복된 원소 제거.

중복된 원소 중 첫 번째를 제외한 나머지 원소를 제거함.

반환값

지워지지 않은 마지막 원소의 iterator(반복자).

주의점

unique를 써서 vector의 중복된 원소를 제거해도 vector의 크기(size)는 줄어들지 않음. resize를 이용해서 줄여줘야 한다.

it = std::unique (myvector.begin(), myvector.end());
myvector.resize( std::distance(myvector.begin(),it) );
//resize를 해줘야 vector에 남아있던 명시되지 않은 상태의 원소가 모두 사라진다.
//아니면 std::unique_copy로 다른 곳에 복사하는 것도 깔끔할 것 같다.

unique를 사용해 제거한 중복된 원소는 unspecified state, 즉 명시되지 않은 상태로 남는다. 예컨대 string이 들어있는 vector를 순회하면서 empty()로 상태를 검사하는 경우 생각대로 동작할 수도, 동작하지 않을 수도 있다.

내 경우 중복된 문자열을 unique를 이용하여 제거한 후 이 문자열이 제거되었는지를 string의 empty()를 이용해서 체크했는데, MSVC 환경에서는 생각대로 동작했으나 다른 환경에서는 다르게 동작하는 것으로 보인다.

참조

http://www.cplusplus.com/reference/algorithm/unique/

반응형
반응형

한 번 포스팅으로 정리해야겠다 생각이 들어서 정리해봤다.


1. 전통적인 for

for (int i = 0; i < 10; i++)
{
std::cout << i << std::endl;
}

우리가 C++에서 반복문을 얘기할 때 가장 흔하게 사용하는 for문이다. for문 내에서 사용하는 정수를 선언하고, 이 정수를 키우거나 줄이는 등 값을 바꾸며 반복하는 방법이다.

2. C++11부터 추가된 for

for(int i : {0, 1, 2, 3, 4, 5})
{
    std::cout << i << std::endl;
}

std::vector<std::string> name_vector;
for(const auto& element : name_vector)
{
    std::cout << element << std::endl;
}

Range-based for loop라고 부른다.

변수 선언과 조작 대신 내부적으로 반복자(iterator)를 이용하여 vector와 같은 컨테이너 전체의 원소에 대해 반복을 수행한다.

쓰다보면 기존 for문에 있던 인덱스값(i)이 안 보여 귀찮게 느껴질 수도 있는데, while문 쓸 때처럼 반복문 밖에 변수를 선언하고 가져다 쓰면 된다.

깔끔해 보이는게 가장 큰 장점. 성능이 더 좋고 그런 건 없다.


3. std::for_each(<algorithm>)

std::vector<std::string> name_vector{ "test1", "test2", "test3" };
std::for_each(name_vector.begin(), name_vector.end(), [](auto& input) {std::cout << input << std::endl; });

void print(std::string& input)
{
    std::cout << input << std::endl;
}
std::vector<std::string> name_vector{ "test1", "test2", "test3" };
std::for_each(name_vector.begin(), name_vector.end(), print);

for_each는 <algorithm> 헤더에 정의되어 있는 함수다. 그래서 std::for_each의 형태로 써야 한다.

반복자의 시작과 끝, 그리고 함수를 파라미터로 받는다. 마지막 파라미터는 람다 표현식으로도 나타낼 수 있다.


4. std::transform(<algorithm>)

std::string change(std::string& input)
{
    return std::string("Changed!");
}
std::vector<std::string> name_vector{ "test1", "test2", "test3" };
std::vector<std::string> result{ "result1", "result2", "result3" };

std::transform(name_vector.begin(), name_vector.end(), result.begin(), change);
//result = {"Changed!", "Changed!", "Changed!"}

transform도 반복자를 이용한 반복문이긴 한데, 결과를 다른 컨테이너에 저장하는 역할을 한다. 물론 원본 컨테이너에 바로 저장할 수도 있다. transform도 for_each와 마찬가지로 함수 부분을 람다 표현식으로 나타낼 수 있다.


같은 구현도 다른 함수를 이용해서 서로 다르게 구현할 수 있으니 필요에 따라 적절하게 반복문을 선택해서 만들면 될 것 같다.


덤. std::for_each_n(<algorithm>,C++17)
검색하다보니 C++17에서는 for_each_n이라는 것이 추가되는 모양이다. for_each에서 반복자의 끝 대신 반복자의 시작부터 n번 반복하도록 정수를 넣는 형태인 것 같다.




참고


반응형
반응형

링크: http://stackoverflow.com/questions/9515704/building-a-chrome-extension-inject-code-in-a-page-using-a-content-script


설명이 깔끔하게 잘 되어있는 것 같다.

잘 써먹었으므로 글로 남긴다.

가장 윗 답변의 Method 1을 이용하였다.


manifest.json 파일에 web_accesible_resources 항목에 해당 자바스크립트 파일 이름을 추가한 후


var s = document.createElement('script');
s.src = chrome.extension.getURL("script.js");
s.onload = function() {
    this.parentNode.removeChild(this);
};
(document.head||document.documentElement).appendChild(s);


다음과 같은 함수를 로드하도록 해놓으면 된다.

반응형

+ Recent posts