From ea5c70d1879d0c366433c6adffd638a540770583 Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:05:46 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat=20:=20jwt=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/kobaco/global/jwt/JwtConsts.java | 11 +++ .../core/kobaco/global/jwt/JwtProperties.java | 14 ++++ .../core/kobaco/global/jwt/JwtProvider.java | 76 +++++++++++++++++++ .../core/kobaco/global/jwt/PrivateClaims.java | 53 +++++++++++++ .../core/kobaco/global/jwt/TokenType.java | 7 ++ 5 files changed, 161 insertions(+) create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtConsts.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProperties.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProvider.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/PrivateClaims.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/TokenType.java diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtConsts.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtConsts.java new file mode 100644 index 0000000..5bb9ddc --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtConsts.java @@ -0,0 +1,11 @@ +package core.kobaco.global.jwt; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class JwtConsts { + public static final String TOKEN_ISSUER = "kobaco"; + public static final String USER_CLAIMS = "user_claims"; + public static final String TOKEN_TYPE = "token_type"; +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProperties.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProperties.java new file mode 100644 index 0000000..4fa11ba --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProperties.java @@ -0,0 +1,14 @@ +package core.kobaco.global.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.bind.annotation.GetMapping; + +@Getter +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + private final String secret; + private final long accessTokenExpirationTime; +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProvider.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProvider.java new file mode 100644 index 0000000..f60b0dd --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/JwtProvider.java @@ -0,0 +1,76 @@ +package core.kobaco.global.jwt; + + +import core.kobaco.global.jwt.exception.AuthErrorCode; +import core.kobaco.global.jwt.exception.ExpiredTokenException; +import core.kobaco.global.jwt.exception.InvalidTokenException; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.jackson.io.JacksonDeserializer; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.security.SignatureException; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + private final JwtProperties jwtProperties; + + public String generateAccessToken(final PrivateClaims.UserClaims userClaims) { + return generateToken(userClaims.createPrivateClaims(TokenType.ACCESS_TOKEN), jwtProperties.getAccessTokenExpirationTime()); + } + + public PrivateClaims.UserClaims extractUserClaimsFromToken(String token, TokenType tokenType) { + return initializeJwtParser(tokenType) + .parseSignedClaims(token) + .getPayload() + .get(JwtConsts.USER_CLAIMS, PrivateClaims.UserClaims.class); + } + + private String generateToken(final PrivateClaims privateClaims, final Long expirationTime) { + final Date now = new Date(); + return Jwts.builder() + .issuer(JwtConsts.TOKEN_ISSUER) + .claims(privateClaims.createClaimsMap()) + .issuedAt(now) + .expiration(new Date(now.getTime() + expirationTime)) + .signWith(getSigningKey()) + .compact(); + } + + /** + * @return 서명에 사용할 Key 반환 + */ + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecret())); + } + + /** + * @throws InvalidTokenException 잘못된 토큰이 요청되었을 때 반환(서명 오류, 잘못된 토큰 형식, 잘못된 토큰 발급자, null이거나 공백인 경우) + * @throws ExpiredTokenException 만료된 토큰이 요청되었을 때 반환 + */ + public void validateToken(final String token, final TokenType tokenType) { + final JwtParser jwtParser = initializeJwtParser(tokenType); + try { + jwtParser.parse(token); + } catch (MalformedJwtException | IncorrectClaimException | IllegalArgumentException e) { + throw new InvalidTokenException(AuthErrorCode.INVALID_TOKEN); + } catch (ExpiredJwtException e) { + throw new ExpiredTokenException(AuthErrorCode.EXPIRED_TOKEN); + } + } + + + private JwtParser initializeJwtParser(final TokenType tokenType) { + return Jwts.parser() + .json(new JacksonDeserializer<>(PrivateClaims.getClaimsTypeDetailMap())) + .verifyWith(getSigningKey()) + .requireIssuer(JwtConsts.TOKEN_ISSUER) + .require(JwtConsts.TOKEN_TYPE, tokenType) + .build(); + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/PrivateClaims.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/PrivateClaims.java new file mode 100644 index 0000000..e5c24ff --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/PrivateClaims.java @@ -0,0 +1,53 @@ +package core.kobaco.global.jwt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +@Getter +public class PrivateClaims { + private final UserClaims userClaims; + private final TokenType tokenType; + + public PrivateClaims(UserClaims userClaims, TokenType tokenType) { + this.userClaims = userClaims; + this.tokenType = tokenType; + } + + public Map createClaimsMap() { + return Map.of( + JwtConsts.USER_CLAIMS, userClaims, + JwtConsts.TOKEN_TYPE, tokenType.name() + ); + } + + public static Map> getClaimsTypeDetailMap(){ + return Map.of( + JwtConsts.USER_CLAIMS, UserClaims.class, + JwtConsts.TOKEN_TYPE, TokenType.class + ); + } + + @Getter + @ToString + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class UserClaims{ + @JsonProperty("user_id") + private Long userId; + private UserClaims(Long userId) { + this.userId = userId; + } + + public static UserClaims of(Long userId){ + return new UserClaims(userId); + } + + public PrivateClaims createPrivateClaims(TokenType tokenType) { + return new PrivateClaims(this, tokenType); + } + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/TokenType.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/TokenType.java new file mode 100644 index 0000000..fdbed0b --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/TokenType.java @@ -0,0 +1,7 @@ +package core.kobaco.global.jwt; + + +public enum TokenType { + ACCESS_TOKEN, + REFRESH_TOKEN +} From 215a34a4fb22c8cb885af0e077a2aa157f67e633 Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:05:58 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore=20:=20jwt=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kobaco/kobaco/build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/kobaco/kobaco/build.gradle b/kobaco/kobaco/build.gradle index 5f4ae2f..2294c65 100644 --- a/kobaco/kobaco/build.gradle +++ b/kobaco/kobaco/build.gradle @@ -27,6 +27,15 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.mysql:mysql-connector-j' + + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.5' + + // configuration properties + implementation 'org.springframework.boot:spring-boot-configuration-processor' } tasks.named('test') { From ae7a58604deebdf7e0f6a81dd1f66c35246e6ba3 Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:06:23 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat=20:=20BusinessException=20=EB=B0=8F=20?= =?UTF-8?q?ErrorCode=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kobaco/global/exception/BusinessException.java | 13 +++++++++++++ .../core/kobaco/global/exception/ErrorCode.java | 6 ++++++ 2 files changed, 19 insertions(+) create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/exception/BusinessException.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/exception/ErrorCode.java diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/exception/BusinessException.java b/kobaco/kobaco/src/main/java/core/kobaco/global/exception/BusinessException.java new file mode 100644 index 0000000..e4d9896 --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/exception/BusinessException.java @@ -0,0 +1,13 @@ +package core.kobaco.global.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException{ + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/exception/ErrorCode.java b/kobaco/kobaco/src/main/java/core/kobaco/global/exception/ErrorCode.java new file mode 100644 index 0000000..e2c7659 --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/exception/ErrorCode.java @@ -0,0 +1,6 @@ +package core.kobaco.global.exception; + +public interface ErrorCode { + String getCode(); + String getMessage(); +} From e966cde7bc43c335e0dc7fec0c14344666e6a31f Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:06:43 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat=20:=20jwt=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8,=20=EC=98=88=EC=99=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/exception/AuthErrorCode.java | 28 +++++++++++++++++++ .../jwt/exception/ExpiredTokenException.java | 9 ++++++ .../jwt/exception/InvalidTokenException.java | 10 +++++++ .../global/jwt/exception/TokenException.java | 12 ++++++++ 4 files changed, 59 insertions(+) create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/AuthErrorCode.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/ExpiredTokenException.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/InvalidTokenException.java create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/TokenException.java diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/AuthErrorCode.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/AuthErrorCode.java new file mode 100644 index 0000000..d6ebf54 --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/AuthErrorCode.java @@ -0,0 +1,28 @@ +package core.kobaco.global.jwt.exception; + +import core.kobaco.global.exception.ErrorCode; + + +public enum AuthErrorCode implements ErrorCode { + INVALID_TOKEN("유효하지 않는 토큰입니다.", 1000), + EXPIRED_TOKEN("만료된 토큰입니다.", 1001), + ; + + private final String code; + private final String message; + + AuthErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/ExpiredTokenException.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/ExpiredTokenException.java new file mode 100644 index 0000000..4808898 --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/ExpiredTokenException.java @@ -0,0 +1,9 @@ +package core.kobaco.global.jwt.exception; + +import core.kobaco.global.exception.ErrorCode; + +public class ExpiredTokenException extends TokenException { + public ExpiredTokenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/InvalidTokenException.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/InvalidTokenException.java new file mode 100644 index 0000000..2a18723 --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/InvalidTokenException.java @@ -0,0 +1,10 @@ +package core.kobaco.global.jwt.exception; + +import core.kobaco.global.exception.ErrorCode; + +public class InvalidTokenException extends TokenException{ + + public InvalidTokenException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/TokenException.java b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/TokenException.java new file mode 100644 index 0000000..7d6cedc --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/jwt/exception/TokenException.java @@ -0,0 +1,12 @@ +package core.kobaco.global.jwt.exception; + +import core.kobaco.global.exception.BusinessException; +import core.kobaco.global.exception.ErrorCode; + +import java.nio.Buffer; + +public class TokenException extends BusinessException { + public TokenException(ErrorCode errorCode) { + super(errorCode); + } +} From 580b48fe55e5c52c25df05fe64eb971c56315058 Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:07:05 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore=20:=20ConfigurationProperties=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/ConfigurationPropertiesConfig.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 kobaco/kobaco/src/main/java/core/kobaco/global/config/ConfigurationPropertiesConfig.java diff --git a/kobaco/kobaco/src/main/java/core/kobaco/global/config/ConfigurationPropertiesConfig.java b/kobaco/kobaco/src/main/java/core/kobaco/global/config/ConfigurationPropertiesConfig.java new file mode 100644 index 0000000..2f5ecad --- /dev/null +++ b/kobaco/kobaco/src/main/java/core/kobaco/global/config/ConfigurationPropertiesConfig.java @@ -0,0 +1,10 @@ +package core.kobaco.global.config; + +import core.kobaco.global.jwt.JwtProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(JwtProperties.class) +public class ConfigurationPropertiesConfig { +} From 5918eaf1f551a6f0e65f1833fc96bbe037b89325 Mon Sep 17 00:00:00 2001 From: tlarbals824 Date: Wed, 28 Feb 2024 02:08:17 +0900 Subject: [PATCH 6/6] =?UTF-8?q?chore=20:=20jwt=20=ED=82=A4=20=EA=B0=92=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .deploy/docker-compose.yml | 3 ++- .github/workflows/cd.yml | 1 + kobaco/kobaco/src/main/resources/application.yml | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.deploy/docker-compose.yml b/.deploy/docker-compose.yml index b269bac..5c267e0 100644 --- a/.deploy/docker-compose.yml +++ b/.deploy/docker-compose.yml @@ -9,4 +9,5 @@ services: environment: - DB_URL=${DB_URL} - DB_USERNAME=${DB_USERNAME} - - DB_PASSWORD=${DB_PASSWORD} \ No newline at end of file + - DB_PASSWORD=${DB_PASSWORD} + - TOKEN_SECRET=${TOKEN_SECRET} \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5cec0ed..aaec7df 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -70,6 +70,7 @@ jobs: echo "DB_URL=${{secrets.DB_URL}}" >> .env echo "DB_USERNAME=${{secrets.DB_USERNAME}}" >> .env echo "DB_PASSWORD=${{secrets.DB_PASSWORD}}" >> .env + echo "TOKEN_SECRET=${{secrets.TOKEN_SECRET}}" >> .env sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-kobaco sudo docker stop kobaco diff --git a/kobaco/kobaco/src/main/resources/application.yml b/kobaco/kobaco/src/main/resources/application.yml index 46573fe..79a4da3 100644 --- a/kobaco/kobaco/src/main/resources/application.yml +++ b/kobaco/kobaco/src/main/resources/application.yml @@ -4,3 +4,8 @@ spring: username: ${DB_USERNAME} password: ${DB_PASSWORD} + +jwt: + secret: ${TOKEN_SECRET} + expiration: 86400000 # +