쿠키
로그인 기능이 있는 서비스를 개발하다 보면, 로그인 시 이용할 수 있는 페이지와 그렇지 않은 페이지가 있다. 같은 화면도 로그인 이용자에게 보여줄 화면과 비로그인 이용자에게 보여줄 화면을 다르게 설정해야 할 때도 있다.
이때, 쿠키를 사용해서 로그인 정보를 관리할 수 있다.
서버에서 클라이언트로 loginId가 담긴 쿠키를 전달하고,
클라이언트에서는 그 쿠키를 저장해 뒀다가 HTTP 요청 시 서버로 전달한다.
< 쿠키 >
//Cookie 생성자
Cookie idCookie = new Cookie( String name, String value )
//HTTP 응답에 쿠키 담기
response.addCookie( idCookie );
//쿠키 삭제하는 법
idCookie.setMaxAge(0); //로 지정 후 응답에 쿠키 담기
그러나 쿠키를 사용해서 로그인 Id를 전달하면 심각한 보안 문제가 존재한다.
쿠키의 값은 클라이언트에서 쉽게 변경, 조회할 수 있다. 그 값을 이용해서 악의적인 요청을 할 수도 있다.
따라서 쿠키에 Id와 같은 중요한 값을 넣지 않고 임의의 랜덤 값 (UUID)를 넣어야 한다.
서버에서는 그 임의의 랜덤 값을 사용자 Id와 매핑해서 관리해야 한다. 이것이 바로 세션이다.
세션
세션은 쿠키를 기반하고 있고, 일반적인 쿠키와 똑같이 name, value로 이루어져있지만,
value를 임의의 랜덤 값으로 지정하여 서버에서 진짜 값을 관리한다는 차이점이 있다.
세션 동작 방식
로그인 사용자를 cookie에 담는 예로 진행 순서를 알아보자.
1. 임의의 랜덤 값을 생성한다. (uuid)
2. 서버 측에 생성한 랜덤값을 key로, loginId를 value로 갖는 저장소을 만들어서 key값과 value값을 매핑한다.
3. 위의 랜덤값을 cookie의 값으로 사용하여, HTTP 응답에 쿠키를 담아서 전송한다.
4. 서버는 해당 쿠키를 HTTP header에 갖고있는다.
5. 로그인 객체를 확인하려면 해당 쿠키 값을 가져와서 위에서 만든 저장소에서 찾는다.
6. 로그아웃시에는 클라이언트의 쿠키는 그대로 놔두고, 서버 측 저장소에서 key-value를 삭제한다.
HttpSession
서블릿에서 위의 세션 동작방식을 자동화해 주는 HttpSession을 제공한다. 서블릿을 통해 HttpSession을 생성하면, 쿠키 이름이 JSESSIONID이고, 값은 추정 불가능한 임의의 랜덤 값(UUID)인 쿠키를 생성한다.
HttpSession 사용 방법
우선 메서드에서 HttpServletRequest request를 받아야 한다.
1. 로그인 시
request.getSession()
request에 세션이 있으면 세션을 반환하고, 없으면 신규 세션을 생성하여 HttpSession session에 담는다.
HttpSession session = request.getSession();
session.setAttribute()
세션에 member라는 이름의 loginMember 객체를 담는다.
session.setAttribute("member",loginMember);
2. 로그아웃 시
request.getSession(false)
인자를 false로 전달하면 세션이 없어도 새로 생성하지 않는다.
HttpSession session = request.getSession(false);
session.invalidate()
세션을 제거한다. 위의 표현중, "서버 측 저장소에서 key-value를 삭제한다." 부분이다.
if(session != null){
session.invalidate();
}
3. 세션 조회
session.getAttribute("loginMember")
세션에서 "loginMember"라는 이름의 세션을 가져온다.
@GetMapping()
public String login(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session == null){
//세션이 존재하지 않을 때의 로직
}
//세션이 존재할 때 세션에서 멤버를 불러오는 로직
Member loginMember = (Member) session.getAttribute("loginMember");
if(loginMember == null){
//세션은 존재하지만, 로그인 멤버가 없을 때의 로직
}
//로그인 멤버가 있다면, 위에서 받은 loginMember 변수를 이용하면 된다.
}
@SessionAttribute
스프링에서 제공하는 위의 HttpSession의 로그인 여부 조회 기능을 편리하게 사용할 수 있게 해주는 Annotation이다.
최초 로그인 시에 사용하는 login 메서드에서만 위의 코드를 넣고, 이후에 다른 메서드에서 로그인 여부를 확인할 때는
메서드에 [ @SessionAttribute(name ="세션 이름"), required = false) Member loginMember ]만 추가해서, loginMember 변수를 사용하면 된다.
@GetMapping()
public String serviceMethod(@SessionAttribute(name ="loginMember"), required = false) Member loginMember){
if(loginMember == null){
//로그인 멤버가 존재하지 않을 때의 로직
}
//로그인 멤버가 있다면, 위에서 @SessionAttribute로 받은 loginMember 변수를 이용하면 된다.
}
HttpSession 사용 설정
TrackingModes
로그인을 처음 시도하면 URL에 아래와 같이 표시되어 있다.
http://localhost:9090/board/;jsessionid=FEB7 AFA8 A7925 ED8003154 B781 A60773
이때 application.properties에 아래와 같은 옵션을 추가하면 URL에 jsessionid가 노출되지 않는다.
server.servlet.session.tracking-modes=cookie
세션 타임아웃
사용자가 직접 로그아웃을 하지 않는 경우가 많다.
이때 일정 시간이 지나면 session을 알아서 종료해 주는 기능을 지원한다.
application.properties에 아래와 같은 옵션을 추가하면 된다.
server.servlet.session.timeout=1800
세션의 여러 가지 메서드
HttpSession session = request.getSession(false);
session.getId(); // 세션ID (UUID 값)
session.getMaxInactiveInterval(); //세션 유효시간
session.getCreationTime(); //세션 생성 일시
session.getLastAccessedTime(); //최근 접근 일시
session.isNew(); //이번 요청에서 생성된 값인지, 이전에 생성된 값을 조회한것인지 확인
세션 Id 관리
세션 Id는 계속 사용하는 값이므로, 상수로 따로 빼서 사용하는 것이 편하다.
public interface SessionConst {
String LOGIN_MEMBER = "loginMember";
}
예제 ) @SessionAttribute
게시판에서 searchCode, searchWord 두 변수를 받아서 검색 결과 화면을 GetMapping 한 예제이다.
아래와 같이 한 컨트롤러 내에서 로그인 체크 로직을 자주 사용하면 메서드로 빼내서 사용하면 좋습니다.
세션의 loginMember를 꺼내서 로그인 여부를 확인한 후
로그인 시 model에 show=true와 로그인 멤버의 정보를,
비로그인 시 model에 show=false를 담는 로직입니다.
@GetMapping("/{searchCode}/{searchWord}")
public String searchResult(@PathVariable String searchCode, @PathVariable String searchWord,
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model){
List<Post> searchList = postManager.getSearchedList(searchCode, searchWord);
isLoggedin(loginMember, model);
model.addAttribute("form",new SearchForm());
model.addAttribute("findPosts",searchList);
return "board/findPosts";
}
/**
* 로그인 체크 메소드
* loginMember o : model에 loginMember와 show=true를 담음
* loginMember x : show=false를 담음
* @return Boolean show
*/
private boolean isLoggedin(Member loginMember, Model model) {
boolean show = true;
if(loginMember == null)show = false;
else model.addAttribute("member", loginMember);
model.addAttribute("show",show);
return show;
}
예제 ) 로그인
작성자가 만든 게시판 프로젝트에 있는 로그인 컨트롤러입니다.
https://github.com/HSRyuuu/my_first_board/blob/master/src/main/java/hello/board/web/LoginController.java
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form,
@RequestParam(required = false)boolean loginMsg,
Model model){
//"로그인 시 이용할 수 있는 서비스입니다!" 출력 여부
if(loginMsg){
model.addAttribute("loginMsg",true);
}
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/")String redirectURL,
HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
//Id,pw 입력받아서 loginService.login 로직 실행 -> 성공 : member 반환 , 실패 : null 반환
Member loginMember = loginService.login(form.getLoginId(),form.getPassword());
//null 반환시 없는 id입니다 오류 반환
if(loginMember == null){
bindingResult.reject("loginFail");
return "login/loginForm";
}
//로그인 성공 로직
HttpSession session = request.getSession();//세션이 있으면 있는 세션 반환, 없으면 신규세션 생성
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
//redirectURL에 값이 들어있으면 해당 URL로 redirect 한다.
if (redirectURL != null) {
return "redirect:"+redirectURL;
}
return "redirect:/board";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request){
//세션이 있으면 있는 세션 반환, 없으면 신규세션 생성 -> false : 없어도 신규 세션 생성 x , default = true
HttpSession session = request.getSession(false);//
if(session != null){
session.invalidate();
}
return "redirect:/board";
}
}
(참고) 인프런 - 김영한 님 Spring MVC 2편
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'JAVA & Spring > Spring' 카테고리의 다른 글
[Spring MVC] @RequestBody와 @RequestHeader로 JSON 주고받기 (0) | 2023.08.13 |
---|---|
[Spring / Transaction] 트랜잭션 실제 적용되고 있는지 확인하는 방법 (0) | 2023.05.17 |
[Spring Boot] PRG 패턴 ( Post - Redirect - Get ) (1) | 2023.04.21 |
[Spring Boot] @PathVariable과 @RequestParam - 파라미터 받기 (0) | 2023.04.21 |
[Spring Boot] 스프링 인터셉터 (Spring Interceptor) (0) | 2023.04.21 |