Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ jobs:
client-secret: \${KAKAO_CLIENT_SECRET_KEY}
client-name: kakao
authorization-grant-type: authorization_code
redirect-uri: \${KAKAO_BASE_URL}/login/oauth2/code/kakao
# redirect-uri: \${KAKAO_BASE_URL}/login/oauth2/code/kakao
redirect-uri: http://localhost:3000/login
scope:
- profile_nickname
- profile_image
Expand All @@ -83,21 +84,26 @@ jobs:
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
# Uncomment Redis if needed
# data:
# redis:
# host: \${REDIS_HOST}
# port: \${REDIS_PORT}
# repositories:
# enabled: false
data:
redis:
host: \${REDIS_HOST}
port: \${REDIS_PORT}
repositories:
enabled: false
logging:
level:
bst.bobsoolting: DEBUG
org.springframework.security: TRACE
org.springframework.security: INFO
org.mybatis: DEBUG
java.sql: DEBUG
org.springframework.web: INFO
org.springframework.boot.actuate: INFO
mybatis:
mapper-locations: classpath:/bst/bobsoolting/mapper/**/*.xml
configuration:
default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
map-underscore-to-camel-case: true
type-handlers-package: bst.bobsoolting.util
cors:
allowedOrigins: \${CORS_ALLOWED_ORIGINS}
allowedHeaders: "*"
Expand All @@ -108,10 +114,12 @@ jobs:
client-id: \${KAKAO_CLIENT_KEY}
client-secret: \${KAKAO_CLIENT_SECRET_KEY}
use-basic-authentication-with-access-code-grant: true
oauth2RedirectUrl: \${KAKAO_BASE_URL}/login/oauth2/code/kakao
# oauth2RedirectUrl: \${KAKAO_BASE_URL}/login/oauth2/code/kakao
oauth2RedirectUrl: http://localhost:3000/login
jwt:
secret: \${JWT_SECRET}
access-token-validity: 86400000 # 24시간
access-token-validity: 1800000 # 30분
refresh-token-validity: 604800000 # 7일
EOL

