Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
[BE] refactor: 리프레시 토큰 요청 핸들러 수정 (#520)
Browse files Browse the repository at this point in the history
* refactor: `RefreshTokenAuthInterceptor` 제거

* refactor: 'AuthController'의 ArgumentResolver 제거 및 예외 핸들링

* test: 테스트 케이스 작성

* test: 테스트 케이스 추가

* refactor: 쿼리 개선 및 테스트 케이스 추가

* refactor: `JwtTokenProvider`의 `inValidTokenUsage()` 제거

* chore: 서브모듈 업데이트 반영
  • Loading branch information
HubCreator authored and Eun-chan Cho committed Oct 6, 2023
1 parent ec71ffc commit d637826
Show file tree
Hide file tree
Showing 18 changed files with 331 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.donggle.backend.application.repository;

import org.donggle.backend.application.repository.dto.MemberInfo;
import org.donggle.backend.domain.member.Member;
import org.donggle.backend.domain.oauth.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -10,7 +11,7 @@
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findBySocialIdAndSocialType(final Long socialId, final SocialType socialType);
Optional<MemberInfo> findBySocialIdAndSocialType(final Long socialId, final SocialType socialType);

@Override
@Modifying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void logout(final Long memberId) {
authService.logout(memberId);
}

public TokenResponse reissueAccessTokenAndRefreshToken(final Long memberId) {
return authService.reissueAccessTokenAndRefreshToken(memberId);
public TokenResponse reissueAccessTokenAndRefreshToken(final String refreshToken) {
return authService.reissueAccessTokenAndRefreshToken(refreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
import org.donggle.backend.domain.category.Category;
import org.donggle.backend.domain.member.Member;
import org.donggle.backend.domain.member.MemberCredentials;
import org.donggle.backend.domain.member.MemberName;
import org.donggle.backend.domain.oauth.SocialType;
import org.donggle.backend.exception.authentication.InvalidRefreshTokenException;
import org.donggle.backend.exception.business.DuplicatedMemberException;
import org.donggle.backend.exception.notfound.MemberNotFoundException;
import org.donggle.backend.exception.notfound.RefreshTokenNotFoundException;
import org.donggle.backend.infrastructure.oauth.kakao.dto.response.UserInfo;
import org.donggle.backend.ui.response.TokenResponse;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@Transactional
@RequiredArgsConstructor
Expand All @@ -29,23 +34,27 @@ public class AuthService {
private final MemberCredentialsRepository memberCredentialsRepository;
private final CategoryRepository categoryRepository;


public TokenResponse login(final UserInfo userInfo, final SocialType type) {
final Member loginMember = memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), type)
final Member member = memberRepository.findBySocialIdAndSocialType(userInfo.socialId(), type)
.map(info -> Member.of(info.id(), new MemberName(userInfo.nickname()), info.socialId(), userInfo.socialType()))
.orElseGet(() -> initializeMember(userInfo));
return createTokens(loginMember);
}
final Optional<RefreshToken> refreshTokenOptional = tokenRepository.findByMemberId(member.getId());
final String newAccessToken = jwtTokenProvider.createAccessToken(member.getId());
final String newRefreshToken = jwtTokenProvider.createRefreshToken(member.getId());

public void logout(final Long memberId) {
tokenRepository.deleteByMemberId(memberId);
refreshTokenOptional.ifPresentOrElse(
refreshToken -> refreshToken.update(newRefreshToken),
() -> tokenRepository.save(new RefreshToken(newRefreshToken, member))
);
return new TokenResponse(newAccessToken, newRefreshToken);
}

private Member initializeMember(final UserInfo userInfo) {
Member member = userInfo.toMember();
final Category basicCategory = Category.basic(member);
final MemberCredentials basic = MemberCredentials.basic(member);
try {
member = memberRepository.save(member);
memberRepository.save(member);
categoryRepository.save(basicCategory);
memberCredentialsRepository.save(basic);
} catch (final DuplicateKeyException e) {
Expand All @@ -54,28 +63,37 @@ private Member initializeMember(final UserInfo userInfo) {
return member;
}

public TokenResponse reissueAccessTokenAndRefreshToken(final Long memberId) {
final Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberNotFoundException(memberId));

return createTokens(member);
public void logout(final Long memberId) {
tokenRepository.deleteByMemberId(memberId);
}

private TokenResponse createTokens(final Member loginMember) {
final String accessToken = jwtTokenProvider.createAccessToken(loginMember.getId());
final String refreshToken = jwtTokenProvider.createRefreshToken(loginMember.getId());
public TokenResponse reissueAccessTokenAndRefreshToken(final String refreshToken) {
final Long memberId = jwtTokenProvider.getPayload(refreshToken);
validateExistenceOfMember(memberId);
final RefreshToken findRefreshToken = findRefreshToken(memberId);
validateRefreshTokenValue(refreshToken, findRefreshToken);

final String newAccessToken = jwtTokenProvider.createAccessToken(memberId);
final String newRefreshToken = jwtTokenProvider.createRefreshToken(memberId);
findRefreshToken.update(newRefreshToken);

synchronizeRefreshToken(loginMember, refreshToken);
return new TokenResponse(newAccessToken, newRefreshToken);
}

return new TokenResponse(accessToken, refreshToken);
private void validateExistenceOfMember(final Long memberId) {
if (!memberRepository.existsById(memberId)) {
throw new MemberNotFoundException(null);
}
}

private void synchronizeRefreshToken(final Member member, final String refreshToken) {
tokenRepository.findByMemberId(member.getId())
.ifPresentOrElse(
token -> token.update(refreshToken),
() -> tokenRepository.save(new RefreshToken(refreshToken, member))
);
private void validateRefreshTokenValue(final String refreshToken, final RefreshToken findRefreshToken) {
if (findRefreshToken.isDifferentFrom(refreshToken)) {
throw new InvalidRefreshTokenException();
}
}

private RefreshToken findRefreshToken(final Long memberId) {
return tokenRepository.findByMemberId(memberId)
.orElseThrow(RefreshTokenNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import org.donggle.backend.exception.authentication.ExpiredAccessTokenException;
import org.donggle.backend.exception.authentication.InvalidRefreshTokenException;
import org.donggle.backend.exception.authentication.RefreshTokenSecurityException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -57,21 +60,18 @@ public Long getPayload(final String token) {
.get(MEMBER_ID_KEY, Long.class);
}

public boolean inValidTokenUsage(final String token) {
public Jws<Claims> getClaims(final String token) {
try {
final Jws<Claims> claims = getClaims(token);
return claims.getBody().getExpiration().before(new Date());
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
} catch (final ExpiredJwtException e) {
throw new ExpiredAccessTokenException();
throw new ExpiredAccessTokenException(e);
} catch (final SecurityException e) {
throw new RefreshTokenSecurityException(e);
} catch (final JwtException | IllegalArgumentException e) {
return true;
throw new InvalidRefreshTokenException(e);
}
}

private Jws<Claims> getClaims(final String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@ public class Member extends BaseEntity {
@Enumerated(value = EnumType.STRING)
private SocialType socialType;

private Member(final MemberName memberName, final Long socialId, final SocialType socialType) {
private Member(final Long id, final MemberName memberName, final Long socialId, final SocialType socialType) {
this.id = id;
this.memberName = memberName;
this.socialId = socialId;
this.socialType = socialType;
}

public static Member of(final MemberName memberName, final Long socialId, final SocialType socialType) {
return new Member(memberName, socialId, socialType);
return of(null, memberName, socialId, socialType);
}

public static Member of(final Long id, final MemberName memberName, final Long socialId, final SocialType socialType) {
return new Member(id, memberName, socialId, socialType);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package org.donggle.backend.exception.authentication;

import org.donggle.backend.exception.authentication.UnAuthenticationException;

public class ExpiredAccessTokenException extends UnAuthenticationException {
private static final String MESSAGE = "유효하지 않은 토큰입니다.";
private static final String MESSAGE = "토큰이 만료되었습니다.";
private static final int REFRESH_REQUEST_CODE = 4011;

public ExpiredAccessTokenException() {
Expand All @@ -16,7 +14,7 @@ public ExpiredAccessTokenException(final Throwable cause) {

@Override
public String getHint() {
return "AccessToken이 만료되었습니다. RefreshToken값을 요청하세요.";
return "토큰이 만료되었습니다.";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package org.donggle.backend.exception.authentication;

import org.donggle.backend.exception.authentication.UnAuthenticationException;

public class InvalidAuthorizationHeaderTypeException extends UnAuthenticationException {
private final String authorizationHeader;

public InvalidAuthorizationHeaderTypeException(final String authorizationHeader) {
super(null);
this.authorizationHeader = authorizationHeader;
}

public InvalidAuthorizationHeaderTypeException(final String authorizationHeader, final Throwable cause) {
super(null, cause);
this.authorizationHeader = authorizationHeader;
}

@Override
public String getHint() {
return "Authorization 헤더의 타입이 올바르지 않습니다. 입력한 헤더: " + authorizationHeader;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.donggle.backend.exception.authentication;

import org.donggle.backend.exception.authentication.UnAuthenticationException;

public class InvalidRefreshTokenException extends UnAuthenticationException {
private static final String MESSAGE = "유효하지 않은 토큰입니다.";

public InvalidRefreshTokenException() {
super(null);
super(MESSAGE);
}

public InvalidRefreshTokenException(final Throwable cause) {
super(null, cause);
}

@Override
public String getHint() {
return "유효하지 않은 RefreshToken입니다. 다시 로그인을 진행하세요.";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package org.donggle.backend.exception.authentication;

import org.donggle.backend.exception.authentication.UnAuthenticationException;

public class NoRefreshTokenInCookieException extends UnAuthenticationException {
public NoRefreshTokenInCookieException() {
super(null);
}

public NoRefreshTokenInCookieException(final Throwable cause) {
super(null, cause);
}

@Override
public String getHint() {
return "쿠키에 RefreshToken이 존재하지 않습니다.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.donggle.backend.exception.authentication;

public class RefreshTokenSecurityException extends UnAuthenticationException {
private static final String MESSAGE = "조작된 토큰입니다.";

public RefreshTokenSecurityException(final Throwable e) {
super(MESSAGE, e);
}

@Override
public String getHint() {
return MESSAGE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -65,8 +66,8 @@ public ResponseEntity<Void> logout(@AuthenticationPrincipal final Long memberId)
}

@PostMapping("/token/refresh")
public ResponseEntity<AccessTokenResponse> reissueAccessToken(@AuthenticationPrincipal final Long memberId) {
final TokenResponse response = authFacadeService.reissueAccessTokenAndRefreshToken(memberId);
public ResponseEntity<AccessTokenResponse> reissueAccessToken(@CookieValue final String refreshToken) {
final TokenResponse response = authFacadeService.reissueAccessTokenAndRefreshToken(refreshToken);

final ResponseCookie cookie = createRefreshTokenCookie(response.refreshToken());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.donggle.backend.domain.auth.JwtTokenProvider;
import org.donggle.backend.exception.authentication.ExpiredAccessTokenException;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.servlet.HandlerInterceptor;

Expand All @@ -17,15 +16,8 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp
if (CorsUtils.isPreFlightRequest(request)) {
return true;
}

validateToken(request);
return true;
}

private void validateToken(final HttpServletRequest request) {
final String token = AuthorizationExtractor.extract(request);
if (jwtTokenProvider.inValidTokenUsage(token)) {
throw new ExpiredAccessTokenException();
}
jwtTokenProvider.getPayload(token);
return true;
}
}

This file was deleted.

Loading

0 comments on commit d637826

Please sign in to comment.