로그인 처리 I

허정현·2024년 9월 13일

Spring boot

목록 보기
4/4

부트캠프 전 간단한 게시판을 혼자 구현하고 있었는데,스프링 로그인 동작 과정에 있어서 스스로 이해하고자 작성함.HTTP에 대한 기본적인 개념등을 상세히 설명은 안함.단지 로직 구현에 있어서 기본적인 뼈대들만 정리하는중.


기본 로그인

회원가입 기능만 구현

로그인, 회원가입을 처리할 때, JoinForm으로 클라이언트의 데이터를 받아서 해당 값을 Repository에 저장한다.

회원가입시

  • Controller에서 사용자가 Form에 입력한 데이터를 처리해야한다.
  • Controller에서 POST매핑을 통해 해당 폼 데이터 입력 값을 ModelAttribute에 바인딩한다.
  • 바인딩된 해당 데이터를 처리하고 Repo에 전달하여 DB에 저장한다.

로그인시

  • 로그인 폼에 입력된 데이터를 Controller에서 POST매핑을 통해 폼 데이터 입력 값을 바인딩한다.
  • 해당 바인딩된 데이터를 Repo에서 일치하는지 조회한다.
  • 일치한다면 로그인에 성공하고, 실패한다면 로그인 화면으로 Redirect한다.

COOKIE

로그인의 상태를 유지하기 위한 방법

서버에서 로그인이 성공하면 HTTP응답에 쿠키를 담아서 브라우저에 전달한다.

동작과정

  • 클라이언트로부터 로그인 요청을 받는다.
  • 로그인에 성공할 시 쿠키를 생성하고, HTTP 응답 헤더에 담는다.
  • 웹 브라우저에서 종료전까지 회원의 id를 서버에 계속 보낸다.

쿠키 생성

  • 쿠키는 HttpServletRequest객체와 HttpServletResponse객체를 사용한다.
  • 쿠키 생성자
  public Cookie(String name, String value) {
        validation.validate(name);
        this.name = name;
        this.value = value;
    }
  • 응답에 쿠키를 추가
public void addCookieToResponse(HttpServletResponse response) {
    // 쿠키 생성: 이름과 값을 설정
    Cookie cookie = new Cookie("userToken", "abc123xyz");
    // 쿠키 속성 설정: 쿠키의 유효 기간을 1일로 설정 (단위: 초)
    cookie.setMaxAge(24 * 60 * 60); // 1일
    // 경로 설정: 애플리케이션 전체에서 쿠키가 유효하도록 설정
    cookie.setPath("/");
    // 보안을 위한 설정 (HTTPS 연결에서만 쿠키를 전송)
    cookie.setSecure(true);
    // HTTP 요청에만 쿠키가 전송되도록 설정
    cookie.setHttpOnly(true);
    // 응답에 쿠키를 추가
    response.addCookie(cookie);
}
  • 과정
  1. 로그인 메서드에 HttpServletResponse response를 추가한다. (쿠키를 응답에 담아서 전달하기 위해)
  2. 로그인 성공 로직에 성공했다면, Cookie idCookie = new Cookie("memberId","쿠키 값");을 통해 쿠키를 생성
  3. 응답에 쿠키를 담아서 전달하기 위해 addCookie(Cookie cookie) 메서드에 해당 쿠키를 값으로 전달
  4. return "redirect:/";를 통해 로그인 성공 홈화면으로 복귀

전체적인 로직 코드

loginController

@GetMapping("/")
    public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {

        if (memberId == null) {
            return "home";
        }

        //로그인
        Member loginMember = memberRepository.findById(memberId);
        if (loginMember == null) {
            return "home";
        }
        model.addAttribute("member", loginMember);
        return "loginHome.html";
    }

@PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
        // 폼쪽에서 오류가 났을 경우
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        // 로그인 검증
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        log.info("login? {}", loginMember);

        // 로그인 실패시
        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
        // 로그인 성공시
        // 쿠키에 시간 정보를 주지 않으면 세션 쿠키 ( 브라우저 종료시 모두 종료 )
        Cookie idcookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idcookie);

        return "redirect:/";
    }

@PostMapping("/logout")
    public String logout(HttpServletResponse response) {
        expireCookie(response, "memberId");
        return "redirect:/";
    }

    private void expireCookie(HttpServletResponse httpServletResponse, String cookieName) {
        Cookie cookie = new Cookie(cookieName, null);
        cookie.setMaxAge(0);
        httpServletResponse.addCookie(cookie);

    }

정리

  • 회원가입, 로그인 등의 기능이 구현되었더라면 로그인 화면과 일단 화면의 분리가 필요함.
  • 로그인 검증 -> 성공 -> 쿠키 생성 -> 헤더에 전달 -> loginHome
  • 로그인 검증 -> 실패 -> 로그인화면으로

