패키지 구조 설계
도메인이 가장 중요하다
도메인 = 화면 UI, 기술 인프라 등을 제외한 시스템이 구현해야하는 핵심 비즈니스 업무 영역
(item, member, login...)
향후 web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 한다
web은 domain을 알고있지만 domain은 web을 모르도록 설계해야 한다
즉 web은 domain을 의존하지만, domain은 web을 의존하지 않는다
로그인 처리 - 쿠키 사용
쿠키
웹 브라우저에 저장되는 데이터 조각
기본적으로 HTTP는 Stateless라서 쿠키가 없으면 사용자를 기억 못함 -> 로그인 유지 x
서버 - 클라이언트 간에 state를 유지하기 위해 사용
장바구니, 자동 로그인 등의 기능에 사용
영속 쿠키 : 만료 날짜를 입력하면 해달 날짜까지 유지
세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시 까지만 유지
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResulst.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
@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";
}
}
쿠키가 없으면(로그인x) 기본 홈페이지로
쿠키가 있으면(로그인o) 멤버전용홈페이지로
쿠키를 사용해서 memberId를 전달해 로그인을 유지했다. 그러나 심각한 보안문제가 발생
보안 문제로 인해 쿠키는 장바구니, 방문자 카운트, 설정값 유지 부가기능, 간단 저장용에만 쓰인다
보안문제
쿠키 값은 임의로 변경할 수 있다
쿠키에 보관된 정보는 훔쳐갈 수 있다
해커가 쿠키를 훔쳐가면 평생 사용가능
대안
쿠키에 중요한 값을 노출하지 않고 사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 id를 매핑하여 인식한다.
토큰은 해커가 임의의 값을 넣어 찾을 수 없도록 예상 불가능 해야 한다
해커가 토큰을 탈취해도 시간이 지나면 사용할 수 없도록 토큰의 만료시간을 짧게 유지해야한다 해킹의심되는 경우 서버에서 토큰을 강제로 제거
쿠키에 정보를 보관하는 것은 매우 위험
따라서 중요한 정보를 모두 서버에 저장하고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야 한다
이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다
서버로 데이터를 받아 로그인이 되면 세션을 생성하고 세션id를 쿠키로 전달한다
이때 세션id는 추정 불가능해야 한다.
생성된 세션 id와 세션에 보관할 값을 서버의 세션 저장소에 보관한다
세션은 클라이언트마다 고유로 존재하며 서버의 세션매니저가 이를 관리한다
서버에는 세션저장소가 있어 세션들을 관리하고 각 세션에는 클라이언트에 맞는 데이터가 저장되어있다
Map<String, HttpSession> sessionStore = new HashMap<>();
String = 세션 ID
HttpSession = 자바 객체 (안에 속성들이 Map으로 들어있음)
public class HttpSession {
private Map<String, Object> attributes = new HashMap<>();
}
Login
서블릿이 제공하는 HttpSession을 생성하면 쿠키를 생성한다
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
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";
}
//세션이 있으면 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
//세션에 로그인 정보를 보관
session.setAttribute(LOGIN_MEMBER, loginMember);
return "redirect:/";
}
request.getSession(true)
-> 세션이 있으면 기존 세션 반환 / 없으면 새로운 세션 생성해서 반환
request.getSession(false)
-> 세션이 있으면 기존 세션 반환 / 없으면 새로운 세션을 반환하지 않고 null반환
session.setAttribute("loginMember", loginMember)
-> 값 저장
@RestController
@RequestMapping("/api")
public class LoginApiController {
@PostMapping("/login")
public ResponseEntity<?> login(
@RequestBody @Valid LoginForm form,
HttpServletRequest request) {
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "아이디 또는 비밀번호 오류"));
}
// 세션 생성
HttpSession session = request.getSession();
session.setAttribute(LOGIN_MEMBER, loginMember);
return ResponseEntity.ok(
Map.of(
"status", "success",
"memberId", loginMember.getId(),
"message", "로그인 성공"
)
);
}
}
API의 경우
로그아웃
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null) {
session.invalidate();
}
return "redirect:/";
}
session.invalidate()
세션을 제거한다 (=로그아웃)
로그인확인
@GetMapping("/")
public String homeLogin(@SessionAttribute(name = LOGIN_MEMBER, required = false) Member loginMember, Model model) {
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
@SessionAttribute(name = LOGIN_MEMBER, required = false)
DTO에 붙여서 세션이 있는지 검증
세션은 사용자가 로그아웃을 호출해 session.invalidate()가 호출 되는 경우 삭제된다
그러나 대부분 유저는 로그아웃을 직접 누르지않고 브라우저를 종료한다
문제는 HTTP가 비연결성이므로 서버입장에서는 해당 사용자가 브라우저를 종료한 것을 알 수 없다
따라서 서버에서는 세션을 언제 삭제해야하는지 판단하기 어렵다
세션이 메모리에 클라이언트별로 생성되므로 무한정 유지되면 메모리낭비가 될 수 있고 악의적인 요청을 지속적으로 시도할 수 있다
세션 종료시점을 정해 해당 시간이 지나면 세션이 삭제되도록 설정한다.
이때 시간제한은 클라이언트가 접속한 시간부터가 아니라 마지막 요청한 시간을 기준으로 한다
세션 타임아웃 설정
스프링부트로 글로벌 설정
application.properties
-> server.servelt.session.timeout = 60
단위는 초이며 분 단위로 설정해야한다 (60=1분, 120=2분...)
기본설정은 1800(30분)이다