티스토리 뷰
[Spring] 인증 구현1(로그인/로그아웃) - 쿠키
# 서론 HTTP는 무상태 프로토콜이다. 즉, 요청을 한번 처리하고 서버와 클라이언트는 연결이 끊긴다. 따라서 다음 요청을 보낼 때, 서버는 클라이언트가 누구인지 매번 확인해야 한다. 서버가 클
mopil.tistory.com
지난 포스트에서 사용자 인증을 쿠키로 구현했다 하지만, 여러가지 보안상 이슈가 있어서 해당 부분을 보완해야 했었다
보완해야 하는 부분은 다음과 같다
- 쿠키 값이 예측가능하면 안된다
- 예측 불가능한 쿠키 값을 서버쪽에서 유저 데이터와 매핑해서 관리하고, 쿠키 지속시간을 짧도록 유지한다
# 세션 도입
사실, 세션이라는 개념은 그리 거창한 개념은 아니다
완전한 랜덤값으로된 쿠키를 사용자 데이터와 1:1로 매핑해서 서버측에서 관리하는 개념이 세션이라는 개념이다
따라서 다음과 같이 인증을 개편할 것이다
1. 사용자가 최초 로그인시, 완전한 랜덤값을 만들어 쿠키로 넣어준다
2. 해당 랜덤값과 사용자 정보가 1:1로 매핑된 테이블 데이터는 서버측에서 관리한다 (Java Map을 활용)
3. 해당 쿠키의 지속시간을 적절하게 짧게 유지하도록 설정한다
# 쿠키 값 예측 불가능하게 변경 : UUID 사용
Java에서 기본으로 제공하는 UUID라는 클래스를 활용하면 완전하게 무작위인 특정 값을 생성할 수 있다
나중에 스프링을 통해 의존성을 주입받을수 있도록 컴포넌트 스캔의 대상이 되도록 SessionManager 클래스를 생성한다
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
* sessionId 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
*/
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());
}
}
public 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);
}
}
# 컨트롤러 수정
@PostMapping("/login")
public String loginV2(@Valid @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:/";
}
로그인 / 로그아웃
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
SessionManager를 통해서 세션들을 관리하도록 한다
# 이제 된 것 인가?
이제 보안 문제는 어느정도 해결되었다 (세션 유지 시간은 아직 보류)
그런데, 이러한 SessionManager를 매 프로젝트 마다 새로 구현해서 사용하면 굉장히 귀찮을 것이다
그래서 서블릿이 HttpSession이라는 편리한 기능을 제공한다 (역시 개발자들은 중복을 싫어한다)
Java Collection에 따로 세션을 저장안해도 되고 request객체에 포함시키기만 하면 된다