특징

  • 쿠키 값을 웹 브라우저에서 사용자가 임의로 변경이 가능하다.
  • 클라이언트의 로컬에 저장되기 때문에 서버에 비해 안전하지도 않다.
  • 쿠키는 파일로 저장되기 때문에 브라우저를 종료해도 남아있다. 따라서, 라이프 사이클에 있어서 유리한 면도 있긴 하나, 만료시간이 적절하지 않을 경우 악용가능
  • 파일에 값이 있기 때문에 속도는 빠를 수 있다.

SESSION

동작 과정

서버에 중요한 자원을 보관하고 연결하는 방법으로 쿠키를 기반으로 하고 있으나, 서버에서 세션ID를 부여해 관리하기 때문에 안전함.
1. 클라이언트에서 loginId, password를 전달하면 서버에서 DB와 조회하여 확인함.
2. 성공할 경우 UUID를 서버에서 생성하여 세션의 이름과 값을 세션 저장소에 보관.
3. 서버는 클라이언트에 sessionId만 쿠키의 응답헤더에 담아서 전달한다.

세션 추가

생성

  • 세션 Id를 생성 -> 세션 저장소에 세션id,값쌍을 저장 -> sessionid로 응답 쿠키를 생성
/*
     * 세션 생성
     * sessionId 생성 ( 임의의 추정 불가능한 랜덤 값 )
     * 세션 저장소에 sessionId와 보관할 값 저장
     * sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
     * */
    public void createSession(Object value, HttpServletResponse response) {
        // 세션 아이디를 생성
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);
        // 쿠키 생성
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }

조회

   /*
     * 세션 조회
     * 리펙터링 전
     * */
    public Object getSession(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

만료

  public void expire(HttpServletRequest request) {
      Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
      if (sessionCookie != null) {
          sessionStore.remove(sessionCookie.getValue());
      }
  }
  private Cookie findCookie(HttpServletRequest request, String cookieName) {
      if (request.getCookies() == null) {
          return null;
      }
      return Arrays.stream(request.getCookies()).filter(cookie -> cookie.getName().equals(cookieName)).findAny().orElse(null);
  }

위의 SessionManager에 생성된 코드를 바탕으로 Login처리를 해야한다. MemberController에서 처리를 해야하므로, 해당 메서드를 작성해보자

  • LoginController에서 처리해야 할 로직이 뭐가 있을까
    - loginForm을 통해 들어온 form의 값을 우선bindingResult로 검증한다.
    - 데이터 타입 오류 검증에 문제가 없다면, memberId,password가 일치하는지 Repo를 통해 검증한다. loginService에서 처리
    - 일치했다면, 사용자에게 세션을 생성해서 전송해야한다.

    전체 코드

   @PostMapping("/login")
    public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
        // 성공시
        sessionManager.createSession(loginMember, response);
        return "redirect:/";
    }
  @GetMapping("/")
 public String homeLoginV2(HttpServletRequest request, Model model) {
//세션 관리자에 저장된 회원 정보 조회
  Member = member(Member)sessionManager.getSession(request); 
  if (member == null) {
         return "home";
     }
//로그인
model.addAttribute("member", member); 
  return "loginHome";
}

ServletSession

위의 코드를 보면 직접 Session을 생성하고, 검증하는 과정을 볼 수 있다.
다만, 편리하게도 서블릿과 스프링에서 Session을 생성해주고, 검증을 해주기 때문에 사이드 프로젝트를 할 때는 다음 코드들을 이용했다.

동작과정

  • login 컨트롤러에서 HttpSession를 사용하여 세션을 생성해주고, 세션에 회원 정보를 보관하면 된다.
  • logout 컨트롤러에서 session.invalidate()를 통해 세션을 제거할 수 있다.

컨트롤러 코드 로직

  • 처음의 컨트롤러에서 SessionManager에 작성된 메소드를 통해 세션을 생성했다면, 이제부터는
    HttpSession session=request.getSession(true)를 통해 세션을 생성할 수 있다.
    • request.getSession(true)
      세션이 있으면 기존 세션을 반환한다.
      세션이 없으면 새로운 세션을 생성해서 반환한다. request.getSession(false)
      세션이 있으면 기존 세션을 반환한다.
      세션이 없으면 새로운 세션을 생성하지 않는다. null 을 반환한다.
      session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); : 세션에 데이터를 보관한다.

코드

//sessionManager.createSession(loginMember,response);
  HttpSession session = request.getSession();
  session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
  return "redirect:/";
  public class SessionConst {
     public static final String LOGIN_MEMBER = "loginMember";
}
//세션을 삭제한다.
HttpSession session = request.getSession(false); if (session != null) {
         session.invalidate();
     }
     return "redirect:/";
//세션이 없으면 home
HttpSession session = request.getSession(false); if (session == null) {
         return "home";
     }
     Member loginMember = (Member)
 session.getAttribute(SessionConst.LOGIN_MEMBER);
//세션에 회원 데이터가 없으면 home if (loginMember == null) {
         return "home";
     }
//세션이 유지되면 로그인으로 이동 model.addAttribute("member", loginMember); return "loginHome";
}

