Initial commit
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
= 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<String> 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>>를 참고한다.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user