개발/Spring

스프링 간단한 커스텀 인증 필터 구현하기

깨굴딱지 2024. 6. 26. 21:49

이번 포스팅에서는 스프링 부트 애플리케이션에서 커스텀 인증 필터를 구현하는 방법을 소개하겠습니다.

특히, 인증 로직을 데이터베이스와 연동하여 처리하는 방법에 대해 다룹니다.

 

목차

  1. 시작하며
  2. 프로젝트 셋업
  3. 커스텀 인증 필터 구현
  4. 암호화 로직 구현
  5. 필터 등록 및 설정
  6. 마치며

 

 

1. 시작하며

API 서버를 운영하면서 특정 요청에 대해 인증을 처리해야 할 때가 많습니다.

이번 포스팅에서는 간단하게 커스텀 인증 필터를 통해 요청을 검증하고, 데이터베이스에 저장된 정보를 바탕으로

인증을 처리하는 방법을 알아보겠습니다.

 

2. 프로젝트 셋업

먼저, 프로젝트의 build.gradle 파일에 필요한 의존성을 추가합니다.

스프링 부트, Lombok, MySQL 드라이버 등이 필요합니다.

 

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.myproject'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

    // Lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // MySQL Connector
    runtimeOnly 'com.mysql:mysql-connector-j'
}

 

3. 커스텀 인증 필터 구현

이제 실제로 커스텀 인증 필터를 구현해보겠습니다. AuthFilter 클래스를 생성합니다.

 

@RequiredArgsConstructor
@Slf4j
public class AuthFilter extends HttpFilter {
    private final BlablaDao blablaDao;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        super.init(filterConfig);
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException {
        String method = request.getMethod();
        String url = request.getRequestURI();
        String timestamp = request.getHeader("x-blabla-timestamp");
        String receivedAccessId = request.getHeader("x-blabla-access-id");
        String receivedSignature = request.getHeader("x-blabla-signature");
        String nowYm = String.valueOf(System.currentTimeMillis() / 1000);

        try {
            if (!StringUtils.hasText(timestamp) ||  !StringUtils.hasText(receivedAccessId) || !StringUtils.hasText(receivedSignature)) {
                throw new AuthenticationException("인증 헤더 정보가 누락되었습니다!");
            }

            int calcTm = Integer.parseInt(nowYm) - Integer.parseInt(timestamp);

            if(calcTm < -5 || calcTm > 60) {
                log.error("[AuthFilter] 인증시간 만료 (1분) 감지");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(CommonResponseFail.responseFail(ExceptionEnum.REQUEST_TIMEOUT.getCode(),ExceptionEnum.REQUEST_TIMEOUT.getMessage())));
                return;
            }

            EteamVO eteamVO = blablaDao.selectEteamByJwtKey(receivedAccessId);

            if(eteamVO == null) {
                throw new AuthenticationException("해당 access-id에 맞는 팀 정보가 존재하지 않습니다!");
            }

            String calculatedSignature = SignatureUtil.makeSignature(method, url, timestamp, receivedAccessId, eteamVO.t_jwtkey());

            if (!receivedSignature.equals(calculatedSignature)) {
                throw new AuthenticationException("인증 암호키가 올바르지 않습니다!");
            }

            chain.doFilter(request, response);
        } catch (AuthenticationException e) {
            log.error("[AuthFilter] 인증 로직 중 인증실패 감지 error:", e);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(CommonResponseFail.responseFail(ExceptionEnum.NO_PERMISSION.getCode(),ExceptionEnum.NO_PERMISSION.getMessage())));
        } catch (Exception e) {
            log.error("[AuthFilter] 인증 로직 중 인증에러 발생  error:", e);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(CommonResponseFail.responseFail(ExceptionEnum.NO_PERMISSION.getCode(), ExceptionEnum.NO_PERMISSION.getMessage())));
        }
    }
}

 

4. 서명 로직 구현

메소드, url, 타임스탬프, 액세스키 등 값들을 가지고 서명 로직을 구현해보겠습니다.

public class SignatureUtil {
    public static String makeSignature(String method, String url, String timestamp, String accessId, String accessKey) throws Exception {
        String space = " ";
        String newLine = "\n";

        String message = method +
                space +
                url +
                newLine +
                timestamp +
                newLine +
                accessId;

        SecretKeySpec signingKey = new SecretKeySpec(accessKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);

        byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(rawHmac);
    }
}

 

 

 

 

5. 필터 등록 및 설정

마지막으로, 필터를 등록하고 설정해보겠습니다. WebConfig 클래스를 생성하여 필터를 등록합니다.

 

@Configuration
public class WebConfig {
    private final BlablaDao blablaDao;

    public WebConfig(BlablaDao blablaDao) {
        this.blablaDao = blablaDao;
    }

    @Bean
    public FilterRegistrationBean<AuthFilter> authFilterRegistration() {
        FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new AuthFilter(blablaDao));
        registrationBean.addUrlPatterns("/v1/api/*"); // 필터를 적용할 URL 패턴
        return registrationBean;
    }
}

 

 

이제 모든 설정이 완료되었습니다. 커스텀 인증 필터가 성공적으로 적용되었습니다!

해당 필터를 통해 인증을 완료해야만 해당 컨트롤러로 이동할 수 있습니다.

즉, 지정한 URL 패턴의 HTTP 요청이 들어올 때마다 이 필터를 거쳐야만 컨트롤러로의 접근이 가능합니다.

이를 통해 간단한 인증 로직을 구현할 수 있습니다.


이 포스팅이 도움이 되셨으면 좋겠습니다 

질문이나 더 좋은 방법이 있다면 언제든지 댓글로 남겨주세요. Happy coding! 🐸