Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 리프레시 토큰 DB 저장 PR의 리뷰를 반영한다 #754

Merged
merged 4 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ operation::auth-admin-login[snippets='http-request,http-response']

==== 발생 가능 에러

- 40002, 40104, 40105, 50000, 50001
- 40002, 40104, 40105, 40106, 40107, 50000, 50001

operation::auth-issue-access-token[snippets='http-request,http-response']

Expand Down
2 changes: 2 additions & 0 deletions backend/src/docs/asciidoc/error.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
|40103|REGISTER_NOT_COMPLETED|회원가입이 완료되지 않은 회원이 인증 인가가 필요한 요청을 보냄.
|40104|EXPIRED_REFRESH_TOKEN|리프레시 토큰이 만료됨.
|40105|NOT_EXIST_REFRESH_TOKEN|리프레시 토큰이 존재하지 않음.
|40106|REFRESH_TOKEN_NOT_FOUND|리프레시 토큰이 서버에서 저장하지 않는 값임.
|40107|DUPLICATED_REFRESH_TOKEN|리프레시 토큰이 서버에 중복으로 저장되어 있음.
|40300|PERMISSION_DENIED|권한이 없는 요청임.
|40410|MEMBER_NOT_FOUND|회원 정보가 존재하지 않음.
|40420|PRODUCT_NOT_FOUND|제품 정보가 존재하지 않음.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.woowacourse.f12.exception.forbidden.NotAdminException;
import com.woowacourse.f12.exception.notfound.MemberNotFoundException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenExpiredException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenInvalidException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -84,8 +83,7 @@ private Member addOrUpdateMember(final GitHubProfileResponse gitHubProfileRespon
}

