Skip to content

Commit

Permalink
feat: 의존성 연결 및 로그인 뼈대코드 구현 (#6)
Browse files Browse the repository at this point in the history
* refactor: yml 주입방식 변경 (테스트 용이와 간편함을 위해) 및 불필요한 속성 Config 이전

* refactor: YamlPropertySourceFactory 위치 변경

* refactor: "/api" 프리픽스 적용 및 의존성 불필요한 주석 제거

* refactor: 패키지명 변경

* feat: 간단한 로그인 뼈대 구현 및 테스트 작성

* chore: TestFixtures 모듈 사용을 위한 종속성 추가

* feat: 회원가입 뼈대 코드 및 테스트 작성

* refactor: 패키지 이동

* refactor: CustomException을 Enumd으로 관리해서 중복 코드 줄이도록 변경
  • Loading branch information
sosow0212 authored Aug 1, 2024
1 parent edf7b52 commit a832bc8
Show file tree
Hide file tree
Showing 70 changed files with 1,665 additions and 64 deletions.
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id "org.asciidoctor.jvm.convert" version "4.0.2"
id 'java-test-fixtures'
}

bootJar.enabled = false
Expand All @@ -21,6 +22,7 @@ subprojects {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'org.asciidoctor.jvm.convert'
apply plugin: 'java-test-fixtures' // fixtures-module 사용

configurations {
compileOnly {
Expand Down
4 changes: 4 additions & 0 deletions backend/pcloud-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ dependencies {
implementation project(":pcloud-domain")
implementation project(":pcloud-infrastructure")

// testFixtures import
testImplementation(testFixtures(project(":pcloud-domain")))
testImplementation(testFixtures(project(":pcloud-common")))

// spring web
implementation 'org.springframework.boot:spring-boot-starter-web'

Expand Down
10 changes: 10 additions & 0 deletions backend/pcloud-api/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]
= e-market
:doctype: book
:toc: left
:source-highlighter: highlightjs
:sectlinks:

include::member.adoc[]
30 changes: 30 additions & 0 deletions backend/pcloud-api/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
= Auth API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== 회원가입을 진행한다 (POST /api/signup)

=== Request

include::{snippets}/member-controller-test/do_signup/request-fields.adoc[]
include::{snippets}/member-controller-test/do_signup/http-request.adoc[]

=== Response

include::{snippets}/member-controller-test/do_signup/response-fields.adoc[]
include::{snippets}/member-controller-test/do_signup/http-response.adoc[]

== 로그인을 진행한다 (POST /api/login)

=== Request

include::{snippets}/member-controller-test/do_login/request-fields.adoc[]
include::{snippets}/member-controller-test/do_login/http-request.adoc[]

=== Response

include::{snippets}/member-controller-test/do_login/response-fields.adoc[]
include::{snippets}/member-controller-test/do_login/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.api.global.config;

import com.api.global.config.interceptor.auth.LoginValidCheckerInterceptor;
import com.api.global.config.interceptor.auth.ParseMemberIdFromTokenInterceptor;
import com.api.global.config.interceptor.auth.PathMatcherInterceptor;
import com.api.global.config.resolver.AuthArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

import static com.api.global.config.interceptor.auth.support.HttpMethod.DELETE;
import static com.api.global.config.interceptor.auth.support.HttpMethod.GET;
import static com.api.global.config.interceptor.auth.support.HttpMethod.OPTIONS;
import static com.api.global.config.interceptor.auth.support.HttpMethod.PATCH;
import static com.api.global.config.interceptor.auth.support.HttpMethod.POST;

@RequiredArgsConstructor
@Configuration
public class AuthConfig implements WebMvcConfigurer {

private final AuthArgumentResolver authArgumentResolver;
private final ParseMemberIdFromTokenInterceptor parseMemberIdFromTokenInterceptor;
private final LoginValidCheckerInterceptor loginValidCheckerInterceptor;

@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(parseMemberIdFromTokenInterceptor());
registry.addInterceptor(loginValidCheckerInterceptor());
}

private HandlerInterceptor parseMemberIdFromTokenInterceptor() {
return new PathMatcherInterceptor(parseMemberIdFromTokenInterceptor)
.excludePathPattern("/**", OPTIONS);
}

private HandlerInterceptor loginValidCheckerInterceptor() {
return new PathMatcherInterceptor(loginValidCheckerInterceptor)
.excludePathPattern("/**", OPTIONS)
.addPathPatterns("/members/test", GET, POST, PATCH, DELETE);
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.api.config;
package com.api.global.config;

import com.api.config.filter.CorsCustomFilter;
import com.api.global.config.filter.CorsCustomFilter;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.api.config.filter;
package com.api.global.config.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.api.global.config.interceptor.auth;

import com.api.global.config.interceptor.auth.support.AuthenticationContext;
import com.api.global.config.interceptor.auth.support.AuthenticationExtractor;
import com.common.auth.TokenProvider;
import com.common.exception.AuthException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import static com.common.exception.AuthExceptionType.SIGNATURE_INVALID_EXCEPTION;

@RequiredArgsConstructor
@Component
public class LoginValidCheckerInterceptor implements HandlerInterceptor {

private final TokenProvider tokenProvider;
private final AuthenticationContext authenticationContext;

@Override
public boolean preHandle(final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) throws Exception {
String token = AuthenticationExtractor.extract(request)
.orElseThrow(() -> new AuthException(SIGNATURE_INVALID_EXCEPTION));

Long memberId = tokenProvider.extract(token);
authenticationContext.setAuthentication(memberId);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.api.global.config.interceptor.auth;

import com.api.global.config.interceptor.auth.support.AuthenticationContext;
import com.api.global.config.interceptor.auth.support.AuthenticationExtractor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@RequiredArgsConstructor
@Component
public class ParseMemberIdFromTokenInterceptor implements HandlerInterceptor {

private final LoginValidCheckerInterceptor loginValidCheckerInterceptor;
private final AuthenticationContext authenticationContext;

@Override
public boolean preHandle(final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) throws Exception {
if (AuthenticationExtractor.extract(request).isEmpty()) {
authenticationContext.setAnonymous();
return true;
}

return loginValidCheckerInterceptor.preHandle(request, response, handler);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.api.global.config.interceptor.auth;

import com.api.global.config.interceptor.auth.support.HttpMethod;
import com.api.global.config.interceptor.auth.support.PathContainer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class PathMatcherInterceptor implements HandlerInterceptor {

private final HandlerInterceptor handlerInterceptor;
private final PathContainer pathContainer;

public PathMatcherInterceptor(final HandlerInterceptor handlerInterceptor) {
this.handlerInterceptor = handlerInterceptor;
this.pathContainer = new PathContainer();
}

@Override
public boolean preHandle(final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) throws Exception {
if (pathContainer.isNotIncludedPath(request.getServletPath(), request.getMethod())) {
return true;
}
return handlerInterceptor.preHandle(request, response, handler);
}

public PathMatcherInterceptor addPathPatterns(final String pathPattern, final HttpMethod... httpMethod) {
pathContainer.addIncludePatterns(pathPattern, httpMethod);
return this;
}

public PathMatcherInterceptor excludePathPattern(final String pathPattern, final HttpMethod... pathMethod) {
pathContainer.addExcludePatterns(pathPattern, pathMethod);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.api.global.config.interceptor.auth.support;

import com.common.exception.AuthException;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

import java.util.Objects;

import static com.common.exception.AuthExceptionType.LOGIN_INVALID_EXCEPTION;

@RequestScope
@Component
public class AuthenticationContext {

private static final Long ANONYMOUS_MEMBER = -1L;

private Long memberId;

public void setAuthentication(final Long memberId) {
this.memberId = memberId;
}

public Long getPrincipal() {
if (Objects.isNull(this.memberId)) {
throw new AuthException(LOGIN_INVALID_EXCEPTION);
}

return memberId;
}

public void setAnonymous() {
this.memberId = ANONYMOUS_MEMBER;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.api.global.config.interceptor.auth.support;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;

import java.util.Optional;

public class AuthenticationExtractor {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER = "Bearer";
private static final String HEADER_SPLIT_DELIMITER = " ";
private static final int TOKEN_TYPE_INDEX = 0;
private static final int TOKEN_VALUE_INDEX = 1;
private static final int VALID_HEADER_SPLIT_LENGTH = 2;

public static Optional<String> extract(final HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION_HEADER);

if (!StringUtils.hasText(header)) {
return Optional.empty();
}

return extractFromHeader(header.split(HEADER_SPLIT_DELIMITER));
}

public static Optional<String> extractFromHeader(final String[] headerParts) {
if (headerParts.length == VALID_HEADER_SPLIT_LENGTH &&
headerParts[TOKEN_TYPE_INDEX].equals(BEARER)) {
return Optional.ofNullable(headerParts[TOKEN_VALUE_INDEX]);
}

return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.api.global.config.interceptor.auth.support;

public enum HttpMethod {

GET,
POST,
PUT,
PATCH,
DELETE,
OPTIONS,
HEAD,
TRACE,
CONNECT,
ANY;

public boolean matches(final String pathMethod) {
return this == ANY ||
this.name().equalsIgnoreCase(pathMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.api.global.config.interceptor.auth.support;

import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import java.util.ArrayList;
import java.util.List;

public class PathContainer {

private final PathMatcher pathMatcher;
private final List<PathRequest> includePatterns;
private final List<PathRequest> excludePatterns;

public PathContainer() {
this.pathMatcher = new AntPathMatcher();
this.includePatterns = new ArrayList<>();
this.excludePatterns = new ArrayList<>();
}

public boolean isNotIncludedPath(final String targetPath, final String pathMethod) {
boolean isExcludePattern = excludePatterns.stream()
.anyMatch(pathPattern -> pathPattern.matches(pathMatcher, targetPath, pathMethod));

boolean isNotIncludePattern = includePatterns.stream()
.noneMatch(pathPattern -> pathPattern.matches(pathMatcher, targetPath, pathMethod));

return isExcludePattern || isNotIncludePattern;
}

public void addIncludePatterns(final String path, final HttpMethod... method) {
for (HttpMethod httpMethod : method) {
includePatterns.add(new PathRequest(path, httpMethod));
}
}

public void addExcludePatterns(final String path, final HttpMethod... method) {
for (HttpMethod httpMethod : method) {
excludePatterns.add(new PathRequest(path, httpMethod));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.api.global.config.interceptor.auth.support;

import org.springframework.util.PathMatcher;

public class PathRequest {

private final String path;
private final HttpMethod httpMethod;

public PathRequest(final String path, final HttpMethod httpMethod) {
this.path = path;
this.httpMethod = httpMethod;
}

public boolean matches(final PathMatcher pathMatcher,
final String targetPath,
final String pathMethod) {
return pathMatcher.match(path, targetPath) &&
httpMethod.matches(pathMethod);
}
}
Loading

0 comments on commit a832bc8

Please sign in to comment.