Spring MVC : 로그인 처리하기
- Spring/Spring MVC
- 2022. 1. 2.
이 포스팅은 인프런 영한님의 강의를 듣고 정리한 내용입니다.
로그인 처리의 필요성
지금까지 만들어온 웹 페이지에 로그인 처리기능을 추가하고자 한다. 회원인 사람들만 내가 만든 웹 페이지에서 어떠한 수정 작업을 할 수 있어야 하기 때문이다. 이런 기능을 위해서 아래 화면이 추가되고자 한다.
- 로그인이 되지 않은 사람은 가장 왼쪽의 홈 화면으로 접근해야만한다.
- 홈 화면에서 회원 가입 및 로그인 버튼을 통해 필요한 기능을 할 수 있다.
- 로그인이 된 회원은 홈 화면으로 들어왔을 때, 우측의 상품 목록으로 리다이렉트 해야한다.
위와 같은 기능을 추가해보고자 한다.
로그인 기능은 어떻게 구현할까?
로그인 로직은?
로그인 처리를 하기 위해서는 서버는 클라이언트에게 ID와 비밀번호만 전달해주면 된다. 그리고 서버는 클라이언트가 전달한 ID와 비밀번호로 회원이 맞는지, 그리고 회원이 맞다면 비밀번호는 맞는지를 찾아서 로그인을 처리해주면 된다. 그렇다면 클라이언트와 서버는 어떻게 로그인이 유지된다는 것을 인지할 수 있을까?
로그인 유지는?
클라이언트와 서버의 연결은 비연결성이다. 따라서 항상 연결되어 있는 것이 아니라, 요청이 올 때 마다 연결이 된다. 따라서 로그인 유지를 하기 위해서는 핵심 아이템이 필요하다. 이 때 필요한 핵심은 바로 '쿠키'다. 쿠키를 일종의 PK 값으로 사용해서, 클라이언트가 보낸 쿠키를 바탕으로 서버는 이 사람이 로그인한 사람인지를 계속 판단하는 것이다.
최초 로그인이 되었을 때, 서버에서 이에 대한 증거로 쿠키를 하나 만들어서 내려준다. 그리고 클라이언트는 앞으로 서버에 요청을 할 때 마다 그 쿠키를 포함해서 보내준다. 서버는 그 쿠키를 받으면, 로그인 처리된 사용자인지를 판단해서 필요한 형태의 일을 수행하면 된다.
참고로 웹 브라우저가 가지고 있는 쿠키는 항상 서버에 요청할 때 마다 자동으로 포함되어 전달된다. 따라서 개발자가 요청 시 쿠키 포함 유무에 대해서는 신경쓰지 않아도 된다.
로그인 처리를 한다. 이 때, 로그인이 성공되면 memberId = 1이라는 쿠키를 만들어서 내려준다.
웹 브라우저는 받은 쿠키를 쿠키 저장소에 저장해둔다. 이 후 동일한 서버에 요청을 할 때, 자동으로 항상 쿠키를 포함해서 요청을 보낸다.
쿠키의 종류
쿠키는 아래와 같이 두 가지 종류가 있다고 볼 수 있다.
- 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지됨.
- 세션 쿠키 : 만료 날짜를 생략하면 브라우저를 종료할 때까지만 유지됨.
브라우저 종료 시, 로그아웃이 되면 좋겠다. 그렇다면 우리는 쿠키를 만들고, 그 쿠키에 만료 시간을 설정하지 않아 세션쿠키로 만들어서 사용하면 된다.
로그인 기능 개발
@Component
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
/**
* 핵심 비즈니스 로직 개발
*/
public Member login(String loginId, String password) {
Optional<Member> findMember = memberRepository.findByLoginId(loginId);
return findMember.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
로그인 기능을 개발해보자.
- 로그인 서비스는 도메인으로 분리할 수 있다. 따라서 도메인 패키지에 생성해준다.
- MemberRepository에서 유저가 입력한 loginId에 해당되는 회원을 가져온다
- Optional로 감싸주는데, 이유는 null값이 나올 수 있기 때문이다.
- MemberRepository에서 찾은 회원 중, 유저가 입력한 비밀번호와 같은 회원을 반환한다. 이 때 없으면, null값을 반환한다.
로그인 컨트롤러의 로그인 기능 사용 개발
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
HttpServletResponse response) {
// Field Error 발생
if (bindingResult.hasErrors()){
return "/login";
}
String loginId = form.getLoginId();
String password = form.getPassword();
// 로그인 기능 수행
Member loginMember = loginService.login(loginId, password);
//글로벌 에러 발생
if(loginMember == null){
bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 성공 로직
Cookie cookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(cookie);
return "redirect:/";
}
- 로그인 기능을 컨트롤러에서 사용한다.
- 로그인이 성공하지 않았다면, Reject Value와 함께 login/loginForm View Template으로 이동한다.
- 로그인이 성공했다면, 쿠키를 만들어 준다
- 만들어진 쿠키는 memberId = 1 형태이다.
- HttpServletResponse 객체에 쿠키를 담아준다.
- "redirect:/"를 이용해 Home으로 리다이렉트한다.
위 형태로 로그인 컨트롤러를 작성해준다. 만약 정상적으로 로그인에 성공했다면, 클라이언트는 위처럼 memberId = 1이라는 쿠키값을 받은 후 redirect:/ 되는 것을 알 수 있다.
여기서 하나 더 해야할 일이 있는 것을 알 수 있다. 로그인 처리가 되면, 서버가 클라이언트에게 쿠키를 만들어서 보내주는 것은 이제 되었다. 그런데 그 쿠키를 다시 클라이언트가 보냈을 때, 서버 입장에서는 로그인되었는지 확인할 방법이 현재는 없다.
홈 화면 컨트롤러 기능 개발 → 쿠키 인식하기
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";
}
- 홈 화면 컨트롤러 기능을 개발해본다.
- 클라이언트에서 쿠키를 보내주면, 그 쿠키가 우리 회원인지를 확인하는 절차가 필요하다.
- 쿠키는 @CookieValue를 이용해서 쉽게 바인딩 할 수 있다.
- name은 Cookie의 key값을 입력해준다. key와 대응되는 cookie의 value가 자동으로 Cookie에 들어온다.
- required = false로 설정한다. false는 이 쿠키가 없어도 들어올 수 있고, true면 이 쿠키가 없으면 못 들어온다.
- 로그인을 하지 않은 사람도 홈화면으로 접근은 해야 되기 때문에 False 처리를 해준다.
- 클라이언트가 보내준 쿠키는 member Id다.
- MemberRepository에 MemberId를 가진 회원이 있다면, 로그인이 정상적으로 처리된 것이다.
- 로그인 유무에 따라 랜더링 되는 View가 달라진다.
- 로그인 X : home.html
- 로그인 O : loginHome.html
현재까지 다음과 같은 기능이 구현된 것이다!
- Login이 되면 loginHome.html로 이동한다. 이 때, 로그인 한 사람의 이름이 뜬다. 상품관리 + 로그아웃 가능
- Login이 되지 않으면 home.html로 이동한다. 회원 가입 + 로그인 가능하다
그런데 살펴보니 로그아웃 기능이 구현되지 않은 것을 알 수 있다. 로그아웃 기능은 어떻게 구현을 해야할까?
로그아웃 기능 구현
현재 로그인의 유지는 '세션 쿠키'로 유지하는 것을 알 수 있다. 즉, 쿠키를 매개체로 해서 로그인이 되었느냐 안 되었느냐를 판단하고 있다. 바꿔 말하면 쿠키가 없으면 로그아웃이 된 것으로 이해를 할 수 있다. 현재 우리의 로그인 기능은 다음과 같이 구현되어 있다.
- 로그인 ID + 패스워드 일치하는 멤버가 있다.
- 있으면 그 MemberId로 쿠키를 만들어서 내려준다.
- 클라이언트가 보낸 MemberId 쿠키를 읽어서, MemberRepository에 해당되는 Member가 있는지 찾아서 로그인 유지를 판단한다.
즉, 쿠키를 물리적으로 삭제한다는 곳이 없다는 것을 이해할 수 있다. 왜냐하면 서버의 어떠한 곳에도 따로 쿠키를 저장해두지는 않았기 때문이다. 쿠키를 물리적으로 삭제하지 않고, 내려주는 쿠키를 바로 만료시켜버리면 사용할 수 없는 쿠키가 된다. 이 흐름에서는 위 방식으로 로그아웃 기능을 구현해야 한다.
@PostMapping("/logout")
public String logout(HttpServletResponse response){
expiredCookie(response, "memberId");
return "redirect:/";
}
private void expiredCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie('memberId', null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
쿠키를 제거하는 메서드를 따로 뽑는다
- 쿠키를 만들 때, 쿠키의 이름은 동일하지만 값에 null을 넣어준다.
- set.maxAge를 만들어, 쿠키가 만들자마자 만료되게 처리해준다.
- 이 쿠키를 응답 객체에 담아서 응답해준다.
단순 쿠키로 구현한 로그인 / 로그아웃 기능
앞에서는 단순 쿠키로 로그인과 로그아웃을 구현해보았다. 이 때의 골자는 로그인 ID로 memberId를 찾고, 그 memberId를 쿠키로 사용해서 로그인 처리를 했다는 것이다. 그런데 이렇게 했을 때는 어마어마하게 큰 보안상의 문제가 있다.
- 쿠키 값은 임의로 변경할 수 있다.
- 클라이언트가 쿠키 저장소에 저장된 쿠키를 강제로 변경하면 다른 사용자가 된다.
- 실제 웹브라우저 개발자모드 → Application → Cookie 변경으로 확인 가능
- Cookie: memberId = 1 → Cookie : memberId = 2(다른 사용자의 이름이 보임)
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 쿠키는 웹 브라우저에 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
- 쿠키가 로컬 PC에서 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있다.
- 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
- 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속할 수 있다.
단순 쿠키만 사용한다면 위와 같은 중대한 보안문제가 발생할 수 밖에 없다. 그렇다면 우리는 어떤 대안을 가지고 로그인 기능을 설계해야할까?
- 쿠키에 중요한 값을 저장하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출한다.
- 서버에서 토큰과 사용자 ID를 맵핑해서 인식한다.
- 서버에서 토큰을 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토근의 만료시간을 짧게(예 : 30분) 유지한다.
- 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거한다 (강제 로그아웃)
로그인 처리하기 → 세션으로 처리하기
단순 쿠키로는 앞서 볼 수 있듯이 보안상의 이슈가 있다는 것을 이해할 수 있었다. 이제는 서버에서 추정 불가능한 임의의 식별자 값을 만들어서 로그인 처리를 유지하고자 한다. 즉, 세션 방식으로 처리하고자 한다.
세션 : 서버에 중요한 정보를 보관하고 연결을 유지한다.
'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC : Converter, Formatter 알아보기 (0) | 2022.01.07 |
---|---|
Spring MVC : HTTP API 예외처리 (0) | 2022.01.06 |
Spring MVC : Bean Validation 활용하기 (0) | 2022.01.02 |
Spring MVC : Validation (0) | 2021.12.26 |
Spring MVC : 메세지 기능 구현 (0) | 2021.12.26 |