일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링 mvc
- API 개발 고급
- 값 타입 컬렉션
- 스프링
- 임베디드 타입
- 타임리프
- 스프링 데이터 JPA
- QueryDSL
- 실무활용
- JPA
- 트위터
- JPQL
- Bean Validation
- 김영한
- 페이징
- 컬렉션 조회 최적화
- 검증 애노테이션
- 기본문법
- JPA 활용 2
- 벌크 연산
- 프로젝트 환경설정
- 불변 객체
- jpa 활용
- 일론머스크
- JPA 활용2
- 예제 도메인 모델
- 로그인
- 타임리프 문법
- 스프링MVC
- Spring Data JPA
- Today
- Total
RE-Heat 개발자 일지
스프링 MVC 2편 - [6] 로그인 처리 1 - 쿠키·세션(하편) 본문
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
인프런 김영한님의 스프링 MVC 2편 강의를 토대로 정리한 내용입니다.
[7] 쿠키와 보안 문제
■ 보안 문제
- 쿠키 값은 임의로 변경할 수 있다
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자로 바뀐다.
- 실제 웹브라우저 개발자모드 Application Cookie 변경으로 확인
- Cookie : memberId=1 → Cookie: memberId=2 (다른 사용자의 이름이 보임)
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 만약 쿠키에 중요한 개인정보나, 신용카드 정보가 담겨 있다면?
- 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
- 쿠키의 정보가 나의 로컬 PC에서 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있다.
- 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
- 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.


■ 대안
- 쿠키에서 중요한 값을 빼고, 사용자 별로 예측 불가능한 임의의 토큰을 노출한다. 그리고 서버에서 토큰과 사용자 id를 매핑해 인식하면 된다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능해야 한다.(UUID)
- 해커가 토큰을 훔쳐도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다. 추가로 해킹의 의심되면 해당 토큰을 세션 저장소에서 강제로 제거하면 된다.
[8] 로그인 처리하기 - 세션 동작 방식
목표 : 중요한 정보는 모두 서버에 저장. 클라이언트와 서버는 추정 불가능한 임의의 식별자로 연결
=> 이렇게 서버에 중요한 정보를 관리하고 연결을 유지하는 방법을 세션이라 한다.
■ 세션 동작 방식
① 로그인

- 사용자가 loginId, password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.
② 세션 생성

- 추정 불가능한 세션 ID를 생성해야 한다.
- UUID는 추정이 불가능하므로 UUID를 적용해 세션 ID 생성
- 생성한 세션 ID와 세션에 보관할 값(memberA)을 서버의 세션 저장소에 보관한다.
③ 세션 id를 응답 쿠키로 전달

클라이언트와 서버는 결국, 쿠키로 연결돼야 한다.
- 서버는 클라이언트에 mySessionId라는 이름으로 세션 ID만 쿠키에 담아 전달한다.
- 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
=> 회원 관련 정보는 클라이언트 측에 전달하지 않는다는 게 포인트다. 오로지 추정 불가능한 세션 ID로만 클라이언트와 서버가 상호작용.

