본문 바로가기
Spring & Spring Boot

Springboot & Vue3.js - 쿠키 , 세션을 이용한 로그인 처리

by 지민재 2023. 1. 12.

 

서버에서 쿠키 생성하기

 

new Cookie("memberId", String.valueOf(loginMember.getId()));
: Cookie 라는 클래스 생성자로 key/value 를 인수로 넘겨주어 생성한다.

 

response.addCookie(idCookie);
: 생성된 쿠키(idCookie)를 서버 응답 객체(HttpServletResponse) 에 addCookie를 이용해 담아준다. 그럼 실제로 웹 브라우저에서는 Set-Cookie 프로퍼티에 쿠키정보가 담겨져 반환된다.

 

서버에서 쿠키 조회하기

 

@CookieValue(name = "memberId", required = false) Long memberId
: 쿠키를 편하게 조회할 수 있도록 도와주는 애노테이션이다. 전송된 쿠키정보중 key가 memberId인 쿠키값을 찾아 memberId 변수에 할당해준다.
required가 false이기에 쿠키정보가 없는 비회원도 접근 가능하다.

 

서버에서 쿠키 없애기(로그아웃)

 

cookie.setMaxAge(0);

응답 쿠키의 정보를 보면 Max-Age=0 으로 되어있어 해당 쿠키는 즉시 종료

 

 

쿠키만 사용하여 로그인 처리 문제점

 

1. 쿠키에 보관된 정보(memberId) 를 타인이 훔쳐갈 수 있다.

2. 쿠키 값을 임의대로 변경할 수 있다.

간단히 말해 개인정보들이 클라이언트에 저장되어있기 때문에 위변조 및 도용이 쉽다는 문제이다.

 

이러한 문제들 때문에 세션을 통환 로그인 처리를 해야한다. 

 

세션을 통한 로그인 처리

 

1. 클라이언트에서 회원 정보를 가지고 있지않다.

 

2. 추정 불가한 세션 아이디만 쿠키를 통해 주고받아 보안이 강화됨.

 

3. 세션아이디가 저장된 쿠키의 만료시간을 짧게 유지하여 해커가 해당 키를 도용한다 하더라도 금새 갱신되며 사용하지 못하게 되어 보안이 강화됨.

 

세션생성 

 

 

1. 세션 키는 중복 x , 랜덤값이어햐함.

 

2. key 매칭될 value 가 있어야함.

 

3. 세션키를 응답쿠키에 저장 후 클라이언트에 전달해야함.

 

서블릿 HttpSession의 원리 

package component;

import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;

@Component
public class SessionManger {

	public static final String SESSION_COOKIE_NAME = "mySessionId";
	
	/* ConcurrentHashMap 이란?
	 * 
	 * Hashtable 클래스의 단점을 보완하면서 Multi-Thread 환경에서 사용할 수 있도록 나온 클래스가 바로 ConcurrentHashMap
	 * 시작업 가능한 쓰레드 수인 이유는 위에서 말했던 것처럼 ConcurrentHashMap은 버킷 단위로 lock을 사용하기 때문에
	 * 같은 버킷만 아니라면 Lock을 기다릴 필요가 없다는 특징 즉, 여러 쓰레드에서 ConcurrentHashMap 객체에 동시에 데이터를 삽입, 참조하더라도 그 데이터가 다른 세그먼트에 위치하면
	 * 서로 락을 얻기 위해 경쟁하지 않습니다.
	 */
	private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
	
	public void createSession(Object value, HttpServletResponse response) {
        //세션 생성
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);

        //쿠키 생성 후 저장
        Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(cookie);
    }
	
	public Object getSession(HttpServletRequest request) {
        Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
        if (cookie == null) {
            return null;
        }
        return sessionStore.get(cookie.getValue());
    }
	
	public void expire(HttpServletRequest request) {
        Cookie cookie = findCookie(request, SESSION_COOKIE_NAME);
        if (cookie != null) {
            sessionStore.remove(cookie.getValue());
        }
    }

    public Cookie findCookie(HttpServletRequest request, String cookieName) {
        if (request.getCookies() == null) {
            return null;
        }

        return Arrays.stream(request.getCookies())
                .filter(c -> c.getName().equals(cookieName))
                .findAny()
                .orElse(null);
    }
}

 

ConcurrentHashMap 이란?
 
Hashtable 클래스의 단점을 보완하면서 Multi-Thread 환경에서 사용할 수 있도록 나온 클래스가 바로 ConcurrentHashMap
시작업 가능한 쓰레드 수인 이유는 위에서 말했던 것처럼 ConcurrentHashMap은 버킷 단위로 lock을 사용하기 때문에
같은 버킷만 아니라면 Lock을 기다릴 필요가 없다는 특징 즉, 여러 쓰레드에서 ConcurrentHashMap 객체에 동시에 데이터를 삽입, 참조하더라도 그 데이터가 다른 세그먼트에 위치하면
서로 락을 얻기 위해 경쟁하지 않습니다.


버킷 - bucket 이란?

객체가 파일이라면 버킷은 연관된 객체들을 그룹핑한 최상위 디렉토리라고 할 수 있다. 버킷 단위로 지역(region)을 지정 할 수 있고, 또 버킷에 포함된 모든 객체에 대해서 일괄적으로 인증과 접속 제한을 걸 수 있다.

 

Hashtable 단점 

 

동시에 작업을 하려해도 객체마다 Lock을 하나씩 가지고 있기 때문에 동시에 여러 작업을 해야할 때 병목현상이 발생

 