public IssuedTokensResponse issueAccessToken(final String refreshTokenValue) {
final RefreshToken refreshToken = refreshTokenRepository.findToken(refreshTokenValue)
.orElseThrow(RefreshTokenInvalidException::new);
final RefreshToken refreshToken = refreshTokenRepository.findToken(refreshTokenValue);
checkExpired(refreshTokenValue, refreshToken);
final Long memberId = refreshToken.getMemberId();
final Role memberRole = memberRepository.findById(memberId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.woowacourse.f12.application.auth.token;

import java.util.Optional;

public interface RefreshTokenRepository {

RefreshToken save(RefreshToken refreshToken);

Optional<RefreshToken> findToken(String savedTokenValue);
RefreshToken findToken(String savedTokenValue);

void delete(String savedTokenValue);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.woowacourse.f12.application.auth.token;

import java.util.Optional;
import com.woowacourse.f12.exception.unauthorized.DuplicatedRefreshTokenSavedException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
Expand All @@ -15,36 +18,39 @@ public class RefreshTokenRepositoryImpl implements RefreshTokenRepository {
.memberId(rs.getLong("member_id"))
.build();

private final JdbcTemplate jdbcTemplate;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public RefreshTokenRepositoryImpl(final JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
public RefreshTokenRepositoryImpl(final NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}

@Override
public RefreshToken save(final RefreshToken refreshToken) {
final String sql = "INSERT INTO refresh_token (token_value, expired_at, member_id) VALUES (?, ?, ?)";
final int affectedRowCounts = jdbcTemplate.update(sql, refreshToken.getRefreshToken(),
refreshToken.getExpiredAt(), refreshToken.getMemberId());
final String sql = "INSERT INTO refresh_token (token_value, expired_at, member_id) VALUES (:refreshToken, :expiredAt, :memberId)";
final BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(refreshToken);
final int affectedRowCounts = namedParameterJdbcTemplate.update(sql, parameterSource);
validateAffectedRowCountIsOne(affectedRowCounts);
return refreshToken;
}

@Override
public Optional<RefreshToken> findToken(final String savedTokenValue) {
public RefreshToken findToken(final String savedTokenValue) {
final String sql = "SELECT token_value, expired_at, member_id FROM refresh_token where token_value = ?";
try {
final RefreshToken result = jdbcTemplate.queryForObject(sql, rowMapper, savedTokenValue);
return Optional.of(result);
return namedParameterJdbcTemplate.getJdbcTemplate()
.queryForObject(sql, rowMapper, savedTokenValue);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
throw new RefreshTokenNotFoundException();
} catch (IncorrectResultSizeDataAccessException e) {
throw new DuplicatedRefreshTokenSavedException();
}
}

@Override
public void delete(final String savedTokenValue) {
final String sql = "DELETE FROM refresh_token where token_value = ?";
final int update = jdbcTemplate.update(sql, savedTokenValue);
final int update = namedParameterJdbcTemplate.getJdbcTemplate()
.update(sql, savedTokenValue);
validateAffectedRowCountIsOne(update);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum ErrorCode {
EXPIRED_REFRESH_TOKEN("40104"),
NOT_EXIST_REFRESH_TOKEN("40105"),
REFRESH_TOKEN_NOT_FOUND("40106"),
DUPLICATED_REFRESH_TOKEN("40107"),

PERMISSION_DENIED("40300"),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.woowacourse.f12.exception.unauthorized;

import com.woowacourse.f12.exception.ErrorCode;

public class DuplicatedRefreshTokenSavedException extends UnauthorizedException {

public DuplicatedRefreshTokenSavedException() {
super(ErrorCode.DUPLICATED_REFRESH_TOKEN, "서버에 조회하려는 리프레시 토큰이 2개 이상입니다.");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.woowacourse.f12.exception.unauthorized;

import com.woowacourse.f12.exception.ErrorCode;

public class RefreshTokenNotFoundException extends UnauthorizedException {

public RefreshTokenNotFoundException() {
super(ErrorCode.REFRESH_TOKEN_NOT_FOUND, "서버에 존재하지 않는 리프레시 토큰입니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import com.woowacourse.f12.exception.internalserver.ExternalServerException;
import com.woowacourse.f12.exception.internalserver.InternalServerException;
import com.woowacourse.f12.exception.notfound.NotFoundException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenInvalidException;
import com.woowacourse.f12.exception.unauthorized.DuplicatedRefreshTokenSavedException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenExpiredException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenNotFoundException;
import com.woowacourse.f12.exception.unauthorized.UnauthorizedException;
import com.woowacourse.f12.presentation.auth.RefreshTokenCookieProvider;
import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -88,8 +90,9 @@ public ResponseEntity<ExceptionResponse> handleValidationException(
.body(ExceptionResponse.from(stringBuilder.toString(), INVALID_REQUEST_BODY_TYPE));
}

@ExceptionHandler(RefreshTokenInvalidException.class)
public ResponseEntity<ExceptionResponse> handleRefreshTokenNotFoundException(final RefreshTokenInvalidException e,
@ExceptionHandler({RefreshTokenNotFoundException.class, DuplicatedRefreshTokenSavedException.class,
RefreshTokenExpiredException.class})
public ResponseEntity<ExceptionResponse> handleRefreshTokenNotFoundException(final UnauthorizedException e,
final HttpServletRequest request,
final HttpServletResponse response) {
refreshTokenCookieProvider.removeCookie(request, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import com.woowacourse.f12.dto.result.LoginResult;
import com.woowacourse.f12.exception.forbidden.NotAdminException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenExpiredException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenInvalidException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenNotFoundException;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -204,7 +204,7 @@ class AuthServiceTest {
RefreshToken newRefreshToken = new RefreshToken(newRefreshTokenValue, memberId, expiredAt);

given(refreshTokenRepository.findToken(refreshTokenValue))
.willReturn(Optional.of(refreshToken));
.willReturn(refreshToken);
given(refreshTokenProvider.createToken(memberId))
.willReturn(newRefreshToken);
given(refreshTokenRepository.save(eq(newRefreshToken)))
Expand Down Expand Up @@ -234,11 +234,11 @@ class AuthServiceTest {
void 저장되어_있지않은_리프레시_토큰으로_액세스_토큰_발급하려할_경우_예외_발생() {
// given
given(refreshTokenRepository.findToken(any()))
.willReturn(Optional.empty());
.willThrow(new RefreshTokenNotFoundException());

// when, then
assertThatThrownBy(() -> authService.issueAccessToken("refreshToken"))
.isExactlyInstanceOf(RefreshTokenInvalidException.class);
.isExactlyInstanceOf(RefreshTokenNotFoundException.class);
}

@Test
Expand All @@ -247,7 +247,7 @@ class AuthServiceTest {
String refreshTokenValue = "refreshToken";
LocalDateTime expiredDate = LocalDateTime.now().minusDays(1);
given(refreshTokenRepository.findToken(any()))
.willReturn(Optional.of(new RefreshToken(refreshTokenValue, memberId, expiredDate)));
.willReturn(new RefreshToken(refreshTokenValue, memberId, expiredDate));
willDoNothing().given(refreshTokenRepository).delete(refreshTokenValue);

// when, then
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
package com.woowacourse.f12.application.auth.token;

import static org.assertj.core.api.Assertions.assertThat;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import com.woowacourse.f12.exception.unauthorized.DuplicatedRefreshTokenSavedException;
import com.woowacourse.f12.exception.unauthorized.RefreshTokenNotFoundException;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.jdbc.Sql;

@JdbcTest
@Sql("classpath:refreshTokenDDL.sql")
class RefreshTokenRepositoryImplTest {

@Autowired
private JdbcTemplate jdbcTemplate;
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

private RefreshTokenRepositoryImpl refreshTokenRepositoryImpl;

@BeforeEach
void setUp() {
refreshTokenRepositoryImpl = new RefreshTokenRepositoryImpl(jdbcTemplate);
refreshTokenRepositoryImpl = new RefreshTokenRepositoryImpl(namedParameterJdbcTemplate);
}

@Test
Expand Down Expand Up @@ -54,8 +65,7 @@ void setUp() {
refreshTokenRepositoryImpl.save(refreshToken);

// when
final RefreshToken actual = refreshTokenRepositoryImpl.findToken(tokenValue)
.orElseThrow();
RefreshToken actual = refreshTokenRepositoryImpl.findToken(tokenValue);

// then
assertThat(actual).usingRecursiveComparison()
Expand All @@ -78,6 +88,26 @@ void setUp() {
refreshTokenRepositoryImpl.delete(tokenValue);

// then
assertThat(refreshTokenRepositoryImpl.findToken(tokenValue)).isEqualTo(Optional.empty());
assertThatThrownBy(() -> refreshTokenRepositoryImpl.findToken(tokenValue))
.isExactlyInstanceOf(RefreshTokenNotFoundException.class);
}

@Test
void 조회하려는_리프레시_토큰이_동일한_값의_리프레시_토큰이_여러개_존재하는_경우_예외가_발생한다() {
// given
String tokenValue = "tokenValue";
JdbcTemplate mockedJdbcTemplate = mock(JdbcTemplate.class);
NamedParameterJdbcTemplate mockedNamedTemplate = new NamedParameterJdbcTemplate(mockedJdbcTemplate);
given(mockedJdbcTemplate.queryForObject(anyString(), any(RowMapper.class), anyString()))
.willThrow(IncorrectResultSizeDataAccessException.class);

refreshTokenRepositoryImpl = new RefreshTokenRepositoryImpl(mockedNamedTemplate);

// when, then
assertAll(
() -> assertThatThrownBy(() -> refreshTokenRepositoryImpl.findToken(tokenValue))
.isExactlyInstanceOf(DuplicatedRefreshTokenSavedException.class),
() -> verify(mockedJdbcTemplate).queryForObject(anyString(), any(RowMapper.class), anyString())
);
}
}
Loading