Spring Security를 이용해서 일회성 Api key를 추가해보자

코딩하는범이·2021년 9월 2일
0

JWT를 이용한 인증 관련된 블로그 글은 많이 존재하는데, 외부 API(네이버, 카카오 API 등)에서 api key를 이용해 사용하는 것처럼 인증키를 이용한 일회성 API 호출은 별로 없는것 같아 글을 작성해본다.

Api가 호출되었을때 캐치 할 수 있는 filter가 필요해서 Custom Filter를 만들어서 등록해야 한다.

Custom Filter

처음에는 아래와 같이 등록을 해서 사용하려고 했다.

@Bean
    public FilterRegistrationBean filterRegistration() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new ApiKeyFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setName("Api-Key-Filter");
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }
    
public class ApiKeyFilter extends OncePerRequestFilter {

    private String API_KEY_AUTH_HEADER_NAME = "Authentication";

    private final String TEST_KEY = "test";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String key = request.getHeader(API_KEY_AUTH_HEADER_NAME);

        if (!TEST_KEY.equals(key)) {
            ObjectMapper mapper = new ObjectMapper();
            response.getWriter().write(mapper.writeValueAsString("The API key was not found or not the expected value."));
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return;
        }
        filterChain.doFilter(request, response);
    }
}

ApiKeyFilter 에서 OncePerRequestFilter를 상속받았는데
OncePerRequestFilter는 사용자의 한번에 요청 당 딱 한번만 실행되는 필터이다. 우리는 Api를 호출할때마다 Api key가 유효한 값인지 확인해야 하기 때문에 위의 필터를 상속받았다.

문제점

그런데 무슨 문제인지 10번중 절반은 작동하지 않을 때가 있었다. Spring security의 필터와 내가 Bean으로 등록한 필터가 등록될때의 순서가 문제가 되는것으로 예상이 된다.
(아시는 분은 알려주시면 감사합니다)

그리고 Spring security를 사용하고 있는데 인증 관련 필터를 따로 등록해서 관리하는것도 뭔가 이상했다.

Security를 이용한 Filter 적용

그래서 Security를 통해서 필터를 설정해보기로 했다.
변경된 코드는 아래와 같다.

ApiSecurityConfig

@EnableWebSecurity
@Order(1)
@RequiredArgsConstructor
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {

    private final ApiKeyAuthFilter filter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        filter.setAuthenticationManager(new ApiKeyAuthManager());
        http
                .antMatcher("/api/**")
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
              
    }
}

나는 Security가 Api 뿐만 아니라 다른곳에서 사용해야 했기 때문에 위와 같이 각각의 Security 설정을 나눠서 구현했다.
.antMatcher("/api/**") : api/** 붙은 곳에서만 사용한다고 지정한다
.csrf().disable() : api key를 가지고 인증하기때문에 csrf는 비활성화 시킨다
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) : SessionCreationPolicy에는 여러가지 정책이 있는데 위는 세션을 생성하지 않는다는 뜻이다. 그렇기 때문에 매번 Api key를 인증받는다.
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class) : Security filter chain에서 UsernamePasswordAuthenticationFilter 전에 등록한다는 뜻이다. 그래서 앞의 필터가 인증이 완료되면 UsernamePasswordAuthenticationFilter는 패스하게 된다.

ApiKeyAuthFilter

@Component
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

    private String API_KEY_AUTH_HEADER_NAME = "Authentication";

    @Autowired
    public ApiKeyAuthFilter(ApiKeyAuthManager manager) {
        this.setAuthenticationManager(manager);
    }

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        return request.getHeader(API_KEY_AUTH_HEADER_NAME);
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        return "N/A";
    }
}

Header로 부터 값을 가져와 주는 역할을 담당한다. 자신이 사용할 API_KEY_AUTH_HEADER_NAME을 지정하면 된다.

ApiKeyAuthManager

@Component
public class ApiKeyAuthManager implements AuthenticationManager {

    private final String TEST_KEY = "test";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String principal = (String) authentication.getPrincipal();
        if (!TEST_KEY.equals(principal)) {
            throw new BadCredentialsException("The API key was not found or not the expected value.");
        }
        authentication.setAuthenticated(true);
        return authentication;
    }
}

실제로 api key가 설정한 값과 같은지 비교하는 부분이다. 나는 키를 test로 지정했기 때문에 Authentication 헤더의 값이 test가 맞는지 비교한다.

테스트

AuthApiController

@RestController
@RequestMapping("api/auth")
public class AuthApiController {

    /**
     * Api key가 유효한지 체크하기 위한 api
     *
     * @return
     */
    @GetMapping("check")
    public ResponseEntity<Object> check() {
        return ResponseEntity.status(HttpStatus.OK).body(true);
    }
}

위와 같이 테스트를 위한 컨트롤러를 만들고 테스트를 진행한다

api key 없이 호출

header에 api key를 추가하고 호출

위와 같이 잘 작동하는것을 볼 수 있다![]

profile
기록 그리고 기억

0개의 댓글