diff --git a/mens-api/src/main/java/kr/xit/core/biz/mapper/IAuthApiMapper.java b/mens-api/src/main/java/kr/xit/core/biz/mapper/IAuthApiMapper.java index 125e016..f2dfb0d 100644 --- a/mens-api/src/main/java/kr/xit/core/biz/mapper/IAuthApiMapper.java +++ b/mens-api/src/main/java/kr/xit/core/biz/mapper/IAuthApiMapper.java @@ -1,9 +1,8 @@ package kr.xit.core.biz.mapper; +import egovframework.com.cmm.model.LoginVO; import org.egovframe.rte.psl.dataaccess.mapper.Mapper; -import egovframework.com.cmm.LoginVO; - /** *
  * description :
diff --git a/mens-api/src/main/java/kr/xit/core/biz/service/AuthApiService.java b/mens-api/src/main/java/kr/xit/core/biz/service/AuthApiService.java
index 5e076ef..5a6b274 100644
--- a/mens-api/src/main/java/kr/xit/core/biz/service/AuthApiService.java
+++ b/mens-api/src/main/java/kr/xit/core/biz/service/AuthApiService.java
@@ -1,14 +1,12 @@
 package kr.xit.core.biz.service;
 
-import javax.annotation.Resource;
-
+import egovframework.com.cmm.model.LoginVO;
 import egovframework.com.cmm.util.EgovFileScrty;
+import javax.annotation.Resource;
+import kr.xit.core.biz.mapper.IAuthApiMapper;
 import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
 import org.springframework.stereotype.Service;
 
-import egovframework.com.cmm.LoginVO;
-import kr.xit.core.biz.mapper.IAuthApiMapper;
-
 @Service
 public class AuthApiService extends EgovAbstractServiceImpl implements IAuthApiService {
 
diff --git a/mens-api/src/main/java/kr/xit/core/biz/service/IAuthApiService.java b/mens-api/src/main/java/kr/xit/core/biz/service/IAuthApiService.java
index b0b6eef..3092c8f 100644
--- a/mens-api/src/main/java/kr/xit/core/biz/service/IAuthApiService.java
+++ b/mens-api/src/main/java/kr/xit/core/biz/service/IAuthApiService.java
@@ -1,6 +1,6 @@
 package kr.xit.core.biz.service;
 
-import egovframework.com.cmm.LoginVO;
+import egovframework.com.cmm.model.LoginVO;
 
 /**
  * 일반 로그인을 처리하는 비즈니스 구현 클래스
diff --git a/mens-api/src/main/java/kr/xit/core/biz/web/AuthApiController.java b/mens-api/src/main/java/kr/xit/core/biz/web/AuthApiController.java
index 4ee5622..e721107 100644
--- a/mens-api/src/main/java/kr/xit/core/biz/web/AuthApiController.java
+++ b/mens-api/src/main/java/kr/xit/core/biz/web/AuthApiController.java
@@ -1,7 +1,6 @@
 package kr.xit.core.biz.web;
 
-import egovframework.com.cmm.LoginVO;
-import egovframework.com.jwt.EgovJwtTokenUtil;
+import egovframework.com.cmm.model.LoginVO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.ExampleObject;
@@ -13,6 +12,8 @@ import kr.xit.core.biz.service.IAuthApiService;
 import kr.xit.core.consts.Constants;
 import kr.xit.core.model.ApiResponseDTO;
 import kr.xit.core.model.IApiResponse;
+import kr.xit.core.model.TokenDTO;
+import kr.xit.core.spring.auth.jwt.JwtTokenProvider;
 import kr.xit.core.spring.util.MessageUtil;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
@@ -56,7 +57,7 @@ public class AuthApiController {
 	/** EgovMessageSource */
 
 	private final MessageUtil messageUtil;
-	private final EgovJwtTokenUtil egovJwtTokenUtil;
+	private final JwtTokenProvider jwtTokenProvider;
 
 	/**
 	 * 일반 로그인을 처리한다
@@ -136,10 +137,11 @@ public class AuthApiController {
 
 			Map claimsMap = new HashMap<>();
 	//		claimsMap.put("dkkdk", "kdkkdkdkd");
-			String jwtToken = egovJwtTokenUtil.generateToken(loginVO, claimsMap);
-	//		String jwtToken = egovJwtTokenUtil.generateToken(loginVO.getId());
+			TokenDTO tokenDTO = jwtTokenProvider.generateTokenDto(claimsMap, String.format("{}{}",loginVO.getUserSe(), loginVO.getId()));
 
-			String username = egovJwtTokenUtil.getUsernameFromToken(jwtToken);
+			//		String jwtToken = egovJwtTokenUtil.generateToken(loginVO.getId());
+
+			String username = jwtTokenProvider.getUsernameFromToken(tokenDTO.getAccessToken());
 
 
 	    //	System.out.println("Dec jwtToken username = "+username);
@@ -172,7 +174,7 @@ public class AuthApiController {
 			//String jwtToken = jwtTokenProvider.generateJwtAccessToken(loginVO.getId(), "ROLE_USER");
 
 			resultMap.put("resultVO", loginResultVO);
-			resultMap.put("token", jwtToken);
+			resultMap.put("token", tokenDTO);
 		    return ApiResponseDTO.success(resultMap);
 			
 		}
diff --git a/mens-api/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml b/mens-api/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml
index f4a3684..18e0048 100644
--- a/mens-api/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml
+++ b/mens-api/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml
@@ -4,7 +4,7 @@
 
 
   
-  
     
       /** auth-mysql-mapper|actionLogin-로그인|julim */
       SELECT user_id AS id
