mvc 2 로그인 처리하기(쿠키, 세션) 1

이주인·2023년 2월 4일
0

스프링 공부

목록 보기
5/11

로그인 처리하기(쿠키, 세션)

로그인 요구 사항

  • 로그인 사용자만 상품에 접근하고, 관리할 수 있음
  • 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동

쿠키를 사용하여 로그인 상태를 유지하는 법

  • 서버에서 로그인에 성공하면 http 응답에 쿠키를 담아 브라우저에 전달한다.
  • 모든 요청에 이 쿠키 정보를 자동으로 포함시킨다.
  • 쿠키를 확인하여 로그인 상태를 유지한다.

package 구조

domain

  • item
  • member
  • login

web

  • item
  • member
  • login

도메인: 화면, ui, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비지니스 업무 영역

회원가입 구현

Member

@Data
public class Member {

    private Long id;
    @NotEmpty
    private String loginId;
    @NotEmpty
    private String name;
    @NotEmpty
    private String password;
}

회원가입시 받는 정보는 id, password, 사용자 이름이다.

MemberRepository

@Slf4j
@Repository
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    
    //회원가입시 작성된 정보 저장
    public Member save(Member member){
        member.setId(++sequence);
        log.info("save: member = {}", member);
        store.put(member.getId(), member);
        return member;
    }

	//id를 사용한 member 검색 기능
    public Member findById(Long id){
        return store.get(id);
    }

	//login id를 받아 password를 조회하는 기능
    public Optional<Member> findByLoginId(String loginId) {
        return findAll().stream()
                .filter(m -> m.getLoginId().equals(loginId))
                .findFirst();
    }
    
    public List<Member> findAll(){
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

member를 저장하고 관리하는 클래스

MemberController

@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

    private final MemberRepository memberRepository;

	//
    @GetMapping("/add")
    public String addForm(@ModelAttribute("member") Member member){
        return "members/addMemberForm";
    }

	//만약 비정상적인 id나 password를 생성했을 경우(bindingResult에 에러가 들어있을 경우), 회원가입 창으로 이동
    @PostMapping("/add")
    public String save(@Valid @ModelAttribute Member member, BindingResult bindingResult){
        if (bindingResult.hasErrors()){
            return "members/addMemberForm";
        }

        memberRepository.save(member);
        return "redirect:/";
    }
}

@Valid: view에서 전달 받은 값에 붙여 값을 검증
BindingResult: 스프링이 제공하는 검증 오류 보관 객체 -> 데이터에 유효하지 않은 속성이 있다면, 그에 대한 오류 정보가 담긴다


회원가입 뷰 템플릿(addMemberForm.html)

<h4 class="mb-3">회원 정보 입력</h4>
    <form action="" th:action th:object="${member}" method="post">
        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}"
               th:text="${err}">전체 오류 메시지</p>
        </div>
        <div>
            <label for="loginId">로그인 ID</label>
            <input type="text" id="loginId" th:field="*{loginId}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{loginId}"/>
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" id="password" th:field="*{password}"
                   class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}"/>
        </div>
        <div>
            <label for="name">이름</label>
            <input type="text" id="name" th:field="*{name}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{name}"/>
        </div>


로그인 기능 구현

쿠키를 활용하여 로그인 기능을 구현한다.

LoginService

@Service
@RequiredArgsConstructor
public class LoginService {
    private final MemberRepository memberRepository;

	//로그인 id와 비번을 받은후, 올바른 비밀 번호인지 판단하는 기능
    public Member login(String loginId, String password) {
        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password))
                .orElse(null);
    }
}

로그인의 핵심 비지니스 로직

id를 이용해 password를 조회한 후, 파라미터로 넘어온 password와 비교하여 같으면 회원(member)을 반환하고, password와 다르면 null을 반환

LoginForm

@Data
public class LoginForm {
    @NotEmpty
    private String loginId;
    @NotEmpty
    private String password;
}

id와 password만 받아 값을 넘긴다.

LoginController

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;
	private final SessionManager sessionManager;

    
    //@Model...은 타임리프에서 값을 한번에 받아오기 위해 사용
    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form){
        return "login/loginForm";
    }
	
    @PostMapping("/login")
    public String loginForm(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse resp){
        
        //만약 로그인이 안되어 있을 경우
        if (bindingResult.hasErrors()){
            return "login/loginForm";
        }
        
        //회원을 조회하는 로직 호출
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

		//로직에서 null 반환시(회원이 존재하지 않을 시) 
        if (loginMember == null){
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않음");
            return "login/loginForm";
        }
        
        /*
        로그인 성공시 세션 쿠키 생성
        쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
        */
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        resp.addCookie(idCookie);
        return "redirect:/";
    }
    
    //세션 및 필터 적용
    @PostMapping("/login")
    public String loginFormV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
                              @RequestParam(defaultValue = "/") String redirectURL,HttpServletRequest request){
        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";
        }
        //로그인 성공처리
        //세션이 존재할 경우 존재하는 세션 반환
        //없으면 신규세션을 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원정보를 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
		//인터셉터 되었을 경우, 로그인이 된 뒤 원래 요청했던 페이지로 반환
        return "redirect:" + redirectURL;
    }


}

쿠키 생성로직

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
resp.addCookie(idCookie);
  • 로그인 성공시 쿠키를 생성한 후 HttpServletResponse에 담는다.
  • 쿠키 이름은 merberId이고, 값은 회원의 id를 담아둔다.
    -> 웹 브라우저는 종료전까지 회원의 id를 서버로 계속 보낼 것이다.

loginForm.html

