초기소스

This commit is contained in:
jazz
2025-10-29 17:08:05 +09:00
parent e531aa9540
commit 439a379a02
7551 changed files with 1234427 additions and 3 deletions

View File

@@ -0,0 +1,47 @@
package com.wb.domainserver.realm;
public class RealmContextHolder {
private static ThreadLocal<String> currentRealm = new ThreadLocal<>();
private static ThreadLocal<String> sopmalCdRealm = new ThreadLocal<>();
private static ThreadLocal<String> originalHostRealm = new ThreadLocal<>();
private static ThreadLocal<String> sysDvCdRealm = new ThreadLocal<>();
public static void setRealm(String tenant) {
currentRealm.set(tenant);
}
public static String getRealm() {
return currentRealm.get();
}
public static void setSopmalCdRealm(String tenant) {
sopmalCdRealm.set(tenant);
}
public static String getSopmalCdRealm() {
return sopmalCdRealm.get();
}
public static void setOriginalHostRealm(String tenant) {
originalHostRealm.set(tenant);
}
public static String getOriginalHostRealm() {
return originalHostRealm.get();
}
public static void setSysDvCdRealm(String tenant) {
sysDvCdRealm.set(tenant);
}
public static String getSysDvCdRealm() {
return sysDvCdRealm.get();
}
public static void clear() {
currentRealm.remove();
sopmalCdRealm.remove();
originalHostRealm.remove();
sysDvCdRealm.remove();
}
}

View File

@@ -0,0 +1,98 @@
package com.wb.domainserver.realm;
import com.wb.domainserver.exception.WnbtRestException;
import com.wb.domainserver.realm.entity.Site;
import com.wb.domainserver.realm.exception.RealmNotFoundException;
import com.wb.domainserver.realm.repository.SiteRepository;
import com.wb.domainserver.realm.util.UrlExtractor;
import com.wb.domainserver.util.string.StringUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component
public class RealmResolver {
private static final Logger logger = LoggerFactory.getLogger(RealmResolver.class);
private final SiteRepository siteRepository;
private final ApplicationContext applicationContext;
private final ExecutorService executorService = Executors.newCachedThreadPool();
@Autowired
public RealmResolver(SiteRepository siteRepository, ApplicationContext applicationContext) {
this.siteRepository = siteRepository;
this.applicationContext = applicationContext;
}
public CompletableFuture<Object> resolveRealm(String host) {
return CompletableFuture.supplyAsync(() -> {
String hostUrl;
try {
hostUrl = Objects.requireNonNull(host).replaceAll("/<unresolved>", "");
logger.info("Resolving tenant for host URL: {}", hostUrl);
return resolveRealmInternal(hostUrl); // 동기 호출
} catch (NullPointerException e) {
logger.error("Failed to retrieve host URL from request", e);
throw new RealmNotFoundException("Failed to retrieve host URL from request", e);
}
}, executorService);
}
@Cacheable(cacheNames = "realmCache", key = "#hostUrl")
public Object resolveRealmInternal(String hostUrl) {
return resolveRealmInternalDirect(hostUrl).join();
}
public CompletableFuture<Object> resolveRealmInternalDirect(String hostUrl) {
//url 정제 (로컬, 개발 url 경우 필요)
logger.debug("RealmResolver#hostUrl: {}", hostUrl);
String finalHostUrl = UrlExtractor.getOriginalHost(hostUrl);
logger.debug("RealmResolver#finalHostUrl: {}", finalHostUrl);
return CompletableFuture.supplyAsync(() -> {
try {
Optional<Site> siteOptional = siteRepository.findByUrl(finalHostUrl);
if (siteOptional.isPresent()) {
Site site = siteOptional.get();
if(site != null){
if (site.getRealm() == null) {
logger.error("Tenant not found for site with URL: {}", finalHostUrl);
throw new RealmNotFoundException("Tenant not found for site with URL: " + finalHostUrl);
}
String tenantName = site.getRealm().getName();
logger.info("Resolved tenant name: {}, sysDvCd: {}", tenantName, site.getSysDvCd());
//return tenantName.concat(":").concat(site.getSiteCode());
return tenantName.concat(":")
.concat(site.getSiteCode())
.concat(":")
.concat(site.getSysDvCd());
}else{
throw new RealmNotFoundException("Tenant not found for site with URL: " + finalHostUrl);
}
} else {
logger.error("No site found for URL: {}", finalHostUrl);
throw new RealmNotFoundException("No site found for URL: " + finalHostUrl);
}
} catch (WnbtRestException wre) {
throw new WnbtRestException(wre.getMessage());
} catch (Exception e) {
logger.error("Failed to resolve tenant for host URL: {}", finalHostUrl, e);
throw new RealmNotFoundException("Failed to resolve tenant for host URL: " + finalHostUrl, e);
}
}, executorService);
}
}

