CS

로그인 기능 작동 시 쿠키와 세션은 어떻게 작동하며 JWT 토큰은 왜 사용하는가?

쿠키담임선생님 2024. 10. 20. 21:47

 

 

cs 공부 중 로그인 시 일어나는 모든 동작에 대해 설명해 보아라 라는 질문은 보았는데 너무 겉핥기 식으로 알고있다는 자기반성을 하게 되었고, 한번 쭉 정리해보고 싶어서 공부한 내용을 정리하겠다.

 

세션 vs 쿠키


세션과 쿠키는 너무 대중적으로 알고 있는 정보이다. 

세션은 서버에게 정보제공, 쿠키는 클라이언트가 정보를 가지고 있음. 이건데 실제로는 어떤 기능을 많이 쓰는지 아는가?

정답은 세션이다.

 

그렇다면 왜 세션을 많이 쓰는지 아는가?


세션을 많이 사용하는 이유는 보안성 때문이다. 클라이언트에 하나하나 저장되는 쿠키들은 탈취의 가능성이 있다.

 

그렇다고 쿠키는 사용하지 않는 기능인가?


그건 아니다.

쿠키를 사용하는 상황은 예를들어 쇼핑 추천리스트 제공 기능이 있다.

우리가 보통 쇼핑하는 쇼핑몰들은 우리가 관심있게 쳐다본 상품목록이나 구매한 목록들을 쿠키로 저장시킨다.

 

이때 쿠키에 대한 정보를 서버로 전송하는 시점은 언제일까?


그건 개발하는 사람에 따라다르다. 로그인 시점이 될 수 도 있고, 최근 상품목록 페이지에 들어갔을 때가 되었을때 일수도 있다.

 

세션에는 사용자의 모든 개인 정보들이 저장되는가?


그렇다.

웹 사이트에 로그인 후 최근 구매 목록을 확인한다는 상황을 예시로 쭉 흐름을 따라가 보겠다.

1. 로그인 시 서버에서 해당 id를 가진 user 정보를 담은 session을 생성.

2. 해당 session id 값을 브라우저로 response.

3. 브라우저는 해당 session id 값을 쿠키로 가지고 있음.

4. 이후 브라우저에서 서버로 A라는 유저가 구매한 목록 요청 api 요청 시 session id 가 담긴 쿠키 값을 같이 서버에 전송.

5. 그러면 서버에서는 session id를 통해 어떤유저가 요청을 보내는 지 확인이 가능. (예시로 A라는 사용자임을 session id를 통해 확인했음을 가정)

6. 데이터 베이스를 조회 후 A라는 사용자가 구매한 목록을 response.

 

 

문제 

HttpSession.getAttribute("user") 라고 했을 시 , 사용자 A가 접속해도 "user"를 키로 가져오고, 사용자 B가 접속해도 "user"를 키로 가져온다. 같은 키를쓰는데 어떻게 A와B를 구분해서 가져오는가?


 

정답은 세션에 각 세션 아이디별로 고유한 영역이 생성되기에 호출하는 사람이 userA 라면 자동으로 userA.getAttribute로 호출이 되고,

 

호출하는 사람이 userB라면 자동으로 userB.getAttribute로 호출이 된다. (이게 결국 세션의 단점이다.)

 

사용자가 늘어날 수록 고유영역이 커지기 때문....

 

 

그럼 브라우저에서 쿠키는 어디에 포함시켜서 api를 호출할까?


쿠키는 http 헤더에 같이 들어간다. json 파일에 들어가는 것도 아니고, url에 들어가는 것도 아니다.

 

그러면 쿠키가 없을 경우에도 쿠키를 api로 전송할까?


그거는 아니다.....

서버에서 먼저 클라이언트(브라우저) 측으로 세션id가 포함된 쿠키를 전송했을 경우에만 모든 요청에 대해 쿠키를 포함시켜서 전송한다.

보내는 요청은 예시로 다음과 같다.

GET /recently-viewed HTTP/1.1
Host: www.example.com
Cookie: JSESSIONID=abc123456789xyz

 

하지만 이렇게 쿠키/세션을 활용 하면 문제가 생기지 않을까???


그렇다. 보안적으로 문제가 생긴다.

일단 보안적으로 user1 이라는 사용자가 만든 쿠키에 user1의 정보로 세션id를 만들어서 들어있다고 가정하자.

 

그런데 해커가 user1이가진 쿠키를 탈취해서 세션id로 서버에 접속하는것을 서버에서는 어떻게 검증할 수 있을까?

 

그냥 놓고 보면 서버는 이 부분을 검증 할 수 없다.

그래서 나온 기술들이 있다.

- https 프로토콜이다. 이를 사용하면 쿠키를 암호화해서 전송 할 수 있다.

- httpOnly 플래그를 사용하는 것이다. 이를 사용하면 자바 스크립트에서 쿠키에 접근할 수 없다.

- ip 주소를 쿠키에 같이 담는 것이다. 하지만 이를 사용하면 모바일 환경에서는 잘 구분할  수 없다는 문제가 있다.

