인증/인가

  • 인증(Authentication) : 사용자가 누구인지 확인하는 것
  • 인가(Authorization) : 사용자에게 특정한 권한을 부여하는 것

http의 특징

  • 비연결성 : 클라이언트와 서버가 한 번 연결을 맺은 후 요청에 대한 응답을 보내면 연결을 끊는 특징
  • 무상태성 : 서버는 클라이언트의 상태를 보존하지 않는 특징
  • 요청과 응답이 끝나면 서버는 클라이언트에 대한 정보를 잊어버림
  • 이러한 특징으로 인해 인증이 필요한 서비스를 구현하기 위해서는 추가적인 방법이 필요함.

서버에서 클라이언트에게 전달하는 작은 데이터 조각

쿠키는 클라이언트에 저장되는 데이터로, 클라이언트의 상태를 유지하기 위해 사용된다. 쿠키는 클라이언트에 저장되기 때문에, 서버에 저장된 세션과 다르게 클라이언트가 쿠키를 조작할 수 있다.

쿠키는 다음과 같은 순서로 동작한다.

  1. 클라이언트가 서버에 요청을 보낸다.
  2. 서버는 클라이언트에게 쿠키를 발급한다.
  3. 클라이언트는 쿠키를 저장하고, 이후 요청 시에 쿠키를 헤더에 포함하여 서버에 요청을 보낸다.
  4. 서버는 쿠키를 확인하여 클라이언트를 인증한다.

현대적인 웹 개발에서는 쿠키만으로 인증을 처리하는 것은 아래와 같은 이유로 권장되지 않음.

  • 조작이 쉬움
  • 보안에 취약함
  • 쿠키를 사용하는 방식은 서버의 부하를 높일 수 있음
  • 용량 제한이 있음 (4KB)

session

서버에 클라이언트의 정보를 저장하는 방식

session은 쿠키를 기반으로 동작한다. 쿠키는 클라이언트에 저장되는 반면, 세션은 서버에 저장된다. 세션은 서버에 저장되기 때문에 쿠키보다 보안 측면에서 더 좋다.

세션 인증은 다음과 같은 순서로 동작한다.

  1. 클라이언트가 서버에 로그인 요청을 보낸다.
  2. 서버는 클라이언트에게 고유한 세션 ID를 발급한다.
  3. 클라이언트는 세션 ID를 쿠키에 저장한다.
  4. 클라이언트는 세션 ID를 가지고 서버에 요청을 보낸다.
  5. 서버는 세션 저장소에서 해당 세션 ID가 있는지 확인한다.
  6. 세션 저장소에 세션 ID가 있다면, 클라이언트를 인증한다.

서버에는 세션 ID와 사용자 정보가 key-value 형태로 저장되어 있다. spring에서는 HttpSession을 사용하여 세션을 관리한다.

1
2
3
4
5
@GetMapping("/session")
public String session(HttpSession session) {
    session.setAttribute("name", "session");
    return "session";
}

세션 정보는 default로 메모리에 저장되기 때문에 서버가 재시작되면 세션 정보가 초기화된다. 이를 해결하기 위해 세션 정보를 DB에 저장하거나 Redis와 같은 외부 저장소에 저장하는 방법을 사용한다. HttpSession에서는 세션을 저장하는 방법을 변경할 수 있도록 설정이 가능하다.

(application.properties)

1
spring.session.store-type=redis
1
spring.session.store-type=jdbc

세션 인증은 요청마다 서버에 저장된 세션을 확인해야 하므로 서버에 부하가 생길 수 있다. 또한 http의 특징 중 무상태성을 깨는 방법이기 때문에 서버의 확장성이 떨어질 수 있다.

이러한 단점을 보완하기 위해 JWT(JSON Web Token)를 사용한다.

JWT

JSON 객체를 사용하여 정보를 안전하게 전송하는 방식

JWT는 클라이언트에 저장되는 토큰으로, 쿠키와 기본적인 동작 방식은 비슷하다. 하지만 JWT는 서명된 토큰으로, 클라이언트가 토큰을 조작하기 어렵다. JWT는 서버에 저장된 시크릿 키를 사용하여 서명되기 때문에 클라이언트가 토큰을 조작하려면 시크릿 키를 알아야 한다.

JWT는 다음과 같은 순서로 동작한다.

  1. 클라이언트가 서버에 로그인 요청을 보낸다.
  2. 서버는 클라이언트에게 시크릿 키를 사용하여 암호화된 JWT를 발급한다.
  3. 클라이언트는 JWT를 저장하고, 이후 요청 시에 JWT를 헤더에 포함하여 서버에 요청을 보낸다.
  4. 서버는 JWT의 유효성을 확인하여 클라이언트를 인증한다.

JWT는 다음과 같은 구조로 이루어져 있다.

  • Header : 토큰의 타입과 해싱 알고리즘을 포함
  • Payload : 클레임(claim)이라고 불리는 정보를 포함
  • Signature : 토큰의 유효성을 검증하는 서명
1
2
3
4
{
    "alg": "HS256",
    "typ": "JWT"
}
  • alg : 해싱 알고리즘
    • 토큰을 서명할 때 사용하는 알고리즘
    • HS256, RS256 등이 있음
  • typ : 토큰의 타입
    • JWT
    • 토큰의 타입을 지정

