SpringBootProject

Project Part1 - 로그인 구현하기. (3. 쿠키를 활용한 로그인 기법)

dding-shark 2024. 11. 25. 10:05
728x90
  • 쿠키 :
    • 사용자가 웹사이트 접속시 사용자의 개인 장치에 다운로드되고 브라우저에 저장되는 작은 텍스트 파일을 사용해 통신한다.
  • 쿠키 기반 로그인의 장점 :
    1. 클라이언트에서 자동으로 인증 정보 관리
      • 쿠키는 클라이언트에 저장되어, HTTP 요청마다 자동으로 서버로 전송된다. 이를 통해 추가적인 인증 토큰을 매 요청마다 헤더에 추가할 필요가 없다.
      • 브라우저 자동 관리로 인해 개발자는 별도의 인증 상태 관리 코드를 최소화할 수 있다.
    2. 세션 만료 및 자동 로그아웃 설정 용이
      • 쿠키의 만료 시간을 설정하면 사용자가 세션을 지속하거나 자동으로 로그아웃할 수 있게 되어 보안이 용이하다.
      • 만료 시간이 지나면 쿠키가 자동 삭제되므로, 장시간 접속하지 않으면 보안 리스크가 감소문제가 있긴하다.
    3. 다양한 보안 설정 가능
      • HttpOnly 설정을 통해 JavaScript로 쿠키에 접근하지 못하도록 하여, XSS(교차 사이트 스크립팅) 공격에 대한 방어를 강화할 수 있다.
      • Secure 옵션을 통해 HTTPS 연결에서만 쿠키가 전송되도록 설정 가능해, 전송 시 데이터 암호화를 보장할 수 있다.
      • SameSite 속성을 설정하여, CSRF(사이트 간 요청 위조) 공격 방지에 도움을 줄 수 있다.
    4. 브라우저 호환성 및 표준 지원
      • 모든 주요 브라우저는 쿠키를 지원하며, 클라이언트와 서버 간 상호운용성이 높아 별도의 설정 없이 쿠키를 활용할 수 있다.
      • 쿠키는 HTTP 표준에 포함되어 있어, 서버-클라이언트 간 일관된 인증 방식을 사용할 수 있다.
    5. 간편한 인증 상태 유지
      • 사용자가 새로고침을 하거나 페이지를 이동해도 로그인 상태가 유지된다.
      • 로컬 스토리지나 세션 스토리지에 비해 자동화된 상태 관리가 용이하다.
    6. 서버에서 쉽게 관리 가능
      • 서버는 클라이언트가 보낸 쿠키 값을 통해 사용자의 로그인 상태를 확인하고, 세션 관리 로직을 일관되게 유지할 수 있다.
      • 특히 세션 ID를 쿠키에 저장해 서버에서 세션 데이터로 직접 관리할 때 유용하다.
  • 쿠키 기반 로그인 방식의 단점
    1. 보안 취약점 노출 가능성
      • XSS(교차 사이트 스크립팅) 공격 위험: 쿠키가 클라이언트에 저장되므로 악성 스크립트로 인해 쿠키가 유출될 가능성이 있습니다. 이를 통해 공격자는 사용자의 세션을 탈취할 수 있다.
        • 이를 완화하기 위해 HttpOnly 속성 설정이 필요하지만, 완벽한 방어는 못한다.
      • CSRF(사이트 간 요청 위조) 공격 위험: 사용자가 인증된 상태에서 악성 사이트의 요청을 통해 자동으로 서버에 쿠키가 전송될 수 있어, 서버에서 예기치 않은 동작이 발생할 위험이 있다.
        • 이를 방지하려면 쿠키에 SameSite 속성을 설정해야 하지만, 완전히 차단하기는 어렵다.
    2. 클라이언트 저장 공간 제한
      • 쿠키 용량 제한: 각 쿠키는 보통 4KB 정도로 제한되며, 브라우저별로 저장할 수 있는 쿠키의 개수에도 제한이 있다.
        • 용량 제한으로 인해 복잡한 데이터나 많은 정보를 쿠키에 저장할 수 없고, 큰 데이터는 세션이나 로컬 스토리지에 의존해야 한다.
    3. 네트워크 트래픽 증가
      • 매 요청 시 쿠키 전송: 쿠키는 클라이언트와 서버 간의 모든 HTTP 요청에 포함되어 전송되므로, 쿠키 크기가 클수록 네트워크 트래픽이 증가하고, 성능 저하를 유발할 수 있다.
        • 특히 대역폭이 제한된 네트워크 환경에서는 성능에 큰 영향을 미칠 수 있다.
    4. 사용자 설정에 의한 제약
      • 쿠키 비활성화 가능성: 일부 사용자는 개인정보 보호를 위해 브라우저에서 쿠키를 비활성화할 수 있습니다. 이 경우 쿠키를 기반으로 한 로그인 기능이 정상 작동하지 핞을 수 있다.
        • 쿠키가 비활성화된 경우를 대비한 별도의 예외 처리가 필요할 수 있다.
    5. 자동 로그아웃 및 세션 만료 문제
      • 쿠키 만료 관리의 복잡성: 쿠키 기반 세션 관리에서는 서버와 클라이언트 간 쿠키 만료 시간 동기화가 필요합니다. 만료 시간을 잘못 설정하면 예상치 못한 로그아웃 또는 세션 유지 문제가 발생할 수 있다.
    6. 민감한 정보 저장에 부적합
      • 데이터 보안 문제: 쿠키는 클라이언트 측에 저장되므로, 민감한 정보를 포함하지 않는 것이 권장된다.
        • 중요한 정보는 세션 ID 등으로 대체하고, 서버에서 데이터를 조회하는 방식으로 처리해야 한다..