diff --git a/mens-batch/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml b/mens-batch/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml
deleted file mode 100644
index 87d4f38..0000000
--- a/mens-batch/src/main/resources/egovframework/mapper/core/auth-mysql-mapper.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-	
-	
-
-
diff --git a/mens-core/pom.xml b/mens-core/pom.xml
index a80bcfe..bfee189 100644
--- a/mens-core/pom.xml
+++ b/mens-core/pom.xml
@@ -66,6 +66,24 @@
             spring-boot-configuration-processor
             true
         
+
+        
+        
+            org.springframework
+            spring-test
+        
+        
+            org.springframework.boot
+            spring-boot-test-autoconfigure
+        
+        
+            org.springframework.restdocs
+            spring-restdocs-core
+            2.0.6.RELEASE
+            test
+        
+        
+
         
             org.springframework.boot
             spring-boot-devtools
diff --git a/mens-core/src/main/java/egovframework/com/cmm/LoginVO.java b/mens-core/src/main/java/egovframework/com/cmm/LoginVO.java
deleted file mode 100644
index 9c21226..0000000
--- a/mens-core/src/main/java/egovframework/com/cmm/LoginVO.java
+++ /dev/null
@@ -1,245 +0,0 @@
-package egovframework.com.cmm;
-
-import java.io.Serializable;
-
-/**
- * @Class Name : LoginVO.java
- * @Description : Login VO class
- * @Modification Information
- * @
- * @  수정일         수정자                   수정내용
- * @ -------    --------    ---------------------------
- * @ 2009.03.03    박지욱          최초 생성
- *
- *  @author 공통서비스 개발팀 박지욱
- *  @since 2009.03.03
- *  @version 1.0
- *  @see
- *  
- */
-public class LoginVO implements Serializable{
-	
-	/** 아이디 */
-	private String id;
-	/** 이름 */
-	private String name;
-	/** 주민등록번호 */
-	private String ihidNum;
-	/** 이메일주소 */
-	private String email;
-	/** 비밀번호 */
-	private String password;
-	/** 비밀번호 힌트 */
-	private String passwordHint;
-	/** 비밀번호 정답 */
-	private String passwordCnsr;
-	/** 사용자구분 */
-	private String userSe;
-	/** 조직(부서)ID */
-	private String orgnztId;
-	/** 조직(부서)명 */
-	private String orgnztNm;
-	/** 고유아이디 */
-	private String uniqId;
-	/** 로그인 후 이동할 페이지 */
-	private String url;
-	/** 사용자 IP정보 */
-	private String ip;
-	/** GPKI인증 DN */
-	private String dn;
-	/**
-	 * id attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getId() {
-		return id;
-	}
-	/**
-	 * id attribute 값을 설정한다.
-	 * @param id String
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
-	/**
-	 * name attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getName() {
-		return name;
-	}
-	/**
-	 * name attribute 값을 설정한다.
-	 * @param name String
-	 */
-	public void setName(String name) {
-		this.name = name;
-	}
-	/**
-	 * ihidNum attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getIhidNum() {
-		return ihidNum;
-	}
-	/**
-	 * ihidNum attribute 값을 설정한다.
-	 * @param ihidNum String
-	 */
-	public void setIhidNum(String ihidNum) {
-		this.ihidNum = ihidNum;
-	}
-	/**
-	 * email attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getEmail() {
-		return email;
-	}
-	/**
-	 * email attribute 값을 설정한다.
-	 * @param email String
-	 */
-	public void setEmail(String email) {
-		this.email = email;
-	}
-	/**
-	 * password attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getPassword() {
-		return password;
-	}
-	/**
-	 * password attribute 값을 설정한다.
-	 * @param password String
-	 */
-	public void setPassword(String password) {
-		this.password = password;
-	}
-	/**
-	 * passwordHint attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getPasswordHint() {
-		return passwordHint;
-	}
-	/**
-	 * passwordHint attribute 값을 설정한다.
-	 * @param passwordHint String
-	 */
-	public void setPasswordHint(String passwordHint) {
-		this.passwordHint = passwordHint;
-	}
-	/**
-	 * passwordCnsr attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getPasswordCnsr() {
-		return passwordCnsr;
-	}
-	/**
-	 * passwordCnsr attribute 값을 설정한다.
-	 * @param passwordCnsr String
-	 */
-	public void setPasswordCnsr(String passwordCnsr) {
-		this.passwordCnsr = passwordCnsr;
-	}
-	/**
-	 * userSe attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getUserSe() {
-		return userSe;
-	}
-	/**
-	 * userSe attribute 값을 설정한다.
-	 * @param userSe String
-	 */
-	public void setUserSe(String userSe) {
-		this.userSe = userSe;
-	}
-	/**
-	 * orgnztId attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getOrgnztId() {
-		return orgnztId;
-	}
-	/**
-	 * orgnztId attribute 값을 설정한다.
-	 * @param orgnztId String
-	 */
-	public void setOrgnztId(String orgnztId) {
-		this.orgnztId = orgnztId;
-	}
-	/**
-	 * uniqId attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getUniqId() {
-		return uniqId;
-	}
-	/**
-	 * uniqId attribute 값을 설정한다.
-	 * @param uniqId String
-	 */
-	public void setUniqId(String uniqId) {
-		this.uniqId = uniqId;
-	}
-	/**
-	 * url attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getUrl() {
-		return url;
-	}
-	/**
-	 * url attribute 값을 설정한다.
-	 * @param url String
-	 */
-	public void setUrl(String url) {
-		this.url = url;
-	}
-	/**
-	 * ip attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getIp() {
-		return ip;
-	}
-	/**
-	 * ip attribute 값을 설정한다.
-	 * @param ip String
-	 */
-	public void setIp(String ip) {
-		this.ip = ip;
-	}
-	/**
-	 * dn attribute 를 리턴한다.
-	 * @return String
-	 */
-	public String getDn() {
-		return dn;
-	}
-	/**
-	 * dn attribute 값을 설정한다.
-	 * @param dn String
-	 */
-	public void setDn(String dn) {
-		this.dn = dn;
-	}
-	/**
-	 * @return the orgnztNm
-	 */
-	public String getOrgnztNm() {
-		return orgnztNm;
-	}
-	/**
-	 * @param orgnztNm the orgnztNm to set
-	 */
-	public void setOrgnztNm(String orgnztNm) {
-		this.orgnztNm = orgnztNm;
-	}
-	
-}
diff --git a/mens-core/src/main/java/egovframework/com/cmm/util/EgovUserDetailsHelper.java b/mens-core/src/main/java/egovframework/com/cmm/util/EgovUserDetailsHelper.java
index 6b0902f..d50427b 100644
--- a/mens-core/src/main/java/egovframework/com/cmm/util/EgovUserDetailsHelper.java
+++ b/mens-core/src/main/java/egovframework/com/cmm/util/EgovUserDetailsHelper.java
@@ -1,19 +1,15 @@
 package egovframework.com.cmm.util;
 