Payload

1
2
3
4
5
6
{
    "sub": "1234567890",
    "exp": 1516239022,
    "nbf": 1516239022,
    "iat": 1516239022
}
  • sub : 토큰 제목(일반적으로 사용자 ID)
  • exp : 토큰 만료 시간(만료 시간이 지나면 토큰이 유효하지 않음)
  • nbf : 토큰 활성 시간(토큰이 활성화되기 전에는 토큰이 유효하지 않음)
  • iat : 토큰 발급 시간(토큰이 언제 발급되었는지를 나타냄)

헤더와 페이로드는 base64UrlEncode를 사용하여 인코딩 되어 있기 때문에 클라이언트가 토큰을 복호화하여 정보를 확인할 수 있다. 따라서 민감한 정보는 토큰에 저장하지 않아야 한다.

Signature

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    ${SECRET} // 비밀키
)

Signature는 Header와 Payload를 합친 후, 시크릿 키를 사용하여 해싱한 값이다. 서버는 클라이언트로부터 받은 JWT를 해싱한 후, 시크릿 키를 사용하여 해싱한 값과 비교하여 토큰의 유효성을 확인한다.

단점

JWT는 한 번 발급되면 만료 시간이 지나기 전까지 계속 사용할 수 있기 때문에 보안 측면인 문제가 발생했거나 유저의 권한이 변경되었을 때, 토큰을 강제로 만료시키기 어렵다. 그래서 보통 JWT를 사용할 때는 만료 시간을 짧게 설정한다. 하지만 만료 시간을 짧게 설정하면 사용자가 자주 로그인해야 하는 불편함이 생긴다.

이러한 단점을 보완하기 위해 refresh token을 사용한다.

refresh token

access token이 만료되었을 때, 새로운 access token을 발급하는 방식

session 방식과 JWT 방식을 합친 방식으로, access token과 refresh token을 사용하여 인증을 처리한다. access token의 만료 시간을 짧게 설정하여 보안을 강화하고, access token이 만료되었을 때, refresh token을 사용하여 서버에 인증 요청을 보내면 서버는 refresh token의 유효성을 확인하고, 유효하다면 새로운 access token을 발급한다. refresh token은 session처럼 서버에 저장되어 있기 때문에 문제가 발생하면 서버에서 refresh token을 만료시킬 수 있다.

작동 순서는 다음과 같다.

  1. 클라이언트가 서버에 로그인 요청을 보낸다.
  2. 서버는 클라이언트에게 access token과 refresh token을 발급한다.
  3. 클라이언트는 access token과 refresh token을 저장하고, 이후 요청 시에 access token을 헤더에 포함하여 서버에 요청을 보낸다.
  4. access token이 만료되면, 클라이언트는 refresh token을 사용하여 서버에 인증 요청을 보낸다.
  5. 서버는 refresh token의 유효성을 확인하고, 유효하다면 새로운 access token을 발급한다.

그렇다면 session 방식을 사용할 때와 다르게 JWT와 refresh token을 사용할 때 서버의 부하가 어떻게 변할까?

session 방식은 요청마다 서버에 저장된 세션을 확인해야 하므로, 서버에 부하가 더 많이 생길 수 있다. 하지만 JWT와 refresh token을 사용할 때는 access token이 만료되었을 때만 서버에 인증 요청을 보내기 때문에, 서버에 부하가 더 적게 생길 수 있다.

RTR (Refresh Token Rotation)

매번 새로운 refresh token을 발급하는 방식

기존 refresh token 방식의 보안을 더욱 강화한 방식이다. access token을 새로 발급할 때마다 refresh token도 함께 새로 발급한다.

RTR의 작동 순서

  1. 클라이언트가 서버에 로그인 요청을 보낸다.
  2. 서버는 클라이언트에게 access token과 refresh token을 발급한다.
  3. access token이 만료되면, 클라이언트는 refresh token을 사용하여 서버에 새로운 토큰을 요청한다.
  4. 서버는 refresh token의 유효성을 확인하고, 유효하다면 새로운 access token과 refresh token을 함께 발급한다.
  5. 이전 refresh token은 즉시 만료 처리된다.

RTR의 장점

  • 탈취된 refresh token의 재사용을 방지할 수 있다
  • refresh token의 수명을 더 길게 설정할 수 있다
  • 보안성이 향상된다

RTR의 단점

  • 구현이 복잡하다
  • 서버에 저장해야 할 데이터가 많아질 수 있다
  • 네트워크 통신이 더 발생한다

RTR 구현 시 고려사항

  • refresh token의 재사용 시도를 탐지하면 해당 사용자의 모든 토큰을 만료시켜야 한다
  • 클라이언트는 매번 새로 받은 refresh token을 안전하게 저장해야 한다
  • 네트워크 오류 등으로 인한 토큰 갱신 실패 상황을 고려해야 한다

정리

  • 쿠키 : 클라이언트에 저장되는 데이터
  • 세션 : 서버에 저장되는 데이터
  • JWT : 서명된 토큰 방식
  • refresh token : access token 재발급을 위한 방식
  • RTR : refresh token을 매번 새로 발급하는 보안 강화 방식

각각의 인증 방식은 장단점이 있으며, 서비스의 특성과 보안 요구사항에 따라 적절한 방식을 선택하면 된다.