Initial commit

This commit is contained in:
2026-05-29 17:49:25 +09:00
commit 330105cb27
1081 changed files with 148694 additions and 0 deletions
+287
View File
@@ -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>>를 참고한다.
@@ -0,0 +1,39 @@
= SdlBaseBootApplication
SpringBoot 웹 애플리케이션을 배포할 때는 주로 embedded tomcat이 내장된 jar파일을 이용한다. 하지만 war 파일로 빌드, 배포를 진행해야 하는 경우를 위해 SpringBootServletInitializer를 상속받고 있다.
NOTE: https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.traditional-deployment[Spring Boot supports traditional deployment]
== SpringBootServletInitializer 상속
.SdlBaseBootApplication
[source, java]
----
@SpringBootApplication
public class SdlBaseBootApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SdlBaseBootApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SdlBaseBootApplication.class, args);
}
}
----
== Profile 적용
@Profile 을 이용하여 Profile 별로 다른 설정이 가능하다.
아래는 spring.profiles.active(JAVA OPTS)에 따라서 변경되는 설정이다.
* 아래의 예는 local profile 에서만 적용된다.
[source, java]
----
@Configuration
@Profile({"local"})
public class DbcpDataSourceConfig {
----
IMPORTANT: spring.profiles.active 는 runtime에서 매우 중요한 프로퍼티다. ServletContext에 등록되는 Filter, Servlet이 결정되고,
Spring Bean의 생성도 결정되니 반드시 JAVA OPTS에 설정해야 한다.
+186
View File
@@ -0,0 +1,186 @@
= Spring Config
SDL 6.0에서는 모든 Spring 설정이 Java Config로 되어 있다. com.samsung.config 패지키에 있다.
기본적인 설정파일들은 아래와 같다.
[source, text]
----
com.samsung.config
|- CacheConfig <1>
|- DbcpDataSourceConfig <2>
|- JasyptConfig <3>
|- JpaDatasourceConfig <4>
|- KnoxSyncBatchConfig <5>
|- QuartzClusteringConfig <6>
|- QuartzConfig <7>
|- RedisConfig <8>
|- SpringConfig <9>
|- SpringWebConfig <10>
|- SwaggerConfig <11>
|- SysUseLogBatchConfig <12>
|- TemplateConfig <13>
|- UserBatchConfig <14>
|- WebClientConfig <15>
----
<1> 캐시 설정 (Simple Provider)
<2> 데이터 소스 설정
<3> 프로퍼티 값 암호화를 위한 Jasypt 설정
<4> Jndi 데이터 소스 설정
<5> 결재동기화 배치 쿼츠(Quartz) job/trigger 설정
<6> Quartz 클러스터링 설정 (JDBC Jobstore)
<7> Quartz 설정 (RAM Jobstore)
<8> 캐시 설정 (Redis)
<9> Spring 설정
<10> Spring WebApplicationContext 설정
<11> Swagger 설정
<12> 시스템 로그 배치 쿼츠(Quartz) job/trigger 설정
<13> Thymeleaf 템플릿 엔진 설정
<14> 사용자 관련 배치 쿼츠(Quartz) job/trigger 설정
<15> WebClient 설정
== SpringConfig
Spring 설정 중에 가장 기본이 된다. Transaction, MessageSource 를 사용 할수 있도록
Spring Container 에 등록한다.
* @Configuration
Configuration Annotation은 Spring Container에게 해당 클래스가 Bean들을 등록하는 클래스라는 것을 알려주기 위한 Annotation이다.
프로젝트에서 Bean을 등록 할때는 클래스에 Configuration Annotation을 설정하도록 한다.
* @EnableTransactionManagement
EnableTransactionManagement annotation을 사용하면 Spring에서 @Transactional 을 사용해 Transaction을 관리 할 수 있다.
.xml 설정
[source, xml]
----
<tx:annotation-driven transaction-manager="txManager" />
----
@Transactional 에 대한 세부적인 내용은 Spring
link:https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative-attransactional-settings[Transactional Settings]
를 참고 한다.
* PropertySource
property 파일을 읽기 위해 사용한다. SDL에서는 <<_config_properties, config.properties>>파일과 <<_knox_properties,knox.properties>> 파일을 기본으로 로딩한다.
PropertySource에 등록된 값은 Spring Bean에서 사용할 수 있다.
** Value Injection +
@Value를 사용해 PropertySource의 값을 Injection 한다.
** Environment Injection +
org.springframework.core.env.Environment를 Injection 하고 getProperty("key")를 이용해 값을 얻는다.
.Value Injeciton
[source, java]
----
@Value("${security.access.limit.timeout:30}")
private int limitTimeout;
@Value("${security.check.access.timeout:false}")
private boolean checkTimeout;
----
.Environment Injection
[source, java]
----
String functionUrl = environment.getProperty(KNOX_EMP_SERVICE) + "/employees";
----
* ComponentScan
com.samsung 패키지에 속해 있는 @Service, @Repository, @Component 의 Bean만 찾아서 등록한다.
[source, java]
----
@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Service.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Repository.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Component.class)})
----
CAUTION: @Controller Baen은 SpringConfig가 아닌 SpringWebConfig에서 Scan한다.
* MessageSource
다국어 적용을 위해 MessageSource 를 사용한다. Backend 에서는 MessageSourceAccessor나 MessageSource를 이용해 다국어를 적용한다.
특히 velocity엔진 템플릿에서 다국어를 사용하기 위해서는 반드시 MessageSourceAccessor를 사용하도록 한다.
IMPORTANT: Frontend 가 처음 로딩 될 때 서버에서 "/noauth/messages/all" API를 호출해 시스템의 모든 메세지 리소스를 받는다. MessageBundleService에서는 config.properties 에 설정된 language-set에 해당하는 Message Properties 파일을 읽어 JSON으로 만들어 리턴한다.
== SpringWebConfig
Spring WebApplicationContext 설정을 위한 파일이다. WebMvcConfigurer를 구현하고 있으며,
Formatter, MessageConverter 등을 재정의 할 수 있다.
.xml
[source, xml]
----
<mvc:annotation-driven/>
----
@Configuration, @EnableWebMvc 를 선언하는 것으로 대체될 수 있다.
.java
[source, xml]
----
@Configuration
@EnableWebMvc
public class SpringWebConfig implements WebMvcConfigurer {
}
----
* ComponentScan
[source, java]
----
@ComponentScan(basePackages = "com.samsung", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableSwagger2.class)})
----
@Controller, @EnableSwagger2 로 선언된 Bean을 Scan한다.
* addResourceHandlers
정적 리소스를 관리하는 ResourceHandlerRegistry에 pathPatterns와 리소스 위치를 등록한다.
[source, java]
----
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("index.html").
addResourceLocations(webResourceRoot);
registry.addResourceHandler("favicon.ico").
addResourceLocations(webResourceRoot + "static/");
registry.addResourceHandler("static/**")
.addResourceLocations(webResourceRoot + "static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
----
* MultipartResolver
Multipart 요청에 대한 처리를 담당하는 Resolver다. 파일업로드의 최대 크기 등을 설정한다.
[source, java]
----
@Value("${common.upload.max-request-size:-1}")
private long maxRequestSize;
@Value("${common.upload.max-file-size:-1}")
private long maxFileSize;
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxRequestSize(DataSize.ofBytes(maxRequestSize));
factory.setMaxFileSize(DataSize.ofBytes(maxFileSize));
return factory.createMultipartConfig();
}
----
config.properties에 설정한 multipart/form-data 최대 사이즈, 전체 업로드 파일의 최대 용량을 참조한다.
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
= 시스템공통
include::시스템설정.adoc[leveloffset=+1]
include::SdlBaseBootApplication.adoc[leveloffset=+1]
include::SpringConfig.adoc[leveloffset=+1]
include::HandlerInterceptor.adoc[leveloffset=+1]
include::공통컴포넌트&유틸.adoc[leveloffset=+1]
+489
View File
@@ -0,0 +1,489 @@
= 시스템 설정
SDL은 다양한 시스템 환경에 적용 할 수 있도록 여러가지 설정파일을 제공한다. sdl-base/src/resources-{profile} 폴더안에 profile 별로 다른 설정 파일들이 실행 될 수 있도록 구성되어 있다.
== config.properties
SDL 가장 중요한 설정 파일로 시스템 전반에 영향을 준다. 로컬, 개발, 운영환경마다 내용이 달라질 수 있으니 패키징 시 설정값들이 맞는지 확인하고 배포 할 수 있도록 주의한다.
[cols=".^1h,.^1,.^2,.^2",options="header"]
|====
| key | value(예) | 설명 | 주의
| node-id
| localNode
| 서버 식별을 위한 ID
| 서버가 여러 대 있을 경우 ID값을 다르게 해야한다.
| ssl-port
| 8443
| http로 접근 했을 때 자동으로 redirection 하기 위한 https 포트
| 기본값은 443
| cors.domain
| *
| CORS를 허용하기 위한 값으로 브라우저에서 다른 도메인으로 데이터 호출을 가능하게 한다.
| *, sdl.sec.sasmsung.net
| web-context-path
| /
| 웹서버의 web-context-path
| 페이지 경로 앞에 참조
| datasource.driver
| ENC(txFyC35/92LBzew8iq2DjoFINRYg6e1UMcq7Du2mvs/nIqTSAU6e2r4VcyKrNc+K)
| DMBS의 driver class name
| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조
| datasource.url
| ENC(N2SavucpTKhakXUzUGpVLh+Hy2UQ5EyQJKBMJfagFbTB0VR9m8KO7LmPM6GFRBPWgjEOYJx1hWtFrCsHctjYZQ==)
| DMBS의 url
| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조
|datasource.username
| ENC(xS5l219WYApjTijLj8gwAw==)
| DMBS의 사용자
| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조
| datasource.password
| ENC(WHYsCYza7ffqN8Wi7FtTwTBI0dP9wHy2)
| DMBS의 패스워드
| 중요 프로퍼티 정보 암호화. 자세한 내용은 <<_server_실행, 프로퍼티 값 암호화>> 관련 부분 참조
| db.jndi
| sdl_ds
| WAS Datasource의 JNDI명
| 개발,운영 환경에서는 WAS의 Datasource를 사용한다.
| web-content-root-path
| http://localhost:8081
| Web 서버의 Root의 경로
| 클라이언트로 redirect 할 경우 참조 (ex. AD인증 후)
| static-resource-path
| http://localhost:8081/static
| css, image가 있는 경로
| HTML Template (ex. Velocity) 에서 참조
| base.package
| com.samsung
| 클래스를 찾을 때 해당 패키지 아래에서만 찾는다.
| 결재 문서를 찾을 때 참조
| entity.package-name
| entity
| 결재 문서를 찾을 때 entity 패키지 아래의 클래스만 찾는다.
| 반드시 @ApprovalDocument가 선언되어 있어야 한다.
| login.sso.knox-tray-private-key-path
| /rsaprivkey8.pem
| NewEpTray PrivateKey 경로
| <<_로그인, 로그인>> 부분 참조
| login.auto-sign-up
| true
| Knox 및 AD 사용자 자동 가입 허용
| 사용자 인증 후 시스템에 자동 가입된다.
| user.auto-permission
| true
| 사용자가 시스템 가입 신청시 자동 승인
| false 시 승인대기 후 관리자 승인 절차
| privacy-policy.check.enabled
| true
| 시스템 이용 약관 동의 필터 사용 여부
| false 시 약관 동의 여부 체크하지 않는다
| privacy-policy.check.exclude-path
| /privacypolicy/terms/valid
| 사용자가 약관을 동의 했는지 체크하는 필터 예외 path
|
| email.limit-body
| 1048576
| email 본문의 길이
| 발신 API에서 본문 사이즈는 최대 1mb까지 입력 가능 (Knox기준)
| email.limit-recipients
| 100
| 최대 수신인
| 입력 가능한 최대 수신인 수는 100명 (Knox기준)
| batch.user.sync.retire.enabled
| false
| 퇴직자 처리 배치 사용여부
| 별도의 사용자 정보 데이터가 동기화 되어 있어야 한다.
| batch.user.sync.cron
| 0 10 23 * * ?
| 사용자동기화 배치 스케줄
|
| batch.user.long-term.month
| 3
| 장기미사용자 판단 기준(월)
|
| batch.user.long-term-check.cron
| 0 10 00 * * ?
| 장기미사용자 처리 배치 스케줄
|
| batch.user.auth-expired.alarm.before
| 1,7,14
| 권한만료 알림 메일 발송일
| 1일전,7일전,14일전
| batch.user.auth-expired.cron
| 0 10 02 * * ?
| 권한만료 처리 배치 스케줄
|
| batch.user.auth-expired-mailing.cron
| 0 30 02 * * ?
| 권한만료 알림 메일 배치 스케줄
|
| batch.sys-use-log.menu-use-history.cron
| 0 10 01 * * ?
| 메뉴사용이력 배치 스케줄
|
| batch.sys-use-log.menu-utilization.cron
| 0 30 01 * * ?
| 메뉴활용도 배치 스케줄
|
| security.sql-injection.allowed-pattern
| .\*[^a-zA-Z0-9_\\s,].\*
| SQL에 허용되는 문자
| 영문 대소문자, 숫자, 스페이스, 콤마만 허용
| security.authentication.exclude-path
| /\\**/noauth/**
| 권한,인증 체크에서 제외되는 url
|
| security.jwt.secret-key
|
| jwt를 암복호화 하기 위한 키 (256bits 이상)
| 시스템별로 상이해야 하며 키가 유출되지 않도록 주의해야 한다.
| security.jwt.expiration-time
| 8
| jwt 토큰 유지 시간
| 요청시 마다 체크. 유효하지 않을 경우 로그아웃
| security.eptray.expiration-time
| 24
| eptray 유지 시간
| eptray 로그인시 체크. 이 시간이 경과되면 Knox 재로그인 필요
| security.check.access.timeout
| true
| TimeoutCheckInterceptor 사용 여부
| 시스템을 일정시간 사용하지 않을 경우 로그아웃
| security.access.limit.timeout
| 30
| 타임아웃 체크 기준 시간
|
| security.check.duplicate.login
| false
| 사용자 중복 로그인 체크 여부
|
| access-log.store-type
| db
| Access Log 저장 방식
| db, file
| access-log.exclude-path
| /\\**/noauth/**
| Access Log에 기록 하지 않을 path
|
| access-log.batch.enabled
| true
| 시스템 사용 이력 통계 배치 실행 여부
|
| access-log.file-path
| /logs/access
| Access Log 파일 저장 위치
| Access Log 저장 방식이 file 일 때 참조
| menu-utilization.retention-period
| 24
| 메뉴 활용도 보관 기간
|
| common.upload-path
| /NAS/upload
| 업로드 파일 저장 위치
|
| common.upload.directory-name-len
| 2
| 업로드 파일의 디렉토리명 길이
|
| common.download.zipfilename
| compressed
| 모두 다운 받기 할때 zip 파일 이름 기본값
|
| common.upload.allowed-extensions
| xls,jpg
| 업로드 가능한 파일의 확장자
|
| common.excel-upload-path
| /excel
| 엑셀 업로드 임시 저장 폴더
|
| common.upload.max-file-size
| -1
| 업로드 가능한 개별 파일 크기 (바이트)
| -1일 때 제한 없음
| common.upload.max-request-size
| -1
| 모든 파일 및 폼 데이터를 포함한 전체 멀티파트 요청 크기 (바이트)
| -1일 때 제한 없음
| common.upload.default-encoding
| UTF-8
| 요청을 파싱할 때 사용할 캐릭터 인코딩
|
| custom.upload-path.enabled
| false
| 사용자지정 업로드 패스 설정 여부
|
| custom.upload-path
| notice=/nas/sdl/upload/notice,\ +
faq=/nas/sdl/upload/faq
| 사용자지정 업로드 패스 설정 값
|
| excel.mapping.locations
| classpath*:/excel/*.xml
| excel mapping file 위치
|
| excel.mapping.reloadInterval
| 10000
| excel mapping file 리로딩 시간
| 밀리 세컨, 0일 경우 리로딩 하지 않음
| common.server-time-zone-id
| Asia/Seoul
| 서버 time zone id
| 사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값
| common.server-time-zone
| GMT+9:00
| 서버 time zone
| 사용자 정보내 timezone 설정이 없을떄 기본으로 설정 되는 값
| knox.approval.sync.cron
| 0 0/3 * * * ?
| 결재 문서 상태 동기화 주기
|
| language-set
| ko_KR,en_US
| 시스템 언어셋
| 메세지 프로퍼티 로케일 정보
| api.utrans.url
|
| utrans 서버 주소
|
| api.utrans.type
|
| utrans type
|
| api.utrans.key
|
| utrans key
|
| admin.address.check
| false
| 시스템 관리자의 ip 제한
|
| system.email
|
| 시스템 대표 email 주소
| Knox에 등록된 계정이어야 함
|====
== knox.properties
knox 서비스를 연계를 위해 필요한 설정들이다. Knox Staging, Production 에 따라 값이 다르니 반드시 환경에 맞는 값들이 설정됐는지 확인 후 배포한다.
IMPORTANT: 1. Knox Stage 연계 시 Knox Stage 계정이 있어야 한다. 특히 메일, 결재 연계를 개발 할 때 Stage에 없는 사용자에게 메일을 보내거나 결재를 상신한다면 오류가 발생하니 반드시 사용자 계정이 있는지 확인 한다. +
2. Knox 연계 신청, 연계 오류 관련 문의는 Knox Support를 통해 문의 한다. +
3. Knox 운영 거점은 한국, 구주, 미주 3곳이 있으므로 연계 신청시 사용자 위치에 따라 거점 신청에 주의하도록 한다. 거점 연계가 안되어 있는 사용자는 결재, 메일 기능을 사용 할 수 없다.
[cols="3*",options="header"]
|====
|key | value | 설명
| knox.system-id
|
| Knox 연계 신청 시 받은 시스템 ID
| knox.token
| token1, token2, token3
| Knox 연계 신청 시 받은 token
| knox.address.prefix
| openapi.samsung.net,openapi.w1.samsung.net,openapi.w2.samsung.net
| 한국,구주,미주 서비스 주소
|====
CAUTION: token과 서비스 주소는 반드시 쌍으로 등록한다.
== 캐시 (Cache)
SDL은 빈번하게 요청되는 데이터 값을 저장하고 필요시 데이터를 빠르게 불러 올수 있도록 스프링 프레임워크에서 제공하는 캐시를 사용한다. 스프링 캐싱 서비스는 추상화로 제공되므로 캐시 Provider 별 CacheManager 를 구현하여 빈으로 등록을 해야한다.
NOTE: SDL의 로컬 개발환경에서는 스프링 Simple Provider(ConcurrentHashMap)를 사용하고 있으며, 캐시 공유가 필요한 운영 환경에서는 Redis 라이브러리를 사용하고 있다. 자세한 설정은 스프링 부트 공식문서를 참조하도록 하고 본 문서에서는 SDL에서 사용하고 있는 캐시 데이터 중심으로 설명한다.
* message-all
MessageBundleService getMessage의 결과를 저장한다. Spring MessageSource의 모든 언어에 대한 Key, Value를 리턴한다.
[source, java]
----
@Cacheable(value = "message-all")
public Map<String, Map<String, String>> getMessage() {
Map<String, Map<String, String>> message = new HashMap<>();
for (String locale : languageSet) {
message.put(locale, getMessageByLang(new Locale(locale.split("_")[0], locale.split("_")[1])));
}
return message;
}
----
* message
MessageBundleService getMessageByLang의 결과를 저장한다. locale일 별로 Spring MessageSource의 Key, Value를 리턴한다.
[source, java]
----
@Cacheable(value = "message", key = "#locale")
public Map<String, String> getMessageByLang(Locale locale) {
Set<String> beanNames = messageSource.getBasenameSet();
Map<String, String> message = new HashMap<>();
for (String beanName : beanNames) {
ResourceBundle resourceBundle = ResourceBundle.getBundle(StringUtils.remove(beanName, "classpath:/"), locale);
Enumeration<String> keys = resourceBundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
String javaVersion = StringUtils.substringBeforeLast(System.getProperty("java.version"), ".");
if (Float.parseFloat(javaVersion) < 1.9) {
message.put(key, new String(resourceBundle.getString(key).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
} else {
message.put(key, resourceBundle.getString(key));
}
}
}
return message;
}
----
* menu-all
MenuService getAllMenuPagePaths 시스템의 모든 메뉴, 메뉴 패스를 저장한다.
[source, java]
----
@Cacheable(value = "menu-all")
@Override
public List<Map<String, String>> getAllMenuPagePaths() {
return menuDao.getAllMenuPagePaths();
}
----
* api-user
ApiService getUserAuthApiListByUserId 사용자ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니
캐시 관리에 주의하도록 한다.
[source, java]
----
@Override
@Cacheable(value = "api-user", key = "#userId")
public List<Api> getUserAuthApiListByUserId(String userId) {
return apiDao.getUserAuthApiListByUserId(userId);
}
----
* api-user-menu
ApiService getUserAuthApiListByUserId 사용자ID와 메뉴ID를 기준으로 접근할 수 있는 API 목록을 저장한다. 사용자가 늘어날 수록 캐시되는 데이터가 많으니
캐시 관리에 주의하도록 한다.
[source, java]
----
@Override
@Cacheable(value = "api-user-menu", key = "#userId.concat(':').concat(#menuId)")
public List<Api> getUserAuthApiListByUserIdAndMenuId(String userId, String menuId) {
return apiDao.getApiListByUserIdAndMenuId(userId, menuId);
}
----
* page-all-by-menu-auth
ResourceCacheService getAllPageListByAuth 모든 메뉴의 페이지별 권한 타입을 저장한다.
[source, java]
----
@Cacheable(value = "page-all-by-menu-auth")
public Map<String, Map<String, List<Page>>> getAllPageListByAuth() {
log.debug("PageService Start.");
Map<String, Map<String, List<Page>>> menuAuthMap = new HashMap<>();
Map<String, List<Page>> menuMap = getAllPageListByMenu();
for (Map.Entry<String, List<Page>> entry : menuMap.entrySet()) {
List<Page> authPageList = entry.getValue();
Map<String, List<Page>> authMap = new HashMap<>();
authMap.put("READ", authPageList.stream().filter(page -> "READ".equals(page.getAuthorizationType())).collect(Collectors.toList()));
authMap.put("UPDATE", authPageList.stream().filter(page -> "UPDATE".equals(page.getAuthorizationType())).collect(Collectors.toList()));
authMap.put("DOWNLOAD", authPageList.stream().filter(page -> "DOWNLOAD".equals(page.getAuthorizationType())).collect(Collectors.toList()));
authMap.put("EXECUTE", authPageList.stream().filter(page -> "EXECUTE".equals(page.getAuthorizationType())).collect(Collectors.toList()));
menuAuthMap.put(entry.getKey(), authMap);
}
log.debug("menuAuthMap : {}", menuAuthMap);
return menuAuthMap;
}
----
* page-full-path-all
MenuService getPageFullPathList 모든 메뉴 full path를 저장한다.
[source, java]
----
@Cacheable(value="page-full-path-all")
@Override
public Map<String, String> getPageFullPathList() {
List<Map<String, String>> pageFullPathList = menuDao.getPageFullPathList();
Map<String, String> pageFullPathMap = new HashMap<>();
for(Map<String, String> pageFullPath : pageFullPathList) {
pageFullPathMap.put(pageFullPath.get("pageId"), pageFullPath.get("fullPath"));
}
return pageFullPathMap;
}
----