Skip to content

Commit

Permalink
[#35] 로그인한 유저 인증 기능 추가 (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
Srltas authored Oct 18, 2024
1 parent 5e8ac44 commit 3cf7c41
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package com.srltas.runtogether.adapter.in;

import static com.srltas.runtogether.adapter.in.web.common.SessionAttribute.USER_SESSION;
import static com.srltas.runtogether.adapter.in.web.common.UrlConstants.*;
import static com.srltas.runtogether.adapter.in.web.dto.mapper.NeighborhoodVerificationMapper.*;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttribute;

import com.srltas.runtogether.adapter.in.web.dto.NeighborhoodVerificationRequest;
import com.srltas.runtogether.adapter.out.session.UserSessionDTO;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationCommand;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationResponse;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationUseCase;

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.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand All @@ -32,14 +34,15 @@ public class NeighborhoodVerificationController {
description = "사용자의 현재 위치를 기반으로 인증받고자 하는 동네 범위 안에 있는지 검증하고, 성공 시 해당 동네를 인증 동네로 등록합니다."
)
@ApiResponse(responseCode = "200", description = "동네 인증 성공")
@PostMapping("/neighborhood/verification")
@PostMapping(NEIGHBORHOOD_VERIFICATION)
public ResponseEntity<NeighborhoodVerificationResponse> verifyNeighborhood(
@RequestBody @Valid NeighborhoodVerificationRequest neighborhoodVerificationRequest,
@Parameter(hidden = true) @SessionAttribute(name = "login_user_id", required = false) Long userId) {
@RequestBody @Valid NeighborhoodVerificationRequest neighborhoodVerificationRequest, HttpSession session) {
UserSessionDTO userSession = (UserSessionDTO)session.getAttribute(USER_SESSION);

NeighborhoodVerificationCommand neighborhoodVerificationCommand = toCommand(neighborhoodVerificationRequest);

NeighborhoodVerificationResponse response = neighborhoodVerificationUseCase.verifyAndRegisterNeighborhood(
userId, neighborhoodVerificationCommand);
userSession.userId(), neighborhoodVerificationCommand);

return ResponseEntity.ok(response);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.srltas.runtogether.adapter.in.web.common;

import lombok.experimental.UtilityClass;

@UtilityClass
public class AuthConstants {
public final String AUTHORIZATION = "Authorization";
public final String BEARER_TOKEN_PREFIX = "Bearer ";
public final int BEARER_TOKEN_LENGTH = 7;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.srltas.runtogether.adapter.in.web.common;

import lombok.experimental.UtilityClass;

@UtilityClass
public class SessionAttribute {
public final String USER_SESSION = "USER_SESSION";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.srltas.runtogether.adapter.in.web.common;

import lombok.experimental.UtilityClass;

@UtilityClass
public class UrlConstants {
public final String NEIGHBORHOOD_VERIFICATION = "/neighborhood/verification";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.srltas.runtogether.adapter.in.web.filter;

import static com.srltas.runtogether.adapter.in.web.common.AuthConstants.*;
import static com.srltas.runtogether.adapter.in.web.common.SessionAttribute.USER_SESSION;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static java.util.Objects.isNull;

import java.io.IOException;

import org.springframework.web.filter.OncePerRequestFilter;

import com.srltas.runtogether.adapter.out.session.SessionStorage;
import com.srltas.runtogether.adapter.out.session.UserSessionDTO;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class AuthenticationFilter extends OncePerRequestFilter {

private final SessionStorage sessionStorage;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String authorizationHeader = request.getHeader(AUTHORIZATION);
String token = extractToken(authorizationHeader);
UserSessionDTO userSessionDTO;

if (isNull(token) || isNullUserSessionDTO(userSessionDTO = sessionStorage.getUserFromSessionId(token))) {
response.sendError(SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
return;
}

HttpSession session = request.getSession(true);
session.setAttribute(USER_SESSION, userSessionDTO);

filterChain.doFilter(request, response);
}

private boolean isNullUserSessionDTO(UserSessionDTO userSessionDTO) {
return userSessionDTO == null;
}

private String extractToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith(BEARER_TOKEN_PREFIX)) {
return authorizationHeader.substring(BEARER_TOKEN_LENGTH);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.srltas.runtogether.adapter.out.session;

import org.springframework.stereotype.Repository;

@Repository
public interface SessionStorage {

UserSessionDTO getUserFromSessionId(String sessionId);

void saveUserFromSessionId(String sessionId, UserSessionDTO userSessionDTO);

void removeUserFromSessionId(String sessionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.srltas.runtogether.adapter.out.session;

import org.springframework.stereotype.Repository;

/**
* 로그인 기능을 개발하기 전까지 사용하는 테스트용 SessionStorage 구현체입니다.
*/
@Repository
public class SessionStorageImpl implements SessionStorage {

@Override
public UserSessionDTO getUserFromSessionId(String sessionId) {
return new UserSessionDTO(101L, "user_name");
}

@Override
public void saveUserFromSessionId(String sessionId, UserSessionDTO userSessionDTO) { }

@Override
public void removeUserFromSessionId(String sessionId) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.srltas.runtogether.adapter.out.session;

public record UserSessionDTO(
Long userId,
String userName
) {
}
28 changes: 28 additions & 0 deletions src/main/java/com/srltas/runtogether/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.srltas.runtogether.config;

import static com.srltas.runtogether.adapter.in.web.common.UrlConstants.*;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.srltas.runtogether.adapter.in.web.filter.AuthenticationFilter;
import com.srltas.runtogether.adapter.out.session.SessionStorage;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class WebConfig {

private final SessionStorage sessionStorage;

@Bean
public FilterRegistrationBean<AuthenticationFilter> sessionFilterRegistration() {
FilterRegistrationBean<AuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthenticationFilter(sessionStorage));
registrationBean.addUrlPatterns(NEIGHBORHOOD_VERIFICATION);
registrationBean.setOrder(1);
return registrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.srltas.runtogether.adapter.in;

import static com.srltas.runtogether.adapter.in.web.common.SessionAttribute.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.BDDMockito.any;
import static org.mockito.BDDMockito.*;

import java.time.LocalDateTime;
import java.util.UUID;
import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
Expand All @@ -20,46 +20,53 @@
import org.springframework.http.ResponseEntity;

import com.srltas.runtogether.adapter.in.web.dto.NeighborhoodVerificationRequest;
import com.srltas.runtogether.adapter.out.session.UserSessionDTO;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationCommand;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationResponse;
import com.srltas.runtogether.application.port.in.NeighborhoodVerificationUseCase;

import jakarta.servlet.http.HttpSession;

@ExtendWith(MockitoExtension.class)
class NeighborhoodVerificationControllerTest {

@Mock
private NeighborhoodVerificationUseCase neighborhoodVerificationUseCase;

@Mock
private HttpSession session;

@Mock
private NeighborhoodVerificationResponse neighborhoodVerificationResponse;

@InjectMocks
private NeighborhoodVerificationController neighborhoodVerificationController;

@ParameterizedTest
@MethodSource("provideNeighborhoodVerificationRequests")
@DisplayName("verifyNeighborhood 메서드에 대한 Parameterized 테스트")
void verifyNeighborhood_ShouldReturnOkResponse(NeighborhoodVerificationRequest request, Long userId) {
@MethodSource("provideRequestsForSuccess")
@DisplayName("세션에 사용자 정보가 있을 때 동네 인증 성공 여부 확인")
void testVerifyNeighborhood_Success(NeighborhoodVerificationRequest request, UserSessionDTO userSessionDTO) {
// given
NeighborhoodVerificationCommand command = new NeighborhoodVerificationCommand(request.latitude(),
request.longitude(), request.neighborhoodId());
NeighborhoodVerificationResponse expectedResponse = new NeighborhoodVerificationResponse(
UUID.randomUUID().toString(), true, LocalDateTime.now().toString());

given(neighborhoodVerificationUseCase.verifyAndRegisterNeighborhood(userId, command)).willReturn(
expectedResponse);
when(session.getAttribute(USER_SESSION)).thenReturn(userSessionDTO);
when(neighborhoodVerificationUseCase.verifyAndRegisterNeighborhood(eq(userSessionDTO.userId()),
any(NeighborhoodVerificationCommand.class))).thenReturn(neighborhoodVerificationResponse);

// when
ResponseEntity<NeighborhoodVerificationResponse> response = neighborhoodVerificationController.verifyNeighborhood(
request, userId);
ResponseEntity<NeighborhoodVerificationResponse> response = neighborhoodVerificationController
.verifyNeighborhood(request, session);

// then
verify(neighborhoodVerificationUseCase).verifyAndRegisterNeighborhood(userId, command);
assertThat(response.getStatusCode(), is(HttpStatus.OK));
assertThat(response.getBody(), is(expectedResponse));
assertThat(response.getBody(), is(neighborhoodVerificationResponse));
}

static Stream<Arguments> provideNeighborhoodVerificationRequests() {
static Stream<Arguments> provideRequestsForSuccess() {
return Stream.of(
Arguments.of(new NeighborhoodVerificationRequest(37.579617, 126.977041, 1), 100L),
Arguments.of(new NeighborhoodVerificationRequest(37.556201, 126.972286, 2), 101L),
Arguments.of(new NeighborhoodVerificationRequest(37.497911, 127.027618, 3), 102L));
Arguments.of(new NeighborhoodVerificationRequest(37.579617, 126.977041, 1),
new UserSessionDTO(123L, "user1")),
Arguments.of(new NeighborhoodVerificationRequest(37.556201, 126.972286, 2),
new UserSessionDTO(456L, "user2")),
Arguments.of(new NeighborhoodVerificationRequest(37.497911, 127.027618, 3),
new UserSessionDTO(789L, "user3")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.srltas.runtogether.adapter.in.web.filter;

import static com.srltas.runtogether.adapter.in.web.common.AuthConstants.*;
import static com.srltas.runtogether.adapter.in.web.common.SessionAttribute.*;
import static jakarta.servlet.http.HttpServletResponse.*;
import static org.mockito.Mockito.*;

import java.io.IOException;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.srltas.runtogether.adapter.out.session.SessionStorage;
import com.srltas.runtogether.adapter.out.session.UserSessionDTO;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@ExtendWith(MockitoExtension.class)
class AuthenticationFilterTest {

@Mock
private SessionStorage sessionStorage;

@Mock
private FilterChain filterChain;

@Mock
private HttpServletRequest request;

@Mock
private HttpServletResponse response;

@Mock
private HttpSession session;

@InjectMocks
private AuthenticationFilter authenticationFilter;

private static final String VALID_TOKEN = "valid_token";
private static final String INVALID_TOKEN = "invalid_token";

@Test
@DisplayName("유효한 토큰을 통해 사용자가 인증되는지 확인")
void testValidToken_UserAuthenticated() throws ServletException, IOException {
// given
UserSessionDTO userSessionDTO = new UserSessionDTO(123L, "testUserName");
when(request.getHeader(AUTHORIZATION)).thenReturn(BEARER_TOKEN_PREFIX + VALID_TOKEN);
when(sessionStorage.getUserFromSessionId(VALID_TOKEN)).thenReturn(userSessionDTO);
when(request.getSession(true)).thenReturn(session);

// when
authenticationFilter.doFilterInternal(request, response, filterChain);

// then
verify(session).setAttribute(USER_SESSION, userSessionDTO);
verify(filterChain).doFilter(request, response);
verify(response, never()).sendError(anyInt(), anyString());
}

@Test
@DisplayName("유효하지 않은 토큰일 때 인증되지 않음을 확인")
void testInvalidToken_UserAuthenticated() throws ServletException, IOException {
// given
when(request.getHeader(AUTHORIZATION)).thenReturn(BEARER_TOKEN_PREFIX + INVALID_TOKEN);
when(sessionStorage.getUserFromSessionId(INVALID_TOKEN)).thenReturn(null);

// when
authenticationFilter.doFilterInternal(request, response, filterChain);

verify(response).sendError(SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
verify(filterChain, never()).doFilter(request, response);
}

@Test
@DisplayName("Authorization 헤더가 없는 경우 필터가 인증되지 않음으로 처리하는지 확인")
void testNoAuthorizationHeader() throws ServletException, IOException {
// given
when(request.getHeader(AUTHORIZATION)).thenReturn(null);

// when
authenticationFilter.doFilterInternal(request, response, filterChain);

// then
verify(response).sendError(SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
verify(filterChain, never()).doFilter(request, response);
}
}

0 comments on commit 3cf7c41

Please sign in to comment.