-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 카카오, 구글 소셜 로그인, 유저관련 기능 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
88b2394
9d3d319
83a22be
95749ab
7d23adf
81b2118
42c86a3
32b2920
7fd055a
337f9db
334eb16
b354c5f
d5cd7a0
0e5ae6b
caeaff7
d05fc99
3f327eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,5 +3,9 @@ | |
|
|
||
| ## ✨ 상세 설명 | ||
|
|
||
| ## 🛠️ 추후 리팩토링 및 고도화 계획 | ||
|
|
||
| ## 📸 스크린샷 (선택) | ||
|
|
||
| ## 💬 리뷰 요구사항 | ||
| <!-- 특별히 확인했으면 하는 부분, 궁금한 사항 --> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,5 +12,19 @@ services: | |||||||||||||||||||||||||||||||||||||||||||||||
| PROD_DB_URL: ${PROD_DB_URL} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PROD_DB_USERNAME: ${PROD_DB_USERNAME} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PROD_DB_PASSWORD: ${PROD_DB_PASSWORD} | ||||||||||||||||||||||||||||||||||||||||||||||||
| SPRING_DATA_REDIS_HOST: redis | ||||||||||||||||||||||||||||||||||||||||||||||||
| SPRING_DATA_REDIS_PORT: 6379 | ||||||||||||||||||||||||||||||||||||||||||||||||
| ports: | ||||||||||||||||||||||||||||||||||||||||||||||||
| - "8080:8080" | ||||||||||||||||||||||||||||||||||||||||||||||||
| - "8080:8080" | ||||||||||||||||||||||||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||||||||||||||||||||||||
| - redis | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| redis: | ||||||||||||||||||||||||||||||||||||||||||||||||
| image: redis:7-alpine | ||||||||||||||||||||||||||||||||||||||||||||||||
| container_name: ongil-redis | ||||||||||||||||||||||||||||||||||||||||||||||||
| ports: | ||||||||||||||||||||||||||||||||||||||||||||||||
| - "6379:6379" | ||||||||||||||||||||||||||||||||||||||||||||||||
| restart: always | ||||||||||||||||||||||||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||||||||||||||||||||||||
| - ./redis_data:/data | ||||||||||||||||||||||||||||||||||||||||||||||||
| command: ["redis-server", "--appendonly", "yes"] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redis 인증이 구성되지 않아 보안 위험이 있습니다. Redis 서비스가 비밀번호 없이 구성되어 있으며, 포트 6379가 호스트에 직접 노출되어 있습니다. 이는 프로덕션 환경에서 심각한 보안 위험을 초래합니다:
🔐 Redis 인증 및 보안 강화 설정 redis:
image: redis:7-alpine
container_name: ongil-redis
ports:
- - "6379:6379"
+ - "127.0.0.1:6379:6379" # 로컬호스트만 접근 가능하도록 제한
restart: always
volumes:
- ./redis_data:/data
- command: ["redis-server", "--appendonly", "yes"]
+ command:
+ - redis-server
+ - --appendonly yes
+ - --requirepass ${REDIS_PASSWORD}
+ - --maxmemory 256mb
+ - --maxmemory-policy allkeys-lru그리고 backend 서비스에 Redis 비밀번호 환경 변수를 추가하세요: environment:
SPRING_PROFILES_ACTIVE: prod
PROD_DB_URL: ${PROD_DB_URL}
PROD_DB_USERNAME: ${PROD_DB_USERNAME}
PROD_DB_PASSWORD: ${PROD_DB_PASSWORD}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
+ SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD}
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.ongil.backend.domain.auth.client.google; | ||
|
|
||
| import org.springframework.cloud.openfeign.FeignClient; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.response.GoogleUserInfoResDto; | ||
|
|
||
| @FeignClient(name = "googleApiClient", url = "https://www.googleapis.com") | ||
| public interface GoogleApiClient { | ||
| @GetMapping("/oauth2/v3/userinfo") | ||
| GoogleUserInfoResDto getUserInfo(@RequestHeader("Authorization") String accessToken); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.ongil.backend.domain.auth.client.google; | ||
|
|
||
| import org.springframework.cloud.openfeign.FeignClient; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.response.GoogleTokenResDto; | ||
|
|
||
| @FeignClient(name = "googleAuthClient", url = "https://oauth2.googleapis.com") | ||
| public interface GoogleAuthClient { | ||
| @PostMapping(value = "/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||
| GoogleTokenResDto getAccessToken( | ||
| @RequestParam("grant_type") String grantType, | ||
| @RequestParam("client_id") String clientId, | ||
| @RequestParam("client_secret") String clientSecret, | ||
| @RequestParam("redirect_uri") String redirectUri, | ||
| @RequestParam("code") String code | ||
| ); | ||
|
marshmallowing marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.ongil.backend.domain.auth.client.kakao; | ||
|
|
||
|
|
||
| import org.springframework.cloud.openfeign.FeignClient; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.response.KakaoUserInfoResDto; | ||
|
|
||
| // 유저 정보/API용 (kapi.kakao.com) | ||
| @FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") | ||
| public interface KakaoApiClient { | ||
| @GetMapping("/v2/user/me") | ||
| KakaoUserInfoResDto getUserInfo(@RequestHeader("Authorization") String bearerToken); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.ongil.backend.domain.auth.client.kakao; | ||
|
|
||
| import org.springframework.cloud.openfeign.FeignClient; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.response.KakaoTokenResDto; | ||
|
|
||
| // 인증/토큰용 (kauth.kakao.com) | ||
| @FeignClient(name = "kakaoAuthClient", url = "https://kauth.kakao.com") | ||
| public interface KakaoAuthClient { | ||
| @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | ||
| KakaoTokenResDto getAccessToken( | ||
| @RequestParam("grant_type") String grantType, | ||
| @RequestParam("client_id") String clientId, | ||
| @RequestParam("redirect_uri") String redirectUri, | ||
| @RequestParam("code") String code, | ||
| @RequestParam("client_secret") String clientSecret | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package com.ongil.backend.domain.auth.controller; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.validation.annotation.Validated; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.request.TokenRefreshReqDto; | ||
| import com.ongil.backend.domain.auth.dto.response.AuthResDto; | ||
| import com.ongil.backend.domain.auth.dto.response.TokenRefreshResDto; | ||
| import com.ongil.backend.domain.auth.service.AuthService; | ||
| import com.ongil.backend.domain.auth.service.GoogleLoginService; | ||
| import com.ongil.backend.domain.auth.service.KakaoLoginService; | ||
| import com.ongil.backend.global.common.dto.DataResponse; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Validated | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/auth") | ||
| public class AuthController { | ||
|
|
||
| private final AuthService authService; | ||
| private final KakaoLoginService kakaoLoginService; | ||
| private final GoogleLoginService googleLoginService; | ||
|
|
||
| @PostMapping("/oauth/kakao") | ||
| @Operation(summary = "카카오 회원가입/로그인 API", description = "인가코드(code)로 카카오 토큰 교환 후, 우리 서비스 JWT 발급") | ||
| public ResponseEntity<DataResponse<AuthResDto>> kakaoLogin( | ||
| @Valid @RequestParam("code") @NotBlank String code | ||
| ) { | ||
| AuthResDto res = kakaoLoginService.kakaoLogin(code); | ||
| return ResponseEntity.ok(DataResponse.from(res)); | ||
| } | ||
|
|
||
| @GetMapping("/oauth/google") | ||
| @Operation(summary = "구글 회원가입/로그인 API", description = "인가코드(code)로 구글 토큰 교환 후, 우리 서비스 JWT 발급") | ||
| public ResponseEntity<DataResponse<AuthResDto>> googleLogin( | ||
| @Valid @RequestParam("code") @NotBlank String code | ||
| ) { | ||
| AuthResDto res = googleLoginService.googleLogin(code); | ||
| return ResponseEntity.ok(DataResponse.from(res)); | ||
| } | ||
|
|
||
| @PostMapping("/token/refresh") | ||
| @Operation(summary = "Access/Refresh Token 재발급 API", description = "만료된 accessToken을 refreshToken을 통해 재발급") | ||
| public ResponseEntity<DataResponse<TokenRefreshResDto>> refresh( | ||
| @Valid @RequestBody TokenRefreshReqDto request | ||
| ) { | ||
| TokenRefreshResDto res = authService.refreshAccessToken(request.refreshToken()); | ||
| return ResponseEntity.ok(DataResponse.from(res)); | ||
| } | ||
|
|
||
| @PostMapping("/logout") | ||
| @Operation(summary = "로그아웃 API", description = "Redis에 저장된 리프레시 토큰을 삭제하여 로그아웃 처리") | ||
| public ResponseEntity<DataResponse<String>> logout( | ||
| @Parameter(hidden = true) @AuthenticationPrincipal Long userId | ||
| ) { | ||
| authService.logout(userId); | ||
| return ResponseEntity.ok(DataResponse.from("로그아웃 되었습니다.")); | ||
| } | ||
|
marshmallowing marked this conversation as resolved.
|
||
|
|
||
| @DeleteMapping("/withdraw") | ||
| @Operation(summary = "회원 탈퇴 API", description = "계정 삭제 및 리프레시 토큰 파기") | ||
| public ResponseEntity<DataResponse<String>> withdraw( | ||
| @Parameter(hidden = true) @AuthenticationPrincipal Long userId | ||
| ) { | ||
| authService.withdraw(userId); | ||
| return ResponseEntity.ok(DataResponse.from("회원 탈퇴가 완료되었습니다.")); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.ongil.backend.domain.auth.converter; | ||
|
|
||
| import com.ongil.backend.domain.auth.dto.response.AuthResDto; | ||
| import com.ongil.backend.domain.user.entity.User; | ||
|
|
||
| import lombok.experimental.UtilityClass; | ||
|
|
||
| @UtilityClass | ||
| public class AuthConverter { | ||
|
|
||
| public static AuthResDto toResponse(User user, String accessToken, String refreshToken, | ||
| boolean isNewUser, long accessExpMs) { | ||
| return AuthResDto.builder() | ||
| .userId(user.getId()) | ||
| .accessToken(accessToken) | ||
| .refreshToken(refreshToken) | ||
| .loginType(user.getLoginType()) | ||
| .isNewUser(isNewUser) | ||
| .expires_in((int) (accessExpMs / 1000)) // 초 단위 | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.ongil.backend.domain.auth.dto.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public record TokenRefreshReqDto( | ||
| @Schema(description = "리프레시토큰") | ||
| @NotNull | ||
| String refreshToken | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| import com.ongil.backend.domain.auth.entity.LoginType; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
|
|
||
| @Builder | ||
| public record AuthResDto( | ||
|
|
||
| @Schema(description = "유저아이디") | ||
| Long userId, | ||
|
|
||
| @Schema(description = "액세스토큰") | ||
| @NotNull | ||
| String accessToken, | ||
|
|
||
| @Schema(description = "리프레시토큰") | ||
| @NotNull | ||
| String refreshToken, | ||
|
|
||
| @Schema(description = "로그인 타입") | ||
| @NotNull | ||
| LoginType loginType, | ||
|
|
||
| @Schema(description = "회원가입 여부(첫 로그인 여부)") | ||
| @NotNull | ||
| Boolean isNewUser, | ||
|
|
||
| @Schema(description = "액세스 토큰 만료 시간") | ||
| @NotNull | ||
| Integer expires_in | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| public record GoogleTokenResDto( | ||
| @JsonProperty("access_token") String accessToken, | ||
| @JsonProperty("expires_in") Integer expiresIn, | ||
| @JsonProperty("token_type") String tokenType, | ||
| @JsonProperty("id_token") String idToken | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| public record GoogleUserInfoResDto( | ||
| String sub, // 구글의 고유 식별자 (카카오의 id 역할) | ||
| String name, | ||
| String email, | ||
| String picture | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| public record KakaoTokenResDto( | ||
| @JsonProperty("access_token") String accessToken, | ||
| @JsonProperty("token_type") String tokenType, | ||
| @JsonProperty("refresh_token") String refreshToken, | ||
| @JsonProperty("expires_in") Integer expiresIn, | ||
| @JsonProperty("scope") String scope, | ||
| @JsonProperty("refresh_token_expires_in") Integer refreshTokenExpiresIn | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| public record KakaoUserInfoResDto( | ||
| Long id, | ||
| @JsonProperty("kakao_account") KakaoAccount kakaoAccount | ||
| ) { | ||
| public record KakaoAccount( | ||
| String email, | ||
| Profile profile | ||
| ) { | ||
| public record Profile( | ||
| String nickname, | ||
| @JsonProperty("thumbnail_image_url") String profileImg | ||
| ) {} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.ongil.backend.domain.auth.dto.response; | ||
|
|
||
| public record TokenRefreshResDto( | ||
| String accessToken, | ||
| String refreshToken | ||
| ) { | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.