일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch1
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch12
- 게시글 목록 api
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch5
- 스프링 입문(무료)
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 로그인 처리
- 스프링 db1 - 스프링과 문제 해결
- jpa 활용2 - api 개발 고급
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch8
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch3
- @Aspect
- 스프링 mvc1 - 스프링 mvc
- 스프링 고급 - 스프링 aop
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 서블릿
- Today
- Total
나구리의 개발공부기록
로그인 처리1 - 쿠키/세션, 세션 동작 방식, 세션 직접 만들기, 직접 만든 세션 적용, 서블릿 HTTP 세션, 세션 정보와 타임아웃 설정 본문
로그인 처리1 - 쿠키/세션, 세션 동작 방식, 세션 직접 만들기, 직접 만든 세션 적용, 서블릿 HTTP 세션, 세션 정보와 타임아웃 설정
소소한나구리 2024. 9. 5. 23:15 출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 세션 동작 방식
1) 세션 동작 방식의 개념
(1) 세션
- 쿠키의 보안 문제를 해결하기 위해 중요한 정보는 모두 서버에 저장해야하고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야함
- 이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고함
(2) 로그인
- 사용자가 loginId, password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인함
(2) 세션 관리
- 서버에서는 세션을 관리하는 별도의 저장소에 전달된 정보를 추정 불가능한 세션 Id를 생성하여 함께 보관함
- Java에서 지원하는 UUID는 추정이 불가능함 -> 해당 기능을 이용할 예정
(3) 세션id를 응답 쿠키로 전달
- 클라이언트와 서버는 결국 쿠키로 연결되어야 하는데, 서버는 클라이언트에 mySessionId라는 이름으로 세션ID만 쿠키에 담아서 전달하고 클라이언트는 쿠키 저장소에 해당 값을 보관함
- 여기서 전달된 정보는 회원과 전혀 관련이 없는 정보만 있는 랜덤값이라는 것
(4) 로그인 이후 접근 - 클라이언트의 세션id 쿠키 전달
- 클라이언트는 요청시 항상 mySessionId 쿠키를 전달하는데, 서버에서는 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용함
2) 정리
- 세션을 사용함으로 서버에서 중요한 정보를 관리하게 되어 보안문제들이 해결됨
- 변조 가능했던 쿠키값을 예상 불가능한 복잡한 세션 Id를 사용함으로 변조가 불가능해짐
- 클라이언트 해킹 시 털릴 가능성이 있는 쿠키정보에 중요한 정보가 없음
- 쿠키를 탈취하여도 서버에서 세션의 만료시간을 짧게 유지하거나 해킹의 의심되는 경우 서버에서 해당 세션을 강제로 제거하여 탈취된 쿠키를 무의미한 값이 되도록 만듦
2. 세션 직접 만들기
1) 크게 3가지 기능으로 세션을 관리
(1) 세션 생성
- sessionId 생성(임의의 추정 불가능한 랜덤 값)
- 세션 저장소에 sessionId와 보관할 값 저장
- sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
(2) 세션 조회
- 클라이언트가 요청한 sessionId쿠키의 값으로, 세션 저장소에 보관한 값 조회
(3) 세션 만료
- 클라이언트가 요청한 sessionId쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거
2) 코드 작성
(1) SessionManager - 세션관리 클래스 생성
- web하위에 session 패키지 생성후 작성
- Component로 컴포넌트 스캔 대상이 되어 스프링 빈 자동 등록
- 동시 요청에 안전한 ConcurrentHashMap 사용
- 자주 사용하는 로직을 findCookie로 분리하여 사용(리펙토링 코드 참고)
package hello.login.web.session;
// 세션 관리
@Component
public class SessionManager {
// 상수로 만들기: 파라미터에 값을 입력 후, 옵션 + 커맨드 + c
public static final String SESSION_COOKIE_NAME = "mySessionId";
// 동시성 문제를 해결하기위한 ConcurrentHashMap 사용
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
*/
public void createSession(Object value, HttpServletResponse response) {
// 세션 id를 생성하고 값을 세션에 저장
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[] cookies = request.getCookies(); // 쿠키는 배열로 반환됨
// if (cookies == null) {
// return null;
// }
//
// for (Cookie cookie : cookies) {
// if (cookie.getName().equals(SESSION_COOKIE_NAME)) {
// return sessionStore.get(cookie.getValue());
// }
// }
// return null;
// }
/**
* 세션 조회 - 리펙토링 후
*/
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());
}
}
// 쿠키 검증을 별도로 메서드 분리
public Cookie findCookie(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
// .findAny() 순서와 상관없이 가장 먼저 찾은 값을 반환
// .orElse(null) 없으면 null을 반환
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny().orElse(null);
}
}
(2) SessionManagerTest
- 서버에서 세션을 생성해서 응답, 요청에 응답 쿠키를 저장, 서버에서 세션 조회, 세션을 만료 하는 테스트 시나리오
- HttpServletRequest, HttpServletResponse 객체를 직접 사용할 수 없기 때문에 테스트에서 비슷한 역할을 해주는 가짜 MockHttpServletRequest, MockHttpServletResponse를 사용
- 세션 조회: sessionId로 조회했을 때 반환된 결과와 최초 생성한 member가 같은지 확인
- 세션 만료: expire함수로 요청 세션을 만료시키고 해당 값이 널인지 확인
package hello.login.web.session;
public class SessionManagerTest {
SessionManager sessionManager = new SessionManager();
@Test
void sessionTest() {
// 세션 생성 - 서버가 클라이언트에 응답
/**
* HttpServletResponse는 인터페이스이기 때문에 구현체 없이 테스트가 불가능
* 스프링이 지원하는 MockHttpServletRequest, MockHttpServletResponse 를 활용해서 테스트 사용
*/
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);
// 조회 검증
Assertions.assertThat(result).isEqualTo(member);
// 세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
// 만료 검증
Assertions.assertThat(expired).isNull();
}
}
3. 직접 만든 세션 적용
1) 웹 애플리케이션에 적용
(1) LoginController - loginV2() 생성
- 직접만든 SessionManager를 주입
- 로그인 성공 처리 로직을 SessionManager의 createSession()을 활용해서 세션을 생성
- 로그인 성공시 세션을 등록하고 세션에 loginMember를 저장해두고 쿠키도 함께 발행함
// ... 기존 코드 생략
private final SessionManager sessionManager; // SessionManager 주입
// 직접만든 세션 적용
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult,
HttpServletResponse httpServletResponse) { // 쿠키 저장을 위한 HttpServletResponse 추가
// 기존 코드 동일, 쿠키 생성 및 저장하는 코드는 제거
// 로그인 성공 처리
// 세션 관리자를 통해 세션을 생성하고 회원 데이터 보관
sessionManager.createSession(loginMember, httpServletResponse);
return "redirect:/";
}
(2) LoginController - logoutV2() 생성
- SessionManager의 expire()를 호출하여 세션정보를 제거
@PostMapping("/logout")
public String logoutV2(HttpServletRequest httpServletRequest) {
sessionManager.expire(httpServletRequest);
return "redirect:/";
}
(3) HomeController - homeLoginV2() 생성
- 기존의 @CookieValue를 제거하고 HttpServletRequest를 활용하여 세션 관리자에 저장된 정보를 조회하여 member로 반환
- 회원 정보가 없으면 쿠키나 세션이 없는 것이므로 로그인 되지 않도록 처리
package hello.login.web;
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
// ... 기존 코드 생략
private final SessionManager sessionManager; // 주입
// @CookieValue를 제거하고 HttpServletRequest 활용
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
// 세션 관리자에 저장된 정보를 조회(타입 변환 필요)
Member member = (Member)sessionManager.getSession(request);
// 로그인
if (member == null) { // 세션 정보가 없으면 home으로 이동
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
}
(4) 정리
- 세션과 쿠키의 개념을 명확하게 알아보기 위해 직접 만들어 보았는데, 정리하면 세션이라는 것은 단지 쿠키를 사용하는데 서버에서 데이터를 유지하는 방법임
- 프로젝트마다 세션을 만들어서 적용하는 것은 어려우니 서블릿이 공식으로 지원하는 세션을 활용하면 편리하게 관리할 수 있음
- 직접 만든 세션과 동작방식이 거의 같으며 세션을 일정시간 사용하지 않으면 해당 세션을 삭제하는 기능도 제공함
4. 서블릿 HTTP 세션
1) 서블릿 HTTP 세션 개발
(1) HttpSession 소개
- 세션이라는 개념은 대부분 웹 애플리케이션에 필요함(웹이 등장하면서 부터 나온 문제임)
- 서블릿은 세션을 위해 HttpSession 이라는 기능을 제공하여 지금까지의 문제들을 해결해줌
- 서블릿이 제공하는 HttpSession도 직접만든 SessionManager와 같은 방식으로 동작하며 JSESSIONID라는 쿠키를 생성하게 되며 이 값은 추정 불가능한 랜덤값임
(2) SessionConst 생성
- 단순히 데이터를 보관하고 조회할 때 같은 이름이 중복이 되어 사용되므로 상수를 하나 정의함
- 이런것은 class보다 추상클래스나 interface로 정의하는것이 더 좋다고함
- 해당 클래스 없이 파라미터마다 "loginMember"라고 입력해줘도 되지만 실무에서 유지보수시 번거로워질 수 있음
package hello.login.web;
// 단지 HttpSession에 데이터를 보관하고 조회할 때 같은 이름이 중복되어 사용하기 때문에 정의한 클래스
// 이런것은 interface나 abstract를 붙혀 추상클래스로 만드는 것이 더 좋다고함
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
(3-1) LoginController - loginV3() 생성
// ... 기존 코드 생략
// HttpServletResponse -> HttpServletRequest로 변경
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult,
HttpServletRequest httpServletRequest) {
//...
// 로그인 성공 처리 - HttpServletRequest의 getSession() 사용
// 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = httpServletRequest.getSession();
// 세션에 로그인 회원 정보를 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
(3-2) HttpServletRequest.getSession()
- 세션을 생성하려면 HttpServletRequest의 getSession(true)를 사용하면 되는데 기본값이 true이므로 true는 생략 가능함
- 옵션 true: 세션이 있으면 기존세션을 반환하고 세션이 없으면 새로운 세션을 생성해서 반환함
- 옵션 false: 세션이 있으면 기존 세션을 반환하고 세션이 없으면 생성하지 않고 null을 반환
(3-3) session.setAttribute()
- 세션에 데이터를 보관하는 방법
- 하나의 세션에 여러 값을 저장할 수도 있음
(4) LoginController - logoutV3() 생성
- .getsession()옵션을 false로 하여 세션이 없을 때 생성하지 않도록 옵션을 적용
- sesstion.invalidate(): 세션을 제거
@PostMapping("/logout")
public String logoutV3(HttpServletRequest httpServletRequest) {
HttpSession session = httpServletRequest.getSession(false);
if (session != null) {
session.invalidate(); // 세션과 그안의 데이터 모두 날라감
}
return "redirect:/";
}
(5) HomeController - homeLoginV3() 생성
- 마찬가지로 .getSession(false)로 새로운 세션값을 생성하지 않도록 정의
- .getAttribute()로 로그인 시점에 세션에 보관한 회원 객체를 찾아서 반환
- 애플리케이션을 실행 후 개발자 모드의 애플리케이션에 들어가보면 JSESSIONID 쿠키가 생성되어있
@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) { // 세션에 회원 데이터가 없으면 home으로 이동
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
1) @SessionAttribute 활용
(1) HomeController - homeLoginV3Spring
- 스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 기능을 지원함
- 이미 로그인 된 사용자를 찾을 때 사용하며 이 기능은 세션을 생성하지 않음
- 메인 페이지에 접속하는 컨트롤러의 메서드의 파라미터에 @SessionAttribute(name = "loginMember", required = false)와 Member를 받으면 세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처리해줌
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model) {
// 로그인
if (loginMember == null) { // 세션에 회원 데이터가 없으면 home으로 이동
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
(2) TrackingModes
- 로그인을 처음 시도하면 URL에 jsessionid를 포함하게 되는데, 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법임
- 타임리프같은 템플릿 엔진은 엔진을 통해서 링크를 걸면 jsessionid를 URL에 자동으로 포함해주며 서버 입장에서는 웹 브라우저가 쿠키를 지원하는지 않는지 최초에는 판단하지 못하므로 쿠키값과 함께 URL에 jsessionid를 전달해 주는 것임
- 그러나 최근 스프링에서 URL 매핑 전략이 변경되어 URL이 아래처럼 매핑되면 컨트롤러를 찾지못하고 404 오류가 발생할 수 있음
- application.properties에 server.servlet.session.tracking-modes=cookie 를 입력하여 session.tracking-modes를 사용하면 jsessionid가 URL에 노출 되지 않고 쿠키를 통해서만 세션을 유지함
- 만약에 URL에 jsessionid가 꼭 필요하다면 application.properties에 spring.mvc.pathmatch.matching-strategy=ant_path_matcher 을 추가해주면 됨
ex) http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
5. 세션 정보와 타임아웃 설정
1) 세션 정보를 확인
(1) SessionInfoController 생성
- sessionId: 세션Id, JSESSIONID의 값
- maxInactiveInterval: 세션의 유효 시간, ex) 1800초 (30분)
- creationTime: 세션 생성일시
- lastAccessedTime: 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId를 요청한 경우에 갱신
- isNew: 새로 생성된 세션인지 아니면 과거에 만들어졌고 클라이언트에서 서버로 sessionId를 요청해서 조회된 세션인지 여부
package hello.login.web.session;
@Slf4j
@RestController
public class SessionInfoController {
@GetMapping("/session-info")
public String sessionInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return "세션이 없습니다";
}
// 보관된 세션 데이터를 반복해서 출력하는 코드
session.getAttributeNames().asIterator()
.forEachRemaining(name -> log.info("session name={}, value={}",
name , session.getAttribute(name)));
log.info("sessionId={}", session.getId()); // 세션 ID
log.info("maxInactiveInterval={}", session.getMaxInactiveInterval()); // 세션 유효 시간
log.info("creationTime={}", new Date(session.getCreationTime())); // 생성 시간
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime())); // 마지막에 접근한 시간
log.info("isNew={}", session.isNew()); // 새로운 세션
return "세션출력";
}
}
2) 세션 타임아웃 설정
(1) 설명
- 세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate()가 호출되는 경우에 삭제됨
- 그러나 대부분의 사용자는 로그아웃하지 않고 그냥 웹 브라우저를 종료하는데, HTTP는 비 연결성이므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없어서 서버에서는 세션 데이터를 언제 삭제해야 하는지 판단하기 어려움
- 남아있는 세션을 무한정 보관하면 세션과 관련된 쿠키가 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청을 할 수 있게 됨
- 그리고 세션 정보는 기본적으로 메모리에 생성되는데 메모리의 크기가 무한하지 않기 때문에 수많은 사용자가 로그인하면 해당 데이터가 메모리에 누적되고 메모리가 뻗어서 장애가 날 수 있음
(2) 세션의 종료 시점
- 세션의 유지 시간이 세션 생성 시점으로부터 30분 정도로 설정 했다고 가정하면 유저는 30분마다 로그인을 해서 세션을 계속 생성해주어야 하는 번거로움이 발생함
- 그래서 세션 생성 시점이 아닌 사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도 유지해주면 사용자가 서비스를 이용하고 있을 때는 세션의 생존시간이 30분으로 계속 늘어나게 됨
- 30분마다 로그인해야 하는 번거로움이 사라지며 HttpSession은 이 방식을 사용함
(3-1) 세션 타임아웃 설정 - 글로벌 설정
- application.properties에 server.servlet.session.timeout=60 입력
- 값은 분단위로 설정해야함, 60 = 1분, 120 = 2분 이런식
(3-2) 세션 타임아웃 설정 - 특정 세션 단위로 설정
- session.setMaxInactiveInterval(1800);
- 초단위로 특정 세션 단위로 시간을 설정 할 수 있음
(4) 세션 타임아웃 설정
- 세션의 타임아웃 시간은 해당 세션과 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화 됨
- 초기화 되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용할 수 있음
- LastAccessedTime 이후로 timeout 시간이 지나면 WAS가 내부에서 해당 세션을 제거함
(5) 정리
- 서블릿의 HttpSession의 타임아웃 기능으로 세션을 안전하고 편리하게 사용할 수 있음
- 그러나 실무에서 주의할 점은 세션에는 최소한의 데이터만 보관해야 함
- 예제에서는 멤버를 통채로 세션에 저장했지만 최소한의 데이터로 memberId 같은 자주 사용하는 몇가지정도만 핏하게 설정해서 사용해야 서버의 메모리 사용량을 줄일 수 있음(보관한 데이터 용량 * 사용자 수 -> 서버의 메모리가 터질 수 있음)
- 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적 될 수 있으므로 적당한 시간을 선택하는 것이 필요한데, 기본이 30분이라는 것을 기준으로 고민하면 됨