- 🛠 GitHub 연동 기능 구현기2025년 05월 15일 01시 01분 01초에 업로드 된 글입니다.작성자: do_hyuk728x90반응형
1. 처음에 기획한 방향
처음 의도는 이랬다:
- 사용자가 내 블로그 플랫폼에서 글을 작성한 후, 버튼 하나로 자신의 GitHub 저장소에 자동으로 Push하는 기능을 만들고 싶었음.
- 이미 구글 로그인으로 회원 식별이 되어 있기 때문에, GitHub은 로그인 대신 "인가(authorization)"만 하도록 구현하고자 함.
- 사용자 GitHub에 access token을 발급받아 DB에 저장하고, 향후 글 Push 시 활용.
- Push 버튼을 누르면 다음 절차를 밟음:
- 사용자에게 GitHub 인가 요청 → GitHub 로그인 및 승인
code
를 받은 후 백엔드에 전송- 백엔드에서 access token을 받아 DB에 저장
- 이후 GitHub API를 통해 사용자의
ori
저장소에 글을 업로드
2. 시행착오와 막혔던 부분
2-1. GitHub OAuth 흐름 헷갈림
처음에는 access token을 프론트에서 직접 받아서 처리할까 고민했지만,
보안상 위험하고 CORS 이슈도 있어 백엔드에서 처리하기로 함.
2-2.
code
전달 형식 문제await axiosInstance.post('/v1/api/github/callback', { code });
이렇게 보냈더니 GitHub에서 다음과 같은 에러가 발생함
{error: 'bad_verification_code', ...}
🔍 원인
- code가 이중 JSON으로 감싸져 "{\"code\":\"...\"}" 형태로 전달됨
- 이로 인해 GitHub은 이를 유효하지 않은 code로 판단하고 거절
- 서버에서 잘못된 형식으로 받음
@PostMapping("/callback") public ResponseEntity<Long> callbackAndSave(@RequestParam Stirng code) { ... }
String 타입으로 바로 받으려 했으나 Json 형태로 왔기 때문에 Json 형태 그대로 String으로 표현되어 온 것.
✅ 해결
@PostMapping("/callback") public ResponseEntity<Long> callbackAndSave(@RequestBody GithubCodeRequestDto requestDto) { String accessToken = githubService.getAccessToken(requestDto); return ResponseEntity.ok().body(githubService.save(accessToken)); }
3. 최종적으로 완성한 흐름
- React MyPage에서 GitHub 연동 버튼 클릭 시, https://github.com/login/oauth/authorize?... 주소로 리다이렉트
- GitHub에서 인가 승인 → 프론트엔드로 code 반환
- React가 이 code를 Spring Boot 백엔드에 POST 요청으로 전달
- 백엔드는 GitHub에 access token 요청 → 성공 시 사용자 DB에 저장
- 연동 완료 후 /My 페이지로 이동 및 완료 알림
최종 코드
Entity
@Getter @NoArgsConstructor @Entity public class GithubToken extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; private String githubToken; @Builder public GithubToken(String email, String githubToken) { this.email = email; this.githubToken = githubToken; } public Long update(String githubToken) { this.githubToken = githubToken; return this.id; } }
Controller
private final GithubService githubService; @Operation(description = "깃헙 인증 토큰이 있는지 확인") @GetMapping("/token/check") public ResponseEntity<Boolean> checkToken(@AuthenticationPrincipal UserDetails userDetails) { return ResponseEntity.ok().body(githubService.tokenCheck(userDetails.getUsername())); } @Operation(description = "깃헙 리다이렉트 된 일회성 인증 코드로 인증 토큰 발급 후 저장") @PostMapping("/callback") public ResponseEntity<Long> callbackAndSave(@RequestBody GithubCodeRequestDto requestDto, @AuthenticationPrincipal UserDetails userDetails) { String accessToken = githubService.getAccessToken(requestDto); return ResponseEntity.ok().body(githubService.save(accessToken, userDetails.getUsername())); }
Service
@Value("${github.client-id}") private String clientId; @Value("${github.client-secret}") private String clientSecret; private final GithubTokenRepository githubTokenRepository; private final WebClient webClient; @Transactional public Long save(String accessToken, String email) { GithubToken githubToken = GithubToken.builder() .githubToken(accessToken) .email(email) .build(); return githubTokenRepository.save(githubToken).getId(); } @Transactional public Long update(GithubTokenRequestDto requestDto) { GithubToken githubToken = githubTokenRepository.findByEmail(requestDto.email()).orElseThrow( () -> new NotFoundException(ErrorCode.NOT_FOUND_GITHUB_TOKEN) ); return githubToken.update(requestDto.githubToken()); } public boolean tokenCheck(String email) { return githubTokenRepository.existsByEmail(email); } public String getAccessToken(GithubCodeRequestDto requestDto) { Map<String, String> body = Map.of( "client_id", clientId, "client_secret", clientSecret, "code", requestDto.code() ); Map response = webClient.post() .uri("https://github.com/login/oauth/access_token") .header(HttpHeaders.ACCEPT, "application/json") .bodyValue(body) .retrieve() .bodyToMono(Map.class) .block(); return (String) response.get("access_token"); }
🔐 보안 고려
- access token은 로컬스토리지에 저장하지 않고 서버 DB에만 저장함
- GitHub 연동 여부는 /token/check API로 확인
- 연동이 안 되어 있으면 버튼 클릭 시 인가 요청을 새로 보냄
✍️ 마무리하며
GitHub OAuth는 문서만 보면 쉬워 보이지만, 실제 구현 시에는 다음과 같은 작은 실수들로 꽤 시간을 쓰게 됨:
- code 전달 형식 실수
- 리다이렉트 URI mismatch
- React 상태값의 비동기 업데이트
이런 점들을 정리하며 "프론트에서 어디까지 하고, 백엔드가 어디서부터 처리하는 게 적절한가" 를 명확히 나눠보는 계기가 되었다.
728x90반응형'포트폴리오 > AutoReview' 카테고리의 다른 글
Spring Boot 프로젝트에서 Github REST API로 Push 기능 구현하기 (0) 2025.05.17 Code Post에 북마크 기능 추가 (0) 2025.05.10 게시글 공개 여부 기능 추가 (0) 2025.05.08 [트러블 슈팅] Nginx 502 Bad Gateway 에러 발생 (0) 2025.04.29 검색 쿼리 개선 (LIKE -> Full Text Search) (0) 2025.04.23 댓글