인증과 인가, 그리고 세션과 JWT

이진일·2026년 4월 9일
post-thumbnail

인증과 인가란 무엇인가?

인증(Authentication)

  • 인증은 해당 유저가 실제 유저인지 인증하는 것이다.
  • 로그인을 생각하면 된다.

인가(Authorization)

  • 인가는 해당 유저가 특정 리소스에 접근이 가능한지 허가하는 개념이다.
  • 등급별로 접근 가능한 기능에 차이가 있다던지, 회원/비회원 여부에 따라 다른 권한을 받는 것을 생각하면 된다.

웹 애플리케이션에서의 인증

일반적으로 서버-클라이언트 구조로 되어 있고 HTTP 프로토콜을 이용하여 통신하는 그 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어져 있다.

비연결성(Connectionless)은 서버와 클라이언트가 연결되어 있지 않다는 것이다.
그 이유는 리소스를 절약하기 위해서 인데, 만약 서버와 클라이언트가 실제로 계속 연결되어있다면 클라이언트는 그렇다고 쳐도 서버의 비용이 기하급수적으로 늘어나기 때문이다.
그래서 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버리고있다 라고 생각하면 좋다.

무상태(Stateless)는 서버가 클라이언트의 상태를 저장하지 않는다는 것이다.
기존의 상태를 저장하는 것들도 마찬가지로 서버의 비용과 부담을 증가시키는 것 이기 때문에 기존의 상태가 없다고 가정하는 프로토콜을 이용해 구현되어 있다.
실제로 서버는 클라이언트가 직전에 혹은 그 전에 어떠한 요청을 보냈는지 관심도 없고 전혀 알지 못한다.

HTTP통신이 무상태로 이루어진다면 어떻게 우리가 서비스에 로그인해서 인증되어 계속 서비스를 이용할 수 있는걸까?
이를 해결하기 위해 일반적으로 웹 애플리케이션은 두 가지 방법으로 인증을 처리한다.

쿠키-세션 방식

쿠키-세션 방식은 무상태 원칙을 깨고 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식이다.
인증과 관련된 아주 약간의 정보만 서버가 가지고 있게 되고 유저의 이전 상태의 전부는 아니더라도 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다는 개념이다.

1. 사용자가 로그인 요청을 보낸다.
2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조한다.
3. 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣는다.
4. 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급한다.
5. 서버는 로그인 요청의 응답으로 session-id를 준다.
6. 클라이언트는 그 session-id를 쿠키라는 저장소에 보관하고 앞으로의 요청마다 세션아이디를 같이 보낸다. (주로 HTTP header에 담아서 보낸다.)
7. 클라이언트의 요청에서 쿠키를 발견했다면 서버는 세션 저장소에서 쿠키를 검증한다.
8. 만약 유저정보를 받아왔다면 이 사용자는 인증 성공
9. 이후에는 로그인 된 유저에 따른 응답을 준다.

JWT 기반 인증

JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미한다. JWT 기반 인증은 쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별한다.

1. 사용자가 로그인 요청을 보낸다.
2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조한다.
3. 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화 해서 내보낸다.
4. 서버는 로그인 요청의 응답으로 jwt 토큰을 내어준다.
5. 클라이언트는 그 토큰을 저장소에 보관하고 앞으로의 요청마다 토큰을 같이 보낸다.
6. 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증한다.
7. 이후에는 로그인 된 유저에 따른 응답을 내어준다.


쿠키와 세션 알아보기

쿠키

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일이다.
  • 구성 요소
    • Name: 쿠키를 구별하는데 사용되는 키
    • Value: 쿠키의 값
    • Domain: 쿠키가 저장된 도메인
    • Path: 쿠키가 사용되는 경로
    • Expires: 쿠키의 만료기한

쿠키 다루기

쿠키 생성

