본문 바로가기
Develop/Spring

Spring Security | 로그인 방식

by Hoya324 2023. 9. 28.

로그인 방식

용어 정리

인증과 인가

  • 시스템의 자원을 적절하고 유효한 사용자에게 전달하고 공개하는 방법

인증 (Authentication)

  • 인증은 쉽게 말하자면, 로그인이다.
  • 클라이언트가 자기자신이라고 주장하고 있는 사용자가 맞는지를 검증하는 과정이다.
    • 예: 클라이언트에서 보낸 유저 아이디와 서버에 등록돼있는 유저 아이디를 확인한다.

인가 (Authorization)

  • 인가는 인증 작업 이후에 행해지는 작업으로, 인증된 사용자에 대한 자원에 대한 접근 확인 절차를 의미한다.

여기에 일반 유저인 USER1과 USER2가 있다. 일반 유저인 USER1 은 글 작성, 조회, 수정, 삭제 등 일반적인 작업에 대한 권한이 부여되어 있다. 하지만 USER1 은 USER2가 작성한 글을 수정하거나 제거할 수는 없다. 타인의 리소스에 대해서는 인가되어 있지 않기 때문이다. 또한 USER1과 USER2 는 모두 관리자 페이지에 접속할 수 없다. 일반 유저는 관리자 페이지에 대해 인가되어 있지 않기 때문이다.

HTTP의 비상태성(Stateless)

  • 서버는 클라이언트의 상태를 저장하지 않으며, 따라서 이전 요청과 다음 요청의 맥락이 이어지지 않는다.
  • HTTP는 바로 직전에 발생한 통신을 기억하지 못한다. 따라서 HTTP 단독으로는 요청한 클라이언트가 이전에 이미 인증과정을 거쳤는지 알 방법이 없다.
  • 매 요청마다 로그인을 할 수는 없기 때문에 웹에서는 세션 또는 토큰을 사용해서 해결한다. 즉, 세션과 토큰은 인가와 관련된 기술이다.

쿠키(Cookie)

  • 쿠키는 HTTP 요청과 응답에 함께 실려 전송된다.
  • 쿠키는 클라이언트 즉, 브라우저에 저장된다.
  • 웹 서버가 클라이언트로 보내는 응답의 헤더 중 Set-Cookie 라는 헤더에 키와 값을 함께 실어 보내면 그 응답을 받은 브라우저는 해당 쿠키를 저장하고, 그 다음 요청부터 자동으로 쿠키를 헤더에 넣어 송신한다.

쿠키의 문제점

  • 쿠키는 한번 생성되면 매 요청마다 헤더에 실려 서버로 전송된다. 만약 쿠키에 저장된 정보가 많다면, 매번 요청마다 큰 오버헤드가 발생할 것이다.
  • 이런 이유로 브라우저마다 다르겠지만 일반적으로 쿠키의 데이터는 4kb로 제한이 되어있다. 또한 인터넷 익스플로러를 제외하고는 사이트당 쿠키의 갯수도 20개로 제한이 되어있다고 한다.
  • 또한 쿠키는 만료시각을 명시할 때 파일로 저장되어 브라우저가 종료되더라도 휘발하지 않기 때문에 보안상의 이슈가 발생할 수 있다.

세션

  • 세션은 쿠키와 다르게 정보를 서버측에 저장하는 방식이다. 세션을 사용하면 위에서 언급한 쿠키의 문제점의 다수를 해결할 수 있다.

  • 세션이란 브라우저로 웹서버에 접속한 시점부터 브라우저를 종료하여 연결을 끝내는 시점까지의 일련의 요청을 하나의 상태로 간주하고, 그 상태를 일정하게 유지하는 기술이다.

세션 생성 과정

  • 맨 처음, 사용자가 HTTP 요청의 Body에 인증 정보 (유저이름이나 패스워드 같은 것들)을 실어 서버로 보낸다.
  • 서버에서는 해당 인증정보가 유효하면 사용자와 데이터를 식별하는 Session ID를 생성한다.
  • 생성된 Session ID는 응답의 Set-Cookie 헤더에 생성된 세션 아이디를 실어 보내진다.
  • 클라이언트는 해당 세션 아이디를 쿠키에 저장하고, 매 요청마다 세션 아이디를 Cookie 헤더에 실어 전송한다. 서버는 전달받은 세션 아이디를 통해 해당 요청의 송신자가 누구인지 식별할 수 있다.

세션 아이디에 대한 쿠키의 Key는 세션을 관리하는 주체에 따라 다르다. 즉, 무조건 쿠키에 SESSIONID 라는 이름으로 저장되는 것은 아니다. 예를들어 톰캣은 JSESSIONID 라는 이름으로 세션 아이디를 저장하고, node.js 는 connect.sid 라는 이름으로 저장한다고 한다.

