로그인 처리 - 쿠키, 세션
인텔리제이 기본 설정 및 트러블 슈팅
- Trouble Shooting
- 문제 : 강의 소스 파일을 열었더니 Cause: zip END header not found 에러 발생
- 해결 : Java와 Spring 버전과 호환되는 Gradle 버전을 사용해야한다
- gradle/gradle-wrapper.properties 파일에서 호환되는 gradle 버전으로 변경
- 1번 방법으로 해결되지 않는 경우
- gradle-wrapper.properties 파일에서 distributionUrl 내용 중 service부터 끝까지 복사 후 웹 브라우저에 붙여넣기
- 사용자폴더\.gradle\wrapper\dists\해당gradle버전\고유값\에 해당 버전 gradle 파일을 넣어준다.
- 해결 : Java와 Spring 버전과 호환되는 Gradle 버전을 사용해야한다
- 문제 : 파일 내 한글이 깨지는 경우 (인코딩 문제)
- 해결 : intellij setting -> Editor
-> File Encodings 내 모든 설정을 UTF-8로 변경, Transparent native-to-ascii conversion 체크
- 해결 : intellij setting -> Editor
- 문제 : 강의 소스 파일을 열었더니 Cause: zip END header not found 에러 발생
- 인텔리제이 프로젝트 시작 시 초기 설정
- Lombok 라이브러리를 사용하는 경우
- intellij setting -> Build, Execution, Deployment
-> Compiler -> Annotation Processors -> Enable annotation processing 체크 필수
- intellij setting -> Build, Execution, Deployment
- 인텔리제이 빠른 빌드를 위한 설정
- intellij setting -> Build, Execution, Deployment -> Build Tools
-> Gradle -> Build and run using, Run tests using 항목을 Gradle에서 Intellij IDEA로 변경
- intellij setting -> Build, Execution, Deployment -> Build Tools
- Lombok 라이브러리를 사용하는 경우
패키지 구조 설계
- 패키지 구조 예시
- domain (도메인 객체, 레포지토리, 서비스 ...)
- item
- member
- login
- web (컨트롤러, DTO ...)
- item
- member
- login
- domain (도메인 객체, 레포지토리, 서비스 ...)
- 가장 중요한 것은 도메인이다.
- 도메인 : 화면, UI, 기술 인프라등의 영역은 제외한 시스템이 구축해야 하는 핵심 비지니스 업무 영역이다.
- web을 다른 기술로 바꾸더라도 도메인은 그대로 유지할 수 있어야한다.
- web은 도메인을 알지만(의존하지만) 도메인은 web을 모르게(의존하지 않도록) 설계해야한다.
- web은 도메인을 참조하지만 도메인은 web을 참조해서는 안된다.
- ex) DTO는 도메인을 참조해도 되지만 도메인은 DTO를 참조해서는 안된다.
- 항상 web -> domain 의존관계가 단방향으로 흐르도록 패키지 설계를 잘 해야한다.
로그인 기능
//lambda 사용 x
public Optional<Member> findByLoginId(String loginId) {
List<Member> all = findAll();
for (Member member : all) {
if (member.getLoginId().equals(loginId)) {
return Optional.of(member);
}
}
return Optional.empty();
}
//lambda 사용
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream()
.filter(member -> member.getLoginId().equals(loginId))
.findFirst();
}
- Java Stream과 lambda를 사용하면 더 간결하고 좋은 코드를 작성할 수 있다.
//로그인 컨트롤러 처리
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
- Service 로직에서 회원 아이디와 비밀번호가 일치하지 않으면 null을 반환하도록 한다.
- 필드 오류는 어노테이션을 사용할 수 있지만 if 문에서와 같이 글로벌 오류(오브젝트 오류)는 객체만으로 되지 않는 경우도 있기에 @ScriptAssert를 사용하기에는 한계가 존재한다.
로그인 처리하기 - 쿠키
- 로그인 상태를 유지하기 위해서 쿠키를 사용한다.
- 서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달
=> 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.
- 로그인 시 쿠키 동작 방식
- 로그인 시 정보가 웹 브라우저에서 서버로 전송
- 서버는 DB에서 해당 회원 정보를 찾아서 쿠키로 전송
- 웹 브라우저는 해당 쿠키를 내부의 쿠키 저장소에 저장
- 로그인 이후 웹 브라우저는 서버의 모든 요청을 할 때마다 지속적으로 해당 쿠키를 서버에 함께 보내준다.
- 서버는 웹 브라우저에서 보내주는 쿠키를 활용 (로그인 상태 유지 ...)
- 영속 쿠키 vs 세션 쿠키
- 영속 쿠키 : 만료 날짜를 입력하여 해당 날짜까지 유지한다.
- 세션 쿠키 : 만료 날짜를 생략하여 브라우저 종료시까지 유지한다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
//로그인 실패 처리
...
//로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
- 로그인 성공 시 쿠키를 생성하고 HttpServletResponse에 담는다.
- 예제에서는 회원의 id를 쿠키에 담아서 보낸다.
- 이때 웹 브라우저는 종료 전까지 회원 id를 서버에 계속 보내게 된다.
@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";
}
- 로그인 한 사용자에게 다른 화면을 보여주기 위해서 화면을 분리
- @CookieValue를 통해 쿠키를 조회할 수 있다.
- name 속성으로 쿠키 이름을 지정할 수 있다.
- 로그인 하지 않은 사용자도 접근할 수 있기 때문에 Cookie의 required 속성은 false로 한다.
- 로그인 쿠키가 없는 사용자, 로그인 쿠키가 있어도 회원이 없는 경우 모두 검증한다.
- @CookieValue를 통해 쿠키를 조회할 수 있다.
//컨트롤러 로그아웃 로직
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
Cookie cookie = new Cookie("memberId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/";
}
- 로그아웃 시 쿠키를 생성하는데 해당 쿠키의 유효 시간을 0으로 해서 쿠키가 즉시 종료되도록 한다.
쿠키와 보안 문제
- 보안 문제
- 쿠키 값은 임의로 변경할 수 있다.
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
- 쿠키에 보관된 정보는 훔칠 수 있다.
- 쿠키 정보는 웹 브라우저에도 보관되고 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
- 만약 쿠키에 신용카드 정보나 개인정보 등이 있다면 문제가 된다.
- 해커가 쿠키를 한 번 훔치면 평생 사용할 수 있다.
- 쿠키 값은 임의로 변경할 수 있다.
- 보안 문제에 대한 해결책
- 쿠키에 중요한 값을 노출하지 않고 사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식해야 하며 서버에서 토큰을 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능해야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지해야한다.
- 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거한다.
로그인 처리하기 - 세션
- 세션 : 서버에 중요한 정보를 보관하고 연결을 유지시키는 방법이다.
- 세션 동작 방식
- 로그인 시 정보가 웹 브라우저에서 서버로 전송
- 서버는 DB에서 해당 회원 정보를 찾아서 세션 id를 생성
- 세션 ID는 추정 불가능해야하므로 UUID로 생성한다.
- 생성된 세션 ID와 세션에 보관할 값을 서버의 세션 저장소에 보관한다.
- 결국 클라이언트와 서버는 쿠키로 연결되어 있어야 하므로 서버는 세션 ID를 쿠키 값으로 담아서 클라이언트에게 전달한다.
- 클라이언트는 쿠키 저장소에 쿠키를 보관한다.
- 로그인 이후 클라이언트는 모든 요청에 서버에 쿠키를 전달한다.
- 서버는 전달받은 쿠키에서 세션 ID값을 통해 세션 저장소에서 회원 정보를 찾는다.
- 세션 방식은 회원과 관련된 정보를 클라이언트에게 전혀 전달하지 않는다는 것이다.
- 세션을 사용해서 쿠키의 보안 문제를 해결할 수 있다.
- 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에게 전달한다.
@Test
public void sessionTest() throws Exception {
//세션 생성
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManager.createSession(member, response);
//요청에 응답 쿠키 저장
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies());
//세션 조회
Object result = sessionManager.getSession(request);
assertThat(result).isEqualTo(member);
//세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
assertThat(expired).isNull();
}
- 테스트 시 HttpServletResponse / Request는 인터페이스이기에 객체를 직접 사용하기 어렵다.
- 스프링에서 테스트용으로 제공하는 비슷한 역할을 해주는 가짜 MockHttpServletRequest ,
MockHttpServletResponse 를 사용할 수 있다. - 예제 코드는 직접 구현한 세션을 적용한 코드이다.
- 스프링에서 테스트용으로 제공하는 비슷한 역할을 해주는 가짜 MockHttpServletRequest ,
- Servlet에서 세션 기능을 지원한다.
- 세션을 일정 시간 사용하지 않으면 해당 세션을 삭제하는 기능을 제공한다.
로그인 처리하기 - 서블릿 HTTP 세션
- 서블릿이 제공하는 HttpSession을 생성한 쿠키
- 쿠키의 이름 - JSESSIONID
- 쿠키 값은 추정 불가능한 값이다.
- ex) Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
//추상 클래스 예시
public abstract class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
//인터페이스 예시
public interface SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
- 상수를 사용하기 위한 용도의 클래스와 같이 객체를 생성해서 사용하지 않는 경우
- abstract class로 추상 클래스로 만든다.
- interface로 만든다.
//세션 생성
request.getSession(true);
- 세션 생성
- true로 지정 - true는 기본 값으로 생략 가능하다.
- 세션이 있으면 기존 세션을 반환하고 없으면 새로운 세션을 생성해서 반환한다.
- false로 지정
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환한다.
- true로 지정 - true는 기본 값으로 생략 가능하다.
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
//로그인 실패 처리
...
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
- 세션에 데이터를 보관하는 방법 session.setAttribute()는 request.setAttribute(...)와 유사하다.
- 하나의 세션에 여러 값을 보관할 수 있다.
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
- 세션 제거
- getSession(false)로 기존 세션을 반환받되 세션을 새로 생성하지는 않는다.
- 기존 세션이 있으면 그 세션을 invalidate()로 제거한다.
//로그인 한 사용자와 로그인하지 않은 사용자 화면을 구분
@GetMapping("/")
public String homeLoginV3(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";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- 로그인 여부로 사용자 화면을 분리
- getSession()를 false로 지정한 이유는 true로 하면 로그인 하지 않은 사용자도 세션이 생성된다.
- getAttribute()로 세션에 저장된 회원 정보를 가져올 수 있다.
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
//세션에 회원 데이터가 없으면
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- @SessionAttribute : 스프링에서 세션을 더 편리하게 사용할 수 있도록 제공하는 기능
- @SessionAttribute는 세션을 찾고 해당 세션의 데이터를 가져와서 객체에 담는 기능을 한다.
- TrackingModes : 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을통해서 세션을 유지하는 방법이다.
- 로그인을 처음 시도하면 URL이 아래와 같이 jessionid를 포함한다.
- ex) http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
- 이 방법을 사용하려면 URL에 이 값을 계속 포함해서 전달해야 하기에 효율적이지 못하고 잘 사용하지 않는다.
- 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 여부를 최초에는 판단하지 못하기에 쿠키 값도 전달하고 URL에도 함께 전달한다.
- 별도의 설정을 통해 쿠키로만 전달받을 수 있다.
- 로그인을 처음 시도하면 URL이 아래와 같이 jessionid를 포함한다.
//application.properties 설정
server.servlet.session.tracking-modes=cookie
- URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하려면 해당 옵션을 application.properties에 설정하면 된다.
세션 정보와 타임 아웃 설정
- 세션 정보
//세션 정보
session.getId();
session.getMaxInactiveInterval();
session.getCreationTime();
session.getLastAccessedTime();
session.isNew();
- sessionId : 세션 ID로 JSESSIONID의 값이다.
- maxInactiveInterval : 세션의 유효 시간 ex) 1800초
- creationTime : 세션 생성일시
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId
( JSESSIONID )를 요청해서 조회된 세션인지 여부
- 세션 타임아웃 설정
- 세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate()가 호출되는 경우 삭제된다.
하지만 대부분 사용자의 경우 로그아웃을 하지 않고 웹 브라우저를 종료한다.
HTTP는 비연결성(ConnectionLess)이기에 서버 입장에서는 사용자가 웹 브라우저를 종료한 것인지 알 수 없다. - 그렇기에 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기 어렵다.
- 세션을 무한정 보관하면 문제가 발생한다. (쿠키 탈취, 서버 메모리 부족)
- 세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate()가 호출되는 경우 삭제된다.
- 세션의 종료 시점
- 사용자가 서버에 최근 요청한 시간을 기준으로 세션을 유지해주고 특정 시간을 넘어서면 세션을 삭제하는 방식
=> HttpSession이 사용하는 방식
- 사용자가 서버에 최근 요청한 시간을 기준으로 세션을 유지해주고 특정 시간을 넘어서면 세션을 삭제하는 방식
//application.properties 설정
server.servlet.session.timeout=60
//특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800);
- 세션 타임아웃 설정
- 글로벌 설정 (application.properties)
- 글로벌 설정은 분 단위로 설정해야 한다. (60, 120 ....)
- 특정 세션 단위 설정
- setMaxInactiveInterval() 메소드 사용
- 글로벌 설정 (application.properties)
- 항상 세부적인 설정의 우선 순위가 높다.
- 세션 타임아웃
- 세션의 타임아웃 시간은 해당 세션과 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화 된다.
- session.getLastAccessedTime() : 최근 세션 접근 시간
- LastAccessedTime 이후로 timeout 지정된 시간이 지나면 WAS가 내부적으로 해당 세션을 제거한다.
- 실무에서 세션 사용 시 주의할 점
- 세션에는 최소한의 데이터만 보관해야 한다.
- 보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수 있다.
- 세션 유지 시간을 적절히 지정하는 것이 중요하다.
- 세션에는 최소한의 데이터만 보관해야 한다.
출처 : [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com
'Spring > [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 예외 처리와 오류 페이지 (2) | 2024.10.18 |
---|---|
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 로그인 처리 - 필터, 인터셉터 (6) | 2024.10.16 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Bean Validation (1) | 2024.10.14 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Validation (4) | 2024.10.14 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 메시지, 국제화 (8) | 2024.10.11 |