이미 로그인 된 사용자를 찾을 때

  • 이제 로그인한 유저들만 Home에 들어가는 controller로직에도 Spring에서 어노테이션을 제공해준다
    이 기능은 세션을 생성하지 않는다.
    @SessionAttribute(name = "loginMember", required = false) Member loginMember
  GetMapping("/")
 public String homeLogin(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
 Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
  if (loginMember == null) {
         return "home";
     }
//세션이 유지되면 로그인으로 이동 
  model.addAttribute("member", loginMember); return "loginHome";
}

추가로, 세션의 타임아웃을 설정하려면 application.properties에서 설정하면 된다.

  • session.setMaxInactiveInterval();
  • 초단위로 설정됨.

흐름 정리

  1. 회원가입, 로그인을 구현할 때는 입력되는 폼 데이터를 검증하고, 로그인 성공 화면과 로그인 실패시 로그인에 성공한 회원만 홈페이지를 볼 수 있도록 구현해야한다. ( 물론 로그인 안 해도 홈 화면을 보고 기능만 사용할 수 없게끔 security를 통해서도 할 수 있지만, 말했듯이 여기서는 기본적인 게시판을 만들어보기 위함 )
  2. LoginForm을 통해 입력된 데이터에 대해 타입 검증이 끝나면 해당 유저가 있는지 Repo를 통해 검증한다.
  3. 해당 유저가 확인된다면 HttpSession을 사용하여 해당 유저에게 세션을 생성해주고, 기존 세션이 있다면 기존 세션을 반환해줘야한다.
  4. 사용자가 로그인에 성공하였을 시 내 정보를 확인할 수 있다.

과정

직접 로그인 컨트롤러에서 세션을 생성해봤음.
로그인이 완료된 멤버에 대해 member객체에 저장후,
response의 값과 함께 넘겨 sessionManager에서 해당 세션을 생성

// 세션 생성 
        SessionManager sessionManager = new SessionManager();
        sessionManager.createSession(member, response);

회원가입 후 로그인을 해보면
SessionManager : 세션 생성되었음을 볼 수 있음.

이제 개발자 도구를 통해서 session을 확인해보겠음.
UUID로 생성된 값과 mySessionId로 저장된 세션을 볼 수 있음.

최종 정리

쿠키(Cookie)

  • 저장 위치
    쿠키는 클라이언트(사용자의 브라우저)에 저장
    생성
    서버나 클라이언트(자바스크립트를 통해)에서 쿠키를 생성할 수 있음. 서버는 Set-Cookie 헤더를 통해 클라이언트에 쿠키를 전송
  • 수명
    쿠키에는 만료 시간(Expiration Time)을 설정할 수 있음 만료 시간이 설정되지 않은 쿠키는 세션 쿠키(Session Cookie)가 되어 브라우저가 닫힐 때 삭제
    만료 시간이 설정된 영구 쿠키(Persistent Cookie)는 지정된 시간이 지나면 자동으로 삭제
  • 라이프사이클
    서버가 Set-Cookie 헤더를 통해 클라이언트에 쿠키 전송. 클라이언트는 쿠키를 저장하고, 이후 같은 도메인으로 요청할 때마다 Cookie 헤더에 쿠키를 포함시켜 서버에 전송. 쿠키가 만료되거나, 클라이언트에서 삭제할 때까지 사용 가능
  • 삭제
    브라우저에서 직접 삭제하거나, 쿠키의 만료 시간을 과거로 설정하여 삭제할 수 있음.

세션(Session)

  • 저장 위치
    세션 데이터는 서버에 저장되고, 세션 ID만 클라이언트의 쿠키 또는 URL 파라미터에 저장
  • 생성
    클라이언트가 서버에 접속할 때 서버는 세션을 생성하고 고유한 세션 ID를 발급하여 클라이언트에 전달 클라이언트는 쿠키를 통해 세션 ID를 서버에 다시 전달하여 서버에서 세션 데이터를 조회
  • 수명
    세션은 서버 측에서 관리되며, 보통 세션 타임아웃을 설정하여 지정된 시간이 지나면 세션이 만료
    클라이언트가 일정 시간 동안 서버에 요청을 보내지 않으면 세션이 만료
    세션은 서버가 재시작되거나, 명시적으로 삭제될 때도 종료될 수 있음.
  • 라이프사이클
    클라이언트가 서버에 접속하면 서버는 새로운 세션을 생성하고 세션 ID를 발급.
    클라이언트는 세션 ID를 쿠키에 저장하고 이후 요청 시 서버에 세션 ID를 포함하여 전송.
    서버는 세션 ID를 통해 클라이언트의 세션 데이터를 조회하고, 필요 시 데이터 갱신.
    세션이 만료되거나 삭제될 때까지 유지.
  • 삭제
    서버 측에서 세션 만료 시간을 설정하거나 명시적으로 세션을 삭제할 수 있습니다.

2024-09-20T13:56:32.763+09:00  INFO 34146 --- [board] [nio-8080-exec-3]t.board.controller.BoardController       
: 게시판 바인딩 성공 = BoardDTO(writer=local, title=hellomyn, content=my name is hello world ! ) 
profile
지식 정리를 잘 할 수 있을 때까지

0개의 댓글