세션 정보 저장 장소

  • 일반적으로 생성된 세션 데이터는 서버의 메모리에 저장된다.
  • 하지만 로드 밸런싱등의 이유로 서버를 수평 확장(Scale Out) 하는 경우가 많을 것이다.
    • 수평 확장은 수직 확장(Scale Up, 고성능 장비로 교체) 서버를 여러 대 두기만 하면 되므로 경제적 부담이 적고 확장에 유연하다.
  • 이런 경우 최초 세션이 생성된 서버와 그 이후의 요청을 받은 서버가 다른 경우 세션이 불일치하는 문제가 발생한다.
  • 이런 문제를 해결하기 위해 Sticky Session(유저의 요청이 무조건 세션을 생성한 서버로 향하도록 한다.), Session Clustering(여러 웹서버가 모두 동일한 세션 정보를 가지고 있다.) 방식이 있다.
  • 하지만, 세션 정보를 관리하는 서버를 아예 별개로 두는 Session Storage 방식이 가장 많이 쓰인다고 한다.
  • 이를 위해 MySQL 같은 일반적인 RDBMS를 사용할수도 있겠지만, Key-Value로 저장되는 세션 특성상 Redis나 memcached 와 같은 Key-Value 쌍으로 저장되는 인메모리 스토어를 사용하는 것이 일반적이다.

세션기반 인증

  • 세션기반 인가는 사용자의 인증 정보가 서버의 세션 저장소에 저장되는 방식이다. 사용자가 로그인을 하면, 해당 인증 정보를 서버의 세션 저장소에 저장하고, 사용자에게는 저장된 세션 정보의 식별자인 Session ID를 발급한다.
  • 발급된 Session ID는 브라우저에 쿠키 형태로 저장되지만, 실제 인증 정보는 서버에 저장되어 있다.
  • 브라우저는 인증 절차를 마친 이후의 요청마다 HTTP Cookie 헤더에 Session ID 를 함께 서버로 전송한다.
  • 서버는 요청을 전달받고, Session ID에 해당하는 세션 정보가 세션 저장소에 존재한다면 해당 사용자를 인증된 사용자로 판단한다.

JWT

JWT란?

  • Json Web Token 의 줄임말이다. RFC 7519 에 명세되어 있는 국제 표준으로써, 통신 양자간의 정보를 JSON 형식을 사용하여 안전하게 전송하기 위한 방법이다.
  • JWT는 정보가 토큰 자체에 포함된 (Self-Contained) 클레임 (Claim) 기반 토큰이다.
  • JWT는 인증 (Authentication)권한부여(Authorization) 에 사용되는 것이 가장 일반적이다.
  • 인증 절차를 거쳐 서버에서 JWT 를 발급해주면, 클라이언트는 이를 잘 보관하고 있다가 API 등을 사용할 때에 서버에 JWT를 함께 제출하며 서버로부터 행위에 대해 인가 받을 수 있다.
  • JWT 는 해시 혹은 비대칭키 방식을 사용하여 서명 (Signature) 하기 때문에 무결성을 검증할 수 있다는 특징이 있다.
  • 또한 토큰 자신이 정보를 직접 포함하고 있는 특징 덕분에, 통신 양자간 정보를 안전하게 전송할 때에도 사용된다.
  • 또한 JWT 는 URL에 대해 안전한 (URL-Safe) 문자열로 구성되어 있어 어떤 경로로든 전송할 수 있다.

서버기반 인증과 토큰기반 인증

서버기반 인증

  • 서버기반 인증에서는 사용자가 성공적으로 로그인한 이후 서버에서 사용자에 대한 세션(Session) 을 생성한다.
  • 또한 이와 동시에 사용자의 브라우저에는 세션 ID 를 저장하는 쿠키가 생성된다.
  • 서버는 이 세션 ID 를 통해 사용자를 식별하고, 사용자에 대한 정보를 저장, 관리한다.

토큰기반 인증

  • 토큰기반 인증방식은 유저의 정보를 서버에 저장하지 않는다. 유저가 성공적으로 로그인하면, 서버는 클라이언트로 토큰 (가장 일반적으로 JWT 가 사용됨) 을 발급한다.
  • 클라이언트는 토큰을 받아 저장하고, 서버에 요청을 할 때 HTTP header 에 실어 함께 전송한다.
  • 서버는 이를 검증 (Verification) 하고, 유저를 인가한다. 이와 같이 서버는 ‘발급’‘검증’ 두가지 역할만 할뿐 직접 정보를 갖고 있지 않다. 유저 상태의 저장 책임이 서버에서 클라이언트로 이동된 것이다.
  • 수평 확장의 환경에서 여러대의 서버 컴퓨터가 모두 유저에 대한 정보를 기억하고 있을 필요가 없다는 뜻이다.

하지만, JWT 와 같이 토큰 자체에 정보가 저장되는 형태의 토큰은 세션과 달리 클라이언트에 유저의 정보가 저장되므로 노출되기 매우 쉽다. 따라서 민감한 정보를 담아서는 절대 안된다. 또한 토큰의 사이즈는 세션 ID 에 비해 굉장히 비대하다. 토큰기반 인증을 사용하면 이런 토큰을 사용하여 통신하면서 발생하는 오버헤드를 감안해야한다는 단점이 존재한다.