public static void addCookie(String cookieValue, HttpServletResponse res) {
    try {
        cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
        cookie.setPath("/");
        cookie.setMaxAge(30 * 60);

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e.getMessage());
    }
}
  • new Cookie(AUTHORIZATION_HEADER, cookieValue);
    • Cookie에 저장될 Name과 Value를 생성자로 받는 Cookie 객체 생성
  • setPath("/"), setMaxAge(30 * 60)
    • Path와 만료시간 지정
  • HttpServletResponse 객체에 생성한 Cookie 객체를 추가하여 브라우저로 반환
    • 이렇게 반환된 Cookie는 브라우저의 Cookie 저장소에 저장됨.
  • Cookie 생성은 범용적으로 사용될 수 있기 때문에 static 메서드로 선언함.
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
    System.out.println("value = " + value);

    return "getCookie : " + value;
}
  • @CookieValue("Cookie의 Name")
    • Cookie의 Name 정보를 전달해주면 해당 정보를 토대로 Cookie의 Value를 가져온다.

세션

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용된다.
  • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장한다.
  • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용된다.

세션 다루기

HttpSession 생성

@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
    HttpSession session = req.getSession(true);

    // 세션에 저장될 정보 Name - Value 를 추가합니다.
    session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");

    return "createSession";
}
  • HttpServletRequest를 사용하여 세션을 생성 및 반환할 수 있다.
  • req.getSession(true)
    • 세션이 존재할 경우 세션을 반환하고 없을 경우 새로운 세션 생성
  • 세션에 저장할 정보를 Name-Value 형식으로 추가한다.
  • 반환된 세션은 브라우저 Cookie 저장소에 ‘JSESSIONID’라는 Name으로 Value에 저장된다.

HttpSession 읽기

@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
    HttpSession session = req.getSession(false);

    String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
    System.out.println("value = " + value);

    return "getSession : " + value;
}
  • req.getSession(false)
    • 세션이 존재할 경우 세션을 반환하고 없을 경우 null을 반환한다.
  • session.getAttribute(”세션에 저장된 정보 Name”)
    • Name을 사용하여 세션에 저장된 Value를 가져온다.


JWT 알아보기

JWT(Json Web Token)란 JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 이다. 즉, 토큰의 한 종류라고 생각하면 된다.

JWT를 왜 쓸까?

쿠키-세션 방식을 사용할 때 서버가 2대 이상인 경우 서버마다 다른 Client 로그인 정보를 가지고 있을 수 있다. 그럼 특정 Client의 로그인 정보를 가지고 있지 않은 서버에 API요청을 하게 되면 문제가 발생할 수 있다.

이를 해결하는 방법은 두가지 정도가 있다.
1. Sticky Session: Client 마다 요청 Server를 고정하여 로그인 정보를 가지고 있지 않은 서버로 API요청을 보내지 않도록 한다.
2. Session Storage를 생성하여 모든 Session을 저장하야 모든 서버에서 모든 Client의 API 요청을 처리할 수 있도록 한다.

JWT를 사용하면 별도의 세션 저장소나 Sticky Session 설정 없이도 클라이언트를 식별할 수 있다.

JWT 기반 인증의 핵심 원리

JWT는 로그인 정보를 서버에 저장하지 않는다. 대신 클라이언트가 직접 암호화된 로그인 정보를 들고 있게 함으로써 인증과 인가를 처리한다.

  • 동일한 Secret Key 공유: 모든 서버가 동일한 Secret Key를 소유한다.
  • 검증 중심: 서버는 Secret Key를 사용해 토큰을 암호화하거나, 클라이언트가 보낸 토큰의 위조 여부를 검증(복호화 시)한다.

JWT 장단점

장점

  • 서버 부하 감소: 동시 접속자가 많아도 서버가 세션을 관리할 필요가 없어 부담이 적다.
  • 도메인 확장성: 카카오 OAuth 로그인처럼 클라이언트와 서버가 다른 도메인을 사용하는 환경에서도 유연하게 작동한다.

단점

  • 구현 복잡도: 세션에 비해 구현 방식이 복잡할 수 있다.
  • 네트워크 비용: 토큰에 담는 정보가 많아질수록 매 요청 시 전송되는 데이터 양이 늘어난다.
  • 제어의 어려움: 이미 생성된 토큰을 서버에서 강제로 만료시키기가 어렵다.
  • 보안 리스크: Secret Key가 유출되면 토큰 조작이 가능해지므로 관리에 주의가 필요합니다.

JWT 사용 흐름