UUID 이란 ??

 

1. 업로드된 파일명의 중복을 방지하기 위해 파일명을 변경할 때 사용.

2. 첨부파일 파일다운로드시 다른 파일을 예측하여 다운로드하는것을 방지하는데 사용.

3. 일련번호 대신 유추하기 힘든 식별자를 사용하여 다른 컨텐츠의 임의 접근을 방지하는데 사용.

 

 

서블릿 HttpSession을 이용한 로그인 처리

스프링에서는 위에 Sessionmanager 의 역할을 하는 객체를 서블릿에서 HttpServlet 클래스를 통해 제공한다. 

선배들이 개발한 HttpSession 을 이용하여 우리는 편하게 사용할 수 있다. 

 

application.properties에 글로벌 설정

session.setMaxInactiveInterval(1800);

이렇게 1800초(30분)으로 설정을 해두면 LastAccessTime 이후 timeout 시간이 지나면 WAS 내부에서 해당 세션을 삭제한다.

 

@ResponseBody
	@RequestMapping(value = "login", method = RequestMethod.POST)
	public boolean login(@RequestBody Member member, HttpServletResponse response, HttpServletRequest request) {
		boolean idMatch = false;
		boolean idPwMatch = false;

		try {
			Member login = memberservice.login(member);

			logger.info("login : " + login);

			boolean passMatch = passwordEncoder.matches(member.getPwd(), login.getPwd());

			if (member.getMemId().equals(login.getMemId())) {
				idMatch = true;
				System.out.println(idMatch);

			}
			logger.info("member.getPwd() = " + member.getMemId());
			logger.info("login.getPwd() = " + login.getMemId());

			if (passMatch && idMatch == true) {
				idPwMatch = true;

			}

			// 세션 매니저를 통해 세션 생성및 회원정보 보관
			// 세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
			HttpSession session = request.getSession();
			String id = login.getMemId();
			logger.info(id);
			session.setAttribute("memId", id);
			String sessionId = (String) session.getAttribute("memId");
			Enumeration<String> ele = session.getAttributeNames();
			logger.info("ele = " + ele);
			logger.info("sessionId = " + sessionId);
			logger.info("creationTime={}", new Date(session.getCreationTime()));
			logger.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
		} catch (NullPointerException e) {
			e.printStackTrace();
		}

		return idPwMatch;
	}

getAttribute(String name) : 세션 속성명이 name인 속성의 값을 Object 타입으로 리턴한다. 해당 되는 속성명이 없을 경우에는 null 값을 리턴한다.

getAttributeNames(); :세션 속성의 이름들을 Enumeration 객체 타입으로 리턴한다.

 

new Date(session.getCreationTime()) : 1970년 1월 1일 0시 0초를 기준으로 하여 현재 세션이 생성된 시간까지 경과한 시간을 계산하여 1/1000초 값으로 리턴한다. 

 

getMaxInactiveInterval() :현재 생성된 세션을 유지하기 위해 설정된 세션 유지시간을 int형으로 리턴한다.

 

invalidate() : 현재 생성된 세션을 무효화 시킨다. 소멸

 

removeAttribute(String.name) : 세션 속성명이 name인 속성을 제거한다. 

 

setAttribute(String name, Object value) : removeAttribute(String.name) : 세션 속성명이 name인 속성을 제거한다. 

 

setMaxInactiveInterval(int interval)  : 세션을 유지하기 위한 세션 유지시간을 초 단위로 설정한다. 즉, Client가 Second 만큼 동안 요청이 들어오지 않으면 Session을 소멸

 

 

 

로그아웃을 위한 컨트롤러 클래스는 HttpSession을 제거

@ResponseBody
	@RequestMapping(value = "logout", method = RequestMethod.POST)
	public ResultVo logout(HttpSession session, HttpServletResponse response, HttpServletRequest request) {
		ResultVo result = new ResultVo(false, null);
		try {
			 session = request.getSession(false);
			//String sessionId = (String) session.getAttribute("memId");
			
			if (session != null) {
				session.invalidate();
				result.setSuccess(true);
			}
			//logger.info(session);
			System.out.println(result);
		} catch (Exception e) {
			logger.error("logout :" + e.getMessage(), e);
		}
		
		
		return result;
	}
getSession(), getSession(true)는 null 체크없이 바로 getAttribute()를 사용해도 무방하지만, getSession(false)는 null을 리턴할수 있기 때문에 null체크를 해야 한다.

리턴 값 true 반환시 '로그아웃 하였습니다. 메세지 띄우기

리턴 값 false 반환시 '로그아웃 실패하였습니다. 메세지 띄우기

Vue.js 

logout() {
      axios
        .post('http://localhost:3030/member/logout')
        .then((response) => {
          console.log(response.data)
          if (response.data.success === true) {
            alert('로그아웃 하였습니다.')
            this.$router.push('/member/login').catch(() => {})
          }
          if (response.data.success === false) {
            alert('로그아웃 실패했습니다.')
            console.log(response)
          }
        })
        .catch(function (error) {
          console.log(error)
        })
    },

로그인 후 > 발송페이지 출력 > 로그아웃 후 > 로그인페이지 출력

 

출처 https://catsbi.oopy.io/0c27061c-204c-4fbf-acfd-418bdc855fd8

출처: https://offbyone.tistory.com/303

출처 : https://tsop.tistory.com/15

댓글