JWT 세션

SJ.CHO·2024년 10월 11일

1. 인증 / 인가란?

  • 인증 : 사용자의 신원을 확인하는 과정.

  • APP 에 접근이 가능한 사용자인지 확인

    • 이메일/ 휴대폰/ 로그인/ 출입등
  • 인가 : 신원이 확인된 사용자가 기능을 이용할 수 있는지에 대한 권한 을 확인하는 과정

    • 특정 구역입장.
    • 출입시 휴대폰 보안인가.
  • 접근권한 여부 VS 권한 확인

2. 세션인증 VS 토큰인증

세션인증

  • 총 2가지의 인증(로그인, API 호출시 세션제시) 을 거침.
  • ClientSide 에서 K : V 형태로 쿠키로 보관.
  • RequestHeader에 담아서 전달하게된다.
  • 세션은 서버에 저장되며 클라이언트는 평문의 세션키만 가진다.

토큰인증

  • 총 2가지의 인증(로그인, API 호출시 세션제시) 을 거침.
  • Token은 별도로 서버에 저장되지않고, 그대로 클라이언트에게 반환.
    • 해당 토큰을 AccessToken 이라고 칭함.
    • 서버에 저장되는게 아니기에 유효기간을 짧게잡는다.
      (RefreshToken 기법)
  • 해당 서버에서 만들어진건지, 토큰의 정보, 만료시간 등을 확인가능하다.

공통점

  • 요구사항 관점에서의 흐름이 동일하다
  • 사용자가 매번 로그인할수없으니 이를 대체하기위한 무언가 를 이용한다.

차이점

  • 세션은 서버에 저장되지만 토큰은 저장하지않는다
    (Stateless 하다)
  • 세션은 서버에서 관리가 가능하지만 토큰은 자체적인 정보의 집합이라 서버측에서 관리가 어렵다.
  • 세션보다 Stateless 방식의 Token이 확장(Scale-Out)에 유리하다.

3. JWT?

  • 웹상에서 사용자 인증과 정보를 안전하게 교환하기위한 Token 스펙 (JSON 포맷이기 때문에 가볍다.)

  • Header :

    • JWT의 메타데이터를 포함.
    • 토큰의 유형, 해싱 알고리즘을 정의
  • Payload :

    • Claim 이라는 데이터를 포함한다.
    • 일반적으로 발급자, 만료시간, 주체 와 추가적으로 권한 및 사용자 PK등을 정의할수있다.
    • Claim 은 암호화되는게 아닌 인코딩이기에 누구나 읽을 수 있다. 민감한정보를 포함하지말자.
  • Signature

    • Header & Payload 를 기반으로 생성된다.
    • JWT의 무결성을 검증한다 (해당 서버가 작성했는지, 변조여부 확인)

4. Filter

  • HTTP Request를 가로채서 특정작업을 수행하는데 사용한다.
  • 원하는 경로, API 를 지정해 작업되게끔 개발이 가능하다.
  • Controller 에서의 앞 단이 생김으로써 컨트롤러의 책임이 줄어든다. (AOP)

Filter Chain

  • SpringBoot 는 여러개의 필터를 묶어 Filter Chain으로 관리가 가능하다.
  • HTTP Request 가 들어오면 Filter Chain 의 앞에서부터 하나씩 지나가며 모든 Filter 를 한번씩 실행하게 된다.
  • HTTP Response 가 나갈땐 반대로 마지막 Filter 에서부터 순서대로 실행된다.

Filter 의 형태

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class CustomFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 로직 (필요하다면)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        System.out.println("Request URI is: " + req.getRequestURI());
				
				// 1. HTTP Request 가 들어오고나서 수행할 작업작성
        // 2. 다음 필터로 요청을 전달
        chain.doFilter(request, response);
        // 3. HTTP Response 가 나가기 전에 수행할 작업작성
    }

    @Override
    public void destroy() {
        // 필터 종료 시의 로직 (필요하다면)
    }
}
  • Filter 인터페이스를 구현한 객체를 Bean으로 등록하여 사용한다.
  • filterChain.doFilter(request, response) 메소드를 기준으로 진행이되고, 해당 메소드가없다면 바로 HTTP Response 가 반환된다.
  • 기본적으로 Servlet 으로 구현된 filter 보다 편의성을 제공하는 GenericFilterBean , OncePerRequestFilter 가 존재한다
    (Filter < GenericFilterBean < OncePerRequestFilter 순으로 확장성이 넓다.)
    • GenericFilterBean :
      • 기존의 Spring의 범위밖에있는 Filter 를 기존 Bean 처럼 의존성 주입을 통해 해결이 가능하다.
    • OncePerRequestFilter :
      • 위의 두 필터는 하나의 요청에서 동일한 필터기능이 여러번 발생할 가능성이 있다.
      • OncePerRequestFilter 는 사용자의 요청한번당 한번만 실행되는 Filter 를 만들수 있다.

구현

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {
	
	private final MemberService memberService;
	
	@PostMapping("/login")
	public ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto req) {
		LoginResponseDto resp = memberService.login(req);
		return ResponseEntity.ok(resp);
	}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtHelper jwtHelper;

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException {
	  // 필터 실행여부 결정 
		if (!isFilterApplicable(req)) {
			chain.doFilter(req, resp);
		}
		
		// Request Header 에서 AccessToken 추출
		// 일반적인 형태 : "Authorization: Bearer xxxxyyyyyzzzz"
		String accessToken = Optional.ofNullable(req.getHeader("Authorization"))
			.map(header -> header.substring("Bearer ".length()))
			.orElseThrow(() -> new UnAuthorizationException("Not Found AccessToken!"));
		// AccessToken 에 대한 검증
		if (!jwtHelper.validate(accessToken)) {
			throw new UnAuthorizationException("Invalidate AccessToken!");
		}
		chain.doFilter(req, resp);
	}

	private boolean isFilterApplicable(HttpServletRequest req) {
		String path = req.getRequestURI();
		return path.startsWith("/api");
	}
}
  • isFilterApplicable(req) : 해당 API가 실행되어야하는 요청인지 확인.
  • header.substring("Bearer ".length())
    • Request Header 중 Authorization 헤더에서 AccessToken 을 추출해야하는데 일반적으로 Bearer {AccessToken} 와 같은 형태로 저장되어있다. (Bearer 은 토큰을 사용한다는 일종의 규칙)
@Order(0)
@Component
public class AuthenticationExceptionHandlerFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException {
		try {
			chain.doFilter(req, resp);
		} catch (UnAuthorizationException authException) {
			// 요구사항을 만족하기 위한 HTTP Response 데이터 세팅
			resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			resp.getWriter().println(authException.getMessage());
		}
	}
}
  • 요구조건을 맞추기 위해 HttpStatus를 보내주기위한 핸들러.

  • 이러한 일련의 흐름을 가진다.
profile
70살까지 개발하고싶은 개발자

0개의 댓글