- 이전 게시글
쿠키
개요
- 로그인 기능을 구현할 때, 쿠키를 사용할 수 있다.
- 쿠키를 사용하면, 사용자의 로그인 상태를 지속적으로 유지할 수 있다.
목표
- 사용자가 로그인을 하면, 로그인 상태가 유지된다.
- 로그인 직후, 회원 전용 페이지로 리다이렉션된다.
쿠키 동작 원리
-
쿠키 생성
-
클라이언트 요청
쿠키의 종류
쿠키의 종류는 아래와 같다.
- 영속 쿠키
- 만료 날짜를 입력하면 해당 날짜까지 유지된다.
- 세션 쿠키
- 만료 날짜를 생략하면 브라우저 종료시 까지만 유지된다.
쿠키와 보안 문제
쿠키를 사용해서 로그인 Id를 서버에 전달하면, 로그인을 유지할 수 있다. 하지만, 여기에는 보안 문제가 있다.
보안 문제
- 쿠키 값이 임의로 변경될 수 있다.
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 클라이언트의 PC가 털려서, 쿠키 정보를 가져갈 수 있다.
- 해커가 쿠키를 한번 훔치면 평생 사용할 수 있다.
- 쿠키에 저장된 사용자 정보를 해커가 계속해서 사용할 수 있다.
대안
- 쿠키에 중요한 값(사용자 정보 등)을 노출하지 않는다. 사용자 별로 예측 불가능한 임의의 토큰을 노출시킨 후, 서버에서 토큰과 사용자 id를 매핑해서 인식한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
- (문제 예. “토큰=1”일때, “토큰=2”를 넣으면 다른 사용자의 계정에 접근할 수 있겠다! )
- 토큰 만료시간을 짧게 유지하여, 해커가 털어가도 시간이 지나면 사용할 수 없도록 한다.
세션
개요
- 쿠키를 사용하여 로그인 기능을 구현하면, 심각한 보안 문제가 발생한다.
- 세션을 사용하여 해결할 수 있다.
세션 동작 원리
-
로그인
-
세션 생성
-
세션 id를 쿠키로 전달 (클라이언트 측에 전달)
-
세션 id를 쿠키에서 전달 (서버 측에 전달)
주요 포인트
- 회원 관련 정보는 전혀 클라이언트에 전달하지 않고, 오직 서버에서만 관리한다.
- 클라이언트 측 쿠키가 털려도, 거기에는 중요한 정보가 없다.
- 추정 불가능한 세션 id만 쿠키를 통해 클라이언트에 전달한다.
- 하나의 세션 id을 통해, 다른 사용자의 세션 id를 유추할 수 없다.
- 세션의 만료시간을 짧게 유지한다.
- 세션 id가 털려도, 시간이 지나면 사용할 수 없다.
로그인 처리: 세션 직접 구현
이전에 작성해둔 프로젝트를 기반으로 진행한다.
먼저 세션을 직접 구현해보고, 그 다음 자바와 스프링이 제공하는 세션을 사용해보자.
구현할 기능
- 세션 생성
- sessionId 생성 (임의의 추정 불가능한 랜덤 값)
- 세션 저장소에 sessionId와 보관할 값 저장
- 응답쿠키를 생성해서 sessionId 값을 클라이언트에 전달
- 세션 조회
- 클라이언트가 요청(전송)한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
- 세션 만료
- 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거
세션 기능 구현: SessionManager
클래스
import ...
@Component
public class SessionManager {
//클라이언트에 전달할 세션 id 값의 Key 값
//(mySessionId, 세선id)
public static final String SESSION_COOKIE_NAME = "mySessionId";
//세션 저장소
//CocurrentHashMap => 동시성 문제 해결
private Map<String, Object> sessionStorage = new ConcurrentHashMap<>();
/*
세션 생성
*/
public void createSession(Object value, HttpServletResponse response) {
String sessionId = UUID.randomUUID().toString();
//세션 저장소에 저장
sessionStorage.put(sessionId, value);
//세션 id를 쿠키로 응답
Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
cookie.setPath("/"); //경로 설정
response.addCookie(cookie);
}
/*
세션 조회
*/
public Object getSession(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
Cookie sessionIdCookie = findSessionIdCookie(cookies, SESSION_COOKIE_NAME);
if (sessionIdCookie == null) {
return null;
}
return sessionStorage.get(sessionIdCookie.getValue());
}
/*
세션 만료
*/
public void expire(HttpServletRequest request) {
Cookie sessionIdCookie = findSessionIdCookie(request.getCookies(), SESSION_COOKIE_NAME);
if (sessionIdCookie != null) {
sessionStorage.remove(sessionIdCookie.getValue());
}
}
/**
* @return 해당 cookie이름을 찾으면 cookie, 못찾으면 null
*/
private Cookie findSessionIdCookie(Cookie[] cookies, String cookieName) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie;
}
}
return null;
}
}
- 쿠키를 클라이언트에게 줄때,
setPath()
를 통해 경로를 지정해야 정상적으로 작동한다.
세션 적용: LoginController
클래스
/**
* 로그인
*/
@PostMapping("/login")
public String login(@Validated @ModelAttribute("user") LoginUserForm form,
BindingResult bindingResult,
HttpServletResponse response) {
//검증 오류시
if (bindingResult.hasErrors()) {
return "login";
}
//로그인 실행
User user = loginService.login(form.getId(), form.getPassword());
//로그인 실패시, 오브젝트 에러
if (user == null) {
bindingResult.reject("wrongUser", "아이디나 비밀번호를 확인하세요.");
return "login";
}
//로그인 성공시
//세션 생성
sessionManager.createSession(user, response);
return "redirect:/";
}
/**
* 로그아웃
*/
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
//세션 종료
sessionManager.expire(request);
return "redirect:/";
}
세션 활용: HomeController
클래스
- 로그인된 사용자 ⇒ 사용자 정보 페이지
- 로그인 안된 사용자 ⇒ 기본 home 페이지
import ...
@Controller
public class HomeController {
private final SessionManager sessionManager;
@Autowired
public HomeController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@GetMapping("/")
public String home(HttpServletRequest request, Model model) {
User user = (User) sessionManager.getSession(request);
//로그인 X 일때, 기본 홈페이지로
if (user == null) {
return "home";
}
model.addAttribute("user", user);
//로그인 O 일때, 사용자 정보 페이지로
return "userPage";
}
}
결과
-
첫 접속 (로그인 X일때)
-
로그인
-
로그인 성공
로그인 처리: 서블릿이 제공하는 세션
- 서블릿은
HttpSession
이라는 기능을 통해 세션을 지원한다.
HttpSession
이란?
HttpSession
도 위에서 구현한 것처럼 동작한다.- 서블릿을 통해
HttpSession
을 생성하면 다음과 같은 쿠키를 생성한다.JSESSIONID=추정불가능한값
세션 적용: LoginController
클래스
import ...
@Controller
@RequestMapping("/member")
public class LoginController {
private final LoginService loginService;
@Autowired
public LoginController(LoginService loginService, SessionManager sessionManager) {
this.loginService = loginService;
this.sessionManager = sessionManager;
}
@GetMapping("/login")
public String viewUserLoginForm(@ModelAttribute User user) {
return "login";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("user") LoginUserForm form,
BindingResult bindingResult,
HttpServletRequest request) {
//검증 오류시
if (bindingResult.hasErrors()) {
return "login";
}
//로그인 실행
User user = loginService.login(form.getId(), form.getPassword());
//로그인 실패시, 오브젝트 에러
if (user == null) {
bindingResult.reject("wrongUser", "아이디나 비밀번호를 확인하세요.");
return "login";
}
//로그인 성공시
//세션 생성 후, 값 추가
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
return "redirect:/";
}
/**
* 로그아웃
*/
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
//세션 삭제
if (request.getSession(false) != null) {
request.getSession().invalidate();
}
return "redirect:/";
}
}
request.getSession(true)
,request.getSession()
-
세션이 있으면 기존 세션을 반환한다.
세션: 이전에 구현해본
sessionStorage
와 유사한 개념 - 세션은 사용자별로 생성된다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
-
request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 세션을 생성하지 않고, null을 반환한다.
-
HttpSession
원리- 하나의 세션에 여러가지 값을 저장할 수 있다.
세션 활용: HomeController
클래스
package prac.myPrac.controller;
import ...
@Controller
public class HomeController {
private final SessionManager sessionManager;
@Autowired
public HomeController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
/**
* @SessionAttribute 를 통해 세션에 접근
*/
@GetMapping("/")
public String home(
@SessionAttribute(name = "loginUser", required = false) User user,
Model model) {
//로그인 X 일때, 기본 홈페이지로
if (user == null) {
return "home";
}
model.addAttribute("user", user);
//로그인 O 일때, 사용자 정보 페이지로
return "userPage";
}
}
@SessionAttribute(name = "loginUser" , required = false)
request.getSession(false).getAttribute("loginUser")
와 동일한 기능을 한다.@SessionAttribute
는 세션을 생성하지 않는다.required = false
는user
객체에 값을 필수적으로 받지 않아도 된다는 뜻이다.
세션의 부가기능
TrackingModes
- 로그인을 처음 시도하면 URL이
jsessionid
값을 포함하고 있다.- 예시)
http://localhost:8080/;jsessionid=F26137183SASD23
- 예시)
- 이것은 웹 브라우저가 쿠키를 지원하지 않을 때에 대비하여 작성된 URL이다.
- 해당 기능이 바로 TrackingModes 이다.
- 해당 기능을 끄는 것을 권장한다.
application.properties
파일에 작성server.servlet.session.tracking-modes=cookie
세션 정보 확인
session.getId()
- 세션Id, 즉
jsessionid
값이다.
- 세션Id, 즉
session.getMaxInactiveInterval()
- 세션의 유효 시간 (초 단위)
session.getCreationTime()
- 세션 생성일시
- java.util.Date 타입 반환
session.getLastAccessedTime()
- 세션과 연결된 사용자가 최근에 서버에 접근한 시간
- java.util.Date 타입 반환
session.isNew()
- 새로 생성된 세션인지, 아닌지
세션 기본 종료 시점
- 세션은 사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지한다.
- 세션유지 시간 =
session.getLastAccessedTime()
+ 30분
- 세션유지 시간 =
- 본 게시글은 김영한님의 강의를 토대로 정리한 글입니다.
- 더 자세한 내용을 알고 싶으신 분들이 계신다면, 해당 강의를 수강하시는 것을 추천드립니다.