graph TB
A[프론트엔드] --> B[AuthService:8082]
B --> C[Google OAuth]
B --> D[카카오 OAuth]
B --> E[네이버 OAuth]
B --> F[MySQL Database]
B --> G[Redis Cache]
subgraph "DB Tables"
F --> H[members]
F --> I[member_social_accounts]
end
subgraph "토큰 관리"
G --> J[Access Token]
G --> K[Refresh Token]
end
사용자 소셜 로그인 요청
↓
소셜 플랫폼에서 토큰 획득 (ID Token/Access Token)
↓
AuthService로 토큰 전송
↓
3단계 자동 판단 로직 실행
↓
결과에 따른 처리:
├─ 기존 회원 → 즉시 로그인 (JWT 토큰 발급)
├─ 신규 회원 → 회원가입 후 로그인
└─ 정보 부족 → 추가 정보 입력 요청
| 단계 | 조건 | 결과 | 추가 처리 |
|---|---|---|---|
| 1단계 | 소셜 ID로 기존 소셜 계정 찾기 | 기존 소셜 사용자 | 즉시 로그인 |
| 2단계 | 이메일로 기존 일반 회원 찾기 | 기존 일반 사용자 | 소셜 계정 연결 |
| 3단계 | 필수 정보 검증 | 신규 사용자 | 회원가입 처리 |
public UserSocialResult findOrCreateUserBySocial(SocialUserInfo socialInfo) {
// 1단계: 소셜 ID로 기존 계정 조회
Optional<MemberSocialAccount> existingSocial =
memberSocialAccountRepository.findByProviderAndSocialId(
socialInfo.getProvider(), socialInfo.getSocialId());
if (existingSocial.isPresent()) {
log.info("🔍 1단계: 기존 소셜 계정 발견");
return UserSocialResult.existingUser(existingSocial.get().getMember());
}
// 2단계: 이메일로 기존 일반 회원 조회
if (socialInfo.getEmail() != null) {
Optional<Member> existingMember = memberRepository.findByEmail(socialInfo.getEmail());
if (existingMember.isPresent()) {
log.info("📧 2단계: 기존 이메일 회원 발견 - 소셜 계정 연결");
return linkSocialAccount(existingMember.get(), socialInfo);
}
}
// 3단계: 신규 회원 생성
log.info("🆕 3단계: 신규 회원 생성");
return createNewSocialUser(socialInfo);
}
sequenceDiagram
participant U as 사용자
participant F as 프론트엔드
participant G as Google OAuth
participant A as AuthService
participant D as Database
U->>F: Google 로그인 버튼 클릭
F->>G: GSI 라이브러리로 인증 요청
G->>U: Google 계정 선택/로그인 UI
U->>G: 계정 선택 및 권한 동의
G->>F: ID Token 전달 (JWT)
F->>A: POST /api/auth/social/google<br/>{idToken: "eyJ..."}
A->>G: ID Token 검증 요청
G->>A: 사용자 정보 반환
A->>D: 3단계 자동 판단 로직 실행
D->>A: 회원 상태 결과
A->>F: JWT Access/Refresh Token
F->>U: 로그인 완료 처리
// 프론트엔드: ID Token 방식
google.accounts.id.initialize({
client_id: "642010742671-4tnfj6vsfp6aemq7bj4dfgn0m185v6he.apps.googleusercontent.com",
callback: (response) => {
// ID Token을 백엔드로 전송
fetch('/api/auth/social/google', {
method: 'POST',
body: JSON.stringify({ idToken: response.credential })
});
}
});
// 백엔드: ID Token 검증
public SocialUserInfo getUserInfo(String idToken) {
GoogleIdToken googleIdToken = verifier.verify(idToken);
GoogleIdToken.Payload payload = googleIdToken.getPayload();
return SocialUserInfo.builder()
.provider("google")
.socialId(payload.getSubject())
.email(payload.getEmail())
.name((String) payload.get("name"))
.build();
}
sequenceDiagram
participant U as 사용자
participant F as 프론트엔드
participant K as 카카오 OAuth
participant A as AuthService
participant D as Database
U->>F: 카카오 로그인 버튼 클릭
F->>K: Kakao SDK로 인증 요청
K->>U: 카카오 로그인 팝업/페이지
U->>K: 카카오 계정 로그인 및 권한 동의
K->>F: Access Token 전달
F->>A: POST /api/auth/social/kakao<br/>{accessToken: "kEWG..."}
A->>K: Access Token으로 사용자 정보 요청<br/>GET /v2/user/me
K->>A: 사용자 정보 응답 (JSON)
A->>D: 3단계 자동 판단 로직 실행
D->>A: 회원 상태 결과
A->>F: JWT Access/Refresh Token
F->>U: 로그인 완료 처리
// 프론트엔드: Access Token 방식
Kakao.Auth.login({
success: (authObj) => {
// Access Token을 백엔드로 전송
fetch('/api/auth/social/kakao', {
method: 'POST',
body: JSON.stringify({ accessToken: authObj.access_token })
});
}
});
// 백엔드: 카카오 API 호출
public SocialUserInfo getUserInfo(String accessToken) {
String url = "https://kapi.kakao.com/v2/user/me";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET,
new HttpEntity<>(headers), Map.class);
Map<String, Object> kakaoAccount = (Map) response.getBody().get("kakao_account");
return SocialUserInfo.builder()
.provider("kakao")
.socialId(response.getBody().get("id").toString())
.email((String) kakaoAccount.get("email"))
.build();
}
sequenceDiagram
participant U as 사용자
participant F as 프론트엔드
participant P as 팝업창
participant N as 네이버 OAuth
participant A as AuthService
participant D as Database
U->>F: 네이버 로그인 버튼 클릭
F->>P: window.open()으로 팝업 창 오픈<br/>GET /naver/login?state=random
P->>A: 네이버 인증 URL 요청
A->>P: 네이버 OAuth URL로 리다이렉트
P->>N: 네이버 로그인 페이지 표시
U->>N: 네이버 계정 로그인 및 권한 동의
N->>P: Authorization Code와 함께 콜백<br/>GET /naver/callback?code=xxx&state=xxx
P->>A: Authorization Code 전달
A->>N: Authorization Code로 Access Token 요청<br/>POST /oauth2.0/token
N->>A: Access Token 응답
A->>N: Access Token으로 사용자 정보 요청<br/>GET /v1/nid/me
N->>A: 사용자 정보 응답 (JSON)
A->>D: 3단계 자동 판단 로직 실행
D->>A: 회원 상태 결과
A->>P: 결과를 담은 HTML 페이지 반환
P->>F: postMessage()로 결과 전달
F->>P: 팝업 창 닫기
F->>U: 로그인 완료 처리
// 프론트엔드: 팝업 + postMessage
function naverLogin() {
const popup = window.open(
'http://localhost:8082/api/auth/social/naver/login',
'naverLogin',
'width=500,height=600'
);
// 팝업에서 메시지 수신
window.addEventListener('message', (event) => {
if (event.data.type === 'NAVER_LOGIN_SUCCESS') {
console.log('네이버 로그인 성공:', event.data.result);
popup.close();
}
});
}
// 백엔드: OAuth 2.0 플로우
@GetMapping("/naver/login")
public String naverLogin(@RequestParam String state) {
String authUrl = buildAuthorizationUrl(state);
return "redirect:" + authUrl;
}
@GetMapping("/naver/callback")
public String naverCallback(@RequestParam String code, @RequestParam String state) {
String accessToken = getAccessToken(code, state);
SocialUserInfo userInfo = getUserInfo(accessToken);
// 팝업에 결과 전달하는 HTML 반환
return generatePopupResponseHtml(userInfo);
}
| 구분 | 카카오 | 네이버 | |
|---|---|---|---|
| 인증 방식 | ID Token (JWT) | Access Token | OAuth 2.0 Code Flow |
| 프론트엔드 구현 | GSI 라이브러리 | Kakao SDK | 팝업 + postMessage |
| 토큰 검증 | Google에서 직접 검증 | 카카오 API로 정보 조회 | Code → Token → 정보 조회 |
| 보안 수준 | 높음 (서명된 JWT) | 중간 (Bearer Token) | 높음 (CSRF 방지) |
| 구현 복잡도 | 쉬움 | 쉬움 | 복잡함 (팝업 통신) |
| 백엔드 API 호출 | 1회 (토큰 검증) | 1회 (사용자 정보) | 2회 (토큰 + 정보) |
// POST /api/auth/social/{provider}
{
"idToken": "eyJhbGciOiJSUzI1NiIs...", // Google만
"accessToken": "kEWG7tLkJc3eD...", // 카카오, 네이버
"deviceInfo": {
"platform": "WEB_BROWSER",
"deviceId": "browser-12345",
"appVersion": "1.0.0"
}
}
// 성공 응답
{
"success": true,
"data": {
"member": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "사용자",
"profileImageUrl": "https://..."
},
"token": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600
},
"isNewUser": false,
"needsAdditionalInfo": false
}
}
{
"success": false,
"error": {
"code": "INVALID_SOCIAL_TOKEN",
"message": "소셜 토큰이 유효하지 않습니다"
}
}
server:
port: 8082
# JWT 설정
jwt:
secret: akc-b2c-jwt-secret-key-2024-very-long-and-secure
access-token-expiration: 3600000 # 1시간
refresh-token-expiration: 604800000 # 7일
# 소셜 로그인 설정
social:
google:
client-id: 642010742671-4tnfj6vsfp6aemq7bj4dfgn0m185v6he.apps.googleusercontent.com
dev-mode: false # 개발 시 true로 설정
kakao:
client-id: 62beeadf5fa92745c25ade678f079981
dev-mode: false
naver:
client-id: J7MtZsl9898OFMhgKDur
redirect-uri: http://localhost:8082/api/auth/social/naver/callback
dev-mode: false
-- 회원 테이블
CREATE TABLE members (
id bigint PRIMARY KEY AUTO_INCREMENT,
member_uuid varchar(36) UNIQUE NOT NULL,
email varchar(100) UNIQUE,
name varchar(50),
user_id varchar(30) UNIQUE,
member_type varchar(20) DEFAULT 'SOCIAL', -- GENERAL, SOCIAL
status varchar(20) DEFAULT 'ACTIVE'
);
-- 소셜 계정 테이블
CREATE TABLE member_social_accounts (
id bigint PRIMARY KEY AUTO_INCREMENT,
member_id bigint NOT NULL,
provider varchar(20) NOT NULL, -- GOOGLE, KAKAO, NAVER
social_id varchar(100) NOT NULL, -- 소셜 플랫폼 고유 ID
email varchar(100),
UNIQUE KEY uk_social_account (provider, social_id),
FOREIGN KEY (member_id) REFERENCES members(id)
);
# application-local.yml
social:
google:
dev-mode: true # 더미 데이터 사용
kakao:
dev-mode: true
naver:
dev-mode: true
// 프론트엔드에서 사용할 개발용 토큰
const DEV_TOKENS = {
google: 'dummy_google_token_dev_mode',
kakao: 'dummy_kakao_token_dev_mode',
naver: 'dummy_naver_token_dev_mode'
};
// 개발 환경 테스트 함수
const testSocialLogin = (provider) => {
fetch(`http://localhost:8082/api/auth/social/${provider}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idToken: provider === 'google' ? DEV_TOKENS[provider] : undefined,
accessToken: provider !== 'google' ? DEV_TOKENS[provider] : undefined
})
});
};
social-login-test.html 파일이 프로젝트 루트에 준비되어 있어 로컬에서 바로 테스트 가능합니다.
이 문서는 실제 구현된 코드를 기반으로 핵심 내용만 정리했습니다. 📋✨