Skip to content

Commit

Permalink
Merge branch 'main' into release
Browse files Browse the repository at this point in the history
# Conflicts:
#	.github/workflows/manual-prod-deploy.yaml
#	Dockerfile
#	application/src/main/resources/appenders/console-appender.xml
#	application/src/main/resources/application.yaml
#	infrastructure/src/main/java/org/depromeet/spot/infrastructure/aws/config/ObjectStorageConfig.java
#	infrastructure/src/main/java/org/depromeet/spot/infrastructure/aws/objectstorage/PresignedUrlGenerator.java
#	infrastructure/src/main/resources/application-ncp.yaml
  • Loading branch information
wjdwnsdnjs13 committed Aug 15, 2024
2 parents 11142dc + 716e787 commit 60bb9b6
Show file tree
Hide file tree
Showing 29 changed files with 321 additions and 107 deletions.
16 changes: 10 additions & 6 deletions .github/workflows/dev-build-and-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,18 @@ jobs:
- name: Deploy to Dev NCP Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEV_NCP_SERVER_HOST }}
username: ${{ secrets.DEV_NCP_SERVER_USERNAME }}
password: ${{ secrets.DEV_NCP_SERVER_PASSWORD }}
port: ${{ secrets.DEV_NCP_SERVER_PORT }}
host: ${{ secrets.DEV_SERVER_HOST }}
username: ${{ secrets.DEV_SERVER_USERNAME }}
password: ${{ secrets.DEV_SERVER_PASSWORD }}
port: ${{ secrets.DEV_SERVER_PORT }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/spot-server:dev-${{ github.sha }}
docker stop spot-server-dev || true
docker rm spot-server-dev || true
docker run -d --name spot-server-dev \
-p 8080:8080 \
-p 9292:9292 \
-p 3100:3100 \
-e SPRING_PROFILES_ACTIVE=dev \
-e SPRING_DATASOURCE_URL=${{ secrets.DEV_DB_URL }} \
-e SPRING_DATASOURCE_USERNAME=${{ secrets.DEV_DB_USERNAME }} \
Expand All @@ -117,12 +119,14 @@ jobs:
-e OAUTH_KAUTHTOKENURLHOST=${{ secrets.KAUTH_TOKEN_URL_HOST }} \
-e OAUTH_KAUTHUSERURLHOST=${{ secrets.KAUTH_USER_URL_HOST }} \
-e SPRING_JPA_HIBERNATE_DDL_AUTO=validate \
-e NCP_OBJECT_STORAGE_ACCESS_KEY=${{ secrets.NCP_OBJECT_STORAGE_ACCESS_KEY }} \
-e NCP_OBJECT_STORAGE_SECRET_KEY=${{ secrets.NCP_OBJECT_STORAGE_SECRET_KEY }} \
-e AWS_S3_ACCESS_KEY=${{ secrets.AWS_S3_ACCESS_KEY }} \
-e AWS_S3_SECRET_KEY=${{ secrets.AWS_S3_SECRET_KEY }} \
-e AWS_S3_BUCKET_NAME=${{ secrets.DEV_AWS_S3_BUCKET_NAME }} \
-e TZ=Asia/Seoul \
-e SENTRY_DSN=${{ secrets.SENTRY_DSN }} \
-e SENTRY_ENABLE_TRACING=true \
-e SENTRY_ENVIRONMENT=prod \
-e LOKI_URL=${{ secrets.LOKI_SERVER_URL }} \
${{ secrets.DOCKERHUB_USERNAME }}/spot-server:dev-${{ github.sha }}
docker system prune -af
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/manual-prod-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to deploy (v0.33.8)'
description: 'Tag to deploy (v1.0.0)'
required: true

jobs:
Expand Down Expand Up @@ -45,6 +45,8 @@ jobs:
docker rm spot-server-prod || true
docker run -d --name spot-server-prod \
-p 8080:8080 \
-p 9292:9292 \
-p 3100:3100 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL=${{ secrets.PROD_DB_URL }} \
-e SPRING_DATASOURCE_USERNAME=${{ secrets.PROD_DB_USERNAME }} \
Expand All @@ -54,11 +56,13 @@ jobs:
-e OAUTH_KAUTHTOKENURLHOST=${{ secrets.KAUTH_TOKEN_URL_HOST }} \
-e OAUTH_KAUTHUSERURLHOST=${{ secrets.KAUTH_USER_URL_HOST }} \
-e SPRING_JPA_HIBERNATE_DDL_AUTO=validate \
-e NCP_OBJECT_STORAGE_ACCESS_KEY=${{ secrets.AWS_S3_ACCESS_KEY }} \
-e NCP_OBJECT_STORAGE_SECRET_KEY=${{ secrets.AWS_S3_SECRET_KEY }} \
-e AWS_S3_ACCESS_KEY=${{ secrets.AWS_S3_ACCESS_KEY }} \
-e AWS_S3_SECRET_KEY=${{ secrets.AWS_S3_SECRET_KEY }} \
-e AWS_S3_BUCKET_NAME=${{ secrets.PROD_AWS_S3_BUCKET_NAME }} \
-e TZ=Asia/Seoul \
-e SENTRY_DSN=${{ secrets.SENTRY_DSN }} \
-e SENTRY_ENABLE_TRACING=true \
-e SENTRY_ENVIRONMENT=prod \
-e LOKI_URL=${{ secrets.LOKI_SERVER_URL }} \
${{ secrets.DOCKERHUB_USERNAME }}/spot-server:prod-${{ github.event.inputs.tag }}
docker system prune -af
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ gradle-app.setting
.env

*.application-jwt.yml
*.application-monitoring.yml
application-jwt.yml
application-kakao.yml
application-sentry.yml
application-aws.yaml
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ FROM gradle:7.4-jdk17 AS build
WORKDIR /app
COPY . .
RUN ./gradlew build -x test

# 실행 스테이지
FROM openjdk:17-jdk-slim
WORKDIR /app
Expand All @@ -13,7 +12,7 @@ EXPOSE 8080

# JVM 튜닝 옵션 추가
ENTRYPOINT ["java", \
"-Xms256m", \
"-Xms512m", \
"-Xmx512m", \
"-Xminf0.4", \
"-Xmaxf0.7", \
Expand Down
5 changes: 4 additions & 1 deletion application/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ bin/
### Mac OS ###
.DS_Store

*.application-jwt.yml
*.application-jwt.yml

### loki ###
**/application-monitoring.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.depromeet.spot.application.common.config;

import org.depromeet.spot.application.common.exception.ExceptionHandlerFilter;
import org.depromeet.spot.application.common.jwt.JwtAuthenticationFilter;
import org.depromeet.spot.application.common.jwt.JwtTokenUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -17,7 +17,9 @@
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtTokenUtil jwtTokenUtil;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

private final ExceptionHandlerFilter exceptionHandlerFilter;

private static final String[] AUTH_WHITELIST = {
"/api/**",
Expand Down Expand Up @@ -50,8 +52,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authenticated())
// UsernamePasswordAuthenticationFilter 필터 전에 jwt 필터가 먼저 동작하도록함.
.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenUtil),
UsernamePasswordAuthenticationFilter.class);
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter, JwtAuthenticationFilter.class);
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.depromeet.spot.application.common.exception;