+import egovframework.com.cmm.model.LoginVO;
 import java.util.ArrayList;
 import java.util.List;
-
+import kr.xit.core.consts.Constants;
+import kr.xit.core.support.utils.Checks;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
 
-import egovframework.com.cmm.LoginVO;
-import kr.xit.core.consts.Constants;
-import kr.xit.core.support.utils.Checks;
-
-import org.egovframe.rte.fdl.string.EgovObjectUtil;
-
 /**
  * EgovUserDetails Helper 클래스
  *
diff --git a/mens-core/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java b/mens-core/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java
deleted file mode 100644
index 0bac4ce..0000000
--- a/mens-core/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package egovframework.com.jwt;
-
-import egovframework.com.cmm.LoginVO;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.security.Keys;
-import java.io.Serializable;
-import java.nio.charset.StandardCharsets;
-import java.security.Key;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-import kr.xit.core.consts.ErrorCode;
-import kr.xit.core.exception.BizRuntimeException;
-import kr.xit.core.spring.config.properties.JwtProperties;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-//security 관련 제외한 jwt util 클래스
-
-@Component
-public class EgovJwtTokenUtil implements Serializable{
-
-    private Key key;
-
-    private final transient JwtProperties jwtProp;
-
-    public EgovJwtTokenUtil(@Value("${app.token.secretKey}")String secret, JwtProperties jwtProperties) {
-        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
-        this.jwtProp = jwtProperties;
-    }
-
-    public String getUserIdFromToken(String token) {
-        Claims claims = getClaimFromToken(token);
-        return claims.get("id").toString();
-    }
-
-    public String getUserSeFromToken(String token) {
-        Claims claims = getClaimFromToken(token);
-        return claims.get("userSe").toString();
-    }
-    public String getInfoFromToken(String type, String token) {
-        Claims claims = getClaimFromToken(token);
-        return claims.get(type).toString();
-    }
-
-    public Claims getClaimFromToken(String token) {
-        final Claims claims = getAllClaimsFromToken(token);
-        return claims;
-    }
-
-	//retrieve username from jwt token
-    public String getUsernameFromToken(String token) {
-        return getClaimFromToken(token, Claims::getSubject);
-    }
-
-    //retrieve expiration date from jwt token
-    public Date getExpirationDateFromToken(String token) {
-        return getClaimFromToken(token, Claims::getExpiration);
-    }
-
-    //generate token for user
-    public String generateToken(LoginVO loginVO) {
-        return doGenerateToken(new HashMap<>(), loginVO.getUserSe()+loginVO.getId());
-    }
-
-    /**
-     *
-     * @param loginVO LoginVO
-     * @param claims Map JWT Payload(Claims)에 해당 속성 추가
-     * @return
-     */
-    public String generateToken(LoginVO loginVO, Map claims) {
-        return doGenerateToken(claims, loginVO.getUserSe()+loginVO.getId());
-    }
-
-    /**
-     * 토큰 유효성 검증
-     *
-     * @param token
-     * @param loginVO
-     * @return
-     */
-    public Boolean validateToken(String token, LoginVO loginVO) {
-        final String username = getUsernameFromToken(token);
-        if(!username.equals(loginVO.getUserSe()+loginVO.getId())){
-            throw BizRuntimeException.create(ErrorCode.INVALID_TOKEN);
-        }
-        return isTokenExpired(token);
-    }
-
-
-    //----------------------------------------------------------------------------------------
-
-	//while creating the token -
-	//1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
-	//2. Sign the JWT using the HS512 algorithm and secret key.
-	//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
-	//   compaction of the JWT to a URL-safe string
-    private String doGenerateToken(final Map claimMap, final String subject) {
-        Instant now = new Date().toInstant();
-
-        // payload(claimMap)에 시스템 속성 추가
-        Claims claims = Jwts.claims(claimMap)
-            .setIssuer(jwtProp.getIssuer())
-            .setAudience(jwtProp.getAudience())
-            .setSubject(subject)
-            .setIssuedAt(Date.from(now))
-            .setExpiration(Date.from(now.plus(jwtProp.getTokenExpiry(), ChronoUnit.DAYS)));
-
-        return Jwts.builder()
-            // 2. Signature
-            .signWith(key, SignatureAlgorithm.valueOf(jwtProp.getAlg()))                                // header "alg": "HS512"
-            .setHeaderParam("typ", jwtProp.getTyp())
-            .setHeaderParam("alg", jwtProp.getAlg())
-
-            // 1. Payload claim
-            .setClaims(claims)
-
-            // 3. JWT compact
-            .compact();
-    }
-
-    private  T getClaimFromToken(String token, Function claimsResolver) {
-        final Claims claims = getAllClaimsFromToken(token);
-        return claimsResolver.apply(claims);
-    }
-
-    //for retrieveing any information from token we will need the secret key
-    private Claims getAllClaimsFromToken(String token) {
-        return Jwts.parserBuilder()
-            .setSigningKey(key)
-            .build()
-            .parseClaimsJws(token)
-            .getBody();
-    }
-
-    //check if the token has expired
-    private Boolean isTokenExpired(String token) {
-        final Date expiration = getExpirationDateFromToken(token);
-        if(expiration.before(new Date()))  throw BizRuntimeException.create(ErrorCode.EXPIRED_TOKEN);
-        return true;
-    }
-}
diff --git a/mens-core/src/main/java/kr/xit/core/exception/BizRuntimeException.java b/mens-core/src/main/java/kr/xit/core/exception/BizRuntimeException.java
index bbd9e4d..f479982 100644
--- a/mens-core/src/main/java/kr/xit/core/exception/BizRuntimeException.java
+++ b/mens-core/src/main/java/kr/xit/core/exception/BizRuntimeException.java
@@ -54,7 +54,18 @@ public class BizRuntimeException extends BaseRuntimeException {
         this.code = String.valueOf(errorCode.getHttpStatus().value());
         this.errorCode = errorCode;
     }
