로그인 요구사항
1) 로그인 사용자만 상품에 접근하고, 관리할 수 있다.
2) 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동한다.
1. 로그인 V1 - new Cookie()
로그인 시 response에 쿠키를 같이 담아 반환하면서,
이후 사용자가 재방문 시, 쿠키 정보를 통해 유효한 사용자인지 판별할 수 있다.
문제점.
- 쿠키 값 임의 변경 가능
- 쿠키 값 유추 가능
→ request에서 쿠키 정보를 조작해서 보낼 시, 타 유저의 데이터가 노출될 수 있다.
- 쿠키에 보관된 정보 탈취
→ 웹 브라우저 / 네트워크 요청 과정에서 해킹의 위험이 있다.
- 쿠키 유효 시간 없음
[LoginController]
// 로그인 성공 시: 클라이언트에 new Cookie("memberId", getid) 를 넘겨준다.
// 로그인 실패 시: BindingResult에 에러 코드와 메시지를 담고, 로그인 뷰를 다시 띄운다.
@PostMapping("/login")
public String loginV1(@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) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 잘못되었습니다.");
return "/login/loginForm";
}
// 로그인 성공 처리 TODO
// 쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료 시 모두 종료)
Cookie cookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(cookie);
return "redirect:/";
}
// 로그아웃
// 같은 named의 시간 정보를 0인 새로운 쿠키를 response에 담아, home으로 redirect 한다.
@PostMapping("logout")
public String logoutV1(HttpServletResponse response) {
Cookie cookie = new Cookie("memberId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/";
}
[LoginService]
// MemberRepo 에서 loginId와 일치한 Member를 찾고 password를 비교한다.
public class LoginService {
private final MemberRepository memberRepository;
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
[MemberRepository]
// loginId와 일치한 Member를 찾는다.
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream().filter(m -> m.getLoginId().equals(loginId)).findAny();
}
[HomeController]
// 로그인 성공 : Cookie 정보를 바탕으로 loginHome
// cf. 쿠키 정보가 있어도, 관련된 DB의 회원이 없으면 false
// 로그인 x : 기존 home
@GetMapping("/")
public String loginHomeV1(@CookieValue(name = "memberId", required = false) Long value, Model model) {
if (value == null) {
return "home";
}
// View에 띄우기 위해, model에 loginMember 정보 전달
Member loginMember = memberRepository.findById(value);
model.addAttribute("member", loginMember);
return "loginHome";
}
2. 로그인 V2 - 세션 동작 방식
쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤)을 생성해 클라이언트와 서버를 연결한다.
그리고 서버에서는 토큰을 관리하면서 토큰 - 사용자 id를 매핑해서 사용자를 인식한다. 추가로 토큰의 만료 시간을 짧게(30분) 유지한다.
[SessionManager]
@Component
public class SessionManager {
private static final String SESSION_COOKIE_NAME = "sessionId";
private Map<String, Object> sessionTable = new ConcurrentHashMap<>();
// 1. 세션을 생성하고, 세션 테이블에 저장하기
// Response에 담아야 한다.
public void createSession(Object value, HttpServletResponse response) {
// 세션 id를 생성하고, 값을 세션에 저장
String sessionId = UUID.randomUUID().toString();
sessionTable.put(sessionId, value);
// 쿠키 생성
Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(cookie);
}
// 2. 세션 만료
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
// 세션 테이블에서 제거
if (sessionCookie != null) {
sessionTable.remove(sessionCookie.getValue());
}
}
// 3. 세션 조회 (Request 요청)
public Object getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
// 세션 테이블에 세션 id와 일치한 데이터가 있다면 반환
return sessionTable.get(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);
}
}
[LoginController]
// UUID 랜덤 키를 만들어 loginMember와 매핑지어 세션 테이블에 저장
// 그리고 세션 id로 쿠키 만들어 response에 담아둔다.
@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) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 잘못되었습니다.");
return "/login/loginForm";
}
// 세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
// 로그아웃
// 세션 테이블에서 request의 쿠키 중 세션 id와 같은 행 제거
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
[HomeController]
// 세션 테이블에서 Request의 세션 id와 일치한 회원 정보 조회
@GetMapping("/")
public String loginHomeV2(HttpServletRequest request, Model model) {
Member session = (Member) sessionManager.getSession(request);
if (session == null) {
return "home";
}
// View에 띄우기 위해, model에 loginMember 정보 전달
model.addAttribute("member", session);
return "loginHome";
}
3. 로그인 V3 - 서블릿 HTTP 세션
HttpSession
: 사용자가 웹 서버와 상호작용하는 동안 여러 요청에 걸쳐 상태 정보를 유지하는 방법 제공
ex. Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05`
[SessionConst]
// HttpSession에 데이터를 보관하고 조회할 때, 같은 이름이 중복되어 사용되므로 상수로 정의
public interface SessionConst {
String LOGIN_MEMBER = "loginMember";
}
[LoginController]
// 로그인 사용자에게 신규 세션 id 발급
// 세션 테이블 설정
@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) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 잘못되었습니다.");
return "/login/loginForm";
}
// 로그인 성공 처리
// 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
// UUID.RANDOM : 세션 id
// loginMember, SessionConst.LOGIN_MEMBER : 세션 테이블에서 세션 id와 매핑된 객체
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
// 로그아웃
// Request 요청에서 현재 삭제해야 할 세션을 찾고, 삭제한다.
@PostMapping("logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); // 삭제
}
return "redirect:/";
}
[HomeController]
1) 직접 request에서 유효한 세션 id가 있는지 조회
// Request 요청에서 사용자 세션이 있다면, 세션 테이블에서 검색
// 로그인 사용자 : loginHome
// 로그인 x : home
@GetMapping("/")
public String loginHomeV3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
// 로그인 시점에 세션에 보관한 회원 객체를 조회한다.
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember == null) {
return "home";
}
// View에 띄우기 위해, model에 loginMember 정보 전달
model.addAttribute("member", loginMember);
return "loginHome";
}
2) 어노테이션으로 request에서 유효한 세션 id가 있는지 조회.
세션 id로 세션 테이블에서 세션 객체 검색, 세션 객체 내에서 필요한 데이터 추출
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember
→ 세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 편리하게 처리
@GetMapping("/")
public String loginHomeV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
// 세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
// View에 띄우기 위해, model에 loginMember 정보 전달
model.addAttribute("member", loginMember);
return "loginHome";
}
4. TrackingModes
로그인을 처음 시도하면 URL에 'jsessionid'를 포함하고 있는 것을 볼 수 있다.
jsessionid는 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.
서버는 웹 브라우저가 쿠키 지원 유무를 처음에는 알지 못하기 때문에, 쿠키 값과 함께 jsessionid를 함께 전달한다.
URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음과 같은 옵션을 넣으면 된다.
[application.properties]
server.servlet.session.tracking-modes=cookie
'Spring > MVC 2' 카테고리의 다른 글
8. 예외 처리와 오류 페이지 (0) | 2024.05.05 |
---|---|
7. 로그인 처리2 - 필터, 인터셉터 (0) | 2024.05.04 |
5. 검증 2 - Bean Validation (0) | 2024.05.01 |
4. 검증 1 - Validation (0) | 2024.04.29 |
3. 메시지, 국제화 (0) | 2024.04.29 |