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

[#35] 로그인한 유저 인증 기능 추가 #38

Merged
merged 11 commits into from
Oct 18, 2024
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,58 @@
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);

if (isNull(token) || !authenticateUser(token, request)) {
response.sendError(SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
return;
}

filterChain.doFilter(request, response);
}

private boolean authenticateUser(String token, HttpServletRequest req) {
UserSessionDTO userSessionDTO = sessionStorage.getUserFromSessionId(token);
if (userSessionDTO == null) {
return false;
}

HttpSession session = req.getSession(true);
session.setAttribute(USER_SESSION, userSessionDTO);
return true;
}

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("세션에 사용자 정보가 있을 때 동네 인증 성공 여부 확인")
Srltas marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
Loading