Skip to content

Commit d9a96be

Browse files
authored
✨ User CRUD, 로컬 로그인, 구글 로그인 구현 (#6)
* feat: user, auth * feat: local login * feat: google login * feat: frontend url setting * feat: frontend url setting * fix: spring properties * fix: resolve some reviews * fix: resolve reviews * fix: cd * fix: reviews
1 parent a2382fa commit d9a96be

37 files changed

+1071
-23
lines changed

.github/workflows/cd.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ env:
1111
DB_NAME: mydatabase
1212
DB_USER: ${{ secrets.DB_USER }}
1313
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
14+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
15+
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
1416

1517
jobs:
1618
build-and-push:
@@ -23,6 +25,15 @@ jobs:
2325
- name: Checkout code
2426
uses: actions/checkout@v4
2527

28+
- name: Generate Dynamic JWT Secret
29+
run: |
30+
# Generate a 64-byte hex string
31+
DYNAMIC_JWT=$(openssl rand -hex 64)
32+
# Mask it so it doesn't show up in GitHub logs
33+
echo "::add-mask::$DYNAMIC_JWT"
34+
# Save it to the environment for subsequent steps
35+
echo "JWT_SECRET=$DYNAMIC_JWT" >> $GITHUB_ENV
36+
2637
- name: Set up Docker Buildx
2738
uses: docker/setup-buildx-action@v3
2839

@@ -63,7 +74,7 @@ jobs:
6374
host: ${{ secrets.EC2_HOST }}
6475
username: ${{ secrets.EC2_USERNAME }}
6576
key: ${{ secrets.EC2_SSH_KEY }}
66-
envs: REGISTRY,IMAGE_NAME,DB_NAME,DB_USER,DB_PASSWORD,GITHUB_ACTOR
77+
envs: REGISTRY,IMAGE_NAME,DB_NAME,DB_USER,DB_PASSWORD,GITHUB_ACTOR,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,JWT_SECRET
6778
script: |
6879
# Define the image string inside the script for safety
6980
IMAGE="${REGISTRY}/${IMAGE_NAME}:latest"
@@ -87,6 +98,9 @@ jobs:
8798
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/$DB_NAME \
8899
-e DB_USER="$DB_USER" \
89100
-e DB_PASSWORD="$DB_PASSWORD" \
101+
-e GOOGLE_CLIENT_ID="$GOOGLE_CLIENT_ID" \
102+
-e GOOGLE_CLIENT_SECRET="$GOOGLE_CLIENT_SECRET" \
103+
-e JWT_SECRET="$JWT_SECRET" \
90104
$IMAGE
91105
92106
echo "== PRUNE =="

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [ "main" ]
88

99
jobs:
10-
build:
10+
lint-and-test:
1111

1212
runs-on: ubuntu-latest
1313

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ out/
3838

3939
### Kotlin ###
4040
.kotlin
41+
/.env

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ RUN ./gradlew bootJar
99
# 5. 포트 노출 (컨테이너가 8080 포트를 사용함을 명시)
1010
EXPOSE 8080
1111
# 6. 실행 명령어 (컨테이너 시작 시 실행될 명령어)
12-
ENTRYPOINT exec java $JAVA_OPTS -jar build/libs/team2-server-0.0.1-SNAPSHOT.jar
12+
ENTRYPOINT exec java $JAVA_OPTS -jar build/libs/team2-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,25 @@
11
# 23-5-team2-server
2-
와플스튜디오 23.5기 2조 server
2+
와플스튜디오 23.5기 2조 server
3+
4+
# env
5+
6+
루트 경로에 다음의 `.env` 파일을 작성합니다.
7+
8+
```env
9+
GOOGLE_CLIENT_ID=YOUR-GOOGLE-CLIENT-ID
10+
GOOGLE_CLIENT_SECRET=YOUR-GOOGLE-CLIENT-SECRET
11+
```
12+
13+
| Key Name | Description |
14+
|------------------------|--------------------------|
15+
| `GOOGLE_CLIENT_ID` | 구글 OAuth2 Client ID |
16+
| `GOOGLE_CLIENT_SECRET` | 구글 OAuth2 Client Secret |
17+
18+
# Auth
19+
20+
- AUTH-TOKEN 쿠키를 사용합니다.
21+
22+
## Google OAuth2
23+
24+
- `/oauth2/authorization/google` 로 GET 요청을 보내면 구글 로그인 과정이 진행됩니다.
25+
- 로그인 성공 / 실패 모두 프론트엔드 루트 경로로 리다이렉트됩니다.

build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,21 @@ dependencies {
3131
implementation("org.springframework.boot:spring-boot-starter-flyway")
3232
implementation("org.springframework.boot:spring-boot-starter-webmvc")
3333
implementation("org.springframework.boot:spring-boot-starter-actuator")
34+
implementation("org.springframework.boot:spring-boot-starter-security")
35+
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
36+
implementation("io.github.cdimascio:dotenv-kotlin:6.4.2")
37+
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
38+
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
3439
implementation("com.mysql:mysql-connector-j")
3540
implementation("org.flywaydb:flyway-mysql")
3641
implementation("org.jetbrains.kotlin:kotlin-reflect")
37-
implementation("tools.jackson.module:jackson-module-kotlin")
3842
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
3943
implementation("org.jsoup:jsoup:1.17.2")
4044
compileOnly("org.projectlombok:lombok")
4145
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
4246
runtimeOnly("com.mysql:mysql-connector-j")
47+
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
48+
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
4349
annotationProcessor("org.projectlombok:lombok")
4450
testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test")
4551
testImplementation("org.springframework.boot:spring-boot-starter-flyway-test")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.wafflestudio.team2server
2+
3+
import org.springframework.http.HttpStatus
4+
import org.springframework.http.HttpStatusCode
5+
6+
open class DomainException(
7+
// client 와 약속된 Application Error 에 대한 코드 필요 시 Enum 으로 관리하자.
8+
val errorCode: Int,
9+
// HTTP Status Code, 비어있다면 500 이다.
10+
val httpErrorCode: HttpStatusCode = HttpStatus.INTERNAL_SERVER_ERROR,
11+
val msg: String,
12+
cause: Throwable? = null,
13+
) : RuntimeException(msg, cause) {
14+
override fun toString(): String = "DomainException(msg='$msg', errorCode=$errorCode, httpErrorCode=$httpErrorCode)"
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.wafflestudio.team2server
2+
3+
import org.springframework.http.ResponseEntity
4+
import org.springframework.web.bind.annotation.ControllerAdvice
5+
import org.springframework.web.bind.annotation.ExceptionHandler
6+
7+
@ControllerAdvice
8+
class GlobalControllerExceptionHandler {
9+
@ExceptionHandler(DomainException::class)
10+
fun handle(exception: DomainException): ResponseEntity<Map<String, Any>> =
11+
ResponseEntity
12+
.status(exception.httpErrorCode)
13+
.body(mapOf("error" to exception.msg, "errorCode" to exception.errorCode))
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.wafflestudio.team2server.config
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
5+
import org.springframework.context.annotation.Bean
6+
import org.springframework.context.annotation.Configuration
7+
8+
@Configuration
9+
class JacksonConfig {
10+
@Bean
11+
fun objectMapper(): ObjectMapper = ObjectMapper().registerKotlinModule()
12+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.wafflestudio.team2server.config
2+
3+
import com.wafflestudio.team2server.user.OAuth2SuccessHandler
4+
import com.wafflestudio.team2server.user.service.GoogleOAuth2UserService
5+
import org.springframework.beans.factory.annotation.Value
6+
import org.springframework.context.annotation.Bean
7+
import org.springframework.context.annotation.Configuration
8+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
9+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
10+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
11+
import org.springframework.security.web.SecurityFilterChain
12+
13+
@EnableWebSecurity
14+
@Configuration
15+
class SecurityConfig(
16+
private val googleOAuth2UserService: GoogleOAuth2UserService,
17+
private val oAuth2SuccessHandler: OAuth2SuccessHandler,
18+
@Value("\${app.frontend.url}") private val frontendUrl: String,
19+
) {
20+
@Bean
21+
fun filterChain(http: HttpSecurity): SecurityFilterChain {
22+
// it does not use authorizeHttpRequests
23+
// requiring authorization is handled at the controller level
24+
// when use @LoggedInUser, if there is no authorization, UserArgumentResolver will return 401.
25+
http
26+
.csrf { it.disable() }
27+
.httpBasic { it.disable() }
28+
.formLogin { it.disable() }
29+
.oauth2Login { oauth2 ->
30+
oauth2
31+
.userInfoEndpoint { it.userService(googleOAuth2UserService) }
32+
.successHandler(oAuth2SuccessHandler)
33+
.failureUrl(frontendUrl)
34+
}
35+
return http.build()
36+
}
37+
38+
@Bean
39+
fun bcryptPasswordEncoder(): BCryptPasswordEncoder = BCryptPasswordEncoder()
40+
}

0 commit comments

Comments
 (0)