From 3dd7c141f717ffc73e09b05ab5564b4648e1975c Mon Sep 17 00:00:00 2001 From: "Yunsam.Eo" Date: Wed, 3 Dec 2025 10:33:18 +0900 Subject: [PATCH] =?UTF-8?q?KJB=20=EB=AA=A8=EB=B0=94=EC=9D=BCG/W=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20JWT=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AuthorizationServerConfig.java | 2 +- .../config/OAuthRequestLoggingFilter.java | 4 +- .../authserver/config/RequestContextData.java | 1 + .../custom/KjbMGOAuth2AccessTokenRequest.java | 63 ++++ .../KjbMGOAuth2AccessTokenResponse.java | 105 +++++++ .../custom/KjbMGOAuth2Controller.java | 288 ++++++++++++++++++ 6 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java create mode 100644 src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java create mode 100644 src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java diff --git a/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java b/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java index 33be4d0..7a36d55 100644 --- a/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java +++ b/src/main/java/com/eactive/eai/authserver/config/AuthorizationServerConfig.java @@ -104,7 +104,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap tokenType = vo.getProperty(PROP_TOKEN_TYPE, "JWT"); } - endpoints.pathMapping("/oauth/token", "/api/v1/oauth/token"); +// endpoints.pathMapping("/oauth/token", "/auth/oauth/v2/token"); endpoints.authenticationManager(authenticationManager); endpoints.userDetailsService(userDetailsService); diff --git a/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java b/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java index 59278ca..9ca3fd4 100644 --- a/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java +++ b/src/main/java/com/eactive/eai/authserver/config/OAuthRequestLoggingFilter.java @@ -1,5 +1,6 @@ package com.eactive.eai.authserver.config; +import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -24,9 +25,10 @@ public class OAuthRequestLoggingFilter extends OncePerRequestFilter { data.setGrantType(request.getParameter("grant_type")); data.setScope(request.getParameter("scope")); data.setUsername(request.getParameter("username")); + data.setResource(request.getParameter("resource")); RequestContextData.ThreadLocalRequestContext.set(data); - + filterChain.doFilter(request, response); } finally { RequestContextData.ThreadLocalRequestContext.clear(); diff --git a/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java b/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java index 19815de..a3b43cb 100644 --- a/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java +++ b/src/main/java/com/eactive/eai/authserver/config/RequestContextData.java @@ -9,6 +9,7 @@ public class RequestContextData { private String grantType; private String scope; private String username; + private String resource; // Getters and setters diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java new file mode 100644 index 0000000..a0dac48 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenRequest.java @@ -0,0 +1,63 @@ +package com.eactive.eai.authserver.custom; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@SuppressWarnings("serial") +public class KjbMGOAuth2AccessTokenRequest implements Serializable { + @JsonProperty("grant_type") + private String grantType; + @JsonProperty("client_id") + private String clientId; + @JsonProperty("client_secret") + private String clientSecret; + private String resource; + 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 getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + @Override + public String toString() { + return "KjbMGOAuth2AccessTokenRequest [grantType=" + grantType + ", clientId=" + clientId + ", clientSecret=" + + clientSecret + ", resource=" + resource + ", scope=" + scope + "]"; + } +} diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java new file mode 100644 index 0000000..9c039f7 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2AccessTokenResponse.java @@ -0,0 +1,105 @@ +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.JsonInclude.Include; + +@SuppressWarnings("serial") +@JsonInclude(Include.NON_NULL) +public class KjbMGOAuth2AccessTokenResponse implements Serializable { + private String responseCode; + private String responseMessage; + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("token_type") + private String tokenType; + @JsonProperty("expires_in") + private Long expiresIn; + @JsonProperty("expires_on") + private Long expiresOn; + private String resource; + private String scope; + + public String getResponseCode() { + return responseCode; + } + + public void setResponseCode(String responseCode) { + this.responseCode = responseCode; + } + + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String responseMessage) { + this.responseMessage = responseMessage; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getrefreshToken() { + return refreshToken; + } + + public void setrefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + 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 Long getExpiresOn() { + return expiresOn; + } + + public void setExpiresOn(Long expiresOn) { + this.expiresOn = expiresOn; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + @Override + public String toString() { + return "KjbMGOAuth2AccessTokenResponse [responseCode=" + responseCode + ", responseMessage=" + responseMessage + + ", accessToken=" + accessToken + ", tokenType=" + tokenType + ", expiresIn=" + expiresIn + + ", refreshToken=" + refreshToken+ ", expiresOn=" + expiresOn+ ", resource=" + resource+ ", scope=" + scope + "]"; + } +} diff --git a/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java new file mode 100644 index 0000000..6124580 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/KjbMGOAuth2Controller.java @@ -0,0 +1,288 @@ +package com.eactive.eai.authserver.custom; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.Cipher; +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.security.oauth2.provider.token.TokenEnhancer; +import org.springframework.stereotype.Controller; +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.ResponseBody; + +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.authserver.config.IssueLimitOAuth2Exception; +import com.eactive.eai.authserver.config.AuthorizationServerConfig; +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.UUID; +import com.eactive.eai.data.entity.onl.authserver.TokenIssuanceLog; + +@Controller +public class KjbMGOAuth2Controller { + 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 = "/mapi/oauth2/token2", method = RequestMethod.POST, produces = "application/json; charset=\"UTF-8\"") + @ResponseBody + public KjbMGOAuth2AccessTokenResponse token(@RequestBody KjbMGOAuth2AccessTokenRequest tokenRequest, + HttpServletRequest request, HttpServletResponse response) { + if (logger.isDebug()) { + logger.debug(tokenRequest.toString()); + } + + KjbMGOAuth2AccessTokenResponse responseToken = new KjbMGOAuth2AccessTokenResponse(); + try { + if (!StringUtils.equals(tokenRequest.getGrantType(), "client_credentials")) { + throw new JwtAuthException( + String.format("%d%s%s", HttpStatus.BAD_REQUEST.value(), SERVICE_CODE_ACCESS_TOKEN, "02"), + "Invalid Mandatory Field {grantType}"); + } + + String grantType = tokenRequest.getGrantType(); + String clientId = tokenRequest.getClientId(); + String clientSecret = tokenRequest.getClientSecret(); + + String[] scopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenRequest.getScope(), " "); + Set scopeSet = new HashSet(); + for (String scope : scopeArr) { + scopeSet.add(scope); + } + + String[] resourceArr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenRequest.getResource(), ","); + Set resourceSet = new HashSet(); + for (String resource : resourceArr) { + resourceSet.add(resource); + } + + ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId); + verifyClient(clientDetails, clientId, scopeSet); + + String traceId = UUID.randomUUID().toString().replace("-", ""); + response.setHeader(HEADER_TRACEID, traceId); + + HashMap authorizationParameters = new HashMap(); + authorizationParameters.put(OAuth2Utils.GRANT_TYPE, grantType); + authorizationParameters.put(OAuth2Utils.CLIENT_ID, clientId); + authorizationParameters.put("client_secret", clientSecret); + + Set responseType = new HashSet(); + responseType.add(tokenRequest.getGrantType()); + + OAuth2Request authorizationRequest = new OAuth2Request(authorizationParameters, clientId, null, true, scopeSet, + resourceSet, "", responseType, null); + + Principal principal = new OAuth2Authentication(authorizationRequest, null); + ResponseEntity result = tokenEndpoint().postAccessToken(principal, + authorizationParameters); + OAuth2AccessToken token = result.getBody(); + if(StringUtils.isNotBlank(token.getTokenType()) && StringUtils.equalsIgnoreCase(token.getTokenType(), "bearer")) { + responseToken.setTokenType("Bearer"); + } else { responseToken.setTokenType(token.getTokenType()); } + responseToken.setAccessToken(token.getValue()); + responseToken.setExpiresIn(Long.valueOf(token.getExpiresIn())); + responseToken.setrefreshToken(""); + responseToken.setExpiresOn(System.currentTimeMillis() + (token.getExpiresIn() * 1000)); + responseToken.setScope(tokenRequest.getScope()); + responseToken.setResource(tokenRequest.getResource()); + } catch (JwtAuthException e) { + response.setStatus(NumberUtils.toInt(StringUtils.left(e.getCode(), 3), HttpStatus.UNAUTHORIZED.value())); + responseToken.setResponseCode(e.getCode()); + responseToken.setResponseMessage(e.getMessage()); + } catch (Exception e) { + logger.error(e.getMessage()); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + responseToken.setResponseCode( + String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_ACCESS_TOKEN, "00")); + responseToken.setResponseMessage("Unauthorized. [Unknown]"); + } + + return responseToken; + } + + private void verifyClient(ClientDetails clientDetails, String clientId, Set 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 (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 TokenEndpoint tokenEndpoint() { + return BeanUtils.getBean("tokenEndpoint", TokenEndpoint.class); + } + + private class TokenIssuanceLogEnhancer implements TokenEnhancer { + @Override + public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { + if (!EAIDBLogControl.isEnable()) { + logger.warn("DB logging is disabled. Skipping token issuance logging."); + return accessToken; + } + + String clientId = authentication.getOAuth2Request().getClientId(); + LocalDateTime startDateTime = LocalDateTime.now().minusHours(24); + + int recentTokenCount = tokenIssuanceLogDAO.findRecentLogsForRestrictionCount(clientId, startDateTime); + + try { + ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId); + int dailyTokenLimit = clientVO.getDailyTokenLimit(); + if (dailyTokenLimit > 0 && recentTokenCount >= dailyTokenLimit) { + throw new IssueLimitOAuth2Exception("Token issuance limit exceeded for this client"); + } + + // JWT 변환 이후의 최종 토큰 값 로깅 + logTokenIssuance(true, false, "Token issued successfully", accessToken.getValue()); + } catch (DAOException e) { + logger.warn("cannot find clientVo - " + clientId); + } + return accessToken; + } + } + + 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(); + OAuth2Manager.getInstance(); + + TokenIssuanceLog log = new TokenIssuanceLog(); + String clientId = data.getClientId(); + log.setClientId(clientId); + + if(StringUtils.isNotBlank(clientId)) { + ClientVO clientVO = OAuth2Manager.getInstance().getClientInfo(clientId); + log.setAppName(clientVO.getClientName()); + log.setOrgId(clientVO.getOrgId()); + log.setOrgName(clientVO.getOrgName()); + } + + 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); // Add access token value to the log + + tokenIssuanceLogDAO.saveTokenIssuanceLog(log); + } catch (Throwable th) { + logger.error("Error while logging token issuance: " + th.getMessage(), th); + } + } + + public static void main(String[] args) throws Exception { + // First generate a public/private key pair + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + // generator.initialize(512, new SecureRandom()); + //generator.initialize(1024, new SecureRandom()); + generator.initialize(2048, new SecureRandom()); + KeyPair pair = generator.generateKeyPair(); + + // The private key can be used to sign (not encrypt!) a message. The public key + // holder can then verify the message. + + String message = "sSGJDwlJsel5WeXodzd3J3MQ20KYCZpL|2022-12-21T14:36:19+07:00"; + + // Let's sign our message + Signature privateSignature = Signature.getInstance("SHA256withRSA"); + privateSignature.initSign(pair.getPrivate()); + privateSignature.update(message.getBytes(StandardCharsets.UTF_8)); + System.out.println( + "private key=" + Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded())); + + byte[] signature = privateSignature.sign(); + //System.out.println("signature=" + new String(signature, StandardCharsets.UTF_8)); + System.out.println("signature=" + Base64.getEncoder().encodeToString(signature)); + + // Let's check the signature + Signature publicSignature = Signature.getInstance("SHA256withRSA"); + publicSignature.initVerify(pair.getPublic()); + publicSignature.update(message.getBytes(StandardCharsets.UTF_8)); + boolean isCorrect = publicSignature.verify(signature); + System.out.println("public key=" + Base64.getEncoder().encodeToString(pair.getPublic().getEncoded())); + + System.out.println("Signature correct: " + isCorrect); + + // The public key can be used to encrypt a message, the private key can be used + // to decrypt it. + // Encrypt the message + Cipher encryptCipher = Cipher.getInstance("RSA"); + encryptCipher.init(Cipher.ENCRYPT_MODE, pair.getPublic()); + + byte[] cipherText = encryptCipher.doFinal(message.getBytes(StandardCharsets.UTF_8)); + + // Now decrypt it + Cipher decriptCipher = Cipher.getInstance("RSA"); + decriptCipher.init(Cipher.DECRYPT_MODE, pair.getPrivate()); + + String decipheredMessage = new String(decriptCipher.doFinal(cipherText), StandardCharsets.UTF_8); + + System.out.println(decipheredMessage); + } +}