- name: Set application.yml with Secrets
Expand All @@ -125,11 +133,11 @@ jobs:
DB_NAME: ${{ secrets.DB_NAME }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
REDIS_HOST: ${{ secrets.REDIS_HOST }}
REDIS_PORT: ${{ secrets.REDIS_PORT }}
KAKAO_CLIENT_KEY: ${{ secrets.KAKAO_CLIENT_KEY }}
KAKAO_CLIENT_SECRET_KEY: ${{ secrets.KAKAO_CLIENT_SECRET_KEY }}
KAKAO_BASE_URL: ${{ secrets.KAKAO_BASE_URL }}
# REDIS_HOST: ${{ secrets.REDIS_HOST }}
# REDIS_PORT: ${{ secrets.REDIS_PORT }}
CORS_ALLOWED_ORIGINS: ${{ secrets.CORS_ALLOWED_ORIGINS }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
import bst.bobsoolting.member.command.application.mapper.MemberConverter;
import bst.bobsoolting.member.command.application.service.MemberCommandService;
import bst.bobsoolting.member.command.domain.vo.request.RequestAdditionalRegisterVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestKakaoAuthVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestRefreshTokenVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestUpdateProfileVO;
import bst.bobsoolting.member.command.domain.vo.response.ResponseCreateMemberVO;
import bst.bobsoolting.member.command.domain.vo.response.ResponseProfileVO;
import bst.bobsoolting.security.JwtTokenProvider;
import bst.bobsoolting.util.SecurityUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@RestController
Expand All @@ -27,26 +32,70 @@ public class MemberCommandController implements MemberCommandControllerDocs {
private final MemberCommandService memberCommandService;
private final MemberConverter memberConverter;
private final SecurityUtil securityUtil;
private final JwtTokenProvider jwtTokenProvider;

@PostMapping("/auth/kakao")
public ResponseEntity<?> kakaoLogin(@RequestBody Map<String, String> request) {
String code = request.get("code");
public ResponseEntity<Map<String, String>> kakaoLogin(@RequestBody RequestKakaoAuthVO request) {
String code = request.getCode();
if (code == null || code.isEmpty()) {
log.error("인가 코드가 전달되지 않았습니다.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("error", "인가 코드가 없습니다."));
}

log.info("카카오 로그인 요청. 인가 코드: {}", code);
try {
String accessToken = memberCommandService.getKakaoAccessToken(code);
MemberDTO member = memberCommandService.getKakaoUserInfo(accessToken);
ResponseCreateMemberVO response = memberConverter.fromEntityToCreateVO(member);
String kakaoAccessToken = memberCommandService.getKakaoAccessToken(code);
MemberDTO member = memberCommandService.getKakaoUserInfo(kakaoAccessToken);

return ResponseEntity.ok(response);
String serverJwtToken = jwtTokenProvider.generateAccessToken(member.getKakaoId());
String refreshToken = jwtTokenProvider.generateRefreshToken(member.getKakaoId());

memberCommandService.storeRefreshToken(member.getKakaoId(), refreshToken);

Map<String, String> responseMap = new HashMap<>();
responseMap.put("access_token", serverJwtToken);
responseMap.put("refresh_token", refreshToken);

return ResponseEntity.ok(responseMap);
} catch (CommonException e) {
log.error("카카오 로그인 오류: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("error", e.getMessage()));
} catch (Exception e) {
log.error("예상치 못한 오류 발생", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("카카오 로그인 처리 중 오류 발생");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "카카오 로그인 처리 중 오류 발생"));
}
}

@PostMapping("/auth/refresh")
public ResponseEntity<?> refreshAccessToken(@RequestBody RequestRefreshTokenVO request) {
String refreshToken = request.getRefresh_token();

if (refreshToken == null || refreshToken.isEmpty() || jwtTokenProvider.isTokenExpired(refreshToken)) {
log.warn("유효하지 않은 Refresh Token 요청 감지");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("유효하지 않은 Refresh Token입니다.");
}

Claims claims = jwtTokenProvider.parseToken(refreshToken);
String kakaoId = claims.getSubject();

String storedRefreshToken = memberCommandService.getRefreshToken(kakaoId);

if (storedRefreshToken == null) {
log.warn("Redis에서 Refresh Token Not Found - Kakao ID: {}", kakaoId);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh Token이 만료되었거나 존재하지 않습니다.");
}
if (!refreshToken.equals(storedRefreshToken)) {
log.warn("Refresh Token 불일치 - Kakao ID: {}", kakaoId);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh Token이 일치하지 않습니다.");
}

String newAccessToken = jwtTokenProvider.generateAccessToken(kakaoId);
Map<String, String> response = new HashMap<>();
response.put("access_token", newAccessToken);

return ResponseEntity.ok(response);
}

@PatchMapping("/complete")
public ResponseEntity<ResponseCreateMemberVO> completeRegistration(@RequestBody RequestAdditionalRegisterVO info, @RequestHeader(HttpHeaders.AUTHORIZATION) String token) {
String kakaoId = securityUtil.getKakaoIdFromToken(token.replace("Bearer ", ""));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
package bst.bobsoolting.member.command.application.controller.docs;

import bst.bobsoolting.member.command.domain.vo.request.RequestAdditionalRegisterVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestKakaoAuthVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestRefreshTokenVO;
import bst.bobsoolting.member.command.domain.vo.request.RequestUpdateProfileVO;
import bst.bobsoolting.member.command.domain.vo.response.ResponseCreateMemberVO;
import bst.bobsoolting.member.command.domain.vo.response.ResponseProfileVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Tag(name = "회원 API", description = "회원 관리 관련 API 문서")
@RequestMapping("api/member")
public interface MemberCommandControllerDocs {

@Operation(summary = "카카오 로그인", description = "카카오 OAuth 로그인 및 회원 자동 등록 API (우선 redirect-uri 에서 인가 코드를 받아온 뒤 사용")
@Operation(summary = "카카오 로그인", description = "카카오 OAuth 로그인 및 회원 자동 등록 API (우선 redirect-uri 에서 인가 코드를 받아온 뒤 사용)")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/auth/kakao")
ResponseEntity<?> kakaoLogin(@RequestBody Map<String, String> request);
ResponseEntity<?> kakaoLogin(
@RequestBody @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(schema = @Schema(implementation = RequestKakaoAuthVO.class))
) RequestKakaoAuthVO request);

@Operation(summary = "액세스 토큰 갱신", description = "리프레시 토큰을 이용하여 새 액세스 토큰을 발급받음")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "토큰 갱신 성공"),
@ApiResponse(responseCode = "401", description = "유효하지 않은 리프레시 토큰"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/auth/refresh")
ResponseEntity<?> refreshAccessToken(
@RequestBody @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(schema = @Schema(implementation = RequestRefreshTokenVO.class))
) RequestRefreshTokenVO request);

@Operation(summary = "추가 회원가입 정보 등록", description = "추가 회원가입 정보를 등록하는 API (신규 회원 추가 정보 입력)")
@ApiResponses({
Expand All @@ -34,7 +51,9 @@ public interface MemberCommandControllerDocs {
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PatchMapping("/complete")
ResponseEntity<ResponseCreateMemberVO> completeRegistration(@RequestBody RequestAdditionalRegisterVO info, @RequestHeader(HttpHeaders.AUTHORIZATION) String token);
ResponseEntity<ResponseCreateMemberVO> completeRegistration(
@RequestBody RequestAdditionalRegisterVO info,
@RequestHeader(HttpHeaders.AUTHORIZATION) String token);

@Operation(summary = "회원 정보 수정", description = "회원 프로필을 수정하는 API")
@ApiResponses({
Expand All @@ -43,5 +62,7 @@ public interface MemberCommandControllerDocs {
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PatchMapping("/profile")
ResponseEntity<ResponseProfileVO> updateProfile(@RequestBody RequestUpdateProfileVO updateInfo, @RequestHeader(HttpHeaders.AUTHORIZATION) String token);
}
ResponseEntity<ResponseProfileVO> updateProfile(
@RequestBody RequestUpdateProfileVO updateInfo,
@RequestHeader(HttpHeaders.AUTHORIZATION) String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ public interface MemberCommandService {
MemberDTO getKakaoUserInfo(String accessToken);

@Transactional
MemberDTO createOrUpdateMember(String kakaoId, String nickname);
MemberDTO createOrUpdateMember(String kakaoId);

MemberDTO updateMemberAdditionalInfo(String kakaoId, RequestAdditionalRegisterVO info);

void storeRefreshToken(String kakaoId, String refreshToken);

String getRefreshToken(String kakaoId);
}
Loading
Loading