Step 1. 로그인 성공 및 JWT 발급

사용자가 로그인을 성공하면 서버는 해당 정보를 Secret Key로 암호화하여 JWT를 생성하고, 이를 쿠키에 담아 클라이언트에게 전달한다.

// JWT를 생성하여 쿠키에 담는 과정
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value 쌍
cookie.setPath("/");

// Response 객체에 Cookie 추가하여 전달
res.addCookie(cookie);

전달된 JWT는 브라우저의 쿠키 저장소에 자동으로 저장된다.

Step 2. API 요청 시 인증 처리

클라이언트가 API를 요청할 때마다 브라우저는 쿠키에 포함된 JWT를 함께 보낸다. 서버는 요청 헤더의 쿠키에서 JWT를 추출하여 사용자를 식별한다.

// HttpServletRequest에서 JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if(cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                try {
                    // 인코딩된 값을 다시 디코딩하여 JWT 추출
                    return URLDecoder.decode(cookie.getValue(), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    return null;
                }
            }
        }
    }
    return null;
}

Step 3. 서버의 검증 및 응답

서버는 전달받은 JWT를 Secret Key로 대조하여 다음 사항을 확인한다.

  • 위조 여부: 토큰 데이터가 변조되지 않았는가?
  • 유효 기간: 토큰의 만료 시간이 지나지 않았는가?

검증이 성공하면 토큰 내부의 사용자 정보(Claim)를 확인하여, 해당 사용자가 요청한 리소스를 안전하게 응답한다.

JWT 다루기

1. 설정 및 초기화 (Setup)

JWT를 다루기 위해 가장 먼저 필요한 것은 Secret Key와 기초 설정이다.

  • 상수 설정: Header 키(Authorization), 권한 키(auth), 토큰 식별자(Bearer ), 만료 시간(60분) 정의
  • Secret Key 주입: 보안을 위해 application.properties에 Base64로 인코딩된 키를 저장하고 @Value로 가져감
  • @PostConstruct: 의존성 주입이 완료된 후, 딱 한 번만 실행되어 인코딩된 키를 실제 Key 객체로 변환한다. 이는 매 요청마다 키를 생성하는 낭비를 방지한다.

2. 사용자 권한 관리 (UserRoleEnum)

단순한 텍스트가 아닌 Enum을 사용하여 사용자 권한(USER, ADMIN)을 관리한다. 이는 오타를 방지하고 권한 체계를 명확히 하는 데 도움을 준다.

3. JWT의 핵심 기능 5단계

① 토큰 생성 (Create)
사용자 식별값(ID)과 권한 정보를 바탕으로 토큰을 만듭니다.

  • setSubject: 사용자의 ID를 담습니다.
  • claim: 권한 정보를 Key-Value 형태로 저장합니다.
  • signWith: 설정한 알고리즘(HS256)과 Key로 암호화합니다.

② 쿠키에 저장 (Add to Cookie)
생성된 토큰을 클라이언트에 전달하기 위해 쿠키에 담는다.

  • 쿠키 값에는 공백이 들어갈 수 없으므로 URLEncoder로 인코딩(Bearer%20...)하여 전달하는 것이 포인트다.

③ 토큰 잘라내기 (Substring)
클라이언트가 보낸 값에서 식별자인 Bearer 를 제외한 순수 JWT 문자열만 추출한다.

  • StringUtils.hasText와 startsWith를 통해 유효한 토큰 형태인지 먼저 검증한다.

④ 유효성 검증 (Validate)
토큰이 변조되지 않았는지, 만료되지는 않았는지 확인한다.

  • Jwts.parserBuilder()를 통해 파싱을 시도하며, 서명 오류나 만료 등 발생할 수 있는 다양한 예외 상황을 로그로 기록하여 문제를 쉽게 파악할 수 있게 한다.

⑤ 정보 추출 (Get Claims)
검증이 완료된 토큰의 Payload에서 사용자 정보를 가져옵니다.

  • 토큰의 정보 한 조각을 클레임(Claim)이라고 부르며, 이를 통해 별도의 DB 조회 없이도 사용자의 ID나 권한을 즉시 확인할 수 있다.

0개의 댓글