39 Commits

Author SHA1 Message Date
Hanbin Lee
09fd3457e9 Merge pull request #44 from beaniejoy/feature/43
Http Request, Response 관련 logging filter 적용
2023-04-15 21:31:00 +09:00
beaniejoy
55527dfad3 [#43] modify: log 형식 수정 2023-04-15 21:29:26 +09:00
beaniejoy
5d516c95b6 [#43] feat: client ip 추출 기능 추가
- client ip 후보군 enum 생성
- security, logging에 대한 패키지화(infra)
2023-04-15 21:20:08 +09:00
beaniejoy
743fe11dc6 [#43] modify: Logging Filter에 대한 리팩토링
- utils 성격의 클래스 따로 패키지화
- LoggingFilter의 LogMessage 도메인 따로 구분
2023-04-14 20:19:28 +09:00
beaniejoy
a108cdd466 [#43] feat: request, response 내용 logging 적용해보기
- request, response 내용 로깅을 위한 filter 적용
- request, response 관련 캐싱 기능 적용
- json converting을 위한 Gson library 적용
2023-04-14 02:04:48 +09:00
beaniejoy
8107468562 [#43] feat: logging filter 적용 진행중
- LoggingFilter 초기 설정(리팩토링 필요)
- logback-spring.xml 설정
- security filter order 설정값 지정
2023-04-13 01:59:51 +09:00
Hanbin Lee
30c92f18dd Merge pull request #42 from beaniejoy/feature/41
Cafe 리스트 검색 api 수정 및 전체적인 리팩토링
2023-04-12 00:56:07 +09:00
beaniejoy
e95349f743 [#41] modify: 전체적인 리팩토링
- Security 관련 JWT filter, configurer 공통 코드 common 모듈화
- flyway 불필요한 설정 제거
2023-04-12 00:50:45 +09:00
beaniejoy
ac40f5b42a [#41] feat: migration local용 shell script 수정 2023-04-11 23:23:10 +09:00
Hanbin Lee
49e19666b2 [#41] feat: 카페 검색 api 수정
- 프론트 테스트겸 카페 검색 api 수정(이름 조건 추가)
- security 설정 관련 내용 수정(파일 이동)
- flyway seed data 내용 수정(image)
2023-04-08 00:09:18 +09:00
Hanbin Lee
afa9f93dab feat: local 전용 db migration bash script 작성 2023-03-22 00:12:57 +09:00
Hanbin Lee
01c39b333e modify: db 디렉토리 내용 수정 2023-03-12 19:48:57 +09:00
Hanbin Lee
c95a7ec867 Merge pull request #39 from beaniejoy/feature/38
Jenkins FlywayRunner plugin 이용한 DB Migrate stage 내용 수정
2023-03-12 19:47:18 +09:00
Hanbin Lee
7b5135c109 [#38] feat: 정리
- flyway README 파일 따로 구성
- flyway 관련 기존 gradle 설정 제거
- Jenkinsfile 내용 복구 및 리팩토링
2023-03-12 19:42:06 +09:00
Hanbin Lee
2c93cc3dd2 [#38] feat: feature/33 내용 merge 2023-03-12 19:24:00 +09:00
Hanbin Lee
be217c6b01 Merge branch 'feature/33' into feature/38 2023-03-12 19:23:27 +09:00
Hanbin Lee
dbf8b046b0 [#33] test: 불필요한 요소 제거 테스트 2023-03-12 19:20:39 +09:00
Hanbin Lee
6f183e38ec [#33] fix: jenkinsfile 수정 2023-03-12 19:15:50 +09:00
Hanbin Lee
2c8b4c9a09 [#33] fix: jenkinsfile, flyway 설정내용 수정 2023-03-12 18:57:09 +09:00
Hanbin Lee
c0fcefa8c0 [#33] test: info만 실행 테스트 2023-03-12 18:23:58 +09:00
Hanbin Lee
6036bd63ad [#33] test: info, validate 진행 테스트(build 생략) 2023-03-12 18:11:31 +09:00
Hanbin Lee
cb5179abd7 [#33] test: info, validate 진행 테스트 2023-03-12 18:11:06 +09:00
Hanbin Lee
9e376bfefb [#33] fix: locations workspace 기준으로 변경 2023-03-12 18:06:58 +09:00
Hanbin Lee
f4b11c755c [#33] fix: flyway credentialsId 추가 2023-03-12 17:55:46 +09:00
Hanbin Lee
af1a95b1c7 [#33] fix: flyway runner commandLineArgs 수정 2023-03-12 17:48:19 +09:00
Hanbin Lee
8cc64c879f [#33] fix: flyway runner installation name setting 2023-03-12 17:46:21 +09:00
Hanbin Lee
e08c02a99a [#33] fix: flywayrunner plugin 문법 수정 2023-03-12 17:39:30 +09:00
Hanbin Lee
13566e1f86 [#33] test: jenkins flyway runner 테스트 2023-03-12 17:38:15 +09:00
Hanbin Lee
e9115e5322 Merge pull request #37 from beaniejoy/feature/33
Jenkins Pipeline에 flyway migration 단계 추가 적용
2023-03-04 00:38:03 +09:00
Hanbin Lee
0210499e66 [#33] modify: migration shell script 수정 2023-03-04 00:31:14 +09:00
Hanbin Lee
42c8b3df14 [#33] test: migration > test > build 테스트 2023-03-03 01:28:34 +09:00
Hanbin Lee
54c4481031 [#33] test: DB Migration test 2023-03-03 00:58:50 +09:00
Hanbin Lee
2d142eb829 [#33] test: jenkins 출력 테스트 2023-03-03 00:19:30 +09:00
Hanbin Lee
09b6beee53 [#33] test: flyway migration 작업 테스트 2023-03-02 02:07:06 +09:00
Hanbin Lee
a39084a11a [#33] test: jenkins cloud server 구축 및 테스트2 2023-03-02 02:01:18 +09:00
Hanbin Lee
1beaaad422 [#33] test: jenkins cloud server 구축 및 테스트 2023-03-02 01:57:54 +09:00
Hanbin Lee
826cfb0eaa Merge branch 'main' into feature/33 2023-02-23 00:30:15 +09:00
Hanbin Lee
37169355da Merge pull request #35 from beaniejoy/feature/34
JPA Auditing 기능 수정
2023-02-23 00:28:28 +09:00
Hanbin Lee
3e7b928d8f [#34] feat: JPA Auditing 기능 수정
- auditor에 Security Authentication 적용(createdBy, updatedBy)
- BaseEntity, BaseTimeEntity 분리
- AuditingConfig 파일 common 모듈 내 공통화(including AuditorAware)
- DDL created_by, updated_by type 변경 (varchar(20) > varchar(320), email 최대크기로 설정)
2023-02-20 00:40:37 +09:00
59 changed files with 533 additions and 293 deletions

View File

@@ -1,24 +1,26 @@
pipeline { pipeline {
agent any agent any
tools {
jdk("openjdk-17")
}
stages { stages {
stage('Init') { stage('Init') {
steps { steps {
sh 'printenv' script {
sh 'whoami'
sh 'printenv'
migration_script = './script/db_migration.sh' FLYWAY_CONFIG = '/home/ec2-user/flyway/flyway.conf'
}
} }
} }
stage('DB Migrate') { stage('DB Migrate') {
steps { steps {
sh """ flywayrunner installationName: 'flywaytool-jenkins',
chmod 755 ${migration_script} flywayCommand: 'info migrate validate',
/bin/bash ${migration_script} commandLineArgs: "-configFiles=${FLYWAY_CONFIG}",
""" credentialsId: 'ecb29499-7272-4e8b-b3ab-a7a3ab7eafab',
url: '',
locations: "filesystem:${WORKSPACE}/db/migration"
} }
} }

View File

@@ -5,11 +5,19 @@
<br> <br>
## Specification ## :pushpin: Specification
- java 17 - Lang
- kotlin 1.6.21 - java 17
- Spring Boot 2.7.0 - kotlin 1.6.21
- MySQL 8.0.21 - Framework
- Spring Boot 2.7.0
- DB
- MySQL 8.0.21
- Flyway(migration)
- CI/CD
- Jenkins
- Cloud Server
- AWS Lightsail(Amazon Linux2)
<br> <br>
@@ -21,11 +29,11 @@
- `dongne-common` - `dongne-common`
- entity, repository, error, security(jwt util) 등 관리하는 공통모듈 - entity, repository, error, security(jwt util) 등 관리하는 공통모듈
- `db` - `db`
- flyway migration(gradle) 적용 모듈 - flyway migration 관리 디렉토리
<br> <br>
## :pushpin: Run Application ## :pushpin: Setting
### 💽 로컬 DB 구성 (docker) ### 💽 로컬 DB 구성 (docker)
- local에 DB(MySQL)용 docker container run - local에 DB(MySQL)용 docker container run
@@ -34,31 +42,8 @@
$ docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=beaniejoy -d -p 3306:3306 mysql:8.0.21 $ docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=beaniejoy -d -p 3306:3306 mysql:8.0.21
``` ```
### 💽 DB Migration (flyway) ### 💽 DB Migration (with flyway)
[flyway doc](https://documentation.red-gate.com/fd/flyway-documentation-138346877.html) - [DB migration directory README](https://github.com/beaniejoy/dongne-cafe-api/blob/main/db/README.md)
- **Info**
Prints the details and status information about all the migrations
```bash
$ ./gradlew :db:flywayInfo
```
- **Validate**
Validates the applied migrations against the available ones
DB에 적용된 migration과 local에 적용된 migration 정보 일치 여부 체크
```bash
$ ./gradlew :db:flywayValidate
```
- **Migrate**
Migrates the schema to the latest version
migration 설정 내용들 반영
```bash
$ ./gradlew :db:flywayMigrate
```
- **Clean**
Drops all objects (tables, views, procedures, triggers, …) in the configured schemas
(prodution 단계에서는 절대 사용 X)
```bash
$ ./gradlew :db:flywayClean -i
```
### 💽 docker compose 실행(수정 작업 진행중) ### 💽 docker compose 실행(수정 작업 진행중)
- docker compose를 이용한 nginx, DB(MySQL), application 한꺼번에 실행하는 경우 - docker compose를 이용한 nginx, DB(MySQL), application 한꺼번에 실행하는 경우

View File

@@ -67,6 +67,8 @@ subprojects {
// Logging // Logging
implementation("io.github.microutils:kotlin-logging:${Version.Deps.KOTLIN_LOGGING}") implementation("io.github.microutils:kotlin-logging:${Version.Deps.KOTLIN_LOGGING}")
implementation("com.google.code.gson:gson")
// Test // Test
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")

View File

@@ -15,8 +15,4 @@ object Plugins {
const val PLUGIN_SPRING = "plugin.spring" const val PLUGIN_SPRING = "plugin.spring"
const val PLUGIN_JPA = "plugin.jpa" const val PLUGIN_JPA = "plugin.jpa"
} }
object FlywayDB {
const val FLYWAY = "org.flywaydb.flyway"
}
} }

View File

@@ -15,8 +15,4 @@ object Version {
const val KOTLIN_LOGGING = "3.0.4" const val KOTLIN_LOGGING = "3.0.4"
const val JWT = "0.11.5" const val JWT = "0.11.5"
} }
object FlywayDB {
const val FLYWAY_CORE = "9.8.1"
}
} }

56
db/README.md Normal file
View File

@@ -0,0 +1,56 @@
# DB Migration
- flyway version: `9.15.4`
- [flyway doc](https://documentation.red-gate.com/fd/flyway-documentation-138346877.html)
<br>
## :pushpin: Installation(Local)
LOCAL 환경에 해당
```shell
$ brew install flyway
```
- macOS 전용
<br>
## :pushpin: Flyway Command
- **Clean**
Drops all objects (tables, views, procedures, triggers, …) in the configured schemas
(prodution 단계에서는 절대 사용 X)
```bash
$ flyway clean -configFiles=db/flyway.conf
```
- **Info**
Prints the details and status information about all the migrations
```bash
$ flyway info -configFiles=db/flyway.conf
```
- **Migrate**
Migrates the schema to the latest version
migration 설정 내용들 반영
```bash
$ flyway migrate -configFiles=db/flyway.conf
```
- **Validate**
Validates the applied migrations against the available ones
DB에 적용된 migration과 local에 적용된 migration 정보 일치 여부 체크
```bash
$ flyway validate -configFiles=db/flyway.conf
```
<br>
## :pushpin: Migration for Local Env
```shell
$ cd [PROJECT_ROOT_DIR]
$ chmod 755 ./script/migration-local.sh
$ ./script/migration-local.sh
```
project의 root directory로 이동하는 것이 중요

View File

@@ -1,26 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.flywaydb:flyway-mysql:${Version.FlywayDB.FLYWAY_CORE}")
}
}
plugins {
id(Plugins.FlywayDB.FLYWAY).version(Version.FlywayDB.FLYWAY_CORE)
}
dependencies {
implementation("org.flywaydb:flyway-core:${Version.FlywayDB.FLYWAY_CORE}") // flyway
}
flyway {
baselineDescription = "Start Flyway Migration!"
baselineOnMigrate = true
baselineVersion = "000"
locations = arrayOf("filesystem:./migration", "filesystem:./seed")
configFiles = arrayOf("conf/flyway.conf")
cleanDisabled = false // activate flywayClean
ignoreMigrationPatterns = arrayOf("*:pending") // ignore validating pending(대기) state
}

View File

@@ -1,4 +0,0 @@
flyway.url=jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
flyway.user=root
flyway.password=beaniejoy
flyway.driver=com.mysql.cj.jdbc.Driver

10
db/flyway-local.conf Normal file
View File

@@ -0,0 +1,10 @@
flyway.url=jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
flyway.user=root
flyway.password=beaniejoy
flyway.driver=com.mysql.cj.jdbc.Driver
flyway.locations=filesystem:db/migration,db/seed
flyway.baselineOnMigrate=true
flyway.baselineVersion=000
# flyway.ignoreMigrationPatterns=*:pending
flyway.cleanDisabled=false

View File

@@ -6,8 +6,8 @@ CREATE TABLE `cafe` (
`total_rate` float NOT NULL COMMENT '카페 종합 평가 점수', `total_rate` float NOT NULL COMMENT '카페 종합 평가 점수',
`description` varchar(255) COMMENT '카페 상세설명', `description` varchar(255) COMMENT '카페 상세설명',
`created_at` datetime NOT NULL COMMENT '카페 등록날짜', `created_at` datetime NOT NULL COMMENT '카페 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '카페 등록자', `created_by` varchar(320) NOT NULL COMMENT '카페 등록자',
`updated_at` datetime NULL COMMENT '카페 변경날짜', `updated_at` datetime NULL COMMENT '카페 변경날짜',
`updated_by` varchar(20) NULL COMMENT '카페 변경자', `updated_by` varchar(320) NULL COMMENT '카페 변경자',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -3,9 +3,9 @@ CREATE TABLE `cafe_menu` (
`name` varchar(50) NOT NULL COMMENT '카페 메뉴명', `name` varchar(50) NOT NULL COMMENT '카페 메뉴명',
`price` decimal(10, 2) NOT NULL COMMENT '메뉴 가격', `price` decimal(10, 2) NOT NULL COMMENT '메뉴 가격',
`created_at` datetime NOT NULL COMMENT '메뉴 등록날짜', `created_at` datetime NOT NULL COMMENT '메뉴 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '메뉴 등록자', `created_by` varchar(320) NOT NULL COMMENT '메뉴 등록자',
`updated_at` datetime COMMENT '메뉴 변경날짜', `updated_at` datetime COMMENT '메뉴 변경날짜',
`updated_by` varchar(20) NULL COMMENT '메뉴 변경자', `updated_by` varchar(320) NULL COMMENT '메뉴 변경자',
`cafe_id` bigint unsigned NOT NULL COMMENT '연관된 카페 ID', `cafe_id` bigint unsigned NOT NULL COMMENT '연관된 카페 ID',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `cafe_id` (`cafe_id`), KEY `cafe_id` (`cafe_id`),

View File

@@ -2,9 +2,9 @@ CREATE TABLE `cafe_image` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '카페 이미지 ID', `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '카페 이미지 ID',
`img_url` varchar(255) NOT NULL COMMENT '이미지 경로', `img_url` varchar(255) NOT NULL COMMENT '이미지 경로',
`created_at` datetime NOT NULL COMMENT '이미지 등록날짜', `created_at` datetime NOT NULL COMMENT '이미지 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '이미지 등록자', `created_by` varchar(320) NOT NULL COMMENT '이미지 등록자',
`updated_at` datetime COMMENT '이미지 변경날짜', `updated_at` datetime COMMENT '이미지 변경날짜',
`updated_by` varchar(20) NULL COMMENT '이미지 변경자', `updated_by` varchar(320) NULL COMMENT '이미지 변경자',
`cafe_id` bigint unsigned NOT NULL COMMENT '연관된 카페 ID', `cafe_id` bigint unsigned NOT NULL COMMENT '연관된 카페 ID',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `cafe_id` (`cafe_id`), KEY `cafe_id` (`cafe_id`),

View File

@@ -2,9 +2,9 @@ CREATE TABLE `menu_option`(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '옵션 ID', `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '옵션 ID',
`title` varchar(50) NOT NULL COMMENT '메뉴 옵션 이름', `title` varchar(50) NOT NULL COMMENT '메뉴 옵션 이름',
`created_at` datetime NOT NULL COMMENT '옵션 등록날짜', `created_at` datetime NOT NULL COMMENT '옵션 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '옵션 등록자', `created_by` varchar(320) NOT NULL COMMENT '옵션 등록자',
`updated_at` datetime COMMENT '옵션 변경날짜', `updated_at` datetime COMMENT '옵션 변경날짜',
`updated_by` varchar(20) NULL COMMENT '옵션 변경자', `updated_by` varchar(320) NULL COMMENT '옵션 변경자',
`menu_id` bigint unsigned NOT NULL COMMENT '연관된 카페 메뉴 ID', `menu_id` bigint unsigned NOT NULL COMMENT '연관된 카페 메뉴 ID',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `menu_id` (`menu_id`), KEY `menu_id` (`menu_id`),

View File

@@ -3,9 +3,9 @@ CREATE TABLE `option_detail` (
`name` varchar(50) NOT NULL COMMENT '옵션 상세명', `name` varchar(50) NOT NULL COMMENT '옵션 상세명',
`extra_price` decimal(10, 2) NOT NULL COMMENT '옵션 추가 요금', `extra_price` decimal(10, 2) NOT NULL COMMENT '옵션 추가 요금',
`created_at` datetime NOT NULL COMMENT '옵션 상세 등록날짜', `created_at` datetime NOT NULL COMMENT '옵션 상세 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '옵션 상세 등록자', `created_by` varchar(320) NOT NULL COMMENT '옵션 상세 등록자',
`updated_at` datetime COMMENT '옵션 상세 변경날짜', `updated_at` datetime COMMENT '옵션 상세 변경날짜',
`updated_by` varchar(20) NULL COMMENT '옵션 상세 변경자', `updated_by` varchar(320) NULL COMMENT '옵션 상세 변경자',
`option_id` bigint unsigned NOT NULL COMMENT '연관된 옵션 ID', `option_id` bigint unsigned NOT NULL COMMENT '연관된 옵션 ID',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `option_id` (`option_id`), KEY `option_id` (`option_id`),

View File

@@ -7,8 +7,8 @@ CREATE TABLE `member` (
`role_type` varchar(20) COMMENT '회원 권한', `role_type` varchar(20) COMMENT '회원 권한',
`activated` tinyint NOT NULL COMMENT '계정 활성화 여부', `activated` tinyint NOT NULL COMMENT '계정 활성화 여부',
`created_at` datetime NOT NULL COMMENT '회원 등록날짜', `created_at` datetime NOT NULL COMMENT '회원 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '회원 등록자', `created_by` varchar(320) NOT NULL COMMENT '회원 등록자',
`updated_at` datetime NULL COMMENT '회원 변경날짜', `updated_at` datetime NULL COMMENT '회원 변경날짜',
`updated_by` varchar(20) NULL COMMENT '회원 변경자', `updated_by` varchar(320) NULL COMMENT '회원 변경자',
PRIMARY KEY (`member_id`) PRIMARY KEY (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -1,3 +1,5 @@
DROP PROCEDURE IF EXISTS insertCafeImages;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE insertCafeImages() CREATE PROCEDURE insertCafeImages()
BEGIN BEGIN
@@ -15,10 +17,10 @@ BEGIN
WHILE(j <= 3) DO WHILE(j <= 3) DO
INSERT IGNORE INTO `cafe_image` (img_url, created_at, created_by, updated_at, updated_by, cafe_id) INSERT IGNORE INTO `cafe_image` (img_url, created_at, created_by, updated_at, updated_by, cafe_id)
VALUES (CONCAT('test_img_url_', idx_img), now(), 'system', now(), 'system', var_cafe_id); VALUES (CONCAT('https://d3qy02qh8hbgxp.cloudfront.net/cafe', idx_img, '.jpg'), now(), 'system', now(), 'system', var_cafe_id);
SET j = j + 1; SET j = j + 1;
SET idx_img = idx_img + 1; SET idx_img = idx_img % 7 + 1;
END WHILE; END WHILE;
SET i = i + 1; SET i = i + 1;

View File

@@ -1,8 +0,0 @@
package io.beaniejoy.dongnecafe.common.config
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
@Configuration
@EnableJpaAuditing
class AuditingConfig

View File

@@ -1,9 +1,9 @@
package io.beaniejoy.dongnecafe.common.config package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.JwtTokenUtils import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean

View File

@@ -1,12 +0,0 @@
package io.beaniejoy.dongnecafe.common.entity
import org.springframework.data.domain.AuditorAware
import org.springframework.stereotype.Component
import java.util.*
@Component
class BaseEntityAuditorAware: AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return Optional.of("system")
}
}

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.controller package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.security.JwtTokenUtils import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import io.beaniejoy.dongnecafe.model.TokenResponse import io.beaniejoy.dongnecafe.model.TokenResponse
import io.beaniejoy.dongnecafe.service.AuthService import io.beaniejoy.dongnecafe.service.AuthService

View File

@@ -1,6 +1,7 @@
package io.beaniejoy.dongnecafe.security package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import mu.KLogging import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.BadCredentialsException

View File

@@ -4,6 +4,7 @@ import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import mu.KLogging import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UserDetailsService

View File

@@ -1,52 +0,0 @@
package io.beaniejoy.dongnecafe.security.filter
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE
import mu.KotlinLogging
import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
class JwtAuthenticationFilter(
private val jwtTokenUtils: JwtTokenUtils
) : GenericFilterBean() {
private val log = KotlinLogging.logger {}
/**
* JWT access token 인증 처리
*/
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request as HttpServletRequest
log.info { "[JwtAuthenticationFilter][${request.dispatcherType}] uri: ${request.requestURI}" }
getAccessToken(httpRequest)?.let {
jwtTokenUtils.getAuthentication(it)
}?.also {
SecurityContextHolder.getContext().authentication = it
log.info { "Valid Access Token [${it.name}]" }
}
chain.doFilter(request, response)
}
private fun getAccessToken(request: HttpServletRequest): String? {
val bearer = request.getHeader(HttpHeaders.AUTHORIZATION)
?: return null
val splitBearer = bearer.split(WHITESPACE)
if (splitBearer.first() != BEARER) {
return null
}
if (splitBearer.size != 2 || splitBearer.last().isBlank()) {
return null
}
return splitBearer.last()
}
}

View File

@@ -1,3 +1,6 @@
server:
port: 9090
spring: spring:
profiles: profiles:
active: local active: local
@@ -5,22 +8,17 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
jpa: jpa:
hibernate: hibernate:
ddl-auto: none # use [service-api] flyway migration ddl-auto: none
properties: properties:
hibernate: hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true format_sql: true
show-sql: false show-sql: false
open-in-view: open-in-view:
flyway:
enabled: false
devtools: devtools:
livereload: livereload:
enabled: false # no use devtools' LiveReload Server enabled: false # no use devtools' LiveReload Server
server:
port: 9090
logging: logging:
level: level:
org.hibernate.SQL: debug # logger 통해 로깅 org.hibernate.SQL: debug # logger 통해 로깅

View File

@@ -0,0 +1,25 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.utils.security.getAuthPrincipal
import mu.KLogging
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.domain.AuditorAware
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.security.core.context.SecurityContextHolder
import java.util.*
@Configuration
@EnableJpaAuditing
class AuditingConfig {
companion object: KLogging() {
const val SYSTEM = "system"
}
@Bean
fun auditorProvider(): AuditorAware<String> {
return AuditorAware<String> {
Optional.of(SecurityContextHolder.getContext().authentication?.getAuthPrincipal() ?: SYSTEM)
}
}
}

View File

@@ -0,0 +1,21 @@
package io.beaniejoy.dongnecafe.common.entity
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import javax.persistence.Column
import javax.persistence.EntityListeners
import javax.persistence.MappedSuperclass
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity protected constructor() : BaseTimeEntity() {
@CreatedBy
@Column(updatable = false)
lateinit var createdBy: String
protected set
@LastModifiedBy
lateinit var updatedBy: String
protected set
}

View File

@@ -1,11 +1,10 @@
package io.beaniejoy.dongnecafe.common package io.beaniejoy.dongnecafe.common.entity
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.EntityListeners import javax.persistence.EntityListeners
import javax.persistence.MappedSuperclass import javax.persistence.MappedSuperclass
@@ -13,18 +12,11 @@ import javax.persistence.MappedSuperclass
@EntityListeners(AuditingEntityListener::class) @EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity protected constructor() { abstract class BaseTimeEntity protected constructor() {
@CreatedDate @CreatedDate
var createdAt: LocalDateTime = LocalDateTime.now() @Column(updatable = false)
protected set lateinit var createdAt: LocalDateTime
@CreatedBy
var createdBy: String = ""
protected set protected set
@LastModifiedDate @LastModifiedDate
var updatedAt: LocalDateTime? = null lateinit var updatedAt: LocalDateTime
protected set
@LastModifiedBy
var updatedBy: String? = null
protected set protected set
} }

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import javax.persistence.* import javax.persistence.*
@@ -11,7 +11,7 @@ class Cafe protected constructor(
address: String, address: String,
phoneNumber: String, phoneNumber: String,
description: String, description: String,
) : BaseTimeEntity() { ) : BaseEntity() {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cafe_id", nullable = false) @Column(name = "cafe_id", nullable = false)

View File

@@ -1,11 +1,11 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import javax.persistence.* import javax.persistence.*
@Entity @Entity
@Table(name = "cafe_image") @Table(name = "cafe_image")
class CafeImage( class CafeImage protected constructor(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cafe_image_id", nullable = false) @Column(name = "cafe_image_id", nullable = false)
@@ -17,4 +17,4 @@ class CafeImage(
@ManyToOne @ManyToOne
@JoinColumn(name = "cafe_id", nullable = false) @JoinColumn(name = "cafe_id", nullable = false)
val cafe: Cafe val cafe: Cafe
) : BaseTimeEntity() ) : BaseEntity()

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest
import java.math.BigDecimal import java.math.BigDecimal
import javax.persistence.* import javax.persistence.*
@@ -10,7 +10,7 @@ import javax.persistence.*
class CafeMenu protected constructor( class CafeMenu protected constructor(
name: String, name: String,
price: BigDecimal, price: BigDecimal,
) : BaseTimeEntity() { ) : BaseEntity() {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cafe_menu_id", nullable = false) @Column(name = "cafe_menu_id", nullable = false)
@@ -33,7 +33,11 @@ class CafeMenu protected constructor(
val menuOptionList: MutableList<MenuOption> = arrayListOf() val menuOptionList: MutableList<MenuOption> = arrayListOf()
companion object { companion object {
fun createCafeMenu(name: String, price: BigDecimal, menuOptionRequestList: List<MenuOptionRegisterRequest>): CafeMenu { fun createCafeMenu(
name: String,
price: BigDecimal,
menuOptionRequestList: List<MenuOptionRegisterRequest>
): CafeMenu {
val menuOptionEntityList = menuOptionRequestList.map { menuOptionRequestDto -> val menuOptionEntityList = menuOptionRequestList.map { menuOptionRequestDto ->
MenuOption.createMenuOption( MenuOption.createMenuOption(
title = menuOptionRequestDto.title, title = menuOptionRequestDto.title,

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest
import javax.persistence.* import javax.persistence.*
@@ -8,7 +8,7 @@ import javax.persistence.*
@Table(name = "menu_option") @Table(name = "menu_option")
class MenuOption protected constructor( class MenuOption protected constructor(
title: String title: String
) : BaseTimeEntity() { ) : BaseEntity() {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "menu_option_id", nullable = false) @Column(name = "menu_option_id", nullable = false)

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.entity package io.beaniejoy.dongnecafe.domain.cafe.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import java.math.BigDecimal import java.math.BigDecimal
import javax.persistence.* import javax.persistence.*
@@ -9,7 +9,7 @@ import javax.persistence.*
class OptionDetail protected constructor( class OptionDetail protected constructor(
name: String, name: String,
extraPrice: BigDecimal extraPrice: BigDecimal
) : BaseTimeEntity() { ) : BaseEntity() {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "option_detail_id", nullable = false) @Column(name = "option_detail_id", nullable = false)

View File

@@ -1,8 +1,12 @@
package io.beaniejoy.dongnecafe.domain.cafe.repository package io.beaniejoy.dongnecafe.domain.cafe.repository
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
interface CafeRepository : JpaRepository<Cafe, Long> { interface CafeRepository : JpaRepository<Cafe, Long> {
fun findByName(name: String): Cafe? fun findByName(name: String): Cafe?
fun findByNameContainingIgnoreCase(name: String?, pageable: Pageable): Page<Cafe>
} }

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.member.entity package io.beaniejoy.dongnecafe.domain.member.entity
import io.beaniejoy.dongnecafe.common.BaseTimeEntity import io.beaniejoy.dongnecafe.common.entity.BaseEntity
import io.beaniejoy.dongnecafe.domain.member.constant.RoleType import io.beaniejoy.dongnecafe.domain.member.constant.RoleType
import javax.persistence.* import javax.persistence.*
@@ -11,7 +11,7 @@ class Member(
password: String, password: String,
address: String, address: String,
phoneNumber: String phoneNumber: String
): BaseTimeEntity() { ) : BaseEntity() {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id", nullable = false) @Column(name = "member_id", nullable = false)

View File

@@ -0,0 +1,50 @@
package io.beaniejoy.dongnecafe.infra.logging
import io.beaniejoy.dongnecafe.utils.logging.*
import org.springframework.http.HttpStatus
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
data class HttpLogMessage(
val httpMethod: String,
val requestUri: String,
val httpStatus: HttpStatus,
val clientIp: String,
val elapsedTime: Double,
val headers: String?,
val requestParam: String?,
val requestBody: String?,
val responseBody: String?,
) {
companion object {
fun createInstance(
requestWrapper: ContentCachingRequestWrapper,
responseWrapper: ContentCachingResponseWrapper,
elapsedTime: Double
): HttpLogMessage {
return HttpLogMessage(
httpMethod = requestWrapper.method,
requestUri = requestWrapper.requestURI,
httpStatus = HttpStatus.valueOf(responseWrapper.status),
clientIp = requestWrapper.getClientIp(),
elapsedTime = elapsedTime,
headers = requestWrapper.getRequestHeaders(),
requestParam = requestWrapper.getRequestParams(),
requestBody = requestWrapper.getRequestBody(),
responseBody = responseWrapper.getResponseBody(),
)
}
}
fun toPrettierLog(): String {
return """
|
|[REQUEST] ${this.httpMethod} ${this.requestUri} ${this.httpStatus} (${this.elapsedTime})
|>> CLIENT_IP: ${this.clientIp}
|>> HEADERS: ${this.headers}
|>> REQUEST_PARAM: ${this.requestParam}
|>> REQUEST_BODY: ${this.requestBody}
|>> RESPONSE_BODY: ${this.responseBody}
""".trimMargin()
}
}

View File

@@ -0,0 +1,57 @@
package io.beaniejoy.dongnecafe.infra.logging
import mu.KotlinLogging
import org.slf4j.MDC
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class ReqResLoggingFilter : OncePerRequestFilter() {
private val log = KotlinLogging.logger {}
companion object {
const val REQUEST_ID = "request_id"
}
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
val cachingRequestWrapper = ContentCachingRequestWrapper(request)
val cachingResponseWrapper = ContentCachingResponseWrapper(response)
val requestId = UUID.randomUUID().toString().substring(0, 8)
MDC.put(REQUEST_ID, requestId)
val startTime = System.currentTimeMillis()
filterChain.doFilter(cachingRequestWrapper, cachingResponseWrapper)
val end = System.currentTimeMillis()
try {
log.info {
HttpLogMessage.createInstance(
requestWrapper = cachingRequestWrapper,
responseWrapper = cachingResponseWrapper,
elapsedTime = (end - startTime) / 1000.0
).toPrettierLog()
}
cachingResponseWrapper.copyBodyToResponse()
} catch (e: Exception) {
log.error(e) { "[${this::class.simpleName}] Logging 실패" }
}
MDC.remove(REQUEST_ID)
}
}

View File

@@ -0,0 +1,17 @@
package io.beaniejoy.dongnecafe.infra.logging.constant
/**
* http request client ip possible enum list
* (ref. https://blog.yevgnenll.me/posts/find-client-ip-from-http-request-header)
* @property headerName String client ip header name
*/
enum class HttpClientIp(
val headerName: String,
) {
X_FORWARDED_FOR("X-Forwarded-For"),
PROXY_CLIENT_IP("Proxy-Client-IP"),
WL_PROXY_CLIENT_IP("WL-Proxy-Client-IP"),
HTTP_X_FORWARDED("HTTP_X_FORWARDED"),
HTTP_X_FORWARDED_FOR("HTTP_X_FORWARDED_FOR"),
HTTP_CLIENT_IP("HTTP_CLIENT_IP")
}

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security package io.beaniejoy.dongnecafe.infra.security
import io.beaniejoy.dongnecafe.domain.member.entity.Member import io.beaniejoy.dongnecafe.domain.member.entity.Member
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority

View File

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.security.config package io.beaniejoy.dongnecafe.infra.security.config
import io.beaniejoy.dongnecafe.security.JwtTokenUtils import io.beaniejoy.dongnecafe.infra.security.filter.JwtAuthenticationFilter
import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import org.springframework.security.config.annotation.SecurityConfigurerAdapter import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain import org.springframework.security.web.DefaultSecurityFilterChain

View File

@@ -0,0 +1,11 @@
package io.beaniejoy.dongnecafe.infra.security.constant
object SecurityConstant {
const val BEARER = "Bearer"
const val WHITESPACE = " "
const val ANONYMOUS_USER = "anonymousUser"
const val ROLE_ANONYMOUS = "ROLE_ANONYMOUS"
const val JWT_AUTHORITIES_KEY = "authorities"
}

View File

@@ -1,8 +1,8 @@
package io.beaniejoy.dongnecafe.security.filter package io.beaniejoy.dongnecafe.infra.security.filter
import io.beaniejoy.dongnecafe.security.JwtTokenUtils import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.BEARER
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.WHITESPACE
import mu.KotlinLogging import mu.KotlinLogging
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
@@ -24,9 +24,11 @@ class JwtAuthenticationFilter(
val httpRequest = request as HttpServletRequest val httpRequest = request as HttpServletRequest
log.info { "[JwtAuthenticationFilter][${request.dispatcherType}] uri: ${request.requestURI}" } log.info { "[JwtAuthenticationFilter][${request.dispatcherType}] uri: ${request.requestURI}" }
// 인증 헤더에 토큰값 없는 경우 pass
getAccessToken(httpRequest)?.let { getAccessToken(httpRequest)?.let {
jwtTokenUtils.getAuthentication(it) jwtTokenUtils.getAuthentication(it)
}?.also { }?.also {
// 유효한 인증 토큰 존재하는 경우 SecurityContext 토큰값 저장
SecurityContextHolder.getContext().authentication = it SecurityContextHolder.getContext().authentication = it
log.info { "Valid Access Token [${it.name}]" } log.info { "Valid Access Token [${it.name}]" }
} }
@@ -34,6 +36,10 @@ class JwtAuthenticationFilter(
chain.doFilter(request, response) chain.doFilter(request, response)
} }
/**
* 인증 토큰 획득
* Authorization : Bearer [AUTH_TOKEN]
*/
private fun getAccessToken(request: HttpServletRequest): String? { private fun getAccessToken(request: HttpServletRequest): String? {
val bearer = request.getHeader(HttpHeaders.AUTHORIZATION) val bearer = request.getHeader(HttpHeaders.AUTHORIZATION)
?: return null ?: return null

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security.handler package io.beaniejoy.dongnecafe.infra.security.handler
import mu.KLogging import mu.KLogging
import org.springframework.security.access.AccessDeniedException import org.springframework.security.access.AccessDeniedException

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.security.handler package io.beaniejoy.dongnecafe.infra.security.handler
import mu.KLogging import mu.KLogging
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException

View File

@@ -1,6 +0,0 @@
package io.beaniejoy.dongnecafe.security.constant
object SecurityConstant {
const val BEARER = "Bearer"
const val WHITESPACE = " "
}

View File

@@ -0,0 +1,45 @@
package io.beaniejoy.dongnecafe.utils.logging
import com.google.gson.Gson
import io.beaniejoy.dongnecafe.infra.logging.constant.HttpClientIp
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper
import javax.servlet.http.HttpServletRequest
fun HttpServletRequest.getRequestHeaders(): String? {
val request = this
return Gson().toJson(
mutableMapOf<String, String?>().apply {
request.headerNames.toList().forEach {
this[it] = request.getHeader(it)
}
}
)
}
fun HttpServletRequest.getRequestParams(): String {
return this.parameterMap.mapValues {
it.value.joinToString(",")
}.entries.joinToString("&")
}
fun HttpServletRequest.getClientIp(): String {
HttpClientIp.values().forEach { clientIpHeader ->
this.getHeader(clientIpHeader.headerName).also {
if (it.isNullOrBlank().not() && "unknown".equals(it, true).not()) {
return it
}
}
}
return this.remoteAddr
}
fun ContentCachingRequestWrapper.getRequestBody(): String {
return this.contentAsByteArray.toString(Charsets.UTF_8)
}
// TODO: logging response body maximum size 고려
fun ContentCachingResponseWrapper.getResponseBody(): String {
return this.contentAsByteArray.toString(Charsets.UTF_8)
}

View File

@@ -1,5 +1,7 @@
package io.beaniejoy.dongnecafe.security package io.beaniejoy.dongnecafe.utils.security
import io.beaniejoy.dongnecafe.infra.security.SecurityUser
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.JWT_AUTHORITIES_KEY
import io.jsonwebtoken.Claims import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
@@ -24,9 +26,7 @@ class JwtTokenUtils(
private val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray()) private val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray())
private val validityTimeMilliSec: Long = validityTimeSec * 1000 private val validityTimeMilliSec: Long = validityTimeSec * 1000
companion object : KLogging() { companion object : KLogging()
const val AUTHORITIES_KEY = "authorities"
}
fun createToken(authentication: Authentication): String { fun createToken(authentication: Authentication): String {
val authenticatedMember = (authentication.principal as SecurityUser).member val authenticatedMember = (authentication.principal as SecurityUser).member
@@ -37,7 +37,7 @@ class JwtTokenUtils(
return Jwts.builder() return Jwts.builder()
.setSubject(authenticatedMember.email) .setSubject(authenticatedMember.email)
.claim(AUTHORITIES_KEY, authorities) .claim(JWT_AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS256) .signWith(key, SignatureAlgorithm.HS256)
.setExpiration(expirationDate) .setExpiration(expirationDate)
.compact() .compact()
@@ -47,7 +47,7 @@ class JwtTokenUtils(
val claims = getValidTokenBody(accessToken) val claims = getValidTokenBody(accessToken)
?: return null ?: return null
val authorities = claims[AUTHORITIES_KEY].toString().split(",") val authorities = claims[JWT_AUTHORITIES_KEY].toString().split(",")
.map { SimpleGrantedAuthority(it) } .map { SimpleGrantedAuthority(it) }
return UsernamePasswordAuthenticationToken(claims.subject, accessToken, authorities) return UsernamePasswordAuthenticationToken(claims.subject, accessToken, authorities)
@@ -62,10 +62,10 @@ class JwtTokenUtils(
.parseClaimsJws(accessToken) .parseClaimsJws(accessToken)
.body .body
} catch (e: ExpiredJwtException) { } catch (e: ExpiredJwtException) {
logger.error { "JWT access token expired. > Error: ${e.message}" } logger.info { "JWT access token expired. > Error: ${e.message}" }
null null
} catch (e: Exception) { } catch (e: Exception) {
logger.error { "JWT access token invalid. > Error: ${e.message}" } logger.info { "JWT access token invalid. > Error: ${e.message}" }
null null
} }
} }

View File

@@ -0,0 +1,15 @@
package io.beaniejoy.dongnecafe.utils.security
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ANONYMOUS_USER
import io.beaniejoy.dongnecafe.infra.security.constant.SecurityConstant.ROLE_ANONYMOUS
import org.springframework.security.core.Authentication
fun Authentication.getAuthPrincipal() : String? {
if (this.isAnonymous()) return null
return this.principal.toString()
}
fun Authentication.isAnonymous(): Boolean {
return this.principal == ANONYMOUS_USER || this.authorities.any { it.authority == ROLE_ANONYMOUS }
}

View File

@@ -1,8 +0,0 @@
package io.beaniejoy.dongnecafe.common.config
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
@Configuration
@EnableJpaAuditing
class AuditingConfig

View File

@@ -1,7 +1,9 @@
package io.beaniejoy.dongnecafe.common.config package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.JwtAuthenticationConfigurer import io.beaniejoy.dongnecafe.infra.security.config.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.security.JwtTokenUtils import io.beaniejoy.dongnecafe.utils.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAccessDeniedHandler
import io.beaniejoy.dongnecafe.infra.security.handler.CustomAuthenticationEntryPoint
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
@@ -18,15 +20,22 @@ class SecurityConfig {
@Autowired @Autowired
lateinit var jwtTokenUtils: JwtTokenUtils lateinit var jwtTokenUtils: JwtTokenUtils
@Autowired
lateinit var customAccessDeniedHandler: CustomAccessDeniedHandler
@Autowired
lateinit var customAuthenticationEntryPoint: CustomAuthenticationEntryPoint
@Bean @Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain { fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http return http
// only api 방식 인증 & 인가 적용 위해 csrf & formLogin 비활성화
.csrf().disable() .csrf().disable()
.formLogin().disable() .formLogin().disable()
// FIXME 임시 permitAll 설정
.authorizeRequests() .authorizeRequests()
.anyRequest().authenticated() // .anyRequest().authenticated()
.anyRequest().permitAll()
.and() .and()
.sessionManagement() .sessionManagement()
@@ -34,6 +43,11 @@ class SecurityConfig {
.and() .and()
.also { jwtAuthenticationConfigurer(it) } .also { jwtAuthenticationConfigurer(it) }
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint) // 인증 예외 entryPoint 적용
.accessDeniedHandler(customAccessDeniedHandler) // 인가 예외 handler 적용
.and()
.build() .build()
} }
@@ -43,6 +57,7 @@ class SecurityConfig {
.jwtTokenUtils(jwtTokenUtils) .jwtTokenUtils(jwtTokenUtils)
} }
// Security Filter 미적용 자원 설정
@Bean @Bean
fun webSecurityCustomizer(): WebSecurityCustomizer { fun webSecurityCustomizer(): WebSecurityCustomizer {
return WebSecurityCustomizer { web -> return WebSecurityCustomizer { web ->

View File

@@ -1,13 +0,0 @@
package io.beaniejoy.dongnecafe.common.entity
import org.springframework.data.domain.AuditorAware
import org.springframework.stereotype.Component
import java.util.*
@Component
class BaseEntityAuditorAware: AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
// TODO 추후 사용자 로그인 기능 추가되면 실제 등록한 사용자를 DB에 저장하는 방향으로 수정
return Optional.of("system")
}
}

View File

@@ -40,9 +40,10 @@ class CafeController(
*/ */
@GetMapping @GetMapping
fun searchCafeList( fun searchCafeList(
@RequestParam("name") name: String?,
@PageableDefault(sort = ["name"], direction = Sort.Direction.ASC, page = 0, size = 10) pageable: Pageable @PageableDefault(sort = ["name"], direction = Sort.Direction.ASC, page = 0, size = 10) pageable: Pageable
): ApplicationResponse<Page<CafeSearchInfo>> { ): ApplicationResponse<Page<CafeSearchInfo>> {
val searchCafes = cafeService.searchCafeList(pageable) val searchCafes = cafeService.searchCafeList(name, pageable)
return ApplicationResponse return ApplicationResponse
.success() .success()

View File

@@ -59,8 +59,8 @@ class CafeService(
} }
} }
fun searchCafeList(pageable: Pageable): Page<CafeSearchInfo> { fun searchCafeList(name: String?, pageable: Pageable): Page<CafeSearchInfo> {
val cafeList: Page<Cafe> = cafeRepository.findAll(pageable) val cafeList: Page<Cafe> = cafeRepository.findByNameContainingIgnoreCase(name, pageable)
return cafeList.map { CafeSearchInfo.of(it) } return cafeList.map { CafeSearchInfo.of(it) }
} }

View File

@@ -1,25 +0,0 @@
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
class JwtAuthenticationConfigurer :
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
private lateinit var jwtTokenUtils: JwtTokenUtils
override fun configure(http: HttpSecurity) {
http
.addFilterBefore(
JwtAuthenticationFilter(this.jwtTokenUtils),
UsernamePasswordAuthenticationFilter::class.java
)
}
fun jwtTokenUtils(jwtTokenUtils: JwtTokenUtils): JwtAuthenticationConfigurer {
this.jwtTokenUtils = jwtTokenUtils
return this
}
}

View File

@@ -5,15 +5,18 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
jpa: jpa:
hibernate: hibernate:
ddl-auto: none # flyway migration 사용 ddl-auto: none
properties: properties:
hibernate: hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true format_sql: true
show-sql: false show-sql: false
devtools: devtools:
livereload: livereload:
enabled: false # no use devtools' LiveReload Server enabled: false # no use devtools' LiveReload Server
security:
filter:
order: 10
logging: logging:
level: level:

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- Pattern -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5level) [%15.15t] [%X{request_id}] %clr(%-40.40logger{39}){cyan} : %m%n%wEx"/>
<!-- Request Thread Console Appender -->
<appender name="THREAD_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<springProfile name="local">
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe">
<appender-ref ref="THREAD_CONSOLE"/>
</logger>
<!-- Bootstrap class file -->
<logger additivity="false" level="INFO" name="io.beaniejoy.dongnecafe.DongneServiceApiApplicationKt">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>

View File

@@ -1,7 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.repository package io.beaniejoy.dongnecafe.domain.cafe.repository
import io.beaniejoy.dongnecafe.common.config.AuditingConfig import io.beaniejoy.dongnecafe.common.config.AuditingConfig
import io.beaniejoy.dongnecafe.common.entity.BaseEntityAuditorAware
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
import mu.KLogging import mu.KLogging
@@ -17,7 +16,6 @@ import org.springframework.data.repository.findByIdOrNull
@DataJpaTest( @DataJpaTest(
includeFilters = [ includeFilters = [
ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [AuditingConfig::class]), ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [AuditingConfig::class]),
ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [BaseEntityAuditorAware::class])
] ]
) )
internal class CafeRepositoryTest { internal class CafeRepositoryTest {

View File

@@ -1,7 +0,0 @@
#!/bin/bash
cd ..
./gradlew :db:flywayInfo
./gradlew :db:flywayValidate

60
script/migration-local.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
PROJECT_NAME="dongne-cafe-api"
PROJECT_ROOT_DIR=$(pwd)
FLYWAY_CONFIG_FILE="flyway-local.conf"
check_project_root_path() {
if [[ $PROJECT_ROOT_DIR != *"/$PROJECT_NAME" ]];
then
echo "Error >> move to project's root directory"
exit 1
fi
echo -e "Project's Root Directory: $PROJECT_ROOT_DIR\n"
}
flyway_version_check() {
echo "###################################"
echo "Using Flyway Version"
if ! flyway --version 2> /dev/null;
then
echo "Error >> Flyway Not Supported"
exit 1
fi
echo -e "###################################\n"
}
error_check() {
if [ $? -ne 0 ];
then
echo "Error >> $1 & Exit"
exit 1
fi
printf "\n"
}
flyway_migration_process() {
STEP_1="1. Flyway Info"
STEP_2="2. Flyway Migrate"
STEP_3="3. Flyway Validate"
echo $STEP_1
flyway info -configFiles="$PROJECT_ROOT_DIR/db/$FLYWAY_CONFIG_FILE"
error_check "$STEP_1"
echo $STEP_2
flyway migrate -configFiles="$PROJECT_ROOT_DIR/db/$FLYWAY_CONFIG_FILE" -outputType=json
error_check "$STEP_2"
echo $STEP_3
flyway validate -configFiles="$PROJECT_ROOT_DIR/db/$FLYWAY_CONFIG_FILE" -outputType=json
error_check "$STEP_3"
}
echo "########### [LOCAL] DB Migration ###########"
check_project_root_path
flyway_version_check
flyway_migration_process

View File

@@ -2,4 +2,3 @@ rootProject.name = "dongne-cafe-api"
include("dongne-common") include("dongne-common")
include("dongne-service-api") include("dongne-service-api")
include("dongne-account-api") include("dongne-account-api")
include("db")