- csrf 토큰 방식이다. 브라우저에서 서버로 api 요청을 하면 서버는 브라우저로 토큰을 하나 보내준다. 그러면 다시 브라우저에서 서버로 요청할 때 쿠키에 있는 session ID 뿐만 아니라 해당 토큰을 같이 보내주어 동일한 사용자가 접근했는지 구분한다.

- JWT 토큰 방식이 있다. 서버는 서명을 통해 jwt 토큰과 사용자가 유효한지 검증한다.

 

 

JWT 토큰 인증 방식을 사용하는 이유는 ???


서버는 session id를 브라우저를 통해 받게된다. 이뜻은 서버는 브라우저가 인증된 상태인지 아닌 상태인지를 알고 있다는 뜻이다. 이걸 stateful 하다고 말한다.

 

결국 세션에 사용자들의 세션아이디 정보들이 쌓이게 되고 만약 세션에 오류가 난다면 모든 사용자가 접속하지 못하는 문제도 생긴다.

해당문제의 예시는 세션의 수만큼 서버의 메모리를 차지해서 유저 수가 100만명이라면 세션도 그만큼 들어나게 된다.

 

 

JWT 토큰은 stateLess 이다.

이 뜻은 이미 토큰 내에 인증에 필요한 정보들이 다 들어있다는 뜻이다.

 

 

JWT 토큰 예시와 코드를 통해 조금 더 자세히 설명하겠다.


 

보통은 로그인 후 jwt 토큰을 발급 받는 과정 먼저 보여줄텐데 나는 jwt 토큰을 어떻게 이용하는지 부터 먼저 보여주겠다.

 

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/posts")
public class PostController {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private PostRepository postRepository;

    // 게시글 작성 메소드
    @PostMapping("/create")
    public ResponseEntity<String> createPost(@RequestHeader("Authorization") String token, 
                                             @RequestBody PostRequest postRequest) {
        try {
            // JWT에서 사용자 정보(예: userId) 추출
            Claims claims = jwtUtil.extractClaims(token.replace("Bearer ", ""));
            String userId = claims.get("userId").toString();  // JWT에 담긴 사용자 정보
            
            // 게시글 저장
            Post post = new Post();
            post.setTitle(postRequest.getTitle());
            post.setContent(postRequest.getContent());
            post.setUserId(userId);  // 작성자 정보 저장
            postRepository.save(post);

            return ResponseEntity.ok("Post created successfully");

        } catch (SignatureException e) {
            // JWT가 유효하지 않거나 변조된 경우
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token");
        }
    }
}

 

위 코드를 보면 요청 시 JWT 인증 하는 과정은 @RequestHeader("Authorization")을 통해 인증을 한다.

해당 토큰에는 사용자 정보들이 들어 있지만 복호화 하지 않으면 사용할 수 없다.

 

그래서 jwtUtil.extractClaims를 통해 복호화를 하면서 해당 요청이 올바른 사용자에게 왔는지 검증한다.

만약 다른 사용자에게 왔기에 서명이 기존 서명과 다르다면 catch문으로 빠지게 된다.

그리고 401 에러를 출력시킨다.

 

이제 jwt 토큰을 생성하는 코드와 검증하는 코드를 각각 알아보자


 

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {

    private static final String SECRET_KEY = "your_secret_key";  // 비밀 키

    // JWT 생성 메소드
    public String generateToken(String userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);  // payload에 사용자 정보 추가

        return Jwts.builder()
                .setClaims(claims)  // 사용자 정보 추가
                .setIssuedAt(new Date(System.currentTimeMillis()))  // 토큰 발급 시간
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))  // 토큰 만료 시간 (10시간)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)  // HMAC SHA-256 알고리즘으로 서명
                .compact();
    }
}

 

 

위 코드는 토큰을 생성하는 코드이다.

 

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;

public class JwtUtil {

    private static final String SECRET_KEY = "your_secret_key";  // 비밀 키

    // JWT 검증 메소드
    public Claims extractClaims(String token) throws SignatureException {
        try {
            // JWT 파싱 및 서명 검증
            return Jwts.parser()
                    .setSigningKey(SECRET_KEY)  // 서명 검증에 사용될 비밀 키
                    .parseClaimsJws(token)  // 토큰 파싱 및 서명 검증
                    .getBody();  // 유효한 경우 payload 정보 추출
        } catch (SignatureException e) {
            // 서명이 유효하지 않을 때 (토큰 변조 또는 비정상적인 경우)
            throw new SignatureException("Invalid JWT signature");
        }
    }
}

 

위 코드는 코드가 유효한지 검증하는 코드이다.

 

 

그런데 이렇게 해도 jwt 토큰을 탈취당할 수 있다.

 

탈취당하면 서버는 어차피 서명이 달라져서 유효하지 않은 jwt토큰임을 확인할 수 있다.

그러면 브라우저에서는 jwt토큰을 서버쪽에서 받은 다음에 어떻게 가공하길래 유효하지 않은 jwt토큰을 다시 서버로 전송하게 되는 것일까?

 

그리고 토큰 탈취를 예방하기 위해 "리프레쉬 토큰"이라는 기술을 또한 사용한다.