-    
+
+    /**
+     * BizRuntimeException 생성자.
+     * @param errorCode 에러코드
+     */
+    public static BizRuntimeException of(String errorCode) {
+        BizRuntimeException e = new BizRuntimeException();
+        e.setCode(errorCode);
+        e.setMessage(messageUtil.getMessage(errorCode));
+        return e;
+    }
+
     /**
      * BizRuntimeException 생성자.
      * @param defaultMessage 메세지 지정
diff --git a/mens-core/src/main/java/kr/xit/core/model/RefreshTokenDTO.java b/mens-core/src/main/java/kr/xit/core/model/RefreshTokenDTO.java
new file mode 100644
index 0000000..03a0ac9
--- /dev/null
+++ b/mens-core/src/main/java/kr/xit/core/model/RefreshTokenDTO.java
@@ -0,0 +1,12 @@
+package kr.xit.core.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class RefreshTokenDTO {
+    private final String id;
+    private final String refreshToken;
+    private final Long refreshTokenExpiresIn;
+}
diff --git a/mens-core/src/main/java/kr/xit/core/model/TokenDTO.java b/mens-core/src/main/java/kr/xit/core/model/TokenDTO.java
new file mode 100644
index 0000000..f3b342f
--- /dev/null
+++ b/mens-core/src/main/java/kr/xit/core/model/TokenDTO.java
@@ -0,0 +1,15 @@
+package kr.xit.core.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class TokenDTO {
+    private final String grantType;
+    private final String authorizationType;
+    private final String accessToken;
+    private final String refreshToken;
+    private final Long accessTokenExpiresIn;
+    private final Long refreshTokenExpiresIn;
+}
diff --git a/mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtTokenProvider.java b/mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtTokenProvider.java
new file mode 100644
index 0000000..bfc8564
--- /dev/null
+++ b/mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtTokenProvider.java
@@ -0,0 +1,224 @@
+package kr.xit.core.spring.auth.jwt;
+
+import egovframework.com.cmm.model.LoginVO;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.UnsupportedJwtException;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.io.Encoders;
+import io.jsonwebtoken.security.Keys;
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.Map;
+import java.util.function.Function;
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import kr.xit.core.consts.Constants.JwtToken;
+import kr.xit.core.consts.ErrorCode;
+import kr.xit.core.exception.BizRuntimeException;
+import kr.xit.core.model.TokenDTO;
+import kr.xit.core.spring.config.properties.JwtProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * 
+ * description : JWT 토큰 생성, 검증
+ * packageName : kr.xit.core.spring.auth
+ * fileName    : JwtTokenProvider
+ * author      : julim
+ * date        : 2023-11-29
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-11-29    julim       최초 생성
+ *
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + public static final String REFRESH_HEADER = "Refresh"; + private Key key; + private final transient JwtProperties jwtProp; + // Bean 등록후 Key SecretKey HS256 decode + @PostConstruct + public void init() { + String base64EncodedSecretKey = encodeBase64SecretKey(jwtProp.getSecretKey()); + this.key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + } + + public String encodeBase64SecretKey(String secretKey) { + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { + byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public TokenDTO generateTokenDto(final Map claims, final String subject) { + Instant now = new Date().toInstant(); + + Date accessTokenExpiresIn = getTokenExpiration(jwtProp.getTokenExpiry(), now); + Date refreshTokenExpiresIn = getTokenExpiration(jwtProp.getRefreshTokenExpiry(), now); + + String accessToken = Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setExpiration(accessTokenExpiresIn) + .setIssuedAt(Date.from(now)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + String refreshToken = Jwts.builder() + .setSubject(subject) + .setIssuedAt(Date.from(now)) + .setExpiration(refreshTokenExpiresIn) + .signWith(key) + .compact(); + + return TokenDTO.builder() + .grantType(JwtToken.GRANT_TYPE.getCode()) + .authorizationType(JwtToken.HEADER_NAME.getCode()) + .accessToken(accessToken) + .accessTokenExpiresIn(accessTokenExpiresIn.getTime()) + .refreshToken(refreshToken) + .build(); + } + + // JWT 토큰을 복호화하여 토큰 정보를 반환 +// public Authentication getAuthentication(String accessToken) { +// Claims claims = parseClaims(accessToken); +// +// if (claims.get("role") == null) { +// throw new BusinessLogicException(ExceptionCode.NO_ACCESS_TOKEN); +// } +// +// String authority = claims.get("role").toString(); +// +// CustomUserDetails customUserDetails = CustomUserDetails.of( +// claims.getSubject(), +// authority); +// +// +// log.info("# AuthMember.getRoles 권한 체크 = {}", customUserDetails.getAuthorities().toString()); +// +// return new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); +// } + + // 토큰 검증 + public boolean validateToken(String token, HttpServletResponse response) { + try { + parseClaims(token); + } catch (MalformedJwtException e) { + log.info("Invalid JWT token"); + log.trace("Invalid JWT token trace = {}", e); + throw BizRuntimeException.of("fail.jwt.invalid"); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token"); + log.trace("Expired JWT token trace = {}", e); + throw BizRuntimeException.of("fail.jwt.expired"); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT token"); + log.trace("Unsupported JWT token trace = {}", e); + throw BizRuntimeException.of("fail.jwt.unsupported"); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty."); + log.trace("JWT claims string is empty trace = {}", e); + throw BizRuntimeException.of("fail.jwt.illegalArgument"); + } + return true; + } + + private Date getTokenExpiration(final int expirationDay, final Instant now) { + return Date.from(now.plus(expirationDay, ChronoUnit.DAYS)); + } + + // Token 복호화 및 예외 발생(토큰 만료, 시그니처 오류)시 Claims 객체가 안만들어짐. + public Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public void accessTokenSetHeader(String accessToken, HttpServletResponse response) { + String headerValue = JwtToken.GRANT_TYPE + accessToken; + response.setHeader(JwtToken.HEADER_NAME.getCode(), headerValue); + } + + public void refresshTokenSetHeader(String refreshToken, HttpServletResponse response) { + response.setHeader("Refresh", refreshToken); + } + + // Request Header에 Access Token 정보를 추출하는 메서드 + public String resolveAccessToken(HttpServletRequest request) { + String bearerToken = request.getHeader(JwtToken.HEADER_NAME.getCode()); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtToken.GRANT_TYPE.getCode())) { + return bearerToken.substring(JwtToken.GRANT_TYPE.getCode().length()+1); + } + return null; + } + + // Request Header에 Refresh Token 정보를 추출하는 메서드 + public String resolveRefreshToken(HttpServletRequest request) { + String bearerToken = request.getHeader(REFRESH_HEADER); + if (StringUtils.hasText(bearerToken)) { + return bearerToken; + } + return null; + } + + public Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + public String getUserIdFromToken(String token) { + Claims claims = parseClaims(token); + return claims.get("id").toString(); + } + + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + public String getUserSeFromToken(String token) { + Claims claims = parseClaims(token); + return claims.get("userSe").toString(); + } + public String getInfoFromToken(String type, String token) { + Claims claims = parseClaims(token); + return claims.get(type).toString(); + } + + public Boolean validateToken(String token, LoginVO loginVO) { + final String username = getUsernameFromToken(token); + if(!username.equals(loginVO.getUserSe()+loginVO.getId())){ + throw BizRuntimeException.create(ErrorCode.INVALID_TOKEN); + } + return isTokenExpired(token); + } + + private T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = parseClaims(token); + return claimsResolver.apply(claims); + } + + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + if(expiration.before(new Date())) throw BizRuntimeException.create(ErrorCode.EXPIRED_TOKEN); + return true; + } +} diff --git a/mens-core/src/main/java/egovframework/com/jwt/JwtVerification.java b/mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtVerification.java similarity index 87% rename from mens-core/src/main/java/egovframework/com/jwt/JwtVerification.java rename to mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtVerification.java index dc3f1c3..2f1dc0b 100644 --- a/mens-core/src/main/java/egovframework/com/jwt/JwtVerification.java +++ b/mens-core/src/main/java/kr/xit/core/spring/auth/jwt/JwtVerification.java @@ -1,6 +1,6 @@ -package egovframework.com.jwt; +package kr.xit.core.spring.auth.jwt; -import egovframework.com.cmm.LoginVO; +import egovframework.com.cmm.model.LoginVO; import egovframework.com.cmm.util.EgovStringUtil; import egovframework.com.cmm.util.EgovUserDetailsHelper; import io.jsonwebtoken.ExpiredJwtException; @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component; @Component public class JwtVerification { - private final EgovJwtTokenUtil jwtTokenUtil; + private final JwtTokenProvider jwtTokenProvider; private final Logger log = LoggerFactory.getLogger(JwtVerification.class); @@ -40,7 +40,7 @@ public class JwtVerification { String username = null; try { - username = jwtTokenUtil.getUsernameFromToken(jwtToken); + username = jwtTokenProvider.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { log.debug("Unable to get JWT Token"); } catch (ExpiredJwtException e) { @@ -52,7 +52,7 @@ public class JwtVerification { } // step 4. 가져온 username이랑 2에서 가져온 loginVO랑 비교해서 같은지 체크 & 이 과정에서 한번 더 기간 체크를 한다. - if (username == null || !(jwtTokenUtil.validateToken(jwtToken, loginVO))) { + if (username == null || !(jwtTokenProvider.validateToken(jwtToken, loginVO))) { log.debug("jwtToken not validate"); return false; } diff --git a/mens-core/src/main/java/kr/xit/core/spring/config/auth/util/HeaderUtil.java b/mens-core/src/main/java/kr/xit/core/spring/config/auth/util/HeaderUtil.java index 7d9578d..c140cd9 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/config/auth/util/HeaderUtil.java +++ b/mens-core/src/main/java/kr/xit/core/spring/config/auth/util/HeaderUtil.java @@ -78,7 +78,7 @@ public class HeaderUtil { if(Checks.isEmpty(accessToken)) throw BizRuntimeException.create(ErrorCode.AUTH_HEADER_NOT_EXISTS); try { - String username = CoreSpringUtils.getEgovJwtTokenUtil().getUsernameFromToken(accessToken); + String username = CoreSpringUtils.getJwtTokenProvider().getUsernameFromToken(accessToken); return username; }catch (Exception e){ throw BizRuntimeException.create(ErrorCode.INVALID_TOKEN); @@ -89,7 +89,7 @@ public class HeaderUtil { if(Checks.isEmpty(accessToken)) throw BizRuntimeException.create(ErrorCode.AUTH_HEADER_NOT_EXISTS); try { - return CoreSpringUtils.getEgovJwtTokenUtil().getUsernameFromToken(accessToken); + return CoreSpringUtils.getJwtTokenProvider().getUsernameFromToken(accessToken); }catch (Exception e){ throw BizRuntimeException.create(ErrorCode.INVALID_TOKEN); } diff --git a/mens-core/src/main/java/kr/xit/core/spring/config/properties/JwtProperties.java b/mens-core/src/main/java/kr/xit/core/spring/config/properties/JwtProperties.java index 4e8bda5..b5853e0 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/config/properties/JwtProperties.java +++ b/mens-core/src/main/java/kr/xit/core/spring/config/properties/JwtProperties.java @@ -14,6 +14,7 @@ public class JwtProperties { private String alg; private String issuer; private String audience; + private String secretKey; // minute private int tokenExpiry; // day diff --git a/mens-core/src/main/java/kr/xit/core/spring/util/AES128Config.java b/mens-core/src/main/java/kr/xit/core/spring/util/AES128Config.java new file mode 100644 index 0000000..17b9f35 --- /dev/null +++ b/mens-core/src/main/java/kr/xit/core/spring/util/AES128Config.java @@ -0,0 +1,83 @@ +package kr.xit.core.spring.util; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import javax.annotation.PostConstruct; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import kr.xit.core.exception.BizRuntimeException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + *
+ * description : AES128 암호화 및 복호화 기능을 하는 AES128Config class
+ * packageName : kr.xit.core.spring.util
+ * fileName    : AES128Config
+ * author      : julim
+ * date        : 2023-11-29
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-11-29    julim       최초 생성
+ *
+ * 
+ */ +@Component +public class AES128Config { + private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8; + private static final String INSTANCE_TYPE = "AES/CBC/PKCS5Padding"; + + @Value("${app.aes.secret-key:}") + private String secretKey; + private IvParameterSpec ivParameterSpec; + private SecretKeySpec secretKeySpec; + private Cipher cipher; + + @PostConstruct + public void init() throws NoSuchPaddingException, NoSuchAlgorithmException { + SecureRandom secureRandom = new SecureRandom(); + byte[] iv = new byte[16]; // 16bytes = 128bits + secureRandom.nextBytes(iv); + ivParameterSpec = new IvParameterSpec(iv); + secretKeySpec = new SecretKeySpec(secretKey.getBytes(ENCODING_TYPE), "AES"); + cipher = Cipher.getInstance(INSTANCE_TYPE); + } + + // AES 암호화 + + /** + * AES 암호화 + * @param plaintext String + * @return String + */ + public String encryptAes(String plaintext) { + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE)); + return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE); + } catch (Exception e) { + throw BizRuntimeException.of("fail.aes.encode"); + } + } + + /** + * AES 복호화 + * @param plaintext String + * @return String + */ + public String decryptAes(String plaintext) { + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE)); + return new String(cipher.doFinal(decoded), ENCODING_TYPE); + } catch (Exception e) { + throw BizRuntimeException.of("fail.aes.decode"); + } + } +} diff --git a/mens-core/src/main/java/kr/xit/core/spring/util/CoreSpringUtils.java b/mens-core/src/main/java/kr/xit/core/spring/util/CoreSpringUtils.java index 68e5eaf..bcdba32 100644 --- a/mens-core/src/main/java/kr/xit/core/spring/util/CoreSpringUtils.java +++ b/mens-core/src/main/java/kr/xit/core/spring/util/CoreSpringUtils.java @@ -1,8 +1,8 @@ package kr.xit.core.spring.util; import com.fasterxml.jackson.databind.ObjectMapper; -import egovframework.com.jwt.EgovJwtTokenUtil; -import egovframework.com.jwt.JwtVerification; +import kr.xit.core.spring.auth.jwt.JwtTokenProvider; +import kr.xit.core.spring.auth.jwt.JwtVerification; import kr.xit.core.spring.config.properties.CorsProperties; import kr.xit.core.spring.config.support.ApplicationContextProvider; import lombok.AccessLevel; @@ -75,8 +75,8 @@ public class CoreSpringUtils { * * @return EgovJwtTokenUtil */ - public static EgovJwtTokenUtil getEgovJwtTokenUtil(){ - return (EgovJwtTokenUtil)getBean(EgovJwtTokenUtil.class); + public static JwtTokenProvider getJwtTokenProvider(){ + return (JwtTokenProvider)getBean(JwtTokenProvider.class); } /** diff --git a/mens-core/src/main/resources/config/application-common.yml b/mens-core/src/main/resources/config/application-common.yml index 4762475..0b37fdc 100644 --- a/mens-core/src/main/resources/config/application-common.yml +++ b/mens-core/src/main/resources/config/application-common.yml @@ -144,3 +144,7 @@ app: allow-Credentials: true # 기본적으로 브라우저에게 노출이 되지 않지만, 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정 expose-Headers: Content-Length + + # AES128 암호화 및 복호화 기능을 하는 AES128Config class 에서 사용 + aes: + secret-key: c3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LWlucGl4LWNvcmUtamF2YWZyYW1ld29yay1qYXZhLXRva2VuLWtleQ diff --git a/mens-core/src/main/resources/egovframework/messages/message-common.properties b/mens-core/src/main/resources/egovframework/messages/message-common.properties index 118d030..2bf467c 100644 --- a/mens-core/src/main/resources/egovframework/messages/message-common.properties +++ b/mens-core/src/main/resources/egovframework/messages/message-common.properties @@ -1,8 +1,22 @@ #------------------------------------------------ # \uACF5\uD1B5 \uBA54\uC138\uC9C0 \uC815\uC758 : framework #------------------------------------------------ +fail.jwt.invalid=\uC815\uC0C1\uC801\uC778 \uD1A0\uD070\uC774 \uC544\uB2D9\uB2C8\uB2E4(\uC798\uBABB\uB41C \uAD6C\uC870). +fail.jwt.expired=\uD1A0\uD070 \uC720\uD6A8\uAE30\uAC04\uC774 \uACBD\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. +fail.jwt.unsupported=\uC815\uC0C1\uC801\uC778 \uD1A0\uD070\uC774 \uC544\uB2D9\uB2C8\uB2E4(\uD615\uC2DD \uB610\uB294 \uAD6C\uC131) +fail.jwt.illegalArgument=\uC815\uC0C1\uC801\uC778 \uD1A0\uD070\uC774 \uC544\uB2D9\uB2C8\uB2E4. +fail.jwt.role.invalid=\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD1A0\uD070 \uAD8C\uD55C\uC815\uBCF4 \uC785\uB2C8\uB2E4[{0}] +fail.jwt.role.notExists=\uD1A0\uD070\uC5D0 \uAD8C\uD55C\uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 + +fail.aes.encode=\uC554\uD638\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. +fail.aes.decode=\uBCF5\uD638\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. fail.common.msg=\uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4! + + + + + fail.common.sql=sql \uC5D0\uB7EC\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4! error code: {0}, error msg: {1} info.nodata.msg=\uD574\uB2F9 \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. diff --git a/mens-core/src/test/java/kr/xit/core/spring/test/BaseIntegrationTest.java b/mens-core/src/test/java/kr/xit/core/spring/test/BaseIntegrationTest.java new file mode 100644 index 0000000..80d0b85 --- /dev/null +++ b/mens-core/src/test/java/kr/xit/core/spring/test/BaseIntegrationTest.java @@ -0,0 +1,51 @@ +package kr.xit.core.spring.test; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + *
+ * description : 통합 테스트에 공통적으로 사용할 수 있는 공통 클래스
+ *  @Disabled : 테스트 클래스 또는 테스트 메서드를 실행하지 않는다
+ *              -> 상속만을 위한 클래스이기 때문에 실행할 필요가 없다.
+ *  @Transactional : 각각의 테스트 메서드가 실행될 때마다, 데이터베이스를 롤백
+ *                   -> @Rollback(false) 애너테이션을 추가시 데이타베이스 반영
+ *  @AutoConfigureMockMvc : @WebMvcTest가 아닌 @SpringBootTest 애너테이션을 사용하면서
+ *                          MockMvc를 이용한 테스트를 해야 할 때 필요
+ *  @ActiveProfiles : 테스트 수행시 사용할 프로파일 지정
+ *  @AutoConfigureRestDocs : Spring REST Docs를 사용하기 위해 MockMvc 빈을 커스터마이즈
+ *                           -> Mock MVC, REST Assured 또는 WebTestClient로 테스트할 때 Spring REST Docs 사용 가능
+ *  @ExtendWith(RestDocumentationExtension.class) : Spring REST Docs를 활성화하는 데 사용되는 JUNit 5 애너테이션
+ * packageName : kr.xit.core.spring.test
+ * fileName    : BaseIntegrationTest
+ * author      : julim
+ * date        : 2023-11-29
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-11-29    julim       최초 생성
+ *
+ * 
+ */ +@Disabled +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureRestDocs +@ActiveProfiles("test") +@ExtendWith(RestDocumentationExtension.class) +public class BaseIntegrationTest { + + @Autowired + protected MockMvc mockMvc; + +} diff --git a/mens-core/src/test/java/kr/xit/core/spring/util/AES128ConfigTest.java b/mens-core/src/test/java/kr/xit/core/spring/util/AES128ConfigTest.java new file mode 100644 index 0000000..deba1cc --- /dev/null +++ b/mens-core/src/test/java/kr/xit/core/spring/util/AES128ConfigTest.java @@ -0,0 +1,29 @@ +package kr.xit.core.spring.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import kr.xit.core.spring.test.BaseIntegrationTest; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + + +@Slf4j +class AES128ConfigTest extends BaseIntegrationTest { + + @Autowired + private AES128Config aes128Config; + + @Test + @DisplayName("Aes128 암호화가 잘 이루어지는지 테스트") + void aes128Test() { + String text = "this is test"; + String enc = aes128Config.encryptAes(text); + String dec = aes128Config.decryptAes(enc); + log.info("enc = {}", enc); + log.info("dec = {}", dec); + + assertThat(dec).isEqualTo(text); + } +}