인가(Authorization) 로직 구현하기

Nicky·2024년 2월 16일
0
post-thumbnail

인가 절차


지난 포스팅에서 AuthenticationManager가 인증 완료된 Authentication 객체를 가져오고, JWT 토큰을 발급하는 단계까지 다뤘다.

인가 단계는 다음과 같은 과정을 가진다.

정상 과정

  • 클라이언트에서 토큰을 포함한 API 요청.
  • 인가 필터에서 토큰 유효성 검증.
  • 검증 성공 시 SecurityContextAuthentication 객체 저장

Access 토큰 만료 (검증 실패)

  • access 토큰 만료시 토큰 재발급, 응답.

Refresh 토큰 만료

  • 로그인 재요청 응답.

이제 커스텀 인가 필터를 통해 구현해보자.

JwtAuthorizationFilter

인가 필터는 요청당 한 번만 실행되는 OncePerRequestFilter를 상속하여 만들었으며 클라이언트의 API 요청시에만 실행될 예정이다.

doFilterInternal

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 헤더에 토큰 정보 확인
        String header = request.getHeader(HEADER_STRING.getValue());
        if (header == null || !header.startsWith(TOKEN_PREFIX.getValue())) {
            chain.doFilter(request, response);
            return;
        }
        // 헤더에서 토큰 정보 추출
        String token = request.getHeader(HEADER_STRING.getValue()).replace(TOKEN_PREFIX.getValue(), "");

        // 토큰 검증 및 인가
        try {
            securityService.validateToken(token);
            // 인증 정보 추출
            Authentication authentication = securityService.extractAuthentication(token);
            // 사용자 인증 정보 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        // access 토큰 만료시
        catch (SecurityException e){
            if (checkRefreshRequest(request, response, token)) {
                return;
            }
            responseWriter.writeErrorResponse(response, SC_UNAUTHORIZED, e.getMessage());
        }
        // 나머지 예외 상황
        catch (Exception e) {
            responseWriter.writeErrorResponse(response, SC_UNAUTHORIZED, e.getMessage());
        }
        chain.doFilter(request, response);
   }
  • 요청 헤더에서 access 토큰 추출

    // 헤더에 토큰 정보 확인
    String header = request.getHeader(HEADER_STRING.getValue());
    if (header == null || !header.startsWith(TOKEN_PREFIX.getValue())) {
    chain.doFilter(request, response);
    return;
    }
    // 헤더에서 토큰 정보 추출
    String token = request.getHeader(HEADER_STRING.getValue()).replace(TOKEN_PREFIX.getValue(), "");

  • access 토큰 검증 성공 시

        // 토큰 검증 및 인가
        try {
            securityService.validateToken(token);
            // 인증 정보 추출
            Authentication authentication = securityService.extractAuthentication(token);
            // 사용자 인증 정보 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
  • access 토큰 만료 시

        // access 토큰 만료시
        catch (SecurityException e){
            if (checkRefreshRequest(request, response, token)) {
                return;
            }
            responseWriter.writeErrorResponse(response, SC_UNAUTHORIZED, e.getMessage());
        }

checkRefreshRequest

토큰 재발급 요청일 때 처리

   // 현재 요청이 토큰 재발급 요청인지
   private boolean checkRefreshRequest(HttpServletRequest request, HttpServletResponse response, String token)
           throws IOException {
       String currentPath = request.getRequestURI();
       if ("/api/security/refresh".equals(currentPath)) {
           try {
               AccessTokenResponse accessTokenResponse = securityService.refreshAccessToken(token);
               responseWriter.writeAccessTokenResponse(response, accessTokenResponse);
               return true;
               // refresh 토큰 만료시
           } catch (SecurityException e) {
               responseWriter.writeErrorResponse(response, SC_UNAUTHORIZED, e.getMessage());
               return true;
           }
       }
       return false;
    }

인가 필터 등록

인가 필터에서는 인증 필터와는 다르게 별도로 AuthenticationManager을 필요로 하지 않는다.

// 인가 필터 설정
JwtAuthorizationFilter jwtAuthorizationFilter = new JwtAuthorizationFilter(securityService, responseWriter);

다만 필터 등록 시 등록 순서에 유의해야 한다.

.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);

SecurityContext

doFilterInternal에서 토큰 검증 성공 시 아래의 코드를 통해 사용자 인증 정보 저장한다.

// 사용자 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);

SecurityContext에 인증 객체를 저장하는 이유와 사용하는 방법에 대해 알아보자.

우선 SecurityContext는 인증된 사용자의 세부 정보를 저장하는데 사용되며, 기본적으로 ThreadLocal에 저장된다. 이는 각 요청마다 고유한 SecurityContext를 가지며, 다른 요청과 상태를 공유하지 않음을 뜻한다. 만약 요청을 처리하는 동안 사용자의 인증 상태가 변경되더라도 다른 요청에는 영향을 미치지 않는다.

사용자 인증 정보 사용

SecurityContextHolder를 통해 사용자 인증 정보를 저장하고, 가져올 수 있다(getter, setter 제공). 우리는 ThreadLocal의 특성에 따라 전역에서 SecurityContextHolder.getContext().getAuthentication()를 통해 인증 정보를 가져올 수 있다.

나의 경우.. 사용자 정보가 필요한 Controller 메서드는 다음과 같이 사용하였다.

    // 게시물 업로드
    @PostMapping
    public void uploadPost(@AuthenticationPrincipal(expression = "username") String username,
                           @Valid @ModelAttribute PostUploadRequest requestParam) {
        postService.registerPost(username, requestParam);
    }

마치며

아래 그림과 같이 Security에서 사용되는 많은 필터들이 있는데, 각각 보안 메커니즘을 잘 따져서 커스텀 필터를 구현하도록 해야 한다. 특히, JWT 로그인 같은 경우, 기준이 되는 reference가 없다고 생각되는데, 이 또한 잘 고려해서 설계해야함을 느끼는 바이다.

profile
코딩 연구소

0개의 댓글

관련 채용 정보