diff --git a/.gitignore b/.gitignore index a1692f0..b52f2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -38,10 +38,12 @@ out/ ### custom ### -application-aws.yml -application-credentials.yml +#application-aws.yml +#application-credentials.yml Secret.java application-prod.yml application*.tar application-*.gpg logs/ +application-infra.yml +application-dev.yml diff --git a/build.gradle b/build.gradle index 748208f..fd1c934 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ plugins { - id 'org.springframework.boot' version '2.5.6' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' - id 'java' - // querydsl - id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" + id 'org.springframework.boot' version '2.5.6' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' + // querydsl + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'com.sikhye' @@ -11,86 +11,89 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - // mysql DB - runtimeOnly ('mysql:mysql-connector-java') //mysql8 + // mysql DB + runtimeOnly('mysql:mysql-connector-java') //mysql8 - // spring batch - implementation 'org.springframework.boot:spring-boot-starter-batch' - testImplementation 'org.springframework.batch:spring-batch-test' + // spring batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + testImplementation 'org.springframework.batch:spring-batch-test' - // spring jpa - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // spring jpa + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - // spring template - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' + // spring template + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' - // Bean Validation - implementation 'org.springframework.boot:spring-boot-starter-validation' + // Bean Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' - // Lombok - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' - // spring boot starter - testImplementation 'org.springframework.boot:spring-boot-starter-test' + // spring boot starter + testImplementation 'org.springframework.boot:spring-boot-starter-test' - // Security, Authentication(JWT Token) - implementation('org.springframework.boot:spring-boot-starter-security:2.5.4') - implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.2' - implementation('org.springframework.boot:spring-boot-starter') + // Security, Authentication(JWT Token) + implementation('org.springframework.boot:spring-boot-starter-security:2.5.4') + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.2' + implementation('org.springframework.boot:spring-boot-starter') - // Spring Security for OAuth2) - implementation('org.springframework.boot:spring-boot-starter-oauth2-client') - implementation('org.springframework.boot:spring-boot-starter-mustache') + // Spring Security for OAuth2) + implementation('org.springframework.boot:spring-boot-starter-oauth2-client') + implementation('org.springframework.boot:spring-boot-starter-mustache') - // related to test - implementation('org.springframework.boot:spring-boot-devtools') + // related to test + implementation('org.springframework.boot:spring-boot-devtools') - // for Crawling - implementation('org.jsoup:jsoup:1.14.1') + // for Crawling + implementation('org.jsoup:jsoup:1.14.1') - // p6spy - implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8' + // p6spy + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8' - // querydsl - implementation 'com.querydsl:querydsl-jpa' + // querydsl + implementation 'com.querydsl:querydsl-jpa' - // AWS S3 - implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000') - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.111' + // AWS S3 + implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.1000') + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.111' - // AWS cloud starter aws - implementation('org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE') + // AWS cloud starter aws + implementation('org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE') - // index.html로 forwarding 하기 위함 - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // index.html로 forwarding 하기 위함 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - // @ConfigureProperties를 사용하기 위해 추가 - // configure 파일을 읽어들이기 위함 - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + // @ConfigureProperties를 사용하기 위해 추가 + // configure 파일을 읽어들이기 위함 + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" - // 이메일 인증 - implementation 'org.springframework.boot:spring-boot-starter-mail' + // 이메일 인증 + implementation 'org.springframework.boot:spring-boot-starter-mail' - // spring redis - implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // spring redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // 로그 처리 시 조건 추가 + implementation 'org.codehaus.janino:janino:3.0.12' } test { - useJUnitPlatform() + useJUnitPlatform() } // ========================================== @@ -100,21 +103,21 @@ test { def querydslDir = "$buildDir/generated/querydsl" querydsl { - jpa = true - querydslSourcesDir = querydslDir + jpa = true + querydslSourcesDir = querydslDir } sourceSets { - main.java.srcDir querydslDir + main.java.srcDir querydslDir } configurations { - querydsl.extendsFrom compileClasspath + querydsl.extendsFrom compileClasspath } compileQuerydsl { - options.annotationProcessorPath = configurations.querydsl + options.annotationProcessorPath = configurations.querydsl } //querydsl 추가 끝 /** * comileQuerydsl.doFirst 추가 */ compileQuerydsl.doFirst { - if(file(querydslDir).exists() ) - delete(file(querydslDir)) + if (file(querydslDir).exists()) + delete(file(querydslDir)) } diff --git a/chabak-deploy.sh b/chabak-deploy.sh index 87e6d2d..02d8021 100644 --- a/chabak-deploy.sh +++ b/chabak-deploy.sh @@ -24,4 +24,4 @@ fi echo "> $JAR_PATH 배포" #nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null & #code deploy에 출력이 되기 때문에 nohup.out 파일을 사용해야 한다. -nohup java -jar $JAR_PATH > $REPOSITORY/nohup.out 2>&1 & +nohup java -jar -Dspring.profiles.active=prod $JAR_PATH > $REPOSITORY/nohup.out 2>&1 & diff --git a/src/main/java/com/sikhye/chabak/base/entity/BaseRole.java b/src/main/java/com/sikhye/chabak/base/entity/BaseRole.java new file mode 100644 index 0000000..1251ca5 --- /dev/null +++ b/src/main/java/com/sikhye/chabak/base/entity/BaseRole.java @@ -0,0 +1,5 @@ +package com.sikhye.chabak.base.entity; + +public enum BaseRole { + ROLE_USER, ROLE_ADMIN +} diff --git a/src/main/java/com/sikhye/chabak/interceptor/JwtAdminInterceptor.java b/src/main/java/com/sikhye/chabak/interceptor/JwtAdminInterceptor.java new file mode 100644 index 0000000..98f5572 --- /dev/null +++ b/src/main/java/com/sikhye/chabak/interceptor/JwtAdminInterceptor.java @@ -0,0 +1,51 @@ +package com.sikhye.chabak.interceptor; + +import static com.sikhye.chabak.base.BaseResponseStatus.*; +import static com.sikhye.chabak.base.entity.BaseRole.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.sikhye.chabak.base.entity.BaseRole; +import com.sikhye.chabak.base.exception.BaseException; +import com.sikhye.chabak.utils.JwtTokenService; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class JwtAdminInterceptor implements HandlerInterceptor { + + private final JwtTokenService jwtTokenService; + + public JwtAdminInterceptor(JwtTokenService jwtTokenService) { + this.jwtTokenService = jwtTokenService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws + Exception { + + String requestURI = request.getRequestURI(); + + // places에서 GET 방식이라면 Admin 기능이 아닌 Member도 사용 가능 + if (request.getMethod().equals("GET")) { + return true; + } + + log.info("인증 체크 인터셉터 실행 {}", requestURI); + + // 관리자 권한이 아닌 경우 api 요청 불가 + BaseRole memberRole = jwtTokenService.getMemberRole(); + + if (!memberRole.equals(ROLE_ADMIN)) { + throw new BaseException(INVALID_USER_JWT); + } + + return true; + + } +} diff --git a/src/main/java/com/sikhye/chabak/interceptor/JwtMemberInterceptor.java b/src/main/java/com/sikhye/chabak/interceptor/JwtMemberInterceptor.java new file mode 100644 index 0000000..5b2ca6c --- /dev/null +++ b/src/main/java/com/sikhye/chabak/interceptor/JwtMemberInterceptor.java @@ -0,0 +1,38 @@ +package com.sikhye.chabak.interceptor; + +import static com.sikhye.chabak.base.BaseResponseStatus.*; +import static com.sikhye.chabak.utils.JwtValue.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.sikhye.chabak.base.exception.BaseException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class JwtMemberInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + + String requestURI = request.getRequestURI(); + log.info("토큰명 : {}", X_ACCESS_TOKEN.toString()); + String accessToken = request.getHeader(X_ACCESS_TOKEN.toString()); + + log.info("인증 체크 인터셉터 실행 {}", requestURI); + + if (accessToken == null || accessToken.isBlank()) { + log.info("미인증 JWT 요청"); + throw new BaseException(EMPTY_JWT); + } + + return true; + } + +} diff --git a/src/main/java/com/sikhye/chabak/interceptor/WebConfig.java b/src/main/java/com/sikhye/chabak/interceptor/WebConfig.java new file mode 100644 index 0000000..281c97e --- /dev/null +++ b/src/main/java/com/sikhye/chabak/interceptor/WebConfig.java @@ -0,0 +1,40 @@ +package com.sikhye.chabak.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final JwtMemberInterceptor jwtMemberInterceptor; + private final JwtAdminInterceptor jwtAdminInterceptor; + + public WebConfig(JwtMemberInterceptor jwtMemberInterceptor, JwtAdminInterceptor jwtAdminInterceptor) { + this.jwtMemberInterceptor = jwtMemberInterceptor; + this.jwtAdminInterceptor = jwtAdminInterceptor; + } + + // 인터셉터 등록 + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtMemberInterceptor) // 인터셉터 등록 + .order(1) // 인터셉터 호출 우선순위 + .addPathPatterns("/**") // 인터셉터 적용할 URI 패턴 + .excludePathPatterns("/error", "/members/**", "/auth/**"); // 인터셉터에서 제외할 URI 패턴 + + registry.addInterceptor(jwtAdminInterceptor) + .order(2) + .addPathPatterns("/places/**") + .excludePathPatterns("/places/comments/**"); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowedMethods("GET", "POST", "OPTIONS", "PUT", "PATCH"); + } + +} diff --git a/src/main/java/com/sikhye/chabak/utils/JwtTokenProvider.java b/src/main/java/com/sikhye/chabak/utils/JwtTokenProvider.java index 96c55b3..db2fce5 100644 --- a/src/main/java/com/sikhye/chabak/utils/JwtTokenProvider.java +++ b/src/main/java/com/sikhye/chabak/utils/JwtTokenProvider.java @@ -1,15 +1,19 @@ package com.sikhye.chabak.utils; +import static com.sikhye.chabak.utils.JwtValue.*; + +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.servlet.http.HttpServletRequest; -import java.util.Date; -import java.util.List; @Component public class JwtTokenProvider { @@ -21,10 +25,10 @@ public class JwtTokenProvider { private final long tokenValidTime = 30 * 60 * 1000L; // 객체 초기화, secretKey를 Base64로 인코딩한다. -// @PostConstruct -// protected void init() { -// secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); -// } + // @PostConstruct + // protected void init() { + // secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + // } // JWT 토큰 생성 public String createToken(String userPk, List roles) { @@ -40,20 +44,20 @@ public String createToken(String userPk, List roles) { .compact(); } -// // JWT 토큰에서 인증 정보 조회 -// public Authentication getAuthentication(String token) { -// UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token)); -// return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); -// } + // // JWT 토큰에서 인증 정보 조회 + // public Authentication getAuthentication(String token) { + // UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token)); + // return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + // } -// // 토큰에서 회원 정보 추출 -// public String getUserPk(String token) { -// return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); -// } + // // 토큰에서 회원 정보 추출 + // public String getUserPk(String token) { + // return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + // } // Request의 Header에서 token 값을 가져옵니다. "X-ACCESS-TOKEN" : "TOKEN값' public String resolveToken(HttpServletRequest request) { - return request.getHeader("X-ACCESS-TOKEN"); + return request.getHeader(X_ACCESS_TOKEN.toString()); } // 토큰의 유효성 + 만료일자 확인 diff --git a/src/main/java/com/sikhye/chabak/utils/JwtTokenService.java b/src/main/java/com/sikhye/chabak/utils/JwtTokenService.java index 47bd412..8235eb3 100644 --- a/src/main/java/com/sikhye/chabak/utils/JwtTokenService.java +++ b/src/main/java/com/sikhye/chabak/utils/JwtTokenService.java @@ -1,21 +1,24 @@ package com.sikhye.chabak.utils; -import com.sikhye.chabak.base.entity.BaseRole; -import com.sikhye.chabak.base.exception.BaseException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import static com.sikhye.chabak.base.BaseResponseStatus.*; +import static com.sikhye.chabak.utils.JwtValue.*; + +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.http.HttpServletRequest; -import java.util.Date; +import com.sikhye.chabak.base.entity.BaseRole; +import com.sikhye.chabak.base.exception.BaseException; -import static com.sikhye.chabak.base.BaseResponseStatus.EMPTY_JWT; -import static com.sikhye.chabak.base.BaseResponseStatus.INVALID_JWT; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; @Service public class JwtTokenService { @@ -23,7 +26,6 @@ public class JwtTokenService { @Value("${secret.JWT_SECRET_KEY}") private String JWT_SECRET_KEY; - /* JWT 생성 @param userIdx @@ -46,8 +48,8 @@ public String createJwt(Long memberId, BaseRole role) { @return String */ public String getJwt() { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); - return request.getHeader("X-ACCESS-TOKEN"); + HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); + return request.getHeader(X_ACCESS_TOKEN.toString()); } /* @@ -55,7 +57,7 @@ public String getJwt() { @return String @throws BaseException */ - public String getMemberRole() throws BaseException { + public BaseRole getMemberRole() throws BaseException { //1. JWT 추출 String accessToken = getJwt(); if (accessToken == null || accessToken.length() == 0) { @@ -74,7 +76,7 @@ public String getMemberRole() throws BaseException { // 3. userIdx 추출 // ptpt: Object To Long - return String.valueOf(claims.getBody().get("role")); + return BaseRole.valueOf(String.valueOf(claims.getBody().get("role"))); } /* diff --git a/src/main/java/com/sikhye/chabak/utils/JwtValue.java b/src/main/java/com/sikhye/chabak/utils/JwtValue.java new file mode 100644 index 0000000..d56fc5f --- /dev/null +++ b/src/main/java/com/sikhye/chabak/utils/JwtValue.java @@ -0,0 +1,9 @@ +package com.sikhye.chabak.utils; + +public enum JwtValue { + X_ACCESS_TOKEN; + + public String toString() { + return name().replaceAll("_", "-"); + } +} diff --git a/src/main/resources/application.tar.gpg b/src/main/resources/application.tar.gpg index e882036..91ece9e 100644 Binary files a/src/main/resources/application.tar.gpg and b/src/main/resources/application.tar.gpg differ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5b8744a..a753717 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,8 @@ -server: - port: 9000 - tomcat: - uri-encoding: UTF-8 - spring: + profiles: + include: + - infra + jpa: hibernate: ddl-auto: none @@ -14,18 +13,11 @@ spring: hibernate: format_sql: true show_sql: true - profiles: - active: prod - include: - - oauth - - aws - - credentials servlet: multipart: max-file-size: 10MB max-request-size: 10MB - logging.level: org.hibernate.SQL: debug org.hibernate.type: trace @@ -35,4 +27,3 @@ decorator: datasource: p6spy: enable-logging: true - diff --git a/src/main/resources/logback-dev.properties b/src/main/resources/logback-dev.properties new file mode 100644 index 0000000..40752f7 --- /dev/null +++ b/src/main/resources/logback-dev.properties @@ -0,0 +1,4 @@ +#logback +log.config.mode=dev +# log path +log.config.path=logs diff --git a/src/main/resources/logback-prod.properties b/src/main/resources/logback-prod.properties new file mode 100644 index 0000000..95a90b6 --- /dev/null +++ b/src/main/resources/logback-prod.properties @@ -0,0 +1,4 @@ +#logback +log.config.mode=prod +# log path +log.config.path=/var/log/chabak diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..ae0eace --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,67 @@ + + + + + + + true + + %d{YYYY-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %highlight([File:%F]) %magenta([Line:%L]) - %msg%n + + + + + + + ${log.config.path}/app.log + + logs/app.%d{yyyy-MM-dd}.%i.gz + + 10MB + + 30 + + + %d{YYYY-mm-dd HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] + + + + + ${log.config.path}/error.log + + ERROR + ACCEPT + DENY + + + logs/error-%d{yyyy-MM-dd}.%i.gz + + 10MB + + 30 + + + %d{YYYY-mm-dd HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] + + + + + + + + + + + + + + + + + + + + + + +