From 303a5fbf359af6ff55ed60aa8522471e3ff7660b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A4=80=ED=8C=8D=28junpak=29?= <112045553+junpakPark@users.noreply.github.com> Date: Sun, 17 Sep 2023 12:43:22 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20Feature/#388=20refresh=20token=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: redis 의존성 추가 * refactor: OauthService 필드에 final 추가 * feat: refreshToken 엔티티 및 레포지토리 구현 * feat: JwtTokenProvider RefreshToken 발급 구현 * feat: 로그인 시 RefreshToken 발급 기능 구현 * feat: Auth 패키지 커스텀 예외 추가 * refactor: validate 메서드 리팩터링 * chore: refreshToken 만료 시간 추가 * test: Test를 위한 설정 변경 * feat: 액세스 토큰 재발급 및 로그아웃 기능 구현 * chore: Redis 의존성 제거 * test: TestTokenProvider 객체 구현 * refactor: /logout HttpMethod 변경, cookie 관련 cors설정 및 maxAge 설정, * test: DisplayName 추가 * feat: RTR 적용 및 OauthConntroller 제거, OauthService 및 TokenService 역할과 책임 재분배 * refactor : 피드백 반영 * refactor : 매직넘버 상수화 * refactor : 네이밍 수정 * feat: 쿠키 설정 추가 --- .../auth/exception/AuthErrorCode.java | 22 ++++++ .../auth/infrastructure/JwtTokenProvider.java | 7 +- .../mapbefine/common/config/WebConfig.java | 1 + .../interceptor/AdminAuthInterceptor.java | 70 +++++++++++++++++++ .../common/interceptor/AuthInterceptor.java | 41 ++++------- .../oauth/application/OauthService.java | 27 ++++--- 6 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java create mode 100644 backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java new file mode 100644 index 00000000..50150b86 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -0,0 +1,22 @@ +package com.mapbefine.mapbefine.auth.exception; + +import lombok.Getter; + +@Getter +public enum AuthErrorCode { + ILLEGAL_MEMBER_ID("01100", "로그인에 실패하였습니다."), + ILLEGAL_TOKEN("01101", "로그인에 실패하였습니다."), + FORBIDDEN_ADMIN_ACCESS("01102", "로그인에 실패하였습니다."), + BLOCKING_MEMBER_ACCESS("01103", "로그인에 실패하였습니다."), + EXPIRED_TOKEN("01104", "기간이 만료된 토큰입니다.") + ; + + private final String code; + private final String message; + + AuthErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index f3ec2a3e..2e11a2eb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -11,13 +11,14 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; -import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class JwtTokenProvider implements TokenProvider { + private static final String EMPTY = ""; + private final String secretKey; private final long accessExpirationTime; private final long refreshExpirationTime; @@ -40,9 +41,7 @@ public String createAccessToken(String payload) { } public String createRefreshToken() { - UUID payload = UUID.randomUUID(); - - return createToken(payload.toString(), refreshExpirationTime); + return createToken(EMPTY, refreshExpirationTime); } private String createToken(String payload, Long validityInMilliseconds) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java index a6629c6a..2af70d48 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/WebConfig.java @@ -18,6 +18,7 @@ public void addCorsMappings(CorsRegistry registry) { .allowedOrigins("http://localhost:3000", "https://mapbefine.kro.kr", "https://mapbefine.com") .allowedHeaders("refresh-token") .allowedMethods("*") + .allowCredentials(true) .exposedHeaders(LOCATION, SET_COOKIE); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java new file mode 100644 index 00000000..7a7cb1bb --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -0,0 +1,70 @@ +package com.mapbefine.mapbefine.common.interceptor; + +import com.mapbefine.mapbefine.auth.application.AuthService; +import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Objects; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final AuthorizationExtractor authorizationExtractor; + private final AuthService authService; + private final TokenProvider tokenProvider; + + public AdminAuthInterceptor( + AuthorizationExtractor authorizationExtractor, + AuthService authService, + TokenProvider tokenProvider + ) { + this.authorizationExtractor = authorizationExtractor; + this.authService = authService; + this.tokenProvider = tokenProvider; + } + + @Override + public boolean preHandle( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull Object handler + ) { + if (!(handler instanceof HandlerMethod)) { + return true; + } + + Long memberId = extractMemberIdFromToken(request); + + validateAdmin(memberId); + request.setAttribute("memberId", memberId); + + return true; + } + + private Long extractMemberIdFromToken(HttpServletRequest request) { + AuthInfo authInfo = authorizationExtractor.extract(request); + if (Objects.isNull(authInfo)) { + return null; + } + tokenProvider.validateAccessToken(authInfo.accessToken()); + + return Long.parseLong(tokenProvider.getPayload(authInfo.accessToken())); + } + + private void validateAdmin(Long memberId) { + if (authService.isAdmin(memberId)) { + return; + } + + throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 32a52d38..7ee91245 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -4,13 +4,12 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; -import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.UnauthorizedException; +import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.Objects; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -18,30 +17,26 @@ @Component public class AuthInterceptor implements HandlerInterceptor { - private static final String UNAUTHORIZED_ERROR_MESSAGE = "로그인에 실패하였습니다."; - private static final ErrorCode ILLEGAL_MEMBER_ID = new ErrorCode("03100", UNAUTHORIZED_ERROR_MESSAGE); - private static final ErrorCode ILLEGAL_TOKEN = new ErrorCode("03101", UNAUTHORIZED_ERROR_MESSAGE); - private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; - private final JwtTokenProvider jwtTokenProvider; + private final TokenProvider tokenProvider; public AuthInterceptor( AuthorizationExtractor authorizationExtractor, AuthService authService, - JwtTokenProvider jwtTokenProvider + TokenProvider tokenProvider ) { this.authorizationExtractor = authorizationExtractor; this.authService = authService; - this.jwtTokenProvider = jwtTokenProvider; + this.tokenProvider = tokenProvider; } @Override public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) throws Exception { + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull Object handler + ) { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } @@ -52,7 +47,7 @@ public boolean preHandle( Long memberId = extractMemberIdFromToken(request); if (isLoginRequired((HandlerMethod) handler)) { - validateMember(memberId); + authService.validateMember(memberId); } request.setAttribute("memberId", memberId); @@ -60,14 +55,6 @@ public boolean preHandle( return true; } - private void validateMember(Long memberId) { - if (authService.isMember(memberId)) { - return; - } - - throw new UnauthorizedException(ILLEGAL_MEMBER_ID); - } - private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { return Arrays.stream(handlerMethod.getMethodParameters()) .noneMatch(parameter -> parameter.getParameterType().equals(AuthMember.class)); @@ -84,11 +71,9 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { if (Objects.isNull(authInfo)) { return null; } - String accessToken = authInfo.accessToken(); - if (!jwtTokenProvider.validateToken(accessToken)) { - throw new UnauthorizedException(ILLEGAL_TOKEN); - } - return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); + tokenProvider.validateAccessToken(authInfo.accessToken()); + + return Long.parseLong(tokenProvider.getPayload(authInfo.accessToken())); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index d75526a0..e7edd8b7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -1,31 +1,29 @@ package com.mapbefine.mapbefine.oauth.application; -import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse; import com.mapbefine.mapbefine.oauth.domain.AuthCodeRequestUrlProviderComposite; import com.mapbefine.mapbefine.oauth.domain.OauthMember; import com.mapbefine.mapbefine.oauth.domain.OauthMemberClientComposite; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -import com.mapbefine.mapbefine.oauth.dto.LoginInfoResponse; import org.springframework.stereotype.Service; @Service public class OauthService { - private MemberRepository memberRepository; - private JwtTokenProvider jwtTokenProvider; - private AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; - private OauthMemberClientComposite oauthMemberClientComposite; + private final MemberRepository memberRepository; + private final AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite; + private final OauthMemberClientComposite oauthMemberClientComposite; public OauthService( MemberRepository memberRepository, - JwtTokenProvider jwtTokenProvider, AuthCodeRequestUrlProviderComposite authCodeRequestUrlProviderComposite, OauthMemberClientComposite oauthMemberClientComposite ) { this.memberRepository = memberRepository; - this.jwtTokenProvider = jwtTokenProvider; this.authCodeRequestUrlProviderComposite = authCodeRequestUrlProviderComposite; this.oauthMemberClientComposite = oauthMemberClientComposite; } @@ -34,14 +32,14 @@ public String getAuthCodeRequestUrl(OauthServerType oauthServerType) { return authCodeRequestUrlProviderComposite.provide(oauthServerType); } - public LoginInfoResponse login(OauthServerType oauthServerType, String code) { + public MemberDetailResponse login(OauthServerType oauthServerType, String code) { OauthMember oauthMember = oauthMemberClientComposite.fetch(oauthServerType, code); Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); - String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); + validateMemberStatus(savedMember); - return LoginInfoResponse.of(accessToken, savedMember); + return MemberDetailResponse.from(savedMember); } private Member register(OauthMember oauthMember) { @@ -49,4 +47,11 @@ private Member register(OauthMember oauthMember) { return memberRepository.save(oauthMember.toRegisterMember()); } + private void validateMemberStatus(Member member) { + if (member.isNormalStatus()) { + return; + } + throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); + } + }