Skip to content

Commit

Permalink
[feat] : 로그아웃 api 구현 + 토큰 재발급 관리 로직 추가 (#73)
Browse files Browse the repository at this point in the history
* [feat]: 로그 어노테이션 제거, auth 엔티티에 auditing 추가

* [feat]: AuthService 에서 로그아웃 구현과 테스트코드

* [feat]: BlacklistToken 엔티티와 레포지토리 구현

* [feat]: 로그아웃 api 와 테스트코드 구
  • Loading branch information
ParkJuhan94 authored Jan 14, 2024
1 parent 63d66d5 commit 2332c10
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 13 deletions.
14 changes: 13 additions & 1 deletion api/src/main/java/dev/hooon/auth/AuthApiController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.hooon.auth;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -10,9 +11,10 @@
import dev.hooon.auth.application.AuthService;
import dev.hooon.auth.dto.TokenReIssueRequest;
import dev.hooon.auth.dto.request.AuthRequest;

import dev.hooon.auth.dto.response.AuthResponse;
import dev.hooon.auth.jwt.JwtAuthorization;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand All @@ -37,6 +39,16 @@ public ResponseEntity<AuthResponse> login(
return ResponseEntity.ok(authResponse);
}

@PostMapping("/logout")
@Operation(summary = "로그아웃 API", description = "로그아웃을 한다")
@ApiResponse(responseCode = "200", useReturnTypeSchema = true)
public ResponseEntity<HttpStatus> logout(
@Parameter(hidden = true) @JwtAuthorization Long userId
) {
authService.logout(userId);
return ResponseEntity.ok(HttpStatus.OK);
}

@NoAuth
@PostMapping("/token")
@Operation(summary = "토큰 재발급 API", description = "토큰을 재발급한다")
Expand Down
23 changes: 19 additions & 4 deletions api/src/test/java/dev/hooon/auth/AuthApiControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,18 @@ class AuthApiControllerTest extends ApiTestSupport {
private UserService userService;
@Autowired
private AuthService authService;
private AuthRequest authRequest;

@BeforeEach
void setUp() {
UserJoinRequest userJoinRequest = new UserJoinRequest("user@example.com", "password123", "name123");
userService.join(userJoinRequest);
authRequest = new AuthRequest("user@example.com", "password123");
}

@Test
@DisplayName("[로그인 API를 호출하면 토큰이 응답된다]")
void loginTest() throws Exception {
// given
AuthRequest authRequest = new AuthRequest("user@example.com", "password123");

// when
ResultActions actions = mockMvc.perform(
post("/api/auth/login")
Expand All @@ -56,7 +55,6 @@ void loginTest() throws Exception {
@DisplayName("[토큰 재발급 API를 호출하면 새로운 엑세스 토큰이 응답된다]")
void reIssueAccessTokenTest() throws Exception {
// given
AuthRequest authRequest = new AuthRequest("user@example.com", "password123");
AuthResponse authResponse = authService.login(authRequest);
String refreshToken = authResponse.refreshToken();
TokenReIssueRequest tokenReIssueRequest = new TokenReIssueRequest(refreshToken);
Expand All @@ -72,4 +70,21 @@ void reIssueAccessTokenTest() throws Exception {
actions.andExpect(status().isOk())
.andExpect(content().string(not(emptyOrNullString())));
}

@Test
@DisplayName("[로그아웃 API를 호출하면 200 OK 응답이 반환된다]")
void logoutTest() throws Exception {
// given
AuthResponse authResponse = authService.login(authRequest);
String accessToken = authResponse.accessToken();

// when
ResultActions actions = mockMvc.perform(
post("/api/auth/logout")
.header("Authorization", accessToken)
);

// then
actions.andExpect(status().isOk());
}
}
23 changes: 21 additions & 2 deletions core/src/main/java/dev/hooon/auth/application/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package dev.hooon.auth.application;

import static dev.hooon.auth.exception.AuthErrorCode.*;
Expand All @@ -9,10 +8,13 @@
import org.springframework.transaction.annotation.Transactional;

import dev.hooon.auth.domain.entity.Auth;
import dev.hooon.auth.domain.entity.BlacklistToken;
import dev.hooon.auth.domain.repository.AuthRepository;
import dev.hooon.auth.domain.repository.BlacklistRepository;
import dev.hooon.auth.dto.request.AuthRequest;
import dev.hooon.auth.dto.response.AuthResponse;
import dev.hooon.auth.entity.EncryptHelper;
import dev.hooon.auth.exception.AuthException;
import dev.hooon.common.exception.NotFoundException;
import dev.hooon.user.application.UserService;
import lombok.RequiredArgsConstructor;
Expand All @@ -25,6 +27,7 @@ public class AuthService {
private final JwtProvider jwtProvider;
private final AuthRepository authRepository;
private final EncryptHelper encryptHelper;
private final BlacklistRepository blacklistRepository;

private Auth getAuthByRefreshToken(String refreshToken) {
return authRepository.findByRefreshToken(refreshToken)
Expand All @@ -37,7 +40,7 @@ private AuthResponse saveAuth(Long userId) {
Optional<Auth> auth = authRepository.findByUserId(userId);

auth.ifPresentOrElse(
(none) -> authRepository.updateRefreshToken(auth.get().getId(), refreshToken),
existingAuth -> authRepository.updateRefreshToken(existingAuth.getId(), refreshToken),
() -> {
Auth newAuth = Auth.of(userId, refreshToken);
authRepository.save(newAuth);
Expand All @@ -60,7 +63,23 @@ public AuthResponse login(AuthRequest authRequest) {
throw new NotFoundException(FAILED_LOGIN_BY_ANYTHING);
}

@Transactional
public void logout(Long userId) {
authRepository.findByUserId(userId).ifPresentOrElse(
auth ->
blacklistRepository.save(BlacklistToken.of(auth.getRefreshToken())),
() -> {
throw new NotFoundException(NOT_FOUND_USER_ID);
}
);
}

public String createAccessTokenByRefreshToken(String refreshToken) {
boolean isBlacklisted = blacklistRepository.existsByRefreshToken(refreshToken);
if (isBlacklisted) {
throw new AuthException(BLACKLISTED_TOKEN);
}

Auth auth = getAuthByRefreshToken(refreshToken);
Long userId = userService.getUserById(auth.getUserId()).getId();
return jwtProvider.createAccessToken(userId);
Expand Down
7 changes: 2 additions & 5 deletions core/src/main/java/dev/hooon/auth/domain/entity/Auth.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package dev.hooon.auth.domain.entity;

import static dev.hooon.common.exception.CommonValidationError.*;
import static jakarta.persistence.GenerationType.*;

import org.springframework.util.Assert;

import dev.hooon.user.domain.entity.UserRole;
import dev.hooon.common.entity.TimeBaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand All @@ -18,7 +15,7 @@
@Getter
@NoArgsConstructor
@Table(name = "auth_table")
public class Auth {
public class Auth extends TimeBaseEntity {

@Id
@GeneratedValue(strategy = IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.hooon.auth.domain.entity;

import static jakarta.persistence.GenerationType.*;

import dev.hooon.common.entity.TimeBaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "blacklist_token_table")
public class BlacklistToken extends TimeBaseEntity {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "blacklist_token_id")
private Long id;

@Column(name = "blacklist_token_refresh_token", nullable = false, unique = true)
private String refreshToken;

private BlacklistToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public static BlacklistToken of(String refreshToken) {
return new BlacklistToken(refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.hooon.auth.domain.repository;

import dev.hooon.auth.domain.entity.BlacklistToken;

public interface BlacklistRepository {

boolean existsByRefreshToken(String refreshToken);

void save(BlacklistToken blacklistToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public enum AuthErrorCode implements ErrorCode {
NOT_FOUND_USER_ID("해당 유저의 인증 데이터가 존재하지 않습니다.", "A_007"),
TOKEN_EXPIRED("토큰이 만료 시간을 초과했습니다.", "A_008"),
UNSUPPORTED_TOKEN("토큰 유형이 지원되지 않습니다.", "A_010"),
MALFORMED_TOKEN("토큰의 구조가 올바르지 않습니다.", "A_011");
MALFORMED_TOKEN("토큰의 구조가 올바르지 않습니다.", "A_011"),
BLACKLISTED_TOKEN("해당 토큰은 블랙리스트에 등록되어있으므로 유효하지 않습니다.", "A_012");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.hooon.auth.infrastructure;

import org.springframework.data.jpa.repository.JpaRepository;

import dev.hooon.auth.domain.entity.BlacklistToken;

public interface BlacklistJpaRepository extends JpaRepository<BlacklistToken, Long> {
boolean existsByRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.hooon.auth.infrastructure.adaptor;

import org.springframework.stereotype.Repository;

import dev.hooon.auth.domain.entity.BlacklistToken;
import dev.hooon.auth.domain.repository.BlacklistRepository;
import dev.hooon.auth.infrastructure.BlacklistJpaRepository;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class BlacklistRepositoryAdaptor implements BlacklistRepository {

private final BlacklistJpaRepository blacklistJpaRepository;

@Override
public boolean existsByRefreshToken(String refreshToken) {
return blacklistJpaRepository.existsByRefreshToken(refreshToken);
}

@Override
public void save(BlacklistToken blacklistToken) {
blacklistJpaRepository.save(blacklistToken);
}
}
34 changes: 34 additions & 0 deletions core/src/test/java/dev/hooon/auth/application/AuthServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.hooon.auth.application;

import static dev.hooon.auth.exception.AuthErrorCode.*;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

Expand All @@ -13,7 +15,9 @@
import org.mockito.junit.jupiter.MockitoExtension;

import dev.hooon.auth.domain.entity.Auth;
import dev.hooon.auth.domain.entity.BlacklistToken;
import dev.hooon.auth.domain.repository.AuthRepository;
import dev.hooon.auth.domain.repository.BlacklistRepository;
import dev.hooon.auth.dto.request.AuthRequest;
import dev.hooon.auth.dto.response.AuthResponse;
import dev.hooon.auth.entity.EncryptHelper;
Expand All @@ -36,6 +40,8 @@ class AuthServiceTest {
private JwtProvider jwtProvider;
@Mock
private EncryptHelper encryptHelper;
@Mock
private BlacklistRepository blacklistRepository;

@Test
@DisplayName("[로그인 성공 시 토큰을 발급한다]")
Expand Down Expand Up @@ -74,4 +80,32 @@ void loginFailTest() {
// when & then
assertThrows(NotFoundException.class, () -> authService.login(authRequest));
}

@Test
@DisplayName("로그아웃 성공 시 블랙리스트에 토큰을 추가한다")
void logoutSuccessTest() {
// given
Long userId = 1L;
Auth auth = Auth.of(userId, "refresh-token");
when(authRepository.findByUserId(userId)).thenReturn(Optional.of(auth));

// when
authService.logout(userId);

// then
verify(blacklistRepository).save(any(BlacklistToken.class));
}

@Test
@DisplayName("로그아웃 실패 시 NotFoundException을 던진다")
void logoutFailTest() {
// given
Long userId = 1L;
when(authRepository.findByUserId(userId)).thenReturn(Optional.empty());

// when & then
assertThatThrownBy(() -> authService.logout(userId))
.isInstanceOf(NotFoundException.class)
.hasMessageContaining(NOT_FOUND_USER_ID.getMessage());
}
}

0 comments on commit 2332c10

Please sign in to comment.