</div>
    <form action="item.html" th:action th:object="${loginForm}" method="post">
        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}"
               th:text="${err}">전체 오류 메시지</p>
        </div>
        <div>
            <label for="loginId">로그인 ID</label>
            <input type="text" id="loginId" th:field="*{loginId}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{loginId}"/>
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" id="password" th:field="*{password}"
                   class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}"/>
        </div>

입력받은 id와 password를 LoginController로 전송한다.

HomeController

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final MemberRepository memberRepository;

	//쿠키가 존재하는 지 확인하는 절차가 포함됨
    //@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";
    }
}
	/*
    @SessionAttribute <- 세션을 더 편리하게 사용하는 어노테이션
    세션에 들어있는 데이터를 찾는 과정을 편리하게 처리
    */
    @GetMapping("/")
    public String homeLoginV4(
            @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model){

        //login
        //세션에 회원 데이터가 없으면 home
        if (loginMember == null){
            return "home";
        }
        //세션이 유지되고, 데이터가 존재하는 것을 확인할 경우 -> 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

로직

  • 로그인 쿠키(merberId)가 없는 사용자는 기존 home으로 전송
  • 로그인 쿠키가 존재할 경우, 회원정보가 존재하지 않으면 home으로
  • 회원 정보가 존재할 경우, 로그인 사용자 전용 화면으로 반환


로그아웃 기능

로그아웃 되는 조건

  • 웹브라우저 종료시
  • 해당쿠키의 종료날짜가 0으로 설정될 시

LoginController - logout

//로그아웃 기능
    @PostMapping("/logout")
    public String logout(HttpServletResponse resp){
        expireCookie(resp, "memberId");
        return "redirect:/";
    }

	//쿠키의 종료날짜(MaxAge)를 0으로 지정하여 종료
    private void expireCookie(HttpServletResponse resp, String memberId) {
        Cookie cookie = new Cookie(memberId, null);
        cookie.setMaxAge(0);
        resp.addCookie(cookie);
    }
}
	//세션 매니저를 이용한 로그아웃 구현
    //@PostMapping("/logout")
    public String logoutV2(HttpServletRequest request){
        sessionManager.expire(request);
        return "redirect:/";
    }

    @PostMapping("/logout")
    public String logoutV3(HttpServletRequest request){
        HttpSession session = request.getSession(false);
        if (session != null){
        	//세션을 제거하는 로직
            session.invalidate();
        }
        return "redirect:/";
    }

문제점

쿠키를 이용하여 로그인 id를 전달해서 로그인을 유지하는 경우, 다음과 같은 문제가 발생한다

  1. 쿠키의 값을 임의로 변경할 수 있는 문제
  2. 쿠키에 보관된 정보를 훔쳐가는 문제
  3. 훔처간 정보를 계속해서 사용하는 문제

대안

  1. 쿠키에 중요한 값을 노출하지 안는다.
  2. 해커가 예상하지 못할 임의의 값을 넣는다.
  3. 일정시간이 지나면 토큰의 만료시간을 짧게 설정


세션

클라이언트와 서버는 쿠키로 연결이 되어야한다.

  • 서버는 클라이언트에 mtSessionId라는 이름으로 세션ID만 쿠키에 담아 전달
  • 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관
  • 클라이언트는 요청시 항상 mtSessionId 쿠키를 전달하며, 서버에선 전달받은 쿠키 정보로 세션 저장소를 조회한다

세션관리의 세가지 기능

  1. 세션 생성
    • sissionId 생성
    • 세션 저장소에 sessionId와 보관할 값을 저장
    • 응답 쿠키를 생성후 클라이언트에 전달
  2. 세션 조회
    • 클라이언트가 요청한 sessionId 쿠키의 값으로 세션저장소에 보관한 값을 조회
  3. 세션 만료
    • 세션저장소에 저장된 sessionId와 값을 제거

HttpSession

서블릿에서 세션관리를 위해 HttpSession이라는 기능을 제공한다.

세션 생성

  • request.getSession()
  • 세션 생성시
    • ...(true): 기존 세션 반환/ 새 세션 생성 후 반환
  • 세션을 찾아서 사용시
    • ...(false): 기존 세션 반환/세션 없으면 null 반환

세션에 로그인 회원정보 보관

  • session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
  • 하나의 세션에 여러 값을 저장할 수 있다

세션에서 로그인 회원정보 조회

  • session.getAttribute(SessionConst.LOGIN_MEMBER)
  • 로그인 시점에 세션에 보관한 회원객체를 찾는다
  • @SessionAttribute를 이용하여 조회할 수 있다

세션 제거

  • session.invalidate()

SessionConst

public class SessionConst {
 public static final String LOGIN_MEMBER = "loginMember";
}

데이터를 보관 및 조회시 같은 이름이 중복되어 사용되므로 상수를 하나 정의함

LoginController의 로직

        /*
        로그인 성공처리
        세션이 존재할 경우 존재하는 세션 반환
        없으면 신규세션을 생성
        */
        HttpSession session = request.getSession();
        //세션에 로그인 회원정보를 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
		//인터셉터 되었을 경우, 로그인이 된 뒤 원래 요청했던 페이지로 반환
        return "redirect:" + redirectURL;
    }

logout의 로직

@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
 //세션을 삭제한다.
 	HttpSession session = request.getSession(false);
 	if (session != null) {
    //세션 제거 로직
 	session.invalidate();
 }
 	return "redirect:/";
}

세션 타임아웃 설정

보통 사용자가 서버에 최근에 요청한 시간을 기준으로 일정시간을 유지한다.
-> HttpSession이 사용하는 방식

스트링 부트의 설정파일(application.properties)에 다음 설정을 추가한다.

session.setMaxInactiveInterval(1800); //1800초

profile
소프트웨어공

0개의 댓글