토큰 인증 방식과 세션 인증 방식의 차이점

이수찬·2023년 5월 13일
0

1. 토큰 인증 방식

토큰 인증 방식은 request요청시 header에 담겨져 오는 token으로 로그인을 유
지하는 방식이다.

장점

  • 확장성
    (토큰은 웹 브라우저에 저장되기 때문에, 인증 요청시 헤더에 토큰을 넣어 계속 다른 서버에 요청을 보내도 상관없다.)

단점

  • SessionId에 비해 크기가 크다.
  • 토큰은 웹 브라우저에 저장하기 때문에, 공격에 취약하다.
    (이런 경우를 대비해 토큰에는 중요한 정보를 담지 않는다.
    또한 유효기간을 굉장히 짧게 설정하는데, 짧은 유효기간으로 계속 로그인 요청을 하는것을 막기 위해 첫 인증의 응답으로 accessToken과 함께 refreshToken을 같이 내려준다.)

처음 로그인하는 경우 인증 방식

  • 클라이언트는 로그인 요청을 위해 로그인에 필요한 정보를 담아 request를 보낸다.
  • 서버는 해당 request를 통해 인증 객체를 생성한다.
  • 인증 객체를 통해 db에 존재하는 회원의 정보와 일치하면 accessToken을 클라이언트에게 반환한고, 인증이 종료된다.

토큰이 헤더에 존재하는 경우 인증 방식

  • 클라이언트는 인증 요청시 header에 token을 담아보낸다.
  • request의 header에 token 이 존재한다면 필터에서 token을 파싱하여 해당 토큰으로 인증을 수행한다.
  • 인증이 완료되면, SecurityContextHolder.getContext().setAuthentication(authentication)를 통해 인증 객체를 저장하고, 인증을 종료한다.

1-1. 실제 코드로 구현해보기

첫 로그인 구현하기

public JwtTokenDto login(String username, String password) {

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
            username, password);

        Authentication authentication = authenticationManagerBuilder.getObject()
            .authenticate(authenticationToken);

        return jwtTokenProvider.generateToken(authentication);

    }
  • 로그인 요청을 할때, reqeust에서 보내준 username과 password를 통해 인증 객체를 생성했다.
  • 해당 인증 객체를 통해 AuthenticationManager를 통해 authentication객체를 AuthenticationProvider에게 실제적인 인증 객체의 유효성 검사를 진행한다.
  • 인증이 정상적으로 처리되면, 인증 객체를 jwtTokenProvider를 통해 jwt로 만들어 반환해준다.

토큰이 헤더에 존재하는 경우 인증 요청 처리하기

@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        String token = resolveTokenFromRequest((HttpServletRequest) request);

        if (token != null && jwtTokenProvider.validateToken(token)) {

            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);

        }

        chain.doFilter(request, response);
    }
  • 우선 request객체를 통해 token을 가져와 token을 파싱한다.
  • 만약 토큰이 존재하고, secret key와 expiredTime이 모두 정상적이라면, 검증된 회원이기에 token정보를 통해 인증 객체를 생성한다.
  • 이후 인증객체를 SecurityContext에 저장한 후 SecurityContextHolder에 담아두고 다음 필터로 이동한다.

2. 세션 인증 방식

세션 인증 방식은 서버에 세션저장소를 두어, 인증이 완료되면, SessionId를 생성해 세션 저장소에 해당 ID와 회원을 매핑해 둔 후 클라이언트에 반환해주는 방식이다.

장점

  • SessionId는 크기가 매우 작다.
  • 세션은 서버측에서 관리하기에 공격에 token인증 방식보다 안전하다.
    (세션 방식은 SessionId가 탈취되어도 서버의 세션저장소에 해당 SessionId를 삭제하면 되기 때문에 비교적 안전하다.)

단점

  • 세션 저정소를 서버에서 관리하기 때문에, 요청이 많아지면 많아질수록 서버 부하가 심해진다

처음 로그인하는 경우 인증 방식

  • 클라이언트는 로그인 요청을 위해 로그인에 필요한 정보를 담아 request를 보낸다.
  • 서버는 해당 request를 통해 인증 객체를 생성한다.
  • 인증 객체를 통해 db에 존재하는 회원의 정보와 일치하면 SesstionId를 생성하고, 세션저장소에 user와 SessionId를 매핑해 저장한다.
  • 클라이언트에게 SessionId를 반환한고, 인증이 종료된다.

헤더에 SessionId가 존재하는 경우 인증 방식

  • 클라이언트가 header에 SessionId를 보내면, SessionId를 통해 세션저장소에서 해당 SesstionId와 매핑되어있는 user가 존재하는지 확인한다.
  • 매핑되어있는 user가 존재하면 인증이 종료된다.

Spring Security에서는 기본적으로 세션-쿠키 방식을 사용하고 있다.

AbstractPreAuthenticatedProcessingFilter

private class PreAuthenticatedProcessingRequestMatcher implements RequestMatcher {

		@Override
		public boolean matches(HttpServletRequest request) {
			Authentication currentUser = AbstractPreAuthenticatedProcessingFilter.this.securityContextHolderStrategy
					.getContext().getAuthentication();
			if (currentUser == null) {
				return true;
			}
			if (!AbstractPreAuthenticatedProcessingFilter.this.checkForPrincipalChanges) {
				return false;
			}
			if (!principalChanged(request, currentUser)) {
				return false;
			}
			AbstractPreAuthenticatedProcessingFilter.this.logger
					.debug("Pre-authenticated principal has changed and will be reauthenticated");
			if (AbstractPreAuthenticatedProcessingFilter.this.invalidateSessionOnPrincipalChange) {
				AbstractPreAuthenticatedProcessingFilter.this.securityContextHolderStrategy.clearContext();
				HttpSession session = request.getSession(false);
				if (session != null) {
					AbstractPreAuthenticatedProcessingFilter.this.logger.debug("Invalidating existing session");
					session.invalidate();
					request.getSession();
				}
			}
			return true;
		}

	}

@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (this.requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
			if (logger.isDebugEnabled()) {
				logger.debug(LogMessage.of(
						() -> "Authenticating " + this.securityContextHolderStrategy.getContext().getAuthentication()));
			}
			doAuthenticate((HttpServletRequest) request, (HttpServletResponse) response);
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Did not authenticate since request did not match [%s]",
						this.requiresAuthenticationRequestMatcher));
			}
		}
		chain.doFilter(request, response);
	}

실제 AbstractPreAuthenticatedProcessingFilter의 matches메소드를 보면 SecurityContext에 인증객체가 존재하는지 확인한다.
실제 인증 객체가 존재하면 doAuthenticate()메소드를 통해 인증 객체의 인증을 진행하는 것을 볼 수 있다.

0개의 댓글