쿠키를 활용한 컨트롤러 구현

3.1. 기법1. 쿠키 활용하기.

- 장점 : 편하다 쉽다! 간편하다!
- 단점 : 보안상의 이슈가 발생하기 쉽다.
  • 클라이언트 컴퓨터에 정보를 저장하고 꺼내 쓴다는 아이디어에서 나온 것 같은데, PASSWORD 처럼 민감한 정도를 저장하기엔 도전해야하는 문제들이 많이 생기는 것도 사실이다.
    하지만 기능에 따라서는 충분히 고려해볼만한 데이터 통신 방식임으로 나중에 쿠키를 갖고 뭔가를 하는 서비스를 개발하면(음.. 아마 장바구니 같은거에 활용 할 수 있을거 같다.) 그때 조금 고려해보자.
  • 하지만, 실습의 목적정도로 쿠키를 이용한 로그인(가장 쉽고 가장 직관적이다 라는 장점때문에) 먼저 구현을 해보도록 하겠다.

3.2 쿠키를 사용한 로그인 서비스 로직 (로그인 후에 서비스 사용에 관한것 까지)

  1. 로그인 성공 시 서버가 쿠키에 사용자 정보를 넣어줌
  2. 클라이언트 측에서는 다음 요청을 할 때마다 이 쿠키를 서버에 같이 보낸다. (로그인 한 User가 누구인지 알아야한다.)
  3. 서버에서는 이 쿠키를 확인해 로그인 했는지와 유저 정보, 권한 등을 확인할 수 있음. (Service 제공 목록이 달라진다. ex.내가 쓴 글 삭제에서 '내가'는 어떻게 알아낼 것인가? 의 문제)

기본적인 쿠키를 활용한 로그인 흐름도.

3.3 쿠키를 활용한 구현할 메소드 목록

  1. 쿠키 생성 작업.(로그인)
  2. 쿠키 생명시간 설정 및 파기 작업.(로그아웃)

3.4 구현

- 구현을 원래 Api만 개발하려 했지만, 기본적인 웹뷰 복습도 할겸 풀스택으로 개발하게 되었다.
    @PostMapping("/login")
    public String login(@ModelAttribute LoginRequest loginRequest, BindingResult bindingResult,
                        HttpServletResponse response, Model model) {
        ... 대충 로그인 구현 코드 

        // 로그인 성공 => 쿠키 생성
        Cookie cookie = new Cookie("userId", String.valueOf(user.getId())); //쿠키생성
        cookie.setMaxAge(60 * 60);  // 쿠키 유효 시간 설정: 1시간
        response.addCookie(cookie); // 쿠키 저장

        return "redirect:/cookie-login"; // 로그인후 페이지 이동
    }

    @GetMapping("/logout")
    public String logout(HttpServletResponse response, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        Cookie cookie = new Cookie("userId", null); // userId = null 로 유저 로그인 X 표현
        cookie.setMaxAge(0); // 쿠키 유효 시간 : 만료(0시간) 
        response.addCookie(cookie); //쿠키 생성
        return "redirect:/cookie-login"; // 로그아웃 후 페이지 이동
    }

    // 코드 구현에서 보이는것 처럼 User의 정보가 직접 저장되기때문에 보안상의 이슈가 발생하기 쉽다.

