본문 바로가기

개발

[Java, Spring] 로그인 기능 구현해보기 (쿠키를 통한 인증)

우리가 로그인을 얘기할 때 들어보는 쿠키, 세션, Jwt와 같은 도구들은 모두 Http 프로토콜의 stateless 특성에 기인한다. telnet, ssh와 같은 네트워크 프로토콜은 한번 접속을 하면 서버는 사용자가 누구인지 계속 알 수 있다. 다시 말해, state 한 상태를 유지한다.  

 

반면 Http의 경우 서버에 접속을 해 데이터를 요청한 후 연결을 끊는다. 서버 입장에서는 들어오는 요청은 늘 새로운 요청이고, 클라이언트가 누구인지에는 관심을 두지 않는다. 즉, stateless 하다는 것이다. 

 

따라서 사용자가 웹사이트 로그인 페이지에서 아이디와 비밀번호를 입력해 로그인한 뒤, 웹사이트의 다른 리소스에 접근하기 위해서는 서버에게 내가 누구인지 알리는 과정이 필요하다.

 

이를 인증 과정이라 부른다. 

 

가장 기초적인 방법으로 쿠키를 사용해 볼 수 있다.

 

최초 로그인 시 서버가 유저에게 인증 가능한 유저의 정보 (userId)를 담은 쿠키를 제공하고, 유저는 웹사이트의 리소스 접근 시 브라우저에 저장하고 있던 쿠키를 서버에게 전송해 인증을 진행하는 방식이다.

 

서버는 유저로부터 받은 쿠키에 userId가 존재하는지만을 확인하고 유저의 리소스 접근을 허가한다. (1번 유저인지, 2번 유저인지 확인하지 않기에 사실상 반쪽짜리 인증이라고 할 수 있다)

 

Java 21, SpringBoot 3.4.0, Thymeleaf를 사용해 클라이언트, 서버 통합개발 

 

▼ 로그인 컨트롤러

@PostMapping("/login")
    public String login(@ModelAttribute("loginRequest") LoginRequestDTO loginRequest, 
    					HttpServletResponse response) throws Exception {

        User user = loginService.login(loginRequest);

        if(user == null) {
            throw new Exception("아이디 혹은 비밀번호가 일치하지 않습니다.");
        }

        // 로그인 성공 시 쿠키 생성
        // 쿠키는 value 값을 문자열로 저장함
        Cookie cookie = new Cookie("userId", String.valueOf(user.getId()));
        cookie.setMaxAge(60 * 60);  // 쿠키 유효 시간 : 1시간
        response.addCookie(cookie);

        return "redirect:/home";
    }

 

▲ 로그인 이전 상태

 

userId 가 1 인 유저의 계정으로 로그인을 시도한다.

 

▼ 최초 로그인 이후 "userId"라는 name과 "1"이라는 value값을 가진 쿠키를 서버로부터 전달받았음을 알 수 있다.

 

이후 "코트 찾기" 페이지에 접속하고 싶은 유저는 코트 찾기 버튼을 클릭하고, 코트 찾기 페이지를 요청하는 HTTP GET요청에 쿠키가 담겨 서버로 전송된다. ▼

// 코트 찾기 요청
    @GetMapping(value = "/findLocation")
    public String findLocationPage(@CookieValue(name = "userId", required = true) Long userId,
    								Model model) {

        model.addAttribute("controllerLocationRequest", new ControllerLocationRequestDTO());

        return "findLocation";
    }

 

HttpServletRequest 객체에서 쿠키를 꺼내와 직접 검증할 수 있지만, Spring에서 재공 하는 @CookieValue 어노테이션을 이용해 코드 한 줄로 인증을 진행할 수 있다.

 

@CookieValue(name = "userId", required = true) Long userId의 경우

 

서버는 쿠키에 userId가 존재하는지 확인한 후 있으면 userId를 쿠키에서 가져와 변수 Long userId에 매핑한다.

 

여기서 @CookieValue 어노테이션이 String이었던 userId의 타입을 자동으로 Long으로 바꿔준다.

 

(required = true)는 userId가 존재하지 않으면, MissingRequestCookieException을 던진다. (400 Bad Request로 날아간다)

 

앞서 말했듯이 @CookieValue는 쿠키의 name, value의 존재여부 판단을 통해 (반쪽짜리) 인증을 진행한다. 인증과정에서 이러한 방식은 수 없이 많은 문제점을 야기한다. 

 

대표적인 예시로 userId 1번을 가진 악성 사용자 혹은 해커가 쿠키 탈취를 통해 userId를 2로 바꿔 접근할 수 도 있고, DB에 없는 userId로 접근해도 userId 형식만 맞다면 리소스들에 접근할 수 있다.

 

그렇다면 자연스럽게 (1) DB에 저장되어 있는 검증된 userId만 접근할 수 있고, (2) 그 안에서도 다른 userId로는 접근 못하게 하는 방법을 고민하게 될 것이다. 

 

userId를 암호화해 DB에 저장해 두고 이를 쿠키로 전달하는 방법은 해결책이 될 수 없을까?

 

(1)(2)를 모두 충족하는 방법처럼 보이지만, 해커가 쿠키를 탈취한 후 복호화 과정을 거쳐 민감한 정보인 userId를 알아낼 수 있고, 요청마다 DB를 접근할 경우 성능에 부하가 오게돼 대규모의 트래픽을 처리할 때 문제가 발생할 수 있다.

 

Session 로그인 방식과 Jwt 를 이용한 로그인 방식은 이러한 문제들을 해결할 수 있는 가장 널리 쓰이는 로그인 방법들이다. 

 

다음 포스트에서는 먼저 session을 이용한 로그인 방식을 다루어 보겠다.