6. 로그인 처리1 - 쿠키, 세션

2024. 5. 2. 18:28·Spring/MVC 2

로그인 요구사항

1) 로그인 사용자만 상품에 접근하고, 관리할 수 있다.

2) 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동한다.

 


 

1. 로그인 V1 - new Cookie()

로그인 시 response에 쿠키를 같이 담아 반환하면서,

이후 사용자가 재방문 시, 쿠키 정보를 통해 유효한 사용자인지 판별할 수 있다.

 

 

문제점.

- 쿠키 값 임의 변경 가능

 

- 쿠키 값 유추 가능

     → request에서 쿠키 정보를 조작해서 보낼 시, 타 유저의 데이터가 노출될 수 있다.

 

- 쿠키에 보관된 정보 탈취

     → 웹 브라우저 / 네트워크 요청 과정에서 해킹의 위험이 있다.

 

- 쿠키 유효 시간 없음

 

 

[LoginController]

// 로그인 성공 시: 클라이언트에 new Cookie("memberId", getid) 를 넘겨준다.
// 로그인 실패 시: BindingResult에 에러 코드와 메시지를 담고, 로그인 뷰를 다시 띄운다.

@PostMapping("/login")
public String loginV1(@Validated @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";
    }

    // 로그인 성공 처리 TODO

    // 쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료 시 모두 종료)
    Cookie cookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(cookie);
    return "redirect:/";
}
// 로그아웃
// 같은 named의 시간 정보를 0인 새로운 쿠키를 response에 담아, home으로 redirect 한다.

@PostMapping("logout")
public String logoutV1(HttpServletResponse response) {
    Cookie cookie = new Cookie("memberId", null);
    cookie.setMaxAge(0);
    response.addCookie(cookie);

    return "redirect:/";
}

 

 

 

[LoginService]

// MemberRepo 에서 loginId와 일치한 Member를 찾고 password를 비교한다.

public class LoginService {
    private final MemberRepository memberRepository;

    public Member login(String loginId, String password) {
        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password))
                .orElse(null);
    }
}

 

 

 

[MemberRepository]

// loginId와 일치한 Member를 찾는다.

public Optional<Member> findByLoginId(String loginId) {
    return findAll().stream().filter(m -> m.getLoginId().equals(loginId)).findAny();
}

 

 

 

[HomeController]

// 로그인 성공 : Cookie 정보를 바탕으로 loginHome
//   cf. 쿠키 정보가 있어도, 관련된 DB의 회원이 없으면 false
// 로그인 x : 기존 home

@GetMapping("/")
public String loginHomeV1(@CookieValue(name = "memberId", required = false) Long value, Model model) {
    if (value == null) {
        return "home";
    }

    // View에 띄우기 위해, model에 loginMember 정보 전달
    Member loginMember = memberRepository.findById(value);
    model.addAttribute("member", loginMember);
    return "loginHome";
}

 

 


 

2. 로그인 V2 - 세션 동작 방식

쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤)을 생성해 클라이언트와 서버를 연결한다.

그리고 서버에서는 토큰을 관리하면서 토큰 - 사용자 id를 매핑해서 사용자를 인식한다. 추가로 토큰의 만료 시간을 짧게(30분) 유지한다.

 

 

[SessionManager]

@Component
public class SessionManager {
    private static final String SESSION_COOKIE_NAME = "sessionId";
    private Map<String, Object> sessionTable = new ConcurrentHashMap<>();

    // 1. 세션을 생성하고, 세션 테이블에 저장하기
    // Response에 담아야 한다.
    public void createSession(Object value, HttpServletResponse response) {

        // 세션 id를 생성하고, 값을 세션에 저장
        String sessionId = UUID.randomUUID().toString();
        sessionTable.put(sessionId, value);

        // 쿠키 생성
        Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(cookie);
    }

    // 2. 세션 만료
    public void expire(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);

        // 세션 테이블에서 제거
        if (sessionCookie != null) {
            sessionTable.remove(sessionCookie.getValue());
        }

    }

    // 3. 세션 조회 (Request 요청)
    public Object getSession(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        
        if (sessionCookie == null) {
            return null;
        }

        // 세션 테이블에 세션 id와 일치한 데이터가 있다면 반환
        return sessionTable.get(sessionCookie.getValue());
    }

    // 여러 쿠키 목록 중, 세션 쿠키 찾기
    public Cookie findCookie(HttpServletRequest request, String CookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return null;
        }

        return Arrays.stream(cookies)
                .filter(cookie -> cookie.getName().equals(CookieName))
                .findAny()
                .orElse(null);
    }
}

 

 

[LoginController]

// UUID 랜덤 키를 만들어 loginMember와 매핑지어 세션 테이블에 저장
// 그리고 세션 id로 쿠키 만들어 response에 담아둔다.

@PostMapping("/login")
public String loginV2(@Validated @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:/";
}
// 로그아웃
// 세션 테이블에서 request의 쿠키 중 세션 id와 같은 행 제거

public String logoutV2(HttpServletRequest request) {
    sessionManager.expire(request);

    return "redirect:/";
}

 

 

 

[HomeController]

// 세션 테이블에서 Request의 세션 id와 일치한 회원 정보 조회