나머지 코드 구현및 테스트 사진

@Controller
@RequiredArgsConstructor
@RequestMapping("/cookie-login")
public class CookieLoginController {

    private final UserService userService;

    @GetMapping(value = {"", "/"})
    public String home(@CookieValue(name = "userId", required = false) Long userId, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        User loginUser = userService.getLoginUser(userId);

        if(loginUser != null) {
            model.addAttribute("nickname", loginUser.getNickname());
loginUser.getNickname());
        }

        return "home";
    }

    @GetMapping("/join")
    public String joinPage(Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        model.addAttribute("joinRequest", new JoinRequest());
        return "join";
    }

    @PostMapping("/join")
    public String join(@Valid @ModelAttribute JoinRequest joinRequest, BindingResult bindingResult, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        // loginId 중복 체크
        if(userService.checkLoginIdDuplicate(joinRequest.getLoginId())) {
            bindingResult.addError(new FieldError("joinRequest", "loginId", "중복된 로그인ID."));
        }
        // 닉네임 중복 체크
        if(userService.checkNicknameDuplicate(joinRequest.getNickname())) {
            bindingResult.addError(new FieldError("joinRequest", "nickname", "중복된 닉네임."));
        }
        // password와 passwordCheck가 같은지 체크
        if(!joinRequest.getPassword().equals(joinRequest.getPasswordCheck())) {
            bindingResult.addError(new FieldError("joinRequest", "passwordCheck", "비밀번호 일치 하지 않는다."));
        }

        if(bindingResult.hasErrors()) {
            return "join";
        }

        userService.join(joinRequest);
        return "redirect:/cookie-login";
    }

    @GetMapping("/login")
    public String loginPage(Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        model.addAttribute("loginRequest", new LoginRequest());
        return "login";
    }

    @PostMapping("/login")
    public String login(@ModelAttribute LoginRequest loginRequest, BindingResult bindingResult,
                        HttpServletResponse response, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        User user = userService.login(loginRequest);

        // 로그인 아이디나 비밀번호가 틀린 경우 global error return
        if(user == null) {
            bindingResult.reject("loginFail", "아이디 및 비밀번호를 확인해 주십시오");
        }

        if(bindingResult.hasErrors()) {
            return "login";
        }

        // 로그인 성공 => 쿠키 생성
        Cookie cookie = new Cookie("userId", String.valueOf(user.getId()));
        cookie.setMaxAge(60 * 60);  // 쿠키 유효 시간 : 1시간
        response.addCookie(cookie);

        return "redirect:/cookie-login";
    }

    @GetMapping("/logout")
    public String logout(HttpServletResponse response, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        Cookie cookie = new Cookie("userId", null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return "redirect:/cookie-login";
    }




    @GetMapping("/info")
    public String userInfo(@CookieValue(name = "userId", required = false) Long userId, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        User loginUser = userService.getLoginUser(userId);

        if(loginUser == null) {
            return "redirect:/cookie-login/login";
        }

        model.addAttribute("user", loginUser);
        return "info";
    }


//권한 인가에 관한 Test시 필요한 Admin 페이지 접근.
    @GetMapping("/admin")
    public String adminPage(@CookieValue(name = "userId", required = false) Long userId, Model model) {
        model.addAttribute("loginType", "cookie-login");
        model.addAttribute("pageName", "쿠키 로그인");

        User loginUser = userService.getLoginUser(userId);

        if(loginUser == null) {
            return "redirect:/cookie-login/login";
        }

        if(!loginUser.getRole().equals(UserRole.ADMIN)) {
            return "redirect:/cookie-login";
        }

        return "admin";
    }
}

728x90