가장 많이 쓰이는 암호화 방식 중에 하나인 해싱을 소개합니다. 복호화가 가능한 다른 암호화 방식들과 달리, 해싱은 암호화만 가능합니다.
해싱은 해시 함수(Hash Function)를 사용하여 암호화를 진행하는데, 해시 함수는 다음과 같은 특징을 가집니다.
아래 표는 대표적인 해시 함수중 하나인 SHA1에 특정 입력 값을 넣었을 때 어떤 결과가 리턴되는지 보여주는 예시입니다. 이 링크에서 SHA1 함수를 직접 사용해 볼 수도 있습니다.
‘password’ = ‘5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8’
‘Password’ = ‘8BE3C943B1609FFFBFC51AAD666D0A04ADF83C9D’
‘kimcoding’ = ‘61D17C8312E8BC24D126BE182BC674704F954C5A’
그런데 항상 같은 결과값이 나온다는 특성을 이용해 해시 함수를 거치기 이전의 값을 알아낼 수 있도록 기록해 놓은 표인 레인보우 테이블이 존재합니다. 레인보우 테이블에 기록된 값의 경우에는 유출이 되었을 때 해싱을 했더라도 해싱 이전의 값을 알아낼 수 있으므로 보안상 위협이 될 수 있습니다.
이때 활용할 수 있는 것이 솔트(Salt)입니다. 솔트는 소금이라는 뜻으로, 말 그대로 소금을 치듯 해싱 이전 값에 임의의 값을 더해 데이터가 유출되더라도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법입니다.
비밀번호 + 솔트 /// 해시 함수(SHA1) 리턴 값
‘password’ + ‘salt’ = ‘C88E9C67041A74E0357BEFDFF93F87DDE0904214’
‘Password’ + ‘salt’ = ‘38A8FDE622C0CF723934BA7138A72BEACCFC69D4’
‘kimcoding’ + ‘salt’ = ‘8607976121653D418DDA5F6379EB0324CA8618E6’
그런데, 왜 복호화가 불가능한 암호화 방식을 사용하는 걸까요? 바로 해싱의 목적은 데이터 그 자체를 사용하는 것이 아니라, 동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이기 때문입니다.
예시를 들어보겠습니다. 사이트 관리자는 사용자의 비밀번호를 알고 있을 필요가 없습니다. 오히려 사용자들의 비밀번호를 알고 있다면, 이를 얼마든지 악용할 수 있기 때문에 심각한 문제가 생길 수도 있습니다. 그래서 보통 비밀번호를 데이터베이스에 저장할 때, 복호화가 불가능하도록 해싱하여 저장하게 됩니다. 해싱은 복호화가 불가능하므로 사이트 관리자도 정확한 비밀번호를 알 수 없게 되죠.
그럼 서버 측에서 비밀번호를 모르는 상태에서 어떻게 로그인 요청을 처리할 수 있는 걸까요? 방법은 간단합니다. 해싱한 값끼리 비교해서 일치하는지 확인하는 것이죠. 꼭 정확한 값을 몰라도, 해싱한 값이 일치한다면 정확한 비밀번호를 입력했다는 뜻이 되기 때문에, 해싱 값으로만 로그인 요청을 처리하는 데에도 전혀 문제가 없습니다.
이처럼 해싱은 민감한 데이터를 다루어야 하는 상황에서 데이터 유출의 위험성은 줄이면서 데이터의 유효성을 검증하기 위해서 사용되는 단방향 암호화 방식입니다.
토큰 인증 방식은 최근 웹 애플리케이션에서 많이 사용되는 인증 방식 중 하나입니다. 토큰을 사용하면 사용자의 인증 정보를 서버가 아닌 클라이언트 측에 저장할 수 있습니다.
토큰 기반 인증은 기존의 세션 기반 인증이 가지고 있던 한계를 극복하고자 고안되었습니다. 세션 기반 인증은 서버에서 유저의 상태를 관리합니다. 그래서 개발자들은 서버의 부담을 줄이기 위해 서버가 사용자의 인증 상태를 저장하는 것이 아닌 클라이언트에 이를 저장하는 방법을 고민하게 되었고, 그 결과 토큰 기반 인증 방식이 등장하였습니다.
토큰은 유저의 인증 상태를 클라이언트에 저장할 수 있어서, 세션 인증 방식의 비교해 서버의 부하나 메모리 부족 문제를 줄일 수 있습니다.
토큰은 교통 승차권과 같이 무언가를 이용할 수 있는 권한이나 자격을 나타내는, 일종의 증표입니다. 여러분이 일상생활에서 흔히 접하는 지하철 승차권 혹은 사무실 출입 카드가 토큰에 해당합니다. 웹 보안에서의 토큰은 인증과 권한 정보를 담고 있는 암호화된 문자열을 말합니다. 이를 이용해 특정 애플리케이션에 대한 사용자의 접근 권한을 부여할 수 있습니다.
사용자가 인증 정보를 담아 서버에 로그인 요청을 보냅니다.
서버는 데이터베이스에 저장된 사용자의 인증 정보를 확인합니다.
인증에 성공했다면 해당 사용자의 인증 및 권한 정보를 서버의 비밀 키와 함께 토큰으로 암호화합니다.
생성된 토큰을 클라이언트로 전달합니다.
HTTP 상에서 인증 토큰을 보내기 위해 사용하는 헤더인 Authorization 헤더를 사용하거나, 쿠키로 전달하는 등의 방법을 사용합니다.
5. 클라이언트는 전달받은 토큰을 저장합니다.
저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양합니다.
6. 클라이언트가 서버로 리소스를 요청할 때 토큰을 함께 전달합니다.
토큰을 보낼 때에도 Authorization 헤더를 사용하거나 쿠키로 전달할 수 있습니다.
7. 서버는 전달받은 토큰을 서버의 비밀 키를 통해 검증합니다. 이를 통해 토큰이 위조되었는지 혹은 토큰의 유효 기간이 지나지 않았는지 등을 확인할 수 있습니다.
무상태성 : 서버가 유저의 인증 상태를 관리하지 않습니다. 서버는 비밀 키를 통해 클라이언트에서 보낸 토큰의 유효성만 검증하면 되기 때문에 무상태적인 아키텍처를 구축할 수 있습니다.
확장성 : 다수의 서버가 공통된 세션 데이터를 가질 필요가 없다는 것도 토큰 기반 인증의 장점입니다. 이를 통해 서버를 확장하기 더 용이합니다.
어디서나 토큰 생성 가능 : 토큰의 생성과 검증이 하나의 서버에서 이루어지지 않아도 되기 때문에 토큰 생성만을 담당하는 서버를 구축할 수 있습니다. 이를 잘 활용하면 여러 서비스 간의 공통된 인증 서버를 구현할 수 있습니다.
권한 부여에 용이 : 토큰은 인증 상태, 접근 권한 등 다양한 정보를 담을 수 있기 때문에 사용자 권한 부여에 용이합니다. 이를 활용해 어드민 권한 부여 및 정보에 접근할 수 있는 범위도 설정할 수 있습니다.
토큰 기반 인증 구현 시 대표적으로 사용하는 기술로 JWT(JSON Web Token)가 있습니다. JWT는 JSON 객체에 정보를 담고 이를 토큰으로 암호화하여 전송할 수 있는 기술입니다. 클라이언트가 서버에 요청을 보낼 때, 인증정보를 암호화된 JWT 토큰으로 제공하고, 서버는 이 토큰을 검증하여 인증정보를 확인할 수 있습니다.
JWT는 다음 그림과 같이 .으로 나누어진 세 부분이 존재하며 각각을 Header, Payload, Signature라고 부릅니다.
Header에는 마치 HTTP의 헤더처럼 해당 토큰 자체를 설명하는 데이터가 담겨 있습니다. 토큰의 종류, 그리고 시그니처를 만들 때 사용할 알고리즘을 JSON 형태로 작성합니다.
{
"alg": "HS256",
"typ": "JWT"
}
이 JSON 객체를 base64 방식으로 인코딩하면 JWT의 첫 번째 부분인 Header가 완성됩니다.
Payload는 HTTP의 페이로드와 마찬가지로 전달하려는 내용물을 담고 있는 부분입니다. 어떤 정보에 접근 가능한지에 대한 권한, 유저의 이름과 같은 개인정보, 토큰의 발급 시간 및 만료 시간 등의 정보들을 JSON 형태로 담습니다.
{
"sub": "someInformation",
"name": "phillip",
"iat": 151623391
}
이 JSON 객체를 base64로 인코딩하면 JWT의 두 번째 부분인 Payload가 완성됩니다.
Signature는 토큰의 무결성을 확인할 수 있는 부분입니다. Header와 Payload가 완성되었다면, Signature는 이를 서버의 비밀 키(암호화에 추가할 salt)와 Header에서 지정한 알고리즘을 사용하여 해싱합니다.
예를 들어, 만약 HMAC SHA256 알고리즘을 사용한다면 Signature는 아래와 같은 방식으로 생성됩니다.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
따라서 누군가 권한을 속이기 위해 토큰의 Payload를 변조하는 등의 시도를 하더라도 토큰을 발급할 때 사용한 Secret을 정확하게 알고 있지 못한다면 유효한 Signature를 만들어낼 수 없기 때문에 서버는 Signature를 검증하는 단계에서 올바르지 않은 토큰임을 알아낼 수 있습니다.
Signature을 사용해서 위조된 토큰을 알아낼 수는 있지만, 토큰 자체가 탈취된다면 토큰 인증 방식의 한계가 드러납니다.
인증 상태를 관리하는 주체가 서버가 아니므로, 토큰이 탈취되어도 해당 토큰을 강제로 만료시킬 수 없습니다. 따라서 토큰이 만료될 때까지 사용자로 가장해 계속해서 요청을 보낼 수 있습니다.
토큰이 탈취되는 상황을 대비해서 유효 기간을 짧게 설정하면, 사용자는 토큰이 만료될 때마다 다시 로그인을 진행해야 하기 때문에 좋지 않은 사용자 경험을 제공합니다. 그렇다고 유효 기간을 길게 설정하면 토큰이 탈취될 경우 더 치명적으로 작용할 수 있습니다.
토큰에 여러 정보를 담을 수 있는 만큼, 많은 데이터를 담으면 그만큼 암호화하는 과정도 길어지고 토큰의 크기도 커지기 때문에 네트워크 비용 문제가 생길 수 있습니다.
토큰 인증의 한계를 극복하기 위해 다양한 방법들이 고안되었지만 이 중 대표적인 구현 방법은 액세스 토큰과 리프레시 토큰을 함께 사용하는 것입니다.
액세스 토큰은 말 그대로 서버에 접근하기 위한 토큰으로 앞서 다룬 토큰과 비슷한 역할을 합니다. 따라서 보안을 위해 보통 24시간 정도의 짧은 유효기간이 설정되어 있습니다.
리프레시 토큰은 서버 접근을 위한 토큰이 아닌 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 발급받기 위해 사용되는 토큰입니다. 따라서 리프레시 토큰은 액세스 토큰보다 긴 유효기간을 설정합니다.
이렇게 두 가지의 각기 다른 토큰을 사용하는 경우, 액세스 토큰이 만료되더라도 리프레시 토큰의 유효기간이 남아있다면 사용자는 다시 로그인을 할 필요 없이 지속해서 인증 상태를 유지할 수 있습니다.
물론 리프레시 토큰의 도입도 모든 문제를 해결해주진 않습니다. 리프레시 토큰은 긴 유효 기간을 가지고 있어 해당 토큰마저 탈취된다면 토큰의 긴 유효 기간 동안 악의적인 유저가 계속해서 액세스 토큰을 생성하고 사용자의 정보를 해킹할 수도 있기 때문입니다. 이를 대비하기 위해 리프레시 토큰을 세션처럼 서버에 저장하고 이에 대한 상태를 관리하기도 합니다.
결국 서버에서 상태를 관리하지 않기 위해 고안된 토큰 인증도 보안성을 위해 일정 부분 서버에서 상태 관리를 담당하는 것처럼 결국 이 세상에 완벽한 보안 방법은 없습니다.
세션, 토큰 등 다양한 보안 방식 및 여러 구현 방법들은 절대 뚫리지 않는 궁극의 보안을 위해 만들어진 것이 아닙니다. 이러한 여러 방식들은 단순히 보안뿐만 아니라 보안과 사용자 경험 사이의 적절한 균형을 찾기 위해 만들어졌습니다. 따라서 개발자로서 내가 구현하려는 서비스에 어떤 인증 방식이 가장 적절한지 판단하고 의사결정 할 줄 아는 것이 가장 중요합니다.
여러분이 웹이나 앱에서 흔히 찾아볼 수 있는 소셜 로그인 인증 방식은 OAuth 2.0라는 기술을 바탕으로 구현됩니다.
전통적으로 직접 작성한 서버에서 인증을 처리해 주는 것과는 달리, OAuth는 인증을 중개해 주는 메커니즘입니다. 보안된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공하는 프로세스를 단순화하는 프로토콜입니다.
즉, 이미 사용자 정보를 가지고 있는 웹 서비스(Naver, Kakao, Google 등)에서 사용자의 인증을 대신해 주고, 접근 권한에 대한 토큰을 발급한 후, 이를 이용해 내 서버에서 인증이 가능해집니다.
몇 년 전만 하더라도 특정 웹 앱의 서비스를 이용하기 위해선 해당 웹 앱에 회원가입을 하는 것이 우선이었습니다. 하지만 소셜 로그인이 보편화된 현재는 대부분의 사람들이 네이버 또는 카카오에 이미 가입된 계정을 이용해 빠르게 서비스에 가입하는 것을 택하고 있습니다.
뿐만 아니라 서비스를 구현하는 개발자도 신규 회원가입이나 회원 관리를 신경 쓰지 않아도 되기 때문에 사용자와 기업 모두 소셜 로그인을 선호하고 있는 추세입니다.
유저 입장에서 생각해보면, 우리는 웹상에서 굉장히 많은 서비스를 이용하고 있고 각각의 서비스들을 이용하기 위해서는 회원가입 절차가 필요한 경우가 대부분입니다. 각각의 서비스별로 ID와 Password를 다 기억하는 것은 매우 귀찮은 일입니다.
하지만 OAuth를 활용한다면 자주 사용하고 중요한 서비스들(예를 들어 google, github, facebook)의 ID와 Password만 기억해 놓고 해당 서비스들을 통해서 외부 서비스로 소셜 로그인을 할 수 있습니다.
뿐만 아니라 OAuth는 보안상의 이점도 있습니다. 검증되지 않은 App에서 OAuth를 사용하여 로그인한다면, 유저의 민감한 정보가 직접 App에 노출될 일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 더 안전하게 사용할 수 있습니다.
OAuth 인증 방식에는 여러 가지가 있지만, 그중 Implicit Grant Type, Authorization Code Grant Type, 그리고 Refresh Token Grant Type, 이렇게 세 가지에 대해서 알아보겠습니다.
사용자가 Application에 접속합니다.
Application에서 Authorization Server로 인증 요청을 보냅니다.
Authorizaiton Server는 유효한 인증 요청인지 확인한 후 액세스 토큰을 발급합니다.
Authorization Server에서 Application으로 액세스 토큰을 전달합니다.
Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청합니다.
Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인합니다.
유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달합니다.
이렇게 인증을 중개받아 새로운 서비스를 이용할 수 있게 되었습니다. 하지만 소셜 로그인에서 Implicit Grant Type은 잘 사용하지 않습니다. 기존 서비스에 로그인만 되어있다면 새로운 서비스에 바로 액세스 토큰을 내어주기 때문에 보안성이 조금 떨어지기 때문인데요. 그래서 보통은 여기에 인증 단계를 한 단계 추가한 인증 방식인 Authorization Code Grant Type을 주로 사용하게 됩니다.
사용자가 Application에 접속합니다.
Application에서 Authorization Server로 인증 요청을 보냅니다.
Authorizaiton Server는 유효한 인증 요청인지 확인한 후 Authorization Code를 발급합니다.
Authorization Server에서 Application으로
Authorization Code를 전달합니다.
Application이 Authorization Code로 발급받은 Authorization Code를 전달합니다.
Authorizaiton Server는 유효한 Authorization Code인지 확인한 후 액세스 토큰을 발급합니다.
Authorization Server에서 Application으로 액세스 토큰을 전달합니다.
Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청합니다.
Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인합니다.
유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달합니다.
Implicit Grant Type과 비교해 보면, Authorization Code를 사용한 인증 단계가 추가로 있기 때문에 비교적 더 안전합니다. 또한, 원한다면 아래와 같이 토큰을 Application의 Client에 노출시키지 않고 Server에서만 관리하도록 만들 수도 있기 때문에 소셜 로그인을 구현하는 방식의 선택지가 늘어나게 됩니다.
그런데, 사용자가 새로운 서비스를 이용하다가 액세스 토큰이 만료되었을 때, 매번 이 과정을 거쳐서 액세스 토큰을 다시 발급받아야 한다면 사용자 편의성에 있어서는 좋지 않습니다. 그렇기 때문에 액세스 토큰을 발급해 줄 때 리프레시 토큰을 같이 발급해주기도 합니다. 이때, 리프레시 토큰을 사용해서 액세스 토큰을 받아오는 인증 방식을 Refresh Token Grant Type이라고 합니다.
Refresh Token Grant Type은 간단합니다. Authorization Server로 리프레시 토큰을 보내주면, Authorization Server는 리프레시 토큰을 검증한 다음 액세스 토큰을 다시 발급해 주게 됩니다. Application은 다시 발급받은 액세스 토큰을 사용해서 Resource Server에서 사용자의 정보를 받아오게 됩니다.