feature: DJErp OAuth bypass 인바운드 처리 추가

- DJErpApiAdapterController: /dj/** 경로 전용 inbound 라우팅 컨트롤러
- DJErpApiAdapterService: DJErp 전용 어댑터 서비스
- DJErpOAuth2Controller: /dj/oauth/token, /dj/oauth2/token 토큰 발급 (Spring Security 우회)
- DJErpOAuth2AccessTokenRequest/Response: 토큰 요청/응답 VO
- HttpClient5AdapterServiceBypass: HttpClient5 기반 bypass 어댑터 (OAuth 토큰 자동 갱신 포함)
- RestAdapterMethodUrlFallbackRequestAction: URI 경로 세그먼트 축소 fallback 키 탐색 Action
- ApiAdapterController: 전체 주석처리 (비활성화)
- elink-online-common 서브모듈 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-04-07 00:02:02 +09:00
parent 51724ab0f5
commit e21f134c11
11 changed files with 2893 additions and 383 deletions
@@ -0,0 +1,55 @@
package com.eactive.eai.authserver.custom;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonProperty;
@SuppressWarnings("serial")
public class DJErpOAuth2AccessTokenRequest implements Serializable {
@JsonProperty("grant_type")
private String grantType;
@JsonProperty("client_id")
private String clientId;
@JsonProperty("client_secret")
private String clientSecret;
private String scope;
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
@Override
public String toString() {
return "DJErpOAuth2AccessTokenRequest [grantType=" + grantType + ", clientId=" + clientId
+ ", clientSecret=" + clientSecret + ", scope=" + scope + "]";
}
}
@@ -0,0 +1,79 @@
package com.eactive.eai.authserver.custom;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@SuppressWarnings("serial")
@JsonInclude(Include.NON_NULL)
@JsonPropertyOrder({ "access_token", "token_type", "expires_in", "scope", "jti", "client_id" })
public class DJErpOAuth2AccessTokenResponse implements Serializable {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private Long expiresIn;
private String scope;
private String jti;
@JsonProperty("client_id")
private String clientId;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String toString() {
return "DJErpOAuth2AccessTokenResponse [accessToken=" + accessToken + ", tokenType=" + tokenType
+ ", expiresIn=" + expiresIn + ", scope=" + scope + ", jti=" + jti + ", clientId=" + clientId + "]";
}
}
@@ -0,0 +1,266 @@
package com.eactive.eai.authserver.custom;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.authserver.config.RequestContextData;
import com.eactive.eai.authserver.dao.TokenIssuanceLogDAO;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.util.BeanUtils;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.dao.DAOException;
import com.eactive.eai.common.logger.EAIDBLogControl;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.common.util.UUID;
import com.eactive.eai.data.entity.onl.authserver.TokenIssuanceLog;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.Principal;
@Controller
public class DJErpOAuth2Controller {
public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private static final String HEADER_TRACEID = "traceId";
private static final String SERVICE_CODE_ACCESS_TOKEN = "00";
@Autowired
private TokenIssuanceLogDAO tokenIssuanceLogDAO;
@RequestMapping(value = { "/dj/oauth/token", "/dj/oauth2/token" }, method = RequestMethod.POST,
consumes = "application/json", produces = "application/json; charset=UTF-8")
@ResponseBody
public ResponseEntity<?> tokenJson(@RequestBody DJErpOAuth2AccessTokenRequest tokenRequest,
HttpServletRequest request, HttpServletResponse response) {
return issueToken(tokenRequest, request, response);
}
@RequestMapping(value = { "/dj/oauth/token", "/dj/oauth2/token" }, method = RequestMethod.POST,
consumes = "application/x-www-form-urlencoded", produces = "application/json; charset=UTF-8")
@ResponseBody
public ResponseEntity<?> tokenForm(@RequestParam Map<String, String> params,
HttpServletRequest request, HttpServletResponse response) {
DJErpOAuth2AccessTokenRequest tokenRequest = new DJErpOAuth2AccessTokenRequest();
tokenRequest.setGrantType(params.get("grant_type"));
tokenRequest.setClientId(params.get("client_id"));
tokenRequest.setClientSecret(params.get("client_secret"));
tokenRequest.setScope(params.get("scope"));
return issueToken(tokenRequest, request, response);
}
@GetMapping(value = { "/dj/oauth/token", "/dj/oauth2/token" }, produces = "application/json; charset=UTF-8")
@ResponseBody
public ResponseEntity<?> tokenGet(@RequestParam Map<String, String> params,
HttpServletRequest request, HttpServletResponse response) {
DJErpOAuth2AccessTokenRequest tokenRequest = new DJErpOAuth2AccessTokenRequest();
tokenRequest.setGrantType(params.get("grant_type"));
tokenRequest.setClientId(params.get("client_id"));
tokenRequest.setClientSecret(params.get("client_secret"));
tokenRequest.setScope(params.get("scope"));
return issueToken(tokenRequest, request, response);
}
private ResponseEntity<?> issueToken(DJErpOAuth2AccessTokenRequest tokenRequest,
HttpServletRequest request, HttpServletResponse response) {
if (logger.isDebug()) {
logger.debug(tokenRequest.toString());
}
RequestContextData data = new RequestContextData();
data.setClientId(tokenRequest.getClientId());
data.setIpAddress(extractIpAddress(request));
data.setGrantType(tokenRequest.getGrantType());
data.setScope(tokenRequest.getScope());
data.setUsername("none");
data.setResource("none");
RequestContextData.ThreadLocalRequestContext.set(data);
String grantType = tokenRequest.getGrantType();
String clientId = tokenRequest.getClientId();
String clientSecret = tokenRequest.getClientSecret();
String scopes = tokenRequest.getScope();
try {
if (!StringUtils.equals(grantType, "client_credentials")) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
"Invalid Mandatory Field {grantType}");
}
String[] scopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(scopes, " ");
Set<String> scopeSet = new HashSet<>();
for (String scope : scopeArr) {
scopeSet.add(scope);
}
ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
verifyClient(clientDetails, clientId, clientSecret, scopeSet);
String traceId = UUID.randomUUID().toString().replace("-", "");
response.setHeader(HEADER_TRACEID, traceId);
HashMap<String, String> authorizationParameters = new HashMap<>();
authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType);
authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId);
authorizationParameters.put("client_secret", clientSecret);
Set<String> responseType = new HashSet<>();
responseType.add(grantType);
OAuth2Request authorizationRequest = new OAuth2Request(authorizationParameters, clientId, null, true,
scopeSet, null, "", responseType, null);
Principal principal = new OAuth2Authentication(authorizationRequest, null);
ResponseEntity<OAuth2AccessToken> result = tokenEndpoint().postAccessToken(principal, authorizationParameters);
OAuth2AccessToken token = result.getBody();
DJErpOAuth2AccessTokenResponse responseToken = new DJErpOAuth2AccessTokenResponse();
responseToken.setAccessToken(token.getValue());
responseToken.setTokenType(token.getTokenType());
responseToken.setExpiresIn(Long.valueOf(token.getExpiresIn()));
responseToken.setScope(StringUtils.join(token.getScope(), " "));
responseToken.setJti((String) token.getAdditionalInformation().get("jti"));
responseToken.setClientId((String) token.getAdditionalInformation().get("client_id"));
logTokenIssuance(true, false, "Token issued successfully", token.getValue());
ObjectMapper mapper = new ObjectMapper();
return ResponseEntity.ok(mapper.writeValueAsString(responseToken));
} catch (JwtAuthException e) {
logger.info("Token request[/dj/oauth2/token] => clientId: {}, grantType: {}, scope: {}",
clientId, grantType, scopes);
logger.error(e.getMessage());
logTokenIssuance(false, false, e.getMessage(), null);
String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, e.getMessage());
int statusCode = NumberUtils.toInt(StringUtils.left(e.getCode(), 3), HttpStatus.UNAUTHORIZED.value());
return ResponseEntity.status(statusCode).body(errorJson);
} catch (Exception e) {
logger.info("Token request[/dj/oauth2/token] => clientId: {}, grantType: {}, scope: {}",
clientId, grantType, scopes);
logger.error(e.getMessage());
logTokenIssuance(false, false, e.getMessage(), null);
String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, "Unauthorized. [Unknown]");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorJson);
} finally {
RequestContextData.ThreadLocalRequestContext.clear();
}
}
private void verifyClient(ClientDetails clientDetails, String clientId, String clientSecret, Set<String> scopeSet)
throws JwtAuthException {
if (StringUtils.isBlank(clientId)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
"Invalid Mandatory Field {client_id}");
}
if (clientDetails == null) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
"Unauthorized. [client not found]");
}
if (StringUtils.isBlank(clientSecret)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
"Invalid Mandatory Field {client_secret}");
}
if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
"Unauthorized. [Bad client credentials(Client Secret is not match)]");
}
if (scopeSet.isEmpty()) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"),
"Invalid Mandatory Field {scope}");
}
if (!clientDetails.getScope().containsAll(scopeSet)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00"),
"Unauthorized. [Bad client credentials(Include unacceptable scope)]");
}
}
private String extractIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("Proxy-Client-IP");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("WL-Proxy-Client-IP");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("HTTP_CLIENT_IP");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) ip = request.getRemoteAddr();
return ip;
}
private TokenEndpoint tokenEndpoint() {
return BeanUtils.getBean("tokenEndpoint", TokenEndpoint.class);
}
private void logTokenIssuance(boolean isSuccess, boolean isRestrictionExempt, String resultMessage, String accessToken) {
try {
if (!EAIDBLogControl.isEnable()) {
logger.warn("DB logging is disabled. Skipping token issuance logging.");
return;
}
RequestContextData data = RequestContextData.ThreadLocalRequestContext.get();
TokenIssuanceLog log = new TokenIssuanceLog();
String clientId = data.getClientId();
log.setClientId(clientId);
if (StringUtils.isNotBlank(clientId)) {
try {
ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId);
log.setAppName(clientVO.getClientName());
log.setOrgId(clientVO.getOrgId());
log.setOrgName(clientVO.getOrgName());
} catch (DAOException e) {
logger.warn("cannot find clientVO - " + clientId);
}
}
log.setGrantType(data.getGrantType());
log.setScope(data.getScope());
log.setIpAddress(data.getIpAddress());
log.setSuccessYn(isSuccess ? "Y" : "N");
log.setIssuanceDateTime(LocalDateTime.now());
log.setRestrictionExemptYn(isRestrictionExempt ? "Y" : "N");
log.setResultMessage(resultMessage);
log.setAccessToken(accessToken);
tokenIssuanceLogDAO.saveTokenIssuanceLog(log);
} catch (Throwable th) {
logger.error("Error while logging token issuance: " + th.getMessage(), th);
}
}
}