지난번 포스트 API Gateway에 이어 활용하는 내용입니다.
Netflix Zuul을 API Gateway로 사용하면서 ZuulFilter를 활용하여 JWT인증을 구현합니다.
Gateway에 인증을 구현함으로서, 각 서비스는 자신의 역할에만 집중하여 로직을 구현할 수 있고, 공통된 인증 시스템을 구현할 수 있습니다.
java
1 @Configuration 2 public class JwtFilter extends ZuulFilter { 3 4 private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass()); 5 6 @Override 7 public String filterType() { 8 return FilterConstants.PRE_TYPE; // "pre"를 return합니다. 9 } 10 11 @Override 12 public int filterOrder() { 13 return 0; 14 } 15 16 @Override 17 public boolean shouldFilter() { 18 return true; 19 } 20 21 @Override 22 public Object run() { 23 return null; 24 } 25 }
shouldFilter와 run부분은 아래에서 작성하고,
filterType은 API 서버로 라우팅 되기 전에 인증을 수행하고 그에 따라서 라우팅을 취소하거나 해야하니 pre로 해줍니다.
filterOrder는 0으로 가장 먼저 등록되게 하겠습니다.
shouldFilter부분은 인증을 수행할지 말지를 결정해야 합니다.
대부분의 로직은 인증을 수행해야 하지만 회원가입, 로그인 아이디 찾기 등 토큰을 발급받기 전에 수행해야할 로직들은 인증을 하면 안됩니다.
또한 HTTP Method의 OPTIONS는 통신 옵션만 요청하여 가능 여부만 검사하니 이 또한 인증을 하지 않습니다.
java
1 @Override 2 public boolean shouldFilter() { 3 RequestContext context = RequestContext.getCurrentContext(); 4 5 if ("OPTIONS".equals(context.getRequest().getMethod())) { 6 return false; // OPTIONS는 인증을 하지 않습니다. 7 } 8 9 List<String> whiteList = getWhiteList(); 10 // HttpServletRequest의 getRequestURI는 쿼리 param을 제외한 컨텍스트 경로부터 가져옵니다. 11 if (whiteList.contains(context.getRequest().getRequestURI())) { 12 return false; 13 } 14 15 return true; 16 } 17 18 private List<String> getWhiteList() { 19 return List.of("/auth/sign", "/auth/user/id"); 20 }
getWhiteList 메서드를 이용해서 인증을 거치지 않을 URI들을 가져옵니다.
해당 기능은 DataBase 구조에 따라서 Service를 거쳐서 DB로 부터 가져와도 되고,
혹은 Enum을 활용하거나 혹은 위의 예시처럼 직접 String을 넣어도 됩니다.
Http Method까지 포함하여 객체를 만들어 비교하는 방법도 가능합니다.
여기선 테스트 코드로 직접 String을 넣었습니다.
이 부분에선 실제 인증 과정을 작성해줍니다.
API 요청으로부터 JWT 토큰을 가져오고 해당 토큰을 parse해서 문제가 있는지 확인합니다.
java
1 @Configuration 2 public class JwtFilter extends ZuulFilter { 3 private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass()); 4 5 // Value 어노테이션은 properties나 yml파일로부터 설정값을 가져옵니다. cloud config server를 만들어 값을 숨길 수 있습니다. 6 @Value("${jwt.signKey}") 7 private String jwtSignKey; 8 ...... 9 }
field부분에 JWT의 signKey를 가져오는 코드를 추가합니다.
java
1 @Override 2 public Object run() throws ZuulException { 3 try { 4 RequestContext requestContext = RequestContext.getCurrentContext(); 5 String jwt = requestContext.getRequest().getHeader("Authorization") 6 .substring(7); // "Bearer "의 크기만큼 제거 7 8 Claims claims = Jwts.parser().setSigningKey(jwtSignKey).parseClaimsJws(jwt).getBody(); 9 } catch (ExpiredJwtException ex) { 10 // 토큰의 기간이 지난 경우입니다. Refresh 토큰을 활용할 경우 여기서 처리할 수 있습니다. 11 this.requestRefreshToken(); 12 } catch (UnsupportedJwtException | MalformedJwtException | SignatureException ex) { 13 // jwt의 구조가 잘못되거나, 서명에 실패할 경우입니다. 14 logger.warn("Bad Request" + ex.getMessage()); 15 throw new ZuulException("Bad token", HttpStatus.BAD_REQUEST.value(), ex.getMessage()); 16 } catch (Exception ex) { 17 logger.warn("Something wrong in gateway" + ex.getMessage()); 18 throw new ZuulException("Something wrong in gateway", HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage()); 19 } 20 return null; 21 } 22 23 private void requestRefreshToken() { 24 // auth 서비스로 URI를 변경하여서 refresh 토큰을 얻는 시도를 하도록 변경합니다. 25 RequestContext context = RequestContext.getCurentContext(); 26 context.getRequest().setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/auth/refresh"); 27 }
토큰에 대해 인증을 수행하고 expired 될 경우에는 refresh 토큰을 얻을 수 있는지 auth 서비스에 요청합니다.
그 이외의 인증에 실패할 경우에는 Exception을 일으켜서 요청을 취소하고 로그를 남깁니다.
다음 포스트에는 auth 서비스에서 로그인을 통해 토큰을 발급받고, 또한 refresh 토큰을 발급받는 과정을 설명하겠습니다.