Skip to content

인증 체크

API 경로 정보에 매핑되는 VO 클래스 작성

Section titled “API 경로 정보에 매핑되는 VO 클래스 작성”
  1. 경로와 메소드로 구성된다
  2. 경로와 메소드를 비교하여 동일 API인지 여부를 판별하는 isSame 메소드를 포함한다.
  3. 변수가 바인딩된 경로에 대해 비교하는 기능이 없어 향후 추가할 필요가 있다.
    ApiPath.kt
    @Schema(description = "API 경로와 메소드")
    data class ApiPath(
    @field:Schema(description = "경로")
    val path: String,
    @field:Schema(description = "HTTP 메소드")
    val method: HttpMethod,
    ) {
    constructor( path: String,
    method: String
    ) : this(
    path,
    HttpMethod.valueOf(method)
    )
    fun isSame(apiPath: ApiPath): Boolean {
    return this.path == apiPath.path && this.method == apiPath.method
    }
    }

ApiPath 클래스에 대한 유닛 테스트 작성

Section titled “ApiPath 클래스에 대한 유닛 테스트 작성”
  1. 데이터 클래스는 데이터 클래스 템플릿에 있는 테스트를 작성한다.
    ApiPathTest.kt
    @DisplayName("Junit5 ApiPath 데이터 클래스 테스트")
    class ApiPathTest {
    @DisplayName("ApiPath 인스턴스 생성 테스트")
    @Test
    fun createInstanceTest() {
    //생성자를 통한 인스턴스 생성 테스트(모든 생성자에 대해서 테스트 작성 필요)
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    assertNotNull(instance1)
    assertEquals("/v1/auth/login", instance1.path)
    assertEquals(HttpMethod.POST, instance1.method)
    //생성자를 통한 인스턴스 생성 테스트(모든 생성자에 대해서 테스트 작성 필요)
    val instance2 = ApiPath("/v1/auth/login", "POST")
    assertNotNull(instance2)
    assertEquals("/v1/auth/login", instance1.path)
    assertEquals(HttpMethod.POST, instance1.method)
    }
    @DisplayName("데이터 비교 테스트")
    @Test
    fun equalsTest() {
    //생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    val instance2 = ApiPath("/v1/auth/login", "POST")
    assertEquals(instance2, instance1)
    val different = ApiPath("/v1/auth/logout", HttpMethod.GET)
    assertNotEquals(different, instance1)
    }
    @DisplayName("데이터 수정 테스트")
    @Test
    fun updateTest() {
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    instance1.path = "/v1/auth/logout"
    assertEquals("/v1/auth/logout", instance1.path)
    instance1.method = HttpMethod.GET
    assertEquals(HttpMethod.GET, instance1.method)
    }
    @DisplayName("toString 메소드 정상 작동 테스트")
    @Test
    fun toStringTest() {
    //생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    assertEquals(
    "ApiPath(path=/v1/auth/login, method=POST)",
    instance1.toString()
    )
    //생성자를 통한 인스턴스 생성(모든 생성자에 대해서 인스턴스 작성 필요)
    val instance2 = ApiPath("/v1/auth/login", "POST")
    assertEquals(
    "ApiPath(path=/v1/auth/login, method=POST)",
    instance2.toString()
    )
    }
    @DisplayName("copy 메소드 정상 작동 테스트")
    @Test
    fun copyTest() {
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    assertEquals(instance1.copy(), instance1)
    val instance2 = instance1.copy(
    path = "/v1/auth/logout",
    method = HttpMethod.GET,
    )
    assertEquals("/v1/auth/logout", instance2.path)
    assertEquals(HttpMethod.GET, instance2.method)
    }
    @DisplayName("hashCode 메소드 정상 작동 테스트")
    @Test
    fun hashCodeTest() {
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    assertEquals(instance1.copy().hashCode(), instance1.hashCode())
    val instance2 = instance1.copy(
    path = "/v1/auth/logout",
    method = HttpMethod.GET,
    )
    assertNotEquals(instance2.hashCode(), instance1.hashCode())
    }
    @DisplayName("isSame 메소드 정상 작동 테스트")
    @Test
    fun isSameTest() {
    val instance1 = ApiPath("/v1/auth/login", HttpMethod.POST)
    assertTrue { instance1.isSame(ApiPath("/v1/auth/login", HttpMethod.POST)) }
    assertFalse { instance1.isSame(ApiPath("/v2/auth/login", HttpMethod.GET)) }
    assertFalse { instance1.isSame(ApiPath("/v1", HttpMethod.POST)) }
    assertFalse { instance1.isSame(ApiPath("/v1/auth", HttpMethod.POST)) }
    assertFalse { instance1.isSame(ApiPath("/v1/auth/login/111", HttpMethod.POST)) }
    assertFalse { instance1.isSame(ApiPath("/v1/auth/login", HttpMethod.DELETE)) }
    }
    }

