API Server 구성 (2) (Note_Controller)
API Server 구성 (2) (Note_Controller)
API Server 구성 (1) (Note entity, dto, service, controller, repository) API Server 구성 (1) (Note entity, dto, service, controller, repository) Spring Security 연동 (7) (자동 회원가입 후 처리, Remeber me) Spring Security 연동 (7) (자동
soohykeee.tistory.com
API 서버를 위한 Filter
앞서 Note의 entity, dto, repository, controller, service 들을 만들어주었다. 여태 작성한 '/notes' 라는 경로는 외부에서 데이터를 주고받기 위한 경로이다. 해당 경로를 인증없이 아무나 계속 사용하도록 하는 것은 서버에 부담이 생긴다. 이를 해결하기 위해 인증을 받은 사용자만이 접근이 가능하도록 수정이 필요하다.
기존의 웹 어플리케이션에서 Cookie나 Session을 이용하여 인증을 하는 경우, 동일한 사이트에서만 동작하기에 API 서버처럼 자유롭게 데이터를 주고받는 형태에서는 유용하지 못하다. API 서버에서는 주로 인증정보, 인증 키를 같이 전송하여 처리해야한다. 이렇게 사용하는 키를 Token이라 부르며, 우리는 JWT를 사용하여 인증에 이용할것이다.
외부에서는 특정한 API를 호출할 때 반드시 인증에 사용할 Token을 같이 전송하고, 서버에서는 해당 Token을 검증한다. 이러한 과정에서 전달받은 Token을 검사하는 Filter가 중요하다. Spring Security에서는 원하는 Filter를 사용자가 작성하고, 이를 설정에서 추가해줄 수 있다.
OncePerRequestFilter
[Spring] OncePerRequestFilter란?
목적 OncePerRequestFilter를 이해하고 목적에 맞게 사용하기 위함 목차 학습 이유 OncePerRequestFilter 1. 학습이유 얼마전 Spring Security를 공부하면서 JWT필터를 구현했었다. 나는 GenericFilterBean을 상속받은 A
emgc.tistory.com
OncePerRequestFilter는 위에서 간단하게 설명을 보았다.
즉, 다시말해 OncePerRequestFilter는 추상 클래스로 제공되는 필터로 가장 일반적이며, 매번 동작하는 필터라고 생각하면 된다. 해당 클래스는 말했듯이 추상 클래스이기에, 이를 사용하기 위해서는 상속으로 구현해서 사용하면 된다. 해당 필터를 사용해주기 위해서 프로젝트 security 디렉토리 하위에 filter 디렉토리 생성 후, 그 하위에 ApiCheckFilter 클래스를 생성해줘야 한다.
@Log4j2
public class ApiCheckFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
filterChain.doFilter(request, response);
}
}
위의 ApiCheckFilter 클래스의 doFilterInternal 메소드의 마지막의 filterChain.doFilter()는 다음 필터의 단계로 넘어가는 역할을 위해서 작성해줘야한다. 또한 SecurityConfig에 Bean 으로 등록해준다.
@Bean
public ApiCheckFilter apiCheckFilter() {
return new ApiCheckFilter();
}
위와 같이 작성해준 후 ApiCheckFilter를 적용한 후에, '/sample/all' 과 같이 permitAll() 해준 경로로 접근 시, 콘솔창을 확인해보면 모든 Filter가 거쳐간 후 맨 마지막에 ApiCheckFilter를 타는것을 확인할 수 있다.
또한 앞서 말했지만, Filter의 동작 순서는 SecurityConfig에서 configure메소드에서 조절해줄 수 있다. 만일 예를 들어, UsernamePasswordAuthenticationFilter라는 필터보다 먼저 ApiCheckFilter를 거치도록 하고 싶다면, 아래의 코드를 추가해주면 된다.
http.addFilterBefore(apiCheckFilter(), UsernamePasswordAuthenticationFilter.class);
ApiCheckFilter의 경우 원래 기존의 의도대로, 오직 '/notes/..'로 시작되는 URL의 경우에만 동작하도록 하는게 바람직하기에 아래와 같이 코드를 수정해줘야한다.
@Log4j2
public class ApiCheckFilter extends OncePerRequestFilter {
private AntPathMatcher antPathMatcher;
private String pattern;
public ApiCheckFilter(String pattern) {
this.antPathMatcher = new AntPathMatcher();
this.pattern = pattern;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("RequestURI : " + request.getRequestURI());
log.info(antPathMatcher.match(pattern, request.getRequestURI()));
if (antPathMatcher.match(pattern, request.getRequestURI())) {
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
return;
}
filterChain.doFilter(request, response);
}
}
변경된 ApiCheckFilter 클래스에 생성자를 만들어주었으니, 이에 맞춰서 SecurityConfig에 등록한 Bean도 하단의 코드처럼 수정을 해줘야한다.
@Bean
public ApiCheckFilter apiCheckFilter() {
return new ApiCheckFilter("/notes/**/*");
}
위와 같이 작성한 후, 다시 프로젝트를 실행하여 '/notes/~' 로 시작하는 경로로 접속했을 때만 ApiCheckFilter에 들어가는 것을 콘솔창을 통해 확인이 가능하다.
API를 위한 인증처리
API를 이용한다면 일반적인 로그인 URL이 아닌 별도의 URL 로 로그인 처리를 하는것이 일반적이다. API는 URL이 변경되면 API를 이용하는 입장에서는 호출하는 URL을 변경해야만 하기에 위험하다.
따라서 해당 프로젝트에서는 이를 처리하는 ApiLoginFilter 클래스를 생성하고, 특정한 URL로 외부에서 로그인이 가능하도록 하고, 로그인이 성공하면 클라이언트가 Authorization 헤더의 값으로 이용할 데이터를 전송할 것이다. 해당 ApiLoginFilter는 Spring Security에서 제공하는 AbstractAuthenticationProcessingFilter를 상속받아 구현할 것이다.
해당 클래스는 filter 디렉토리 하위에 생성해줄것이다.
@Log4j2
public class ApiLoginFilter extends AbstractAuthenticationProcessingFilter {
public ApiLoginFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
log.info("--------------ApiLoginFilter----------------");
log.info("attemptAuthentication");
String email = request.getParameter("email");
String pw = "1111";
if (email == null) {
throw new BadCredentialsException("email Cannot be null");
}
return null;
}
}
AbstractAuthenticationProcessingFilter는 추상클래스이고, attemptAuthentication() 추상 메소드와, 생성자를 반드시 작성해줘야한다. 우선은 간단한 동작 여부를 확인하기 위해 위처럼 작성했다. 추후에 기능들을 추가해줘야한다. Security Config 클래스에 ApiLoginFilter를 Bean으로 등록하고, fiter를 추가해준다.
SecurityConfig 클래스에 추가해준 ApiLoginFilter는 '/api/login' 경로에 접근할때 동작하도록 설정해주었다. 또한 앞서 설명한 addFilter를 통해 UsernamePasswordAuthenticationFilter 전에 실행될 수 있도록 설정해주었다.
@Bean
public ApiLoginFilter apiLoginFilter() throws Exception {
ApiLoginFilter apiLoginFilter = new ApiLoginFilter("/api/login");
apiLoginFilter.setAuthenticationManager(authenticationManager());
return apiLoginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.csrf().disable();
http.logout();
http.oauth2Login().successHandler(successHandler());
http.rememberMe().tokenValiditySeconds(60 * 60 * 7).userDetailsService(userDetailsService);
http.addFilterBefore(apiCheckFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(apiLoginFilter(), UsernamePasswordAuthenticationFilter.class);
}
또한 email 이 null 이라면 예외처리를 해주었기에 email 값을 주지 않고 'api/login' 으로 접속시 401 에러가 뜨는 것을 확인할 수 있다.
Authorization 헤더 처리
특정한 API를 호출하는 클라이언트에서는 다른 서버나 Application으로 실행되기 때문에, Cookie나 Session을 활용할 수 없다. 따라서 API를 호출하는 경우에는 Request를 전송할때 Http 헤더에 특별한 값을 지정해서 보내준다.
즉, Authorization 헤더에 값을 넣어 보내주는 것이다. 해당 헤더는 클라이언트에서 전송한 Request에 포함된 Authorization헤더의 값을 파악해서 사용자가 정상적인 요청인지를 알아내는 것이다.
아래의 코드는 ApiCheckFilter에서 Authorization 헤더를 추출하고, 해당 헤더의 값이 '12345678' 인 경우에 인증을 한다고 가정했을 때를 구현한 것이다. 만약 헤더의 값이 동일하다면 다음 단계를 진행하고, 그렇지 않다면 인증에 실패했다는 메시지를 전송해야한다.
@Log4j2
public class ApiCheckFilter extends OncePerRequestFilter {
// ... 생략
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("RequestURI : " + request.getRequestURI());
log.info(antPathMatcher.match(pattern, request.getRequestURI()));
if (antPathMatcher.match(pattern, request.getRequestURI())) {
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
log.info("ApiCheckFilter...................");
boolean checkHeader = checkAuthHeader(request);
if (checkHeader) {
filterChain.doFilter(request, response);
return;
}
return;
}
filterChain.doFilter(request, response);
}
private boolean checkAuthHeader(HttpServletRequest request) {
boolean checkResult = false;
String authHeader = request.getHeader("Authorization");
if (StringUtils.hasText(authHeader)) {
log.info("Authorization Exist : " + authHeader);
if (authHeader.equals("12345678")) {
checkResult = true;
}
}
return checkResult;
}
}
위에서 구현한 checkAuthoHeader() 메소드는 'Authorization'이란 이름의 헤더의 값을 확인하고, boolean 타입으로 '12345678'과 동일한지 체크한 후 반환한다. 해당 코드가 정상적으로 구현이 되었는지 Postman을 통해 확인해보겠다.
Postman을 통해 Header에 Authorization의 값을 12345678로 지정해주고 read 했을 때 정상적으로 값을 가져오는것을 확인할 수 있다. 또한 2번째 사진처럼 만약 Authorization 헤더의 값을 지정해주지 않고 요청했을 때는 에러는 발생하지 않고, 값이 출력되지 않는것을 확인할 수 있다.
이렇게 헤더가 없음에도 불구하고 오류로 처리되지 않는 것은 ApiCheckFilter가 Spring Security가 사용하는 Cookie, Session을 사용하지 않기 때문에 발생하는 문제이다. 이를 해결하기 위해서는 다음과 같은 방식들이 있다.
- 정상적인 인증을 처리하도록 AuthenticationManager를 이용하는 방식
- ApiCheckFilter에서 간단하게 JSON 포맷의 에러 메시지를 전송하는 방식
우리는 두번째 방법을 사용해볼 것이다.
boolean checkHeader = checkAuthHeader(request);
if (checkHeader) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
JSONObject json = new JSONObject();
String message = "FAIL CEHCK API TOKEN";
json.put("code", "403");
json.put("message", message);
PrintWriter out = response.getWriter();
out.print(json);
}
return;
else를 통해 checkHeader가 false 일때, 403 에러가 발생하고, 오류 메세지를 지정한 후 출력해주도록 수정해준다.
위처럼 코드를 작성하고 아까처럼 Authorization 헤더가 값이 올바르지 않을 때를 보내주어 테스트해보면, 앞서 작성한것처럼 403에러코드와, 에러메세지가 출력이 된다.
Authorization 헤더에 따라서 다르게 동작하는 코드가 완성되었다면, 이제 다음과 같은 작업들을 해줘야한다.
다음 포스팅에 아래와 같은 작업을 해줄것이다.
- 외부에서 인증할 수 있는 인증처리
- ApiCheckFilter가 사용할 Authorization 헤더의 값을 발행하기