View File

@@ -0,0 +1,24 @@
package com.wb.domainserver.realm.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.Set;
@Data
@Entity
public class Realm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
@OneToMany(mappedBy = "realm")
private Set<Site> sites;
// getters, setters
}

View File

@@ -0,0 +1,30 @@
package com.wb.domainserver.realm.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.util.Set;
@Data
@Entity
public class Site {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String siteCode; // This will act as the DISCRIMINATOR
private String url;
@ManyToOne
@JoinColumn(name = "realm_id")
private Realm realm;
@OneToMany(mappedBy = "site")
private Set<User> users;
private String sysDvCd;
// getters, setters
}

View File

@@ -0,0 +1,21 @@
package com.wb.domainserver.realm.entity;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToOne
@JoinColumn(name = "site_id")
private Site site;
// getters, setters
}

View File

@@ -0,0 +1,7 @@
package com.wb.domainserver.realm.exception;
public class MissingRealmException extends RuntimeException{
public MissingRealmException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package com.wb.domainserver.realm.exception;
public class RealmNotFoundException extends RuntimeException {
public RealmNotFoundException(String message) {
super(message);
}
public RealmNotFoundException(String message, Exception e) {
super(message);
}
}

View File

@@ -0,0 +1,103 @@
package com.wb.domainserver.realm.filter;
import com.wb.domainserver.exception.WnbtException;
import com.wb.domainserver.exception.code.ErrCode;
import com.wb.domainserver.realm.RealmContextHolder;
import com.wb.domainserver.realm.RealmResolver;
import com.wb.domainserver.realm.util.UrlExtractor;
import com.wb.domainserver.util.string.StringUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
@Slf4j
public class RealmFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RealmFilter.class);
private final RealmResolver realmResolver;
public RealmFilter(RealmResolver realmResolver) {
this.realmResolver = realmResolver;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
logger.info("X-Original-Host: {}", request.getHeader("X-Original-Host"));
String ExtractorHost = UrlExtractor.getOriginalHost(request);
logger.info("Extractor Host: {}", ExtractorHost);
String host = "";
if(request.getHeader("X-Original-Host") != null){
host = request.getHeader("X-Original-Host");
}else{
host = ExtractorHost;
response.setHeader("X-Original-Host", ExtractorHost);
}
logger.info("RealmFilter ::: Host ::: {}", host);
if(!StringUtil.isEmpty(host)) {
try {
String resolve = (String) realmResolver.resolveRealm(host).get();
logger.debug("Resolved realm: {}", resolve);
String[] parts = resolve.split(":");
String realm = parts[0];
String sopmalCd = parts[1];
String sysDvCd = parts[2];
//root context 접근 할 경우 처리
String reqUri = request.getRequestURI();
if(reqUri == null || "/".equals(reqUri) || "/sopmal".equals(reqUri)){
//사용자 일 경우
if("01".contains(sysDvCd)){
reqUri = "/sopmal/index";
}else{
//관리자 일 경우
reqUri = "/mngr";
}
log.debug("mngr redirect from root context ::by realmFilter");
request.getRequestDispatcher(reqUri).forward(request, response);
}
logger.debug("final Resolved realm: {} sopmalCd : {} sysDvCD : {}", realm, sopmalCd, sysDvCd);
RealmContextHolder.setRealm(realm);
RealmContextHolder.setSopmalCdRealm(sopmalCd);
RealmContextHolder.setOriginalHostRealm(host);
RealmContextHolder.setSysDvCdRealm(sysDvCd);
}
catch (InterruptedException | ExecutionException e) {
logger.error("Error occurred while resolving realm {}", e.getMessage());
throw new WnbtException(ErrCode.INTERNAL_SEVER_ERROR);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
// 필터 종료 작업이 필요하면 여기서 수행
RealmContextHolder.clear();
}
}

View File

@@ -0,0 +1,40 @@
package com.wb.domainserver.realm.interceptor;
import com.wb.domainserver.realm.RealmContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
@Component
public class RealmJwtInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(RealmJwtInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
logger.info("X-Original-Host: {}", request.getHeader("X-Original-Host"));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) {
logger.info("All of claims from JWT: {}", jwt.getClaims());
// 요청 속성에 클레임 추가
request.setAttribute("claims", jwt.getClaims());
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return false;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
RealmContextHolder.clear();
}
}