@GetMapping("/")
public String loginHomeV2(HttpServletRequest request, Model model) {

    Member session = (Member) sessionManager.getSession(request);

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

    // View에 띄우기 위해, model에 loginMember 정보 전달
    model.addAttribute("member", session);
    return "loginHome";
}

 

 


 

3. 로그인 V3 - 서블릿 HTTP 세션

HttpSession

: 사용자가 웹 서버와 상호작용하는 동안 여러 요청에 걸쳐 상태 정보를 유지하는 방법 제공

ex. Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05`

 

 

[SessionConst]

// HttpSession에 데이터를 보관하고 조회할 때, 같은 이름이 중복되어 사용되므로 상수로 정의

public interface SessionConst {
    String LOGIN_MEMBER = "loginMember";
}

 

 

[LoginController]

// 로그인 사용자에게 신규 세션 id 발급
// 세션 테이블 설정

@PostMapping("/login")
public String loginV3(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
    if (bindingResult.hasErrors()) {
        return "/login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
    if (loginMember == null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 잘못되었습니다.");
        return "/login/loginForm";
    }

    // 로그인 성공 처리
    // 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
    HttpSession session = request.getSession();

    // 세션에 로그인 회원 정보 보관
    // UUID.RANDOM : 세션 id
    // loginMember, SessionConst.LOGIN_MEMBER : 세션 테이블에서 세션 id와 매핑된 객체
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:/";
}
// 로그아웃
// Request 요청에서 현재 삭제해야 할 세션을 찾고, 삭제한다.

@PostMapping("logout")
public String logoutV3(HttpServletRequest request) {
    HttpSession session = request.getSession(false);

    if (session != null) {
        session.invalidate(); // 삭제
    }

    return "redirect:/";
}

 

 

 

[HomeController]

1) 직접 request에서 유효한 세션 id가 있는지 조회

// Request 요청에서 사용자 세션이 있다면, 세션 테이블에서 검색
// 로그인 사용자 : loginHome
// 로그인 x : home

@GetMapping("/")
public String loginHomeV3(HttpServletRequest request, Model model) {
    HttpSession session = request.getSession(false);
    if (session == null) {
        return "home";
    }

    // 로그인 시점에 세션에 보관한 회원 객체를 조회한다.
    Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);

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

    // View에 띄우기 위해, model에 loginMember 정보 전달
    model.addAttribute("member", loginMember);
    return "loginHome";
}

 

 

2) 어노테이션으로 request에서 유효한 세션 id가 있는지 조회.

세션 id로 세션 테이블에서 세션 객체 검색, 세션 객체 내에서 필요한 데이터 추출

 

@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember

→ 세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 편리하게 처리

@GetMapping("/")
public String loginHomeV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
    // 세션에 회원 데이터가 없으면 home
    if (loginMember == null) {
        return "home";
    }

    // 세션이 유지되면 로그인으로 이동
    // View에 띄우기 위해, model에 loginMember 정보 전달
    model.addAttribute("member", loginMember);
    return "loginHome";
}

 

 


 

4. TrackingModes

로그인을 처음 시도하면 URL에 'jsessionid'를 포함하고 있는 것을 볼 수 있다.

 

jsessionid는 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.

서버는 웹 브라우저가 쿠키 지원 유무를 처음에는 알지 못하기 때문에, 쿠키 값과 함께 jsessionid를 함께 전달한다.

 

URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음과 같은 옵션을 넣으면 된다.

 

 

[application.properties]

server.servlet.session.tracking-modes=cookie

 

저작자표시 (새창열림)

'Spring > MVC 2' 카테고리의 다른 글

8. 예외 처리와 오류 페이지  (0) 2024.05.05
7. 로그인 처리2 - 필터, 인터셉터  (0) 2024.05.04
5. 검증 2 - Bean Validation  (0) 2024.05.01
4. 검증 1 - Validation  (0) 2024.04.29
3. 메시지, 국제화  (0) 2024.04.29
'Spring/MVC 2' 카테고리의 다른 글
  • 8. 예외 처리와 오류 페이지
  • 7. 로그인 처리2 - 필터, 인터셉터
  • 5. 검증 2 - Bean Validation
  • 4. 검증 1 - Validation
wch_t
wch_t
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (171)
      • Architecture (0)
      • Algorithm (67)
        • Math (5)
        • Simulation (1)
        • Data Structure (4)
        • DP (7)
        • Brute Fource (10)
        • Binary Search (6)
        • Greedy (2)
        • Graph (11)
        • Mst (1)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (2)
        • Mitm (1)
      • CS (2)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (6)
      • DevOps (17)
        • AWS (9)
        • Docker (1)
        • CI-CD (5)
      • Error (1)
      • Project (0)
        • kotrip (0)
      • Spring (59)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (8)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • Test (2)
      • TIL (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준 17289 파이썬
    spring-cloud-starter-bootstrap
    aws secrets manager
    Jenkins
    백준 17299 파이썬
    response_mode
    spring-cloud-starter-aws-secrets-manager-config
    form_post
    Merge
    애플
    Sxssf
    docker
    TempTable
    scope
    view algorithm
    백준 3015 파이썬
    apache poi
    docker: not found
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
6. 로그인 처리1 - 쿠키, 세션
상단으로

티스토리툴바