- 클라이언트는 요청 시 항상 mySessionId 쿠키를 전달한다.
- 서버에는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인 시 보관한 세션 정보를 사용한다.
[9] 로그인 처리하기 - 세션 직접 만들기
■ 세션 관리
SessionManager
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
* sessionId 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠리르 생성해서 클라이언트에 전달
* */
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 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;
}
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
1] 세션 생성
String sessionId = UUID.randomUUID().toString();로 랜덤한 세션 id 생성하고 값을 저장.
쿠키도 생성한 뒤 response에 이 쿠키를 담아 보내 줌.
2] 세션 조회
findCookie()로 쿠키이름이 일치한 값이 있는지 확인. 없으면 null. 있으면 세션 저장소에서 값을 가져 옴.
3] 세션 만료
sessionCookie가 있는지 찾고 있으면 세션 저장소에서 제거.
■ 테스트
SessionManagerTest
class SessionManagerTest {
SessionManager sessionManager = new SessionManager();
@Test
void SessionTest(){
//세션 생성
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManager.createSession(member, response);
//요청에 응답 쿠기 저장[웹브라우저가 쿠키 만들어서 서버에 전달함]
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies()); //my sessionId 뭐뭐
//세션 조회
Object result = sessionManager.getSession(request);
assertThat(result).isEqualTo(member);
//세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
assertThat(expired).isNull();
}
}
① HttpServletResponse, HttpServletRequest를 직접 사용할 순 없어서 MockHttpServletResponse, MockHttpServletRequet를 사용한다.
참고]
- 테스트 만들기 단축키 : ctrl+ shift +t
- 상수로 만들기 : ctrl+alt+c
[10] 로그인 처리하기 - 직접 만든 세션 적용
@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){
//특정 필드 문제 아니므로 글로벌 오류[@ScriptAssert()론 불가능. 한 번 찾아야 하기 때문.]
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터를 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request){
sessionManager.expire(request);
return "redirect:/";
}
■ loginV2()
① private final SessionManager sessionManager;로 주입
② 로그인 성공하면 세션을 등록. 세션에 loginMember를 저장하고 쿠키도 발행
■ logoutV2()
로그아웃이 되면 해당 세션 정보를 세션 저장소에서 제거한다. (브라우저에 세션 쿠키는 남아있음)
HomeController - homeLoginV2
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
//세션 관리자에 저장된 회원 정보 조회
Member member = (Member) sessionManager.getSession(request);
//로그인
if (member == null){
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
세션 관리자에 저장된 회원 정보 조회 => Object로 받아오니까 (Member) 캐스팅해줘야 함.
세션 관리자에 저장된 회원 정보를 조회한 후 정보가 없으면 로그인되지 않은 것으로 처리
로그인 화면

로그아웃 후 화면(쿠키는 남아있지만, 서버에선 지워진 상태)

[11] 로그인 처리하기 - 서블릿 HTTP 세션 1
목표 : 서블릿이 공식 지원하는 세션으로 개발하기
■ HttpSession
서블릿이 제공하는 HttpSession도 우리가 직접 만든 SessionManager와 같은 방식으로 동작한다. 쿠키 이름은 JSESSIONID이며 값은 추정 불가능한 랜덤 값이다.
SessionConst
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
HttpSession에 데이터를 보관하고 조회할 때 같은 이름이 중복되므로 상수 정의
참고] abstract class나 interface로 이 객체를 생성(new)하는 걸 애초에 막는 게 좋은 방법이다.
LoginController - loginV3
@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){
//특정 필드 문제 아니므로 글로벌 오류[@ScriptAssert()론 불가능. 한 번 찾아야 하기 때문.]
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
■ 세션 생성과 조회
① 세션 생성 옵션
request.getSession(true) <세션 생성용
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
- 기본값이 true. 그래서 request.getSession()은 request.getSession(true)와 동일하다.
request.getSession(false) <세션 조회용
- 세션이 있으면 기존 세션을 반환한다
- 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환한다.
② 세션에 로그인 회원 정보 보관
session.setAttribute() <세션에 멤버 객체 값 저장.
LoginController - logoutV3()
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);//세션이 없다고 새로 만들면 안되니까 false
if (session !=null){
session.invalidate();
}
return "redirect:/";
}
session이 있는지 확인한 후 있으면 session.invalidate()로 세션을 제거
HomeController - homeLoginV3
//@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);
//세션에 회원 데이터가 없으면 home으로
if (loginMember == null){
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
세션 조회해서 없으면 home화면으로, 세션이 유지되면 로그인 상태 홈 화면으로 이동.
[12] 로그인 처리하기 - 서블릿 HTTP 세션 2
@SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 이 애노테이션을 지원한다.
HomeController - homeLoginV3Spring
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name=SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home으로
if (loginMember == null){
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
세션을 찾고 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한 번에 해결해 준다.
■ TrackingModes
로그인을 처음 시도하면 URL에 jessionid가 포함돼 있다.

이는 웹 브라우저가 쿠키를 제공하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.
서버 입장에선 브라우저의 쿠키 제공 여부를 알 수 없으므로 쿠키도 보내고 URL에 jessionid도 담아 만약의 사태를 대비하는 셈이다.
물론 거의 대다수의 브라우저가 쿠키 방식을 제공하며, 이 옵션을 끄고 싶으면

application.porperties에 이 코드를 추가하면 된다.
[13] 세션 정보와 타임아웃 설정
SessionInfoController
@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());
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
}
- sessionId : 세션Id인 JSESSIONID의 값
- 예) 34B14F008AA3527C9F8ED620EFD7A4E1
- maxInactiveInterval : 세션의 유효 시간
- 예) 1800초(30분)
- creationTime : 세션 생성일시
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간. 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로
sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부

■ 세션 타임아웃 설정
1] 대다수의 사용자들은 로그아웃을 하지 않고 웹 브라우저를 종료해 사이트 이용을 마무리한다.
2] 이 탓에 서버는 사용자가 웹 브라우저를 종료했는지 여부를 알 수 없다. 이는 HTTP의 '비연결성' 특성 때문이다.
3] 이러한 상황에서 서버는 세션 데이터를 언제 삭제해야 할지 판단하기 어려워진다.
4] 그렇다고 무한정 세션 데이터를 보관할 수는 없다.
① 세션과 관련된 쿠키가 탈취되면 오랜 시간이 지나도 해당 쿠키로 악의적인 요청이 가능해진다.
② 세션은 기본적으로 메모리에 생성되며, 메모리 크기는 무한하지 않기 때문에 계속해서 세션을 유지하는 것은 리소스 낭비다.
따라서 위와 같은 이유로 세션 타임아웃을 설정해야 한다.
■ 세션의 종료 시점
세션 생성 시점으로부터 30분을 정하면 30분마다 계속 로그인해야 하는 번거로움이 발생한다.
더 나은 대안은 사용자가 서버에 요청한 시간(lastAccessedTime)을 기준으로 30분 정도를 유지해 주는 것이다. 사용자가 서비스를 이용하고 있으면 서버에 요청한 최신 시간이 갱신되므로 30분마다 로그인하는 불편함은 사라진다.
그리고 서블릿이 공식 지원하는 HttpSession는 이 방식을 사용한다.
■ 세션 타임아웃 설정 방법
① 글로벌 적용
- application.properties
server.servlet.session.timeout=60 (60초. 기본값은 1800초[30분])
② 특정 세션 단위로 시간 설정
sesssion.setMaxInactiveInterval(1800);
'백엔드 > 스프링' 카테고리의 다른 글
스프링 MVC 2편 - [8] 예외 처리와 오류 페이지 (0) | 2023.07.26 |
---|---|
스프링 MVC 2편 - [7] 로그인 처리 2 - 필터·인터셉터 (0) | 2023.07.22 |
스프링 MVC 2편 - [6] 로그인 처리 1 - 쿠키·세션(상편) (0) | 2023.07.20 |
스프링 MVC 2편 - [5] 검증2- Bean Validation(하편) (0) | 2023.07.19 |
스프링 MVC 2편 - [5] 검증2- Bean Validation(상편) (1) | 2023.07.19 |