View File

@@ -0,0 +1,9 @@
package com.wb.domainserver.realm.repository;
import com.wb.domainserver.realm.entity.Realm;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RealmRepository extends JpaRepository<Realm, Long> {
Realm findByName(String name);
}

View File

@@ -0,0 +1,22 @@
package com.wb.domainserver.realm.repository;
import com.wb.domainserver.realm.entity.Realm;
import com.wb.domainserver.realm.entity.Site;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface SiteRepository extends JpaRepository<Site, Long> {
// siteCode를 기반으로 tenant를 조회
@Query("SELECT s.realm FROM Site s WHERE s.siteCode = :siteCode")
Optional<Realm> findRealmBySiteCode(@Param("siteCode") String siteCode);
@Query("SELECT s.realm FROM Site s WHERE s.url = :url")
Optional<Realm> findRealmBySiteUrl(@Param("url") String url);
@Query("SELECT s FROM Site s WHERE :hostUrl LIKE CONCAT('%', s.url, '%')")
Optional<Site> findByUrl(@Param("hostUrl")String hostUrl);
}

View File

@@ -0,0 +1,118 @@
package com.wb.domainserver.realm.util;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UrlExtractor {
public static String redirect(HttpServletRequest request, String url) {
return "redirect:" + getOriginalURL(request) + url;
}
public static String getOriginalHost(HttpServletRequest request) {
// 헤더에서 원래 요청 정보를 추출
String scheme = request.getHeader("X-Forwarded-Proto");
if (scheme == null) {
scheme = request.getScheme(); // 기본값으로 현재 요청의 프로토콜 사용
}
String host = request.getHeader("X-Forwarded-Host");
if (host == null) {
host = request.getServerName(); // 기본값으로 현재 요청의 호스트명 사용
}
int port = 80;
if (request.getHeader("X-Forwarded-Port") == null) {
port = request.getServerPort(); // 기본값으로 현재 요청의 포트 사용
}
String portPart = (port != -1 && port != 80 && port != 443) ? ":" + port : "";
String fullUri = scheme + "://" + host;
log.info("UrlExtractUtil - fullUri : {}",fullUri);
// if(fullUri.contains("it.gcone")) fullUri = fullUri.concat("/admin");
String returnVal;
if(fullUri.contains("loc.")){
returnVal = scheme + "://" + host + portPart;
}else{
returnVal = scheme + "://" + host;
}
// 전체 URI 조합
returnVal = returnVal.replace(":443", "");
log.info("UrlExtractUtil - returnVal : {}",returnVal);
return returnVal;
}
public static String getOriginalURL(HttpServletRequest request) {
// 헤더에서 원래 요청 정보를 추출
String scheme = request.getHeader("X-Forwarded-Proto");
if (scheme == null) {
scheme = request.getScheme(); // 기본값으로 현재 요청의 프로토콜 사용
}
String uri = request.getHeader("X-Forwarded-Uri");
if (uri == null) {
uri = request.getRequestURI(); // 기본값으로 현재 요청의 URI 사용
}
String queryString = request.getHeader("X-Forwarded-Query");
if (queryString == null) {
queryString = request.getQueryString(); // 기본값으로 현재 요청의 쿼리 스트링 사용
}
String url = getOriginalHost(request) + (uri.equals("/") ? "" : uri);
if (queryString != null) {
url += "?" + queryString;
}
return url;
}
public static String getOriginalHost(String url) {
if (url == null || url.isEmpty()) {
return "";
}
// 스키마 제거 (http:// or https://)
String host = url.replaceFirst("https?://", "");
// 로컬 혹은 개발 서브도메인 제거
if (host.startsWith("loc.")) {
host = host.replaceFirst("loc\\.", "");
}
if (host.startsWith("dev.")) {
host = host.replaceFirst("dev\\.", "");
}
// 포트번호 제거
int portIndex = host.indexOf(":");
if (portIndex != -1) {
host = host.substring(0, portIndex);
}
return host;
}
public static String getOriginalHostToHttps(HttpServletRequest request) {
String url = getOriginalHost(request);
// 스키마 제거 (http:// or https://)
String host = url.replaceFirst("https?://", "");
String httpsHost = "https://" + host;
return httpsHost;
}
}