예전에는 의미없는 랜덤 문자열등을 생성해서 토큰 기반 인증을 구현하였는데, 이 토큰에는 만료시각 등의 정보를 담을 수 없어, 따로 만료시킬 수단이 존재하지 않는다. JWT 같은 경우 데이터를 직접 갖고 있는 클레임 (Claim) 기반 토큰이므로 토큰의 만료를 구현할 수 있게 되었다.

JWT 의 구조

  • 토큰은 헤더 (Header), 페이로드 (Payload), 서명 (Signature) 세 부분으로 구성되어 있다. 각 구성요소는 점 (.) 으로 분리된다. 따라서 JWT 는 헤더.페이로드.서명 의 형태를 갖는다.

각각의 구성요소는 JSON 형태로 표현된다. 다만, JSON 의 경우 개행을 포함할 수 있어, 이를 한줄로 나타내기 위해 최종적으로는 각 구성요소를 Base64 로 인코딩한다.

헤더 (Header)

  • 헤더에는 일반적으로 토큰의 유형과 암호화 알고리즘 두가지 정보를 아래와 같이 JSON 의 형태로 담고있다.
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 토큰을 사용하는 측에서 토큰의 유형이 JWT 임을 확신할 수 있다면, typ 필드는 생략되어도 괜찮다.
  • alg 에 넣어둔 암호화 알고리즘은 주로 HMAC SHA256 또는 RSA 가 사용된다. 이는 후술할 서명 (Signature) 에서 사용된다.

페이로드 (Payload)

  • 페이로드는 사용자의 정보 혹은 데이터 속성 등을 나타내는 클레임(Claim) 이라는 정보 단위로 구성된다.
  • 클레임도 3가지로 구분할 수 있는데 각각 등록된 클레임 (Registered Claim), 공개 클레임 (Public Claim), 비공개 클레임 (Private Claim) 으로 구성되어 있다.

등록된 클레임 (Registered Claim)

  • JWT 사양에 이미 정의된 클레임이다.
  • 아래의 7개의 등록된 클레임이 정의되어 있다. 모든 클레임은 선택적이다. 토큰 사이즈를 작게 유지하기 위해 이름이 3글자로 축약되어 있는 것을 확인할 수 있다.
    • iss : Issuer. 토큰 발급자를 나타낸다.
    • sub : Subject. 토큰 제목을 나타낸다.
    • aud : Audience. 토큰 대상자를 나타낸다.
    • exp : Expiration Time. 토큰 만료 시각을 나타낸다. Numeric Date 형식으로 나타낸다.
    • nbf : Not Before. 토큰의 활성 시각을 나타낸다. 쉽게 말해, 이 시각 전에는 토큰이 유효하지 않다는 의미이다. Numeric Date 형식으로 나타낸다.
    • iat : Issued At. 토큰이 발급된 시각을 나타낸다. Numeric Date 형식으로 나타낸다. 이 값으로 토큰이 발급된지 얼마나 오래됐는지 확인할 수 있다.
    • jti JWT ID. JWT 의 식별자를 나타낸다.

공개 클레임 (Public Claim)

  • 공개 클레임은 JWT 를 사용하는 사람들에 의해 정의되는 클레임으로, 충돌 방지를 위해 URI 형태로 이름을 짓거나, IANA JSON Web Token Claims Registry 라는 곳에 직접 클레임을 등록해야한다.
  • 사실 단순히 서버와 클라이언트 사이에서 사용자를 인증하는 용도로 사용한다면 크게 신경쓰지 않아도 좋다.
  • 서버-클라이언트 사이의 단순 통신을 넘어 제 3자도 JWT 토큰을 사용할 때 충돌이 일어나지 않도록 합의된 클레임이라고 생각하면 된다.
{
  "email": "sample@domain.com",
  "profile": "http://domain.com/image.png",
  "http://domain.com/xxx/yyy/is_admin": true
}
  • 위 처럼 등록된 공개 클레임인 email , profile 등을 사용할수도 있고, http://domain.com/xxx/yyy/is_admin 처럼 URI 형태로도 사용할 수 있다.

비공개 클레임 (Private Claim)

서버와 클라이언트 사이에서만 협의된 클레임으로, 공개 클레임과 충돌이 일어나지 않게 사용하면 된다.

{
  "user_id": "123456790",
  "user_age": 25
}

서명 (Signature)

  • 특정 암호화 알고리즘을 사용하여, Base64 인코딩된 헤더와 Base64 인코딩된 페이로드 그리고 비밀키를 이용하여 암호화한다. 서명을 통해 서버는 헤더 혹은 페이로드가 누군가에 의해 변조되었는지 그 무결성을 검증하고 보장할 수 있다.
  • HMAC SHA256 을 사용한 서명 생성을 아래와 같은 수도코드 (pseudo-code) 로 나타낼 수 있다.
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Reference