import lombok.Getter;

@Getter
public class CustomJwtException extends RuntimeException {
private final JwtErrorCode jwtErrorCode;

public CustomJwtException(JwtErrorCode jwtErrorCode) {
super(jwtErrorCode.getMessage());
this.jwtErrorCode = jwtErrorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ public record ErrorResponse(String code, String message) {
public static ErrorResponse from(BusinessException e) {
return new ErrorResponse(e.getCode(), e.getMessage());
}

public static ErrorResponse from(CustomJwtException e) {
return new ErrorResponse(e.getJwtErrorCode().getCode(), e.getJwtErrorCode().getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.depromeet.spot.application.common.exception;

import java.io.IOException;

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

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {

private final String UTF_8 = "UTF-8";

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (CustomJwtException e) {
// logging 안 하면 콘솔에 로그 출력 안 됨.
logger.error("CustomJwtException : {}", e);
setErrorResponse(response, e.getJwtErrorCode());
}
}

private void setErrorResponse(HttpServletResponse response, JwtErrorCode jwtErrorCode)
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(jwtErrorCode.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(UTF_8);
ErrorResponse errorResponse =
new ErrorResponse(jwtErrorCode.getCode(), jwtErrorCode.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}

@Getter
@AllArgsConstructor
public static class ErrorResponse {
private final String code;
private final String message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.depromeet.spot.application.common.exception;

import org.depromeet.spot.common.exception.ErrorCode;
import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum JwtErrorCode implements ErrorCode {
NONEXISTENT_TOKEN(HttpStatus.UNAUTHORIZED, "JWT001", "해당 요청은 Jwt 토큰이 필요합니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "JWT002", "잘못된 토큰입니다."),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "JWT003", "만료된 토큰입니다."),
INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "JWT004", "사용자가 권한이 없습니다.");

private final HttpStatus status;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package org.depromeet.spot.application.common.exception;

import java.util.HashMap;
import java.util.Map;

import org.depromeet.spot.common.exception.BusinessException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

Expand All @@ -24,4 +28,29 @@ protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessExceptio

return ResponseEntity.status(httpStatus).body(response);
}

@ExceptionHandler(CustomJwtException.class)
protected ResponseEntity<ErrorResponse> handleCustomJwtException(CustomJwtException e) {
var code = e.getJwtErrorCode().getCode();
var message = e.getJwtErrorCode().getMessage();
var httpStatus = e.getJwtErrorCode().getStatus();

log.error(EXCEPTION_LOG_TEMPLATE, code, message, e);
var response = ErrorResponse.from(e);

return ResponseEntity.status(httpStatus).body(response);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<Map<String, String>> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult()
.getFieldErrors()
.forEach(
error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return ResponseEntity.status(e.getStatusCode()).body(errors);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

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

import org.depromeet.spot.application.common.exception.CustomJwtException;
import org.depromeet.spot.application.common.exception.JwtErrorCode;
import org.depromeet.spot.domain.member.enums.MemberRole;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.server.ResponseStatusException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -34,38 +37,67 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
"/api-docs",
"/swagger-ui.html",
"/favicon.ico",
"/api/v1/members",
"/actuator",
"/api/v1/levels/info",
"/api/v1/baseball-teams",
"/kakao",
"/api/v1/jwts",
};

private static final Map<String, Set<String>> AUTH_METHOD_WHITELIST =
Map.of(
"/api/v1/members",
Set.of("GET", "POST"),
"/api/v1/members/delete",
Set.of("DELETE"),
"/api/v1/baseball-teams",
Set.of("GET"));

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String header = request.getHeader(HttpHeaders.AUTHORIZATION);
final String requestURI = request.getRequestURI();
if (Arrays.stream(AUTH_WHITELIST).anyMatch(requestURI::startsWith)) {
final String requestMethod = request.getMethod();

if (checkMethodWhitelist(requestURI, requestMethod)) {
filterChain.doFilter(request, response);
return;
}

// header가 null이거나 빈 문자열이면 안됨.
if (header != null && !header.equalsIgnoreCase("")) {
if (header.startsWith(JwtTokenEnums.BEARER.getValue())) {
String accessToken = header.split(" ")[1];
if (jwtTokenUtil.isValidateToken(accessToken)) {
Long memberId = jwtTokenUtil.getIdFromJWT(accessToken);
MemberRole role = MemberRole.valueOf(jwtTokenUtil.getRoleFromJWT(accessToken));
JwtToken jwtToken = new JwtToken(memberId, role);
SecurityContextHolder.getContext().setAuthentication(jwtToken);
filterChain.doFilter(request, response);
return;
}
if (header == null || header.isEmpty()) {
throw new CustomJwtException(JwtErrorCode.NONEXISTENT_TOKEN);
}

if (header.startsWith(JwtTokenEnums.BEARER.getValue())) {
String accessToken = header.split(" ")[1];
if (jwtTokenUtil.isValidateToken(accessToken)) {
Long memberId = jwtTokenUtil.getIdFromJWT(accessToken);
MemberRole role = MemberRole.valueOf(jwtTokenUtil.getRoleFromJWT(accessToken));
JwtToken jwtToken = new JwtToken(memberId, role);
SecurityContextHolder.getContext().setAuthentication(jwtToken);
filterChain.doFilter(request, response);
}
// 토큰 검증 실패 -> Exception
} else throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
// 토큰 검증 실패 -> Exception
else throw new CustomJwtException(JwtErrorCode.INVALID_TOKEN);
}

private boolean checkMethodWhitelist(String requestURI, String requestMethod) {
if (Arrays.stream(AUTH_WHITELIST).anyMatch(requestURI::startsWith)) {
return true;
}

Optional<String> matchUrl =
AUTH_METHOD_WHITELIST.keySet().stream().filter(requestURI::startsWith).findFirst();
if (matchUrl.isPresent()
&& AUTH_METHOD_WHITELIST
.getOrDefault(matchUrl.get(), Set.of())
.contains(requestMethod)) {
return true;
}
return false;
}
}
Loading

0 comments on commit 60bb9b6

Please sign in to comment.