스프링 부트 3.2.1 버전을 기준으로 작성됨
//패키지 구조
* domain
* item
* member
* login
* web
* item
* member
* login
domain : 화면, UI, 기술 인프라 등등의 영역을 제외한 시스템이 구현해야 하는 핵심 비지니스 업무 영역
의존관계
web -> O domain
domain -> X web
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
/**
* @return null이면 로그인 실패
*/
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리 TODO
return "redirect:/";
}
}
로그인 성공시 home 화면
실패시 로그인 창으로 다시 이동
쿠키를 사용하여 로그인 상태를 유지한다.
쿠키에 대한 자세한 내용은 HTTP를 참조
영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지
//로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId",
String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
쿠키 이름은 memberId
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
@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";
}
}
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
쿠키 유효시간을 0으로 설정했기에 로그아웃시 쿠키가 바로 삭제된다.
클라이언트가 쿠키를 변경할 수 있다.
대안
중요한 정보를 보관하고 연결을 유지하는 방법
핵심
// 세션 관리
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
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 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());
}
}
private Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
ConcurrentHashMap
: 동시 요청에 안전
로그인 성공시 세션관리자를 통해 세션 생성 및 회원 데이터 보관
로그아웃시 세션관리자를 통해 쿠키를 expire로 만료
HttpSession은 잘 구현된 기능이다.
SessionManager와 같은 방식으로 동작
쿠키 생성시
쿠키 이름 : JSESSIONID
값 : 추정 불가능한 랜덤 값
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
HttpSesion에 데이터 보관 및 조회시,
같은 이름이 중복되어 사용되므로, 상수를 하나 정의
@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:/";
}
세션 생성과 조회
public HttpSession getSession(boolean create);
null
반환session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 삭제한다.
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
session.invalidate()
: 세션을 제거
// 세션 생성 x
@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";
}
로그인 처음 시도시 URL에 jsessionid를 포함함
URL 전달 방식 끄기
application.properties
server servlet.session.tracking-modes=cookie
HttpSession session = request.getSession(false);
session.getAttributeNames().asIterator()
.forEachRemaining(name -> log.info("session name={}, value={}", name, session.getAttribute(name)));
session.getId()
session.getMaxInactiveInterval()
new Date(session.getCreationTime())
new Date(session.getLastAccessedTime())
session.isNew()
return "세션 출력";
sessionId : 세션Id, JSESSIONID 의 값
maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)
creationTime : 세션 생성일시
lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신
isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부
로그아웃시 -> session.invalidate() 호출되어 세션 삭제
브라우저 강제 종료시 -> ?
HTTP가 비 연결성이기에 서버 입장에서 해당 상요자가 웹 브라우저를 종료한 것인지 여부 확인 불가
-> 남아 있는 세션을 무한정 보관시 다음과 같은 문제 발생
세션과 관련된 쿠키(JsessionId)를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키를 악의적으로 요청할 수 있다.
세션은 기본적으로 메모리에 생성, 메모리의 크기는 무한하지 않기에 꼭 필요한 경우만 생성해서 사용해야함.
스프링 부트로 글로벌 설정
application.properties
server.servlet.session.timeout=60 (defualt = 1800초 [30분])
//특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800);
//세션 타임아웃 사용
// 세션 타임 아웃 시간은 해당 세션관 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화 됨
// 이렇게 초기화 되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용 가능
session.getLastAccessedTime() : 최근 세션 접근 시간
LastAccessedTime이후로 timeout 시간이 지나면,
WAS가 내부에서 해당 세션 제거
로그인 유지
🔖 학습내용 출처