From 30b0c83fa628e6d561438823edc8e0d260899c87 Mon Sep 17 00:00:00 2001 From: sojungpp Date: Fri, 8 Dec 2023 23:19:39 +0900 Subject: [PATCH] =?UTF-8?q?#3=20feat:=20UserAccount=20resolver=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++ .../s1350/sooljangmacha/global/Constants.java | 7 +++ .../global/exception/BaseResponseCode.java | 11 +++- .../global/resolver/UserAccount.java | 11 ++++ .../global/resolver/UserAccountResolver.java | 46 ++++++++++++++ .../sooljangmacha/global/utils/JwtUtil.java | 60 +++++++++++++++++++ .../user/repository/UserRepository.java | 3 + 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/s1350/sooljangmacha/global/Constants.java create mode 100644 src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccount.java create mode 100644 src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccountResolver.java create mode 100644 src/main/java/com/s1350/sooljangmacha/global/utils/JwtUtil.java diff --git a/build.gradle b/build.gradle index 3b366c0..348fb65 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,10 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' // swagger implementation("org.springdoc:springdoc-openapi-ui:1.6.11") + // 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' } tasks.named('test') { diff --git a/src/main/java/com/s1350/sooljangmacha/global/Constants.java b/src/main/java/com/s1350/sooljangmacha/global/Constants.java new file mode 100644 index 0000000..daa44c2 --- /dev/null +++ b/src/main/java/com/s1350/sooljangmacha/global/Constants.java @@ -0,0 +1,7 @@ +package com.s1350.sooljangmacha.global; + +public class Constants { + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BEARER_PREFIX = "bearer "; + public static final String CLAIM_NAME = "userId"; +} diff --git a/src/main/java/com/s1350/sooljangmacha/global/exception/BaseResponseCode.java b/src/main/java/com/s1350/sooljangmacha/global/exception/BaseResponseCode.java index efecd47..17a609c 100644 --- a/src/main/java/com/s1350/sooljangmacha/global/exception/BaseResponseCode.java +++ b/src/main/java/com/s1350/sooljangmacha/global/exception/BaseResponseCode.java @@ -10,10 +10,19 @@ public enum BaseResponseCode { SUCCESS("S0001", HttpStatus.OK, "요청에 성공했습니다."), + NULL_TOKEN("T0001", HttpStatus.UNAUTHORIZED, "토큰 값을 입력해주세요."), + INVALID_TOKEN("T0002", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 값입니다."), + UNSUPPORTED_TOKEN("T0003", HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰 값입니다."), + MALFORMED_TOKEN("T0004", HttpStatus.UNAUTHORIZED, "잘못된 구조의 토큰 값입니다."), + EXPIRED_TOKEN("T0005", HttpStatus.FORBIDDEN, "만료된 토큰 값입니다."), + NOT_ACCESS_HEADER("T0006", HttpStatus.INTERNAL_SERVER_ERROR, "헤더에 접근할 수 없습니다."), + REQUEST_VALIDATION("E0001", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), REQUEST_NOT_READABLE("E0002", HttpStatus.BAD_REQUEST, "잘못된 데이터 포맷입니다."), DATABASE_ERROR("E0003", HttpStatus.INTERNAL_SERVER_ERROR, "데이터베이스 관련 에러입니다."), - INTERNAL_SERVER_ERROR("E0004", HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."); + INTERNAL_SERVER_ERROR("E0004", HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러입니다."), + + USER_NOT_FOUND("U0001", HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."); public final String code; public final HttpStatus status; diff --git a/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccount.java b/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccount.java new file mode 100644 index 0000000..0af0131 --- /dev/null +++ b/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccount.java @@ -0,0 +1,11 @@ +package com.s1350.sooljangmacha.global.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserAccount { +} diff --git a/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccountResolver.java b/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccountResolver.java new file mode 100644 index 0000000..26eeeae --- /dev/null +++ b/src/main/java/com/s1350/sooljangmacha/global/resolver/UserAccountResolver.java @@ -0,0 +1,46 @@ +package com.s1350.sooljangmacha.global.resolver; + +import com.s1350.sooljangmacha.global.exception.BaseException; +import com.s1350.sooljangmacha.global.exception.BaseResponseCode; +import com.s1350.sooljangmacha.global.utils.JwtUtil; +import com.s1350.sooljangmacha.user.entity.User; +import com.s1350.sooljangmacha.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.constraints.NotNull; + +import static com.s1350.sooljangmacha.global.Constants.AUTHORIZATION_HEADER; + +@RequiredArgsConstructor +@Component +public class UserAccountResolver implements HandlerMethodArgumentResolver { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserAccount.class) && User.class.equals(parameter.getParameterType()); + } + + @Override + public User resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String header = request.getHeader(AUTHORIZATION_HEADER); + + if (!StringUtils.hasText(header)) throw new BaseException(BaseResponseCode.NULL_TOKEN); + final String token = JwtUtil.replaceBearer(header); + + if (!jwtUtil.validateToken(token)) throw new BaseException(BaseResponseCode.INVALID_TOKEN); + return userRepository.findByIdAndIsEnable(jwtUtil.getJwtContents(token), true).orElseThrow(() -> new BaseException(BaseResponseCode.USER_NOT_FOUND)); + } +} + diff --git a/src/main/java/com/s1350/sooljangmacha/global/utils/JwtUtil.java b/src/main/java/com/s1350/sooljangmacha/global/utils/JwtUtil.java new file mode 100644 index 0000000..7ec9fcf --- /dev/null +++ b/src/main/java/com/s1350/sooljangmacha/global/utils/JwtUtil.java @@ -0,0 +1,60 @@ +package com.s1350.sooljangmacha.global.utils; + +import com.s1350.sooljangmacha.global.exception.BaseException; +import com.s1350.sooljangmacha.global.exception.BaseResponseCode; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; + +import static com.s1350.sooljangmacha.global.Constants.BEARER_PREFIX; +import static com.s1350.sooljangmacha.global.Constants.CLAIM_NAME; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String jwtSecret; + + public static String replaceBearer(String header) { + return header.substring(BEARER_PREFIX.length()); + } + + public boolean validateToken(String token) { + try { + getBody(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException e) { + throw new BaseException(BaseResponseCode.INVALID_TOKEN); + } catch (MalformedJwtException e) { + throw new BaseException(BaseResponseCode.MALFORMED_TOKEN); + } catch (ExpiredJwtException e) { + throw new BaseException(BaseResponseCode.EXPIRED_TOKEN); + } catch (UnsupportedJwtException e) { + throw new BaseException(BaseResponseCode.UNSUPPORTED_TOKEN); + } catch (IllegalArgumentException e) { + throw new BaseException(BaseResponseCode.NULL_TOKEN); + } + } + + private Key getSigningKey() { + final byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + public Long getJwtContents(String token) { + String userId = String.valueOf(getBody(token).get(CLAIM_NAME)); + return Long.parseLong(userId); + } + + private Claims getBody(String token) { + try { + return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/src/main/java/com/s1350/sooljangmacha/user/repository/UserRepository.java b/src/main/java/com/s1350/sooljangmacha/user/repository/UserRepository.java index aaf460a..b6c1924 100644 --- a/src/main/java/com/s1350/sooljangmacha/user/repository/UserRepository.java +++ b/src/main/java/com/s1350/sooljangmacha/user/repository/UserRepository.java @@ -3,5 +3,8 @@ import com.s1350.sooljangmacha.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + Optional findByIdAndIsEnable(Long id, boolean isEnable); }