diff --git a/build.gradle b/build.gradle index 2ba8ef4..d4d33ff 100644 --- a/build.gradle +++ b/build.gradle @@ -24,11 +24,32 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + // https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' + + + // lombok 뒤에 위치 필요 + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + //JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // com.sun.xml.bind + implementation 'com.sun.xml.bind:jaxb-impl:4.0.1' + implementation 'com.sun.xml.bind:jaxb-core:4.0.1' + // javax.xml.bind + implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' } tasks.named('test') { diff --git a/src/main/java/com/bside/familyrecipes/common/message/ExceptionMessage.java b/src/main/java/com/bside/familyrecipes/common/message/ExceptionMessage.java new file mode 100644 index 0000000..c85fb83 --- /dev/null +++ b/src/main/java/com/bside/familyrecipes/common/message/ExceptionMessage.java @@ -0,0 +1,13 @@ +package com.bside.familyrecipes.common.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum ExceptionMessage { + /** auth **/ + INVALID_TOKEN("유효하지 않은 토큰입니다."); + + private final String message; +} diff --git a/src/main/java/com/bside/familyrecipes/config/AuthConfig.java b/src/main/java/com/bside/familyrecipes/config/AuthConfig.java new file mode 100644 index 0000000..048a652 --- /dev/null +++ b/src/main/java/com/bside/familyrecipes/config/AuthConfig.java @@ -0,0 +1,12 @@ +package com.bside.familyrecipes.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Getter +@Configuration +public class AuthConfig { + @Value("${jwt.secret}") + private String jwtSecretKey; +} diff --git a/src/main/java/com/bside/familyrecipes/config/WebConfig.java b/src/main/java/com/bside/familyrecipes/config/WebConfig.java new file mode 100644 index 0000000..d26c839 --- /dev/null +++ b/src/main/java/com/bside/familyrecipes/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.bside.familyrecipes.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("*") + .maxAge(3600); + } +} diff --git a/src/main/java/com/bside/familyrecipes/config/jwt/JwtTokenManager.java b/src/main/java/com/bside/familyrecipes/config/jwt/JwtTokenManager.java new file mode 100644 index 0000000..26a69aa --- /dev/null +++ b/src/main/java/com/bside/familyrecipes/config/jwt/JwtTokenManager.java @@ -0,0 +1,82 @@ +package com.bside.familyrecipes.config.jwt; + +import com.bside.familyrecipes.config.AuthConfig; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import com.bside.familyrecipes.common.message.ExceptionMessage; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.security.auth.message.AuthException; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.stereotype.Service; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; +import java.security.SignatureException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; + +@RequiredArgsConstructor +@Service +public class JwtTokenManager { + private final AuthConfig authConfig; + private final ZoneId KST = ZoneId.of("Asia/Seoul"); + + //AccessToken 발급 + public String createAccessToken(Long userId) { + val signatureAlgorithm = SignatureAlgorithm.ES256; + val secretKeyBytes = DatatypeConverter.parseBase64Binary(authConfig.getJwtSecretKey()); + val signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName()); + val exp = ZonedDateTime.now(KST).toLocalDateTime().plusHours(6).atZone(KST).toInstant(); + + return Jwts.builder() + .setSubject(Long.toString(userId)) + .setExpiration(Date.from(exp)) + .signWith(signingKey, signatureAlgorithm) + .compact(); + } + + //RefreshToken 발급 + public String createRefreshToken(Long userId) { + val signatureAlgorithm = SignatureAlgorithm.HS256; + val secretKeyBytes = DatatypeConverter.parseBase64Binary(authConfig.getJwtSecretKey()); + val signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName()); + val exp = ZonedDateTime.now(KST).toLocalDateTime().plusDays(14).atZone(KST).toInstant(); + + return Jwts.builder() + .setSubject(Long.toString(userId)) + .setExpiration(Date.from(exp)) + .signWith(signingKey, signatureAlgorithm) + .compact(); + } + + // JWT 토큰 검증 + public boolean verifyAuthToken(String token) { + try { + getClaimsFromToken(token); + return true; + } catch ( SignatureException | ExpiredJwtException e ) { + return false; + } + } + + // userId 조회 + public String getUserIdFromAuthToken(String token) throws AuthException { + try{ + val claims = getClaimsFromToken(token); + return claims.getSubject(); + } catch ( SignatureException | ExpiredJwtException e) { + throw new AuthException(ExceptionMessage.INVALID_TOKEN.getMessage()); + } + } + + private Claims getClaimsFromToken(String token) throws SignatureException { + return Jwts.parserBuilder() + .setSigningKey(DatatypeConverter.parseBase64Binary(authConfig.getJwtSecretKey())) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index a010ea7..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,9 +0,0 @@ -spring: -profiles: -active: prod - ---- - -spring: -profiles: -active: dev \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..1bf7299 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + profiles: + active: prod + +--- + +spring: + profiles: + active: dev \ No newline at end of file