로그인 요구 사항
쿠키를 사용하여 로그인 상태를 유지하는 법
domain
web
도메인: 화면, ui, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비지니스 업무 영역
@Data
public class Member {
private Long id;
@NotEmpty
private String loginId;
@NotEmpty
private String name;
@NotEmpty
private String password;
}
회원가입시 받는 정보는 id, password, 사용자 이름이다.
@Slf4j
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
//회원가입시 작성된 정보 저장
public Member save(Member member){
member.setId(++sequence);
log.info("save: member = {}", member);
store.put(member.getId(), member);
return member;
}
//id를 사용한 member 검색 기능
public Member findById(Long id){
return store.get(id);
}
//login id를 받아 password를 조회하는 기능
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
public List<Member> findAll(){
return new ArrayList<>(store.values());
}
public void clearStore(){
store.clear();
}
}
member를 저장하고 관리하는 클래스
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberRepository memberRepository;
//
@GetMapping("/add")
public String addForm(@ModelAttribute("member") Member member){
return "members/addMemberForm";
}
//만약 비정상적인 id나 password를 생성했을 경우(bindingResult에 에러가 들어있을 경우), 회원가입 창으로 이동
@PostMapping("/add")
public String save(@Valid @ModelAttribute Member member, BindingResult bindingResult){
if (bindingResult.hasErrors()){
return "members/addMemberForm";
}
memberRepository.save(member);
return "redirect:/";
}
}
@Valid: view에서 전달 받은 값에 붙여 값을 검증
BindingResult: 스프링이 제공하는 검증 오류 보관 객체 -> 데이터에 유효하지 않은 속성이 있다면, 그에 대한 오류 정보가 담긴다
<h4 class="mb-3">회원 정보 입력</h4>
<form action="" th:action th:object="${member}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류 메시지</p>
</div>
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{loginId}"/>
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}"
class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{password}"/>
</div>
<div>
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{name}"/>
</div>
쿠키를 활용하여 로그인 기능을 구현한다.
@Service
@RequiredArgsConstructor
public class LoginService {
private final MemberRepository memberRepository;
//로그인 id와 비번을 받은후, 올바른 비밀 번호인지 판단하는 기능
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
로그인의 핵심 비지니스 로직
id를 이용해 password를 조회한 후, 파라미터로 넘어온 password와 비교하여 같으면 회원(member)을 반환하고, password와 다르면 null을 반환
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
id와 password만 받아 값을 넘긴다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final SessionManager sessionManager;
//@Model...은 타임리프에서 값을 한번에 받아오기 위해 사용
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form){
return "login/loginForm";
}
@PostMapping("/login")
public String loginForm(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse resp){
//만약 로그인이 안되어 있을 경우
if (bindingResult.hasErrors()){
return "login/loginForm";
}
//회원을 조회하는 로직 호출
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
//로직에서 null 반환시(회원이 존재하지 않을 시)
if (loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않음");
return "login/loginForm";
}
/*
로그인 성공시 세션 쿠키 생성
쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
*/
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
resp.addCookie(idCookie);
return "redirect:/";
}
//세션 및 필터 적용
@PostMapping("/login")
public String loginFormV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,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(SessionConst.LOGIN_MEMBER, loginMember);
//인터셉터 되었을 경우, 로그인이 된 뒤 원래 요청했던 페이지로 반환
return "redirect:" + redirectURL;
}
}
쿠키 생성로직
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
resp.addCookie(idCookie);
</div>
<form action="item.html" th:action th:object="${loginForm}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">전체 오류 메시지</p>
</div>
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{loginId}"/>
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}"
class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{password}"/>
</div>
입력받은 id와 password를 LoginController로 전송한다.
@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";
}
}
/*
@SessionAttribute <- 세션을 더 편리하게 사용하는 어노테이션
세션에 들어있는 데이터를 찾는 과정을 편리하게 처리
*/
@GetMapping("/")
public String homeLoginV4(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model){
//login
//세션에 회원 데이터가 없으면 home
if (loginMember == null){
return "home";
}
//세션이 유지되고, 데이터가 존재하는 것을 확인할 경우 -> 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
로직
로그아웃 되는 조건
//로그아웃 기능
@PostMapping("/logout")
public String logout(HttpServletResponse resp){
expireCookie(resp, "memberId");
return "redirect:/";
}
//쿠키의 종료날짜(MaxAge)를 0으로 지정하여 종료
private void expireCookie(HttpServletResponse resp, String memberId) {
Cookie cookie = new Cookie(memberId, null);
cookie.setMaxAge(0);
resp.addCookie(cookie);
}
}
//세션 매니저를 이용한 로그아웃 구현
//@PostMapping("/logout")
public String logoutV2(HttpServletRequest request){
sessionManager.expire(request);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
if (session != null){
//세션을 제거하는 로직
session.invalidate();
}
return "redirect:/";
}
쿠키를 이용하여 로그인 id를 전달해서 로그인을 유지하는 경우, 다음과 같은 문제가 발생한다
대안
클라이언트와 서버는 쿠키로 연결이 되어야한다.
세션관리의 세가지 기능
서블릿에서 세션관리를 위해 HttpSession이라는 기능을 제공한다.
세션 생성
세션에 로그인 회원정보 보관
세션에서 로그인 회원정보 조회
세션 제거
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
데이터를 보관 및 조회시 같은 이름이 중복되어 사용되므로 상수를 하나 정의함
/*
로그인 성공처리
세션이 존재할 경우 존재하는 세션 반환
없으면 신규세션을 생성
*/
HttpSession session = request.getSession();
//세션에 로그인 회원정보를 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
//인터셉터 되었을 경우, 로그인이 된 뒤 원래 요청했던 페이지로 반환
return "redirect:" + redirectURL;
}
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 삭제한다.
HttpSession session = request.getSession(false);
if (session != null) {
//세션 제거 로직
session.invalidate();
}
return "redirect:/";
}
보통 사용자가 서버에 최근에 요청한 시간을 기준으로 일정시간을 유지한다.
-> HttpSession이 사용하는 방식
스트링 부트의 설정파일(application.properties)에 다음 설정을 추가한다.
session.setMaxInactiveInterval(1800); //1800초