= HandlerInterceptor org.springframework.web.servlet.HandlerInterceptor의 구현클래스에 대해서 설명한다. HandlerInterceptor는 Controller 전후 에 실행되며 preHandle, postHandle, afterCompletion 메소드를 제공한다. [source, java] ---- default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } ---- == LoggingInterceptor LoggingInterceptor는 시스템 이용 로그를 남기기 위한 Interceptor이다. 대부분의 로직은 postHandle에 구현되어 있으며 TN_CF_SYS_USE_LOG 테이블에 데이터를 기록한다. 사용자정보(ID, IP, 브라우저), 요청시간, 응답시간, 요청 파라미터를 기록하며 이 데이터는 메뉴 사용 이력, 메뉴 활용도, 파일 다운로드 이력등에 사용된다. [source, java] ---- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) { String requestMethod = request.getMethod(); if(HttpMethod.OPTIONS.name().equals(requestMethod)) { return; } String requestURI = request.getRequestURI().substring(request.getContextPath().length()); SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmmssSSS", Locale.getDefault()); String sTime = formatter.format(new Date()); long start = (long) request.getAttribute("start"); long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); String userId = "anonymous"; if (ObjectUtils.isNotEmpty(Account.currentUser())) { userId = Account.currentUser().getUserId(); } String getDecodedRequestURI = WebUtil.getDecodedRequestUrl(request, requestURI); SysUseLog sysUseLog = new SysUseLog(); sysUseLog.setLogOccurId(idGenService.getNextStringId()); String menuId = request.getHeader("menu-Id"); String pageId = request.getHeader("page-id"); if(pageId != null) sysUseLog.setDescription(menuService.getPageFullPath(pageId)); sysUseLog.setNodeId(node); sysUseLog.setUserId(userId); sysUseLog.setUseFromDate(sTime.substring(0, 8)); sysUseLog.setUseFromHhmmss(sTime.substring(9, 15)); sysUseLog.setUseThruDatetime(new Date()); sysUseLog.setResponseTime(elapsedTime); sysUseLog.setPath(getDecodedRequestURI); String query = request.getQueryString(); if (query != null) { try { query = URLDecoder.decode(query, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error(e.getMessage(), e); } String queryStr = StringUtils.replace(query, "'", "\\'"); sysUseLog.setParameter(queryStr); sysUseLog.setUrl(requestURI + "?" + queryStr); } sysUseLog.setUseIp(WebUtil.getClientIp(request)); sysUseLog.setBrowserTypeName(request.getHeader("User-Agent")); sysUseLog.setLogFlag(LogFlag.ACCESSLOG.getValue()); sysUseLog.setReqType(ReqType.WEB); sysUseLog.setFirstRegDatetime(new Date()); sysUseLog.setMenuId(menuId); sysUseLog.setPageId(pageId); sysUseLog.setMethod(requestMethod); // 대리 로그인 시 별도 이력 남김 String originalUserId = request.getHeader("original-user-id"); if(StringUtils.isNotEmpty(originalUserId)){ sysUseLog.setLogFlag(LogFlag.IMPERSONATION_ACCESS.getValue()); sysUseLog.setDescription("Original User Id : " + originalUserId); } try { if ("db".equalsIgnoreCase(storeType)) { accessLogService.insertAccessLog(sysUseLog); } else { accessLogService.logAccess(sysUseLog); } } catch (Exception e) { log.error(e.getMessage()); } } ---- config.properties 파일의 access-log.store-type 값에 따라 db또는 file에 저장된다. == AuthenticationInterceptor AuthenticationInterceptor는 사용자가 인증이 되어 있는지 체크하는 인터셉터다. 인증이 되어 있지 않을 경우 로그인 화면으로 이동한다. [source. java] ---- String token = request.getHeader("x-auth-token"); <1> // Token 값 유무 if (StringUtils.isEmpty(token)) { throw new NotFoundTokenException("Not Found Token"); } ---- 인증이 된 사용자는 모든 요청 해더에 x-auth-token 값을 같이 보내야 한다. Frontend에서 x-auth-token값을 공통으로 Set하는 코드는 main.js 를 참고한다. [source, javascript] ---- axios.defaults.headers.common['x-auth-token'] = localStorage.getItem('userToken'); ---- loginService.js에서 로그인이 완료되면 localStorage에 toekn값을 저장하고, axios 요청 시마다 해더에 추가 한다. [source, java] ---- String originalUserId = request.getHeader("original-user-id"); <1> ---- <1> 대리로그인 시 원래 사용자의 ID가 헤더에 담겨있다. 대리로그인은 개발 시 기능, 권한 테스트를 하기 위한 용도로 관리자가 다른 사용자로 로그인하는 기능이다. 이 기능은 운영중인 시스템에 사용할 경우 보안에 문제가 발생하니 반드시 개발 시 테스트 용도로만 사용한다. CAUTION: 대리로그인은 개발 시 테스트를 위한 기능으로 운영 시 사용하지 않는다. [source, java] ---- Boolean jwtValid = jwtUtil.validateToken(token, user); <1> if (Boolean.TRUE.equals(jwtValid)) { if (user.isActiveFlag()) { user.setSystemAdminUser(roleService.isSystemAdmin(userId)); Account.updateCurrentAccount(user); return true; } else { // 시스템 승인 대기중 throw new WaitingUserException("Waiting User"); <2> } } else { throw new InvalidTokenException("Token Expired"); } ---- <1> 유효한 Token인치 확인 <2> Active Flag가 false일 경우 시스템 승인 대기중 상태 로그인 Token 관련해서는 <<_로그인>>을 참고한다. NOTE: 중복로그인 방지를 위해 로그인시 발급된 jwt값을 db에 저장하고, 현재 jwt값을 비교한다. config.properties파일 security.check.duplicate.login이 true일 때만 동작한다. == AuthorizationInterceptor AuthorizationInterceptor는 요청 URL(API)가 사용자에게 접근이 가능한지 판단하는 인터셉터다. [source, java] ---- if (user == null) throw new NoSearchUserException("No Search User"); <1> if (roleService.isSystemAdmin(user.getUserId())) { <2> if(adminAddressCheck) { List adminAddresses = adminAddressService.getAdminAddresses(null); <3> String remoteAddr = WebUtil.getClientIp(request); if(ObjectUtils.isNotEmpty(adminAddresses) && !adminAddresses.contains(remoteAddr)) { throw new AuthorizationException("Remote address is not admin ip address."); } } return true; } ---- <1> 사용자가 null일 경우에는 Exception을 발생한다. <2> 현재 사용자가 System Admin일 경우 <3> 접근 IP가 System Admin IP에 포함 되어 있는지 판단한다. NOTE: config.properties의 admin.address.check값이 true일 때만 System Admin IP를 체크 한다. [source, java] ---- for (Api api : authMenuList) { if (api.getHttpMethod().equals(httpMethod) && checkUriMatch(api.getApiPath(), api.getApiParameters(), decodedUrl, queryString)) { return true; } } throw new AuthorizationException("API 권한 없음"); ---- IMPORTANT: URL 권한 체크 시에는 파라미터 단위까지 체크한다. test?abc=123과 test?adb=123은 다른 권한이다. == PrivacyPolicyInterceptor PrivacyPolicyInterceptor는 이용 약관 동의 여부를 체크하는 Interceptor이다. [source, java] ---- /** * 약관동의 사용 여부 */ @Value("${privacy-policy.check.enabled:true}") private boolean privacyPolicyCheck; /** * 약관동의 인터셉터 체크 제외 URI */ @Value("${privacy-policy.check.exclude-path}") private String privacyPolicyCheckExcludePath; ---- 약관 동의 체크 인터셉터 사용 여부와 제외 URI는 위 프로퍼티 값을 이용한다. [source, java] ---- if (user != null && !user.isPrivacyPolicy()) { throw new PrivacyPolicyException("Privacy policy agreement required."); } else { return true; } ---- 약관 동의가 완료되지 않은 경우 PrivacyPolicyException이 발생하고 약관 동의 페이지로 이동한다. == TimeoutCheckInterceptor TimeoutCheckInterceptor는 일정시간 동안 사용하지 않을 경우 자동 로그아웃 하는 기능이다. config.properties파일 security.check.access.timeout 값이 true 일때만 동작한다. security.access.limit.timeout(분) 동안 시스템을 사용하지 않을 경우 자동 로그아웃 한다. [source, java] ---- @Value("${security.access.limit.timeout:30}") private int limitTimeout; static final long MILLISECONDS_PER_MINUTE = 60L*1000L; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){ String lastAccessTimeHeader = request.getHeader("last-access-time"); String impersonate = request.getHeader("original-user-id"); if(StringUtils.isNotEmpty(lastAccessTimeHeader) && !StringUtils.equals("undefined", lastAccessTimeHeader) && !StringUtils.equals("NaN", lastAccessTimeHeader) && StringUtils.isEmpty(impersonate)){ long lastActivityTime = Long.parseLong(lastAccessTimeHeader); long currentTime = System.currentTimeMillis(); long jwtRetentionTime = currentTime - lastActivityTime; if (jwtRetentionTime > (limitTimeout * MILLISECONDS_PER_MINUTE)) { //limitTimeout 동안 사용하지 않으면 자동 로그아웃 throw new TokenRetentionTimeoutException("Token Retention Timeout"); } } return true; } ---- 동작 방식은 Backend 요청 시 Response Header에 마지막으로 요청한 시간을 세팅하고 다음 요청시 Request Header에 last-access-time에 설정된다. 현재 시간과 last-access-time을 비교해 로그아웃 여부를 결정한다. == UploadFileExtensionCheckInterceptor UploadFileExtensionCheckInterceptor는 파일 업로드 시 파일 확장자를 검사한다. config.properties파일 common.upload.allowed-extensions에 정의된 확장자를 가진 파일만 업로드 할 수 있다. NOTE: UI에서도 허용되는 파일 확장자를 정의해줘야한다. MultipleFileUploader.vue를 부모컴포넌트에서 사용할때 pros useExtList을 설정해야 한다. 자세한것은 <<_file_component>>를 참고한다.