*개인적으로 공부하고 작성하는 글이므로 틀린 부분이 있을 수 있습니다. 만약 틀린 부분이 있다면 댓글로 알려주시기 바랍니다.
spring boot로 로그인 및 로그아웃을 구현하는 방법은 다양한데, 나의 경우는 spring security + JWT를 이용하여 구현을 했었다. 이전에 진행했던 부분에 대해서 다시 한 번 공부를 해보고, 기록을 해두기 위해서 글을 작성한다.
먼저 로그인을 진행하는 전체적인 로직에 대해서 알 필요가 있다. 사용자 입장으로서 로그인을 보았을 때는, 회원가입 이후에 아이디와 비밀번호를 입력하여 사이트에 가입한 회원으로서 로그인을 하는 과정이다. 이를 BE의 입장으로 보았을 때, 로그인 처리에 필요한 과정은 크게 나누었을 때 다음 두가지일 것이다.
1)아이디 및 비밀번호 식별
2)로그인 처리 및 유지
1번의 경우는 프론트단에서 받아온 아이디, 비밀번호 값에 대하여 DB에 저장된 회원 정보와 비교를 진행한다. DB에는 회원 식별 번호(회원 테이블의 PK는 서비스에 따라 다를 것이나, 소셜 로그인 등의 문제로 인하여 회원 식별 번호를 따로 지정하는 경우가 처리하기 편하다고 들었다.)와 아이디, 비밀번호와 그 외 회원정보들이 저장될 것이다. 이때 DB에 비밀번호는 암호화하여 저장한다.
암호화는 대개 Bcrypt를 이용하여 진행한다. Bcrypt의 경우, 단방향 해싱 함수로서 비밀번호에 대해 hash 함수를 적용하여 길이가 60인 String을 반환함으로써 암호화를 진행하는 방식이다. 이때 Bcrypt는 내부적으로 랜덤 솔트를 생성하여, 같은 문자열에 대해서도 다른 인코드 결과를 반환한다. 따라서 DB 내부에 있는 비밀번호와 프론트단에서 들어온 비밀번호를 비교할 때에도 단순히 비교하는 것이 아니라, Bcrypt 내부 메소드를 이용하여서 비교해야한다.
이를 통해서 아이디 및 비밀번호가 유효하며 일치함을 확인하면 로그인 처리를 할 것이다. 로그인 처리를 한 이후에도 사용자가 해당 로그아웃하기 전까지는 로그인 상태를 저장하여 서비스를 이용할 수 있게 해주어야한다. 그러나 HTTP는 Stateless 와 Connectionless의 특징을 갖고있어, 서버가 클라이언트의 이전 상태를 보존하지 못한다. 그렇다면 어떻게 로그인 상태를 유지하는가? 이를 위하여 사용할 수 있는 방법은 일단 대표적으로 다음 두 가지가 있다.
1)쿠키
2)세션
먼저, 쿠키의 경우 웹 브라우저가 보관하는 데이터로서 웹 서버는 쿠키를 생성하여 웹 브라우저에 정보를 전송할 수 있다. key - value 형태로 웹 브라우저의 쿠키 저장소에 해당 정보가 저장된다. (인터넷을 이용할때 개발자 도구를 확인해보면 쿠키를 확인할 수 있다!) 서버에서 쿠키를 전달하면, 웹 브라우저는 서버에 요청을 보낼때 쿠키에 헤더를 실어서 함께 전송 한다. 즉, 회원 정보를 쿠키 저장소에 저장하고, 매번 Request를 보낼때마다 헤더에 cookie값을 전송하므로, 서버는 클라이언트의 request를 받을때 헤더의 cookie값을 확인하여 해당 요청이 어떤 사용자에게서 왔는지 파악할 수 있기 때문에 로그인 상태를 유지할 수 있게 되는 것이다.
그러나 쿠키는 네트워크를 통해 전달되기 때문에 중간에 탈취가 될 수 있다는 취약점이 존재한다. 따라서 이를 보완하기 위해서 세션 방식을 사용하게 된다. 세션도 마찬가지로 클라이언트의 상태를 저장할 수 있으며, 쿠키를 기반으로 한다. 그러나, 브라우저의 별도 쿠키 저장소에 저장되는 쿠키와 다르게 세션은 서버에 저장된다. 클라이언트가 서버에 접속시 서버에서는 클라이언트를 구분하기 위하여 세션 ID를 부여하며, 클라이언트는 세션 ID에 대해 쿠키를 사용하여 저장하게 된다. 클라이언트는 서버에 요청할때, 쿠키의 세션 ID를 서버에 전달해서 요청하게 된다. 서버는 세션 ID를 전달받아서 세션에 있는 클라이언트 정보를 가져와서 사용하고, 클라이언트 정보를 가지고 서버 요청을 처리하여 클라이언트에게 응답한다.
결국 둘 다 사용 원리는 비슷한다. 사용자의 정보가 저장되는 위치에 차이점이 있을 뿐이다. 보안 면에서는 세션이 더 우수하나, 요청 속도는 쿠키가 더 빠르다. 세션의 경우 서버의 처리가 필요하기 때문이다. 또한 세션의 경우에는 사용자가 많아질수록 서버 메모리를 많이 차지하게 되고, 속도가 느려질 수 있다. 특히 서버를 다중화할 경우, 세션은 처리에 어려움이 발생한다. 쿠키는 보안의 위험성이 있고, 세션은 서버에 부담이 발생할 수 있다면 도대체 무슨 방법을 사용해야 하는가? 물론 왕도는 없다. 이때 추가적으로 고려할 수 있는 방식이 토큰을 이용하는 방식이다.
세션과 쿠키는 저장소에 유저 정보를 넣게 된다. 그러나 토큰을 이용하면 토큰 안에 유저의 정보가 암호화하여 넣어지게 된다. 즉, 로그인 이후 서버에서 클라이언트 측으로 토큰 정보를 넘겨주면 클라이언트에서는 Request를 보낼때 헤더에 토큰만 넘겨주면 되는 것이다. 그러면 서버에서는 토큰을 복호하여 유저 정보를 파악하게 된다. 클라이언트의 입장에서는 HTTP 헤더에 정보를 넣는 것이므로 큰 차이가 없지만, 서버의 경우는 별도의 저장소 이용(세션) vs 암호화 이용(토큰)으로 차이가 발생한다.
토큰은 header, payload, signature으로 이루어지며 header에 암호화 방식, payload의 유저 id, 유효기간등의 정보가 포함된다. verify signature은 header, payload, secret key를 더한 후에 서명된다. 즉, 서버에서는 verify signature을 secret key로 복화한 후에 조작 여부나 유효기간등을 확인한 후에 payload를 디코딩하여 사용자 ID 등의 정보를 가져오게 된다. 즉 encoded header + "." + encoded payload + "." + verify signature으로 이루어지는 것이다. header와 payload는 특별한 암호화가 걸린것이 아니기 때문에 payload에 민감한 정보를 담아서는 안된다. 식별을 위한 정보만 담고있어야 한다. 다만 signature은 서버에 있는 개인키로만 암호화를 풀 수 있어서, 다른 클라이언트는 임의로 signature을 복호화할 수 없다.
물론 토큰도 완벽한 방식은 아니다. 토큰은 만료시간이 지나기 전까지 강제로 만료시키는 것이 불가능하며, 토큰이 탈취되었을 때 토큰을 갖고있는 클라이언트가 정말 클라이언트 본인이 맞는지 확인할 수가 없다. (이외에도 보안 취약점이 존재하나 이정도만 간단하게 언급하고 넘어가기로 한다.)
이를 보완하기 위하여 access token, refresh token의 구조를 사용하게 된다.
1)로그인시에 access token, refresh token 발급하여 클라이언트에 전송.
2)access token이 만료되면 refresh token을 이용하여 사용자 인증을 하고, access token 재발급하여 사용.
위와 같은 방법으로 각 토큰을 사용하게 되며 클라이언트에서 서버에 요청을 보낼때 access token을 사용하게 되는데, access token은 만료시간이 매우 짧으며 refresh token은 상대적으로 만료시간이 길다. 각각의 만료시간의 경우 서비스 자체적으로 결정하게 되며 역시 어떤 것이 적절한지에 대해서는 의견이 부분하다. (보안을 위하여 refresh token의 시간 조차 짧게 주는 경우도 있다.)
각각의 방법에 대해 장단점이 존재하고, 보안적 측면에서 어떤 방법이 적합한지에 대한 논쟁 또한 존재하나 해당 글은 단순히 로그인 로직을 이해하기 위한 글이므로 자세한 내용은 생략하도록 한다.
그렇다면 로그아웃은 어떻게 구현하는가? 쿠키나 세션의 경우 단순히 해당 값들을 만료시켜주면 될 것이다. 그러나 JWT의 경우에는 자체적으로 만료시킬 수 없기 때문에 다른 방법이 필요하다. 이를 위해 가장 많이 사용되는 방식은 블랙리스트 방식이다.
JWT 토큰을 발급할때 refresh token, access token 두 종류의 토큰이 발급된다고 상기에 언급하였다. 이때 access token을 이용해서 각 요청을 보낼때 사용자 인증을 하고, access token이 만료되면 refresh token을 통해서 갱신한다. 이런 경우 기본적으로 DB에는 refresh token만 저장된다. access token은 계속해서 갱신되고, 어차피 복호를 통해서 사용자 정보를 인식할 수 있기 때문에 DB에 저장할필요가 없다. refresh token의 경우에만 갱신시에 저장된 토큰이 맞는지를 확인하기 위해 DB에 저장할 필요가 있는 것이다. 하지만, 로그아웃을 하기 위해서는 access token이 현재 아직 유효하더라도 더 이상 사용할 수 없게 시켜야한다. 강제로 만료처리를 해야한다는 것이다. 그러나 JWT는 만료시키는 것이 불가능하다.
이를 위해서 redis와 같은 인메모리DB를 캐시 저장소로 사용하여, 이곳에 access token 정보를 저장한다. (실제 DB에서 refresh token정보도 지워준다.) 로그아웃 처리를 하면 캐시 서버에 해당 토큰 정보를 저장하여, 해당 토큰으로 요청이 올 때는 무시하도록 한다. (즉, 인증이 필요한 모든 api에서 해당 access token이 블랙리스트에 존재하는지 확인하는 로직을 추가해주면 된다.) 이러한 방식을 통해 로그아웃을 구현할 수 있다. (사실 이러한 방법을 사용하면 매번 redis를 통해 체크를 해야하므로 JWT의 장점을 제대로 활용하지 못하며 이를 해친다고도 볼 수 있는데, JWT는 강제 만료를 시킬 수 없기 때문에 로그아웃 구현을 위해서는 이 방법을 쓸 수밖에 없는 것으로 알고있다... 혹시나 다른 방법이 있다면 알려주시길 바랍니다!)
사실 간략한 개념, 그리고 로그인 및 로그아웃 흐름과 대표적으로 사용하는 방식에 대해 소개한 글이며 여러가지 보안상의 이슈, 논쟁거리에 대해서는 제대로 다루지 않았다. 로그인, 로그아웃 과정만해도 공부할 건덕지가 정말 많기 때문에... 보안상 이슈들에 대해서는 차후에 다른 글로 한 번 따로 정리해보고자 한다.
로그인, 로그아웃 과정에 대해서 간략하게 정리하였으니 이 다음으로는 1)spring security로 로그인/로그아웃 처리 (spring security 개념) 2)spring security+JWT로 로그인/로그아웃 처리(응용) 3)spring security로 사용할 수 있는 또 다른 기능들(응용)의 순서로 차근차근 정리해보고자 한다.