경로로 인증 대상여부를 확인할 수 있는 헬퍼 클래스 작성

Section titled “경로로 인증 대상여부를 확인할 수 있는 헬퍼 클래스 작성”
  1. 경로와 메소드로 구성된다
    PermitApiPathHelper.kt
    @Component
    class PermitApiPathHelper(private val envHelper: EnvironmentHelper) {
    private val permitPaths: List<ApiPath> = listOf(ApiPath("/v1/auth/login", HttpMethod.POST))
    fun isPermit(apiPath: ApiPath): Boolean {
    //테스트일 때는 로그아웃 API도 허용
    if (envHelper.isTest() && apiPath.path == "/v1/auth/logout") {
    return true
    }
    return permitPaths.any { apiPath.isSame(it) }
    }
    }
  1. 세션 정보를 체크하여 로그인 여부를 확인한다.
    AuthInterceptor.kt
    @Component
    class AuthInterceptor(
    private val permitApiPathHelper: PermitApiPathHelper,
    ) : HandlerInterceptor {
    private val log = logger()
    override fun preHandle(
    request: HttpServletRequest,
    response: HttpServletResponse,
    handler: Any
    ): Boolean {
    val path = request.requestURI
    val method = request.method
    if ("OPTIONS" == method) {
    return true
    }
    if (permitApiPathHelper.isPermit(ApiPath(path, method))) {
    log.debug("Permitted Path: {} {}", path, method)
    return true
    }
    val sessionUser: SessionUser? = sessionUser()
    log.debug("sessionUser: {}", sessionUser)
    if (sessionUser === null) {
    throw BizException(ApiResultCode.NO_SESSION_USER)
    }
    return true
    }
    }
  1. 스프링 부트 설정에 인터셉터 등록
    WebConfig.kt
    @Bean
    fun addCorsMappings(authInterceptor: AuthInterceptor): WebMvcConfigurer {
    return object: WebMvcConfigurer {
    //Interceptor
    override fun addInterceptors(registry: InterceptorRegistry) {
    registry
    .addInterceptor(authInterceptor)
    .order(1)
    .addPathPatterns("/v1/**")
    }
    }
    }
  1. SpringBootTest 환경에서 테스트
    AuthInterceptorTest.kt
    @DisplayName("API 요청시 인증정보 체크 인터셉터 테스트")
    class AuthInterceptorTest : AcceptanceTest() {
    @BeforeEach
    fun setUp() {
    //로그아웃
    Given {
    spec(jsonRequestSpecification)
    } When {
    post("/auth/logout")
    } Then {
    statusCode(HttpStatus.SC_OK)
    } Extract {
    println(body().asString())
    }
    }
    @DisplayName("인증정보가 없는 상태에서 로그인 테스트")
    @Test
    fun loginTest() {
    //로그인
    val param: RequestDto<LoginRequest> = RequestDto(LoginRequest("hong@test.com", "abcd4321", null))
    Given {
    spec(jsonRequestSpecification)
    body(param)
    } When {
    post("/auth/login")
    } Then {
    statusCode(HttpStatus.SC_OK)
    body(
    "isSuccess", equalTo(true),
    "resultCode", equalTo("SUCCESS"),
    "payload", notNullValue(),
    "payload.sessionUser", notNullValue(),
    "payload.sessionUser.userId", equalTo(1000),
    "payload.sessionUser.email", equalTo("hong@test.com"),
    "payload.sessionUser.name", equalTo("홍길동"),
    )
    } Extract {
    println(body().asString())
    }
    }
    @DisplayName("로그인을 제외한 다른 API 호출시 UNAUTHORIZED 오류 발생 테스트")
    @Test
    fun unauthorizedTest() {
    //API 목록 조회
    val param: RequestDto<SelectApiListRequest> = RequestDto(SelectApiListRequest(null, null, null, null, null))
    Given {
    spec(jsonRequestSpecification)
    body(param)
    } When {
    post("/api/list")
    } Then {
    statusCode(HttpStatus.SC_UNAUTHORIZED)
    body(
    "isSuccess", equalTo(false),
    "resultCode", equalTo("NO_SESSION_USER"),
    "payload", nullValue(),
    )
    } Extract {
    println(body().asString())
    }
    }
    }