[JSP/Servlet] OAuth 2.0 소셜 로그인 흐름 분석 (SISEON 프로젝트 · 네이버)
목차
1. OAuth 2.0이란
2. 전체 흐름
3. 1단계 : 로그인 요청 ~ 네이버 인증 화면(NaverLoginStart.java)
4. 2단계 : 네이버 callback 처리 ~ 회원 처리 (NaverCallback.java)
5. 시퀀스 다이어그램으로 전체 흐름 정리
6. 마치며 (Spring Security OAuth2와 비교)
1. OAuth 2.0이란
OAuth 2.0은 제 3자 서비스(네이버, 카카오 등)에게 사용자 인증을 위임하는 프로토콜임. 우리 서버에서 직접 비밀번호를 관리하지 않고, 외부 서버(네이버 등)가 대신 인증해준다는 개념임. 소셜로그인에 쓰임.
공식문서 :
https://developers.naver.com/docs/login/devguide/devguide.md
네이버 로그인 개발가이드 - LOGIN
네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디
developers.naver.com
| 역할 | 설명 | 이 프로젝트에서는 |
| Resource Owner | 사용자(인증을 허락하는 주체) | 쇼핑몰 이용자 |
| Client | 우리 서비스(인증을 요청하는 측) | SISEON 서버 |
| Authorization Server | 인증을 처리해주는 서버 | 네이버 로그인 서버 |
| Resource Server | 사용자 정보를 가진 서버 | 네이버 사용자 정보 API |
■ Authorization Code Grant 방식
OAuth 2.0에는 여러 인증 방식(Grant Type)이 있는데, 소셜 로그인에서 가장 많이 쓰이는 방식은 Autorization Code Grant임.
Authorization Code Grant 흐름 요약
① 사용자가 소셜 로그인 버튼 클릭
② 우리 서버 → 네이버 인증 서버로 302 Redirect
③ 사용자가 네이버에서 로그인 + 동의
④ 네이버 → callback URL로 Authorization Code 전달
⑤ 우리 서버 → 네이버에 Code로 Access Token 교환 요청 (서버 간 직접 통신)
⑥ Access Token으로 사용자 정보 조회
⑦ 우리 서비스 회원 처리 (신규/기존 분기)
※ 왜 Code를 먼저 발급받고, 그걸로 다시 Token을 교환하는 2단계 방식을 쓰는지
-> Code는 브라우저(URL)을 통해 전달되기 때문에 노출 위험이 있음. Code는 일회성으로, Access Token은 브라우저를 거치지 않고 서버 간 직접 통신으로만 교환함. 즉, 탈취 위험 최소화를 위해 2단계 방식으로 설계함.
2. 전체 흐름
프로젝트의 소셜 로그인 흐름을 살펴보면 하기와 같음
[브라우저 개입 O]
① 로그인 버튼 클릭
② GET /naverLoginStart.sp → 302 Redirect → 네이버 authorize URL
③ 네이버 로그인 + 동의 화면
④ 네이버 → callback URL로 302 Redirect (code, state 포함)
[브라우저 개입 X, 서버-서버 직접 통신]
⑤ 우리 서버 → 네이버 Token API (code로 access_token 교환)
⑥ 우리 서버 → 네이버 UserInfo API (access_token으로 사용자 정보 조회)
[우리 서버 내부 처리]
⑦ DB에서 기존 회원 조회
⑧-A (기존 회원) Session 저장 → 마이페이지 이동
⑧-B (신규 회원) 임시 회원 생성 → 추가정보 입력 페이지 이동
핵심은 1~4는 브라우저가 중간에서 redirect 요청을 전달하고, 5~6은 서버끼리 직접 통신한다는 점임. 그래서 5 이후의 흐름은 F12 Network 탭에서 확인되지 않음.
3. loginSelect.jsp -> NaverLoginStart.java (소셜 로그인 버튼 클릭 ~ 소셜 로그인 화면 띄우기까지) : 로그인 요청
전체흐름이 상기 2번과 같다면, 이제부터 상세하게 기능 흐름을 처음부터 끝까지 설명하겠음.
(1) 먼저 loginSelect.jsp 에서 로그인 버튼을 통해 네이버 로그인 요청을 시작하도록 구현함. location.herf = ... 실행되면 HTTP요청 (GET/ naverLoginStart.sp) 보내면서 컨트롤러가 실행됨.(NaverLoginStartContrlloer.java)
<button type="button"
class="btn btn-social btn-naver btn-block"
onclick="location.href='<%=ctxPath%>/naverLoginStart.sp'">
<i class="fas fa-leaf"></i> 네이버로 로그인
</button>
(2) NaverLoginStartController를 뜯어보면
String state = UUID.randomUUID().toString(); // state 생성
HttpSession session = request.getSession(); // state값 세션에 저장
session.setAttribute("NAVER_STATE", state);
String apiURL = "https://nid.naver.com/oauth2.0/authorize" // 네이버 로그인 요청 url 생성
+ "?response_type=code"
+ "&client_id=" + clientId
+ "&redirect_uri=" + redirectURI
+ "&state=" + state;
// 네이버 로그인 페이지로 이동
super.setRedirect(true);
super.setViewPage(apiURL);
1. OAuth에서는 요청 위조 방지를 위해 state 값을 사용함. UUID를 사용해 랜덤 문자열을 생성하도록 구현함. 실제로 생성되는 값은 b682b3b4-84f4-4b52-a5c9-xxxx 이런 형태임.
2. 생성된 state값을 서버 Session에 저장함. 이후 callback 단계에서 네이버가 반환된 state값과 비교하기 위해 미리 저장해두는 것임.
3. 네이버 로그인 요청 url을 생성함. (authorize URL)
4. Redirect 응답 반환
즉, 표로 정리하면 하기와 같음
| 항목 | 설명 |
| state | UUID로 생성한 랜덤한 문자열. CSRF 공격 방지용 |
| session.setAttribute | callback 단계에서 비교하기 위해 서버 측에 미리 저장 |
| response_type = code | Authorization Code 방식 지정 |
| redirect_uri | 인증 후 네이버가 돌아올 callback URL (네이버 개발자센터에 미리 등록 필요) |
| 302 Redirect | 브라우저가 url로 이동하고, 다시 반환하게 하는 코드 |
+ 더해서 OAuth 초기 로그인 흐름은: 우리 서버 ↔ 브라우저 ↔ 네이버 서버 구조로 동작함. 브라우저가 중간에서 redirect 요청을 전달하는 역할을 수행한다고 보면 됨.
/naverLoginStart.sp -> 302 Redirect -> 네이버 authorize URL 이동
■ 실제 흐름을 확인하기 위해 Chrome DevTools의 Network 탭을 사용해보겠음. (f12 -> Network)

/naverLoginStart.sp 요청 후 서버는 302 Redirect 응답과 함께 네이버 authorize URL(Location 헤더)을 반환하고, 브라우저는 해당 URL로 다시 요청을 보내게 됨. (https://nid.naver.com/oauth2.0/authorize)--> client_id, redirect_uri, state 실어서 네이버 서버로 보냄.
브라우저가 authorize URL로 이동하면, 이후 작업은 네이버 서버에서 자동으로 진행함.
네이버 서버 내부 처리 순서
① client_id 유효성 확인 (등록된 앱인지)
② redirect_uri 일치 확인 (등록된 URI인지)
③ 사용자 로그인 여부 확인
④ (미로그인) 네이버 로그인 페이지 출력
⑤ (로그인 완료) 동의 화면 출력
⑥ 동의 완료 → callback URL로 code, state 반환
즉, 우리 서버는 redirect URL만 던져주고, 그 이후 인증 과정은 전부 네이버에서 처리함.
4. NaverCallback.java (로그인 버튼 클릭 -> 로그인 성공 까지) : 네이버 callback 처리

(1) callback 요청 처리
사용자가 네이버 로그인 완료하면, 네이버 서버는 미리 등록된 callback URL로 브라우저를 다시 redirect 시킴.
이 때 callback URL 뒤에 code, state 값을 함께 즉, http://localhost:9090/SemiProject/naverCallback.sp
?code=xxxxxxxx&state=b682b3... 형태로 Contoller에 반환됨.
※ code : 네이버 서버가 발급한 Authorization Code
※ state : 최초 요청 시 생성했던 검증 값

(2) state 검증
String sessionState = (String) session.getAttribute("NAVER_STATE");
if(sessionState == null || state == null || !sessionState.equals(state)) {
request.setAttribute("message", "잘못된 접근입니다.(state 불일치)");
request.setAttribute("loc", request.getContextPath() + "/loginSelect.sp");
super.setRedirect(false);
super.setViewPage("/WEB-INF/msg.jsp");
return;
}
if(code == null) {
request.setAttribute("message", "네이버 로그인 실패(code 없음)");
request.setAttribute("loc", request.getContextPath() + "/loginSelect.sp");
super.setRedirect(false);
super.setViewPage("/WEB-INF/msg.jsp");
return;
}
최초에 우리 서버 session에 저장해 두었던 state 값과, 네이버 서버가 callback 한 state 값을 비교함. 즉, Session의 state == callback으로 전달받은 state 인지 검증하는 과정. 이 때 만약 state 값이 다르면 위조된 요청으로 판단하고 로그인 처리를 중단함. state 값은 CSRF 공격 방지를 위한 검증 역할을 수행함.
+ CSRF 공격 예시
공격자가 피해자에게 조작된 callback URL을 클릭하도록 유도
→ state가 없거나 다름
→ sessionState != callbackState
→ 처리 중단 → 공격 차단
(3) access_token 발급 요청 --> 여기부터는 브라우저 기반 redirect 흐름이 아니기 때문에 f12탭에서는 확인할 수 없음.
// 우리 서버 <-> 네이버 서버 통신(우리 서버가 네이버한테 발급받은 코드 검증 요청 보내는거)
String tokenUrl = "https://nid.naver.com/oauth2.0/token"
+ "?grant_type=authorization_code"
+ "&client_id=" + clientId
+ "&client_secret=" + clientSecret
+ "&code=" + code
+ "&state=" + state;
// access token 발급(네이버 서버에서 json 응답 받음) --> 네이버 API 호출 권한
String tokenResult = requestGET(tokenUrl);
state 검증이 완료되면, 우리 서버가 Authorization Code를 사용하여 네이버 서버에게 access_token 발급을 요청함. 여태까지와 달리 브라우저 개입없이 우리 서버 <-> 네이버 서버간 통신함. 네이버 서버는 JSON 형태로 응답을 반환 시킴.
하기와 같이 반환됨. (access token이 json 형태로 반환됨)
{
"access_token":"AAAAAAA",
"refresh_token":"BBBBBBB",
"token_type":"bearer",
"expires_in":"3600"
}
이후 JSON 객체로 변환하여 access_token 추출함. 이 때 access_token은 네이버 API를 호출할 수 있는 권한 토큰 역할을 수행함. 즉, 사용자 로그인이 된게 아니고 네이버 API를 호출할수 있는 권한(access_token)을 발급받은 상태임.
(4) 사용자 정보 조회
access_token 발급되면 해당 토큰을 사용하여 네이버 서버에 사용자 정보를 요청함.(requestGETwithToken()메서드를 통해 네이버 사용자 정보 조회 API를 호출함.) 마찬가지로 사용자 정보가 json 형태로 반환됨.
String userInfoUrl = "https://openapi.naver.com/v1/nid/me";
String userResult = requestGETWithToken(userInfoUrl, accessToken);
con.setRequestProperty(
"Authorization",
"Bearer " + accessToken);
즉, 네이버 서버는 Bearer access_token 값을 검증하고, 사용자 정보를 JSON 형태로 반환함. 이후 JSON 데이터를 파싱하여 실제 사용자 정보를 추출함. 실제로는 하기 형태의 JSON 응답이 반환됨.
{
"response": {
"id": "AAAAAAA",
"name": "홍길동",
"email": "test@naver.com",
"mobile": "010-1234-5678"
}
}
- id -> 네이버 사용자 고유 식별값
- name -> 사용자 이름
- email -> 이메일
- mobile -> 전화번호
네이버 계정 자체를 우리 서비스 계정으로 바로 사용할 수 있는 것이 아니라, String userid= "naver_" + socialId; 처리를 해서 네이버 사용자 고유 ID 기반의 계정을 생성하도록 구현함. 즉, 우리 서비스 회원 시스템과 연결하는 과정이 진행되어야 함.
// 네이버 서버에서 가져온 정보 userid에 넣기
String userid = "naver_" + socialId;
String name = res.optString("name", "네이버회원");
String email = res.optString("email", "");
String mobile = res.optString("mobile", "");
우리 서비스 자체적으로 가입한 회원 ID와 구분하기 위해 prefix를 붙여서 관리함. (naver_)
(5) DB 처리 및 분기
이후, DB에서 해당 사용자가 존재하는지 조회함.(userid)
MemberDTO member = mdao.getMemberByUserid(userid);
// 없으면 임시가입 insert
if(member == null) {
mdao.insertSocialTempMember(userid, name, email, mobile);
member = mdao.getMemberByUserid(userid);
}
DB 조회후, 정보가 없으면 -> 임시회원 생성하고 -> 추가정보를 입력할 수 있는 페이지로 이동하게끔 설계함.
// ==========================
// 4) 추가정보 입력 필요하면 socialJoin.jsp로 이동
// ==========================
if("추가입력필요".equals(member.getDetailaddress())) {
request.setAttribute("userid", member.getUserid());
super.setRedirect(false);
super.setViewPage("/WEB-INF/hk_login/socialJoin.jsp");
return;
}
즉, 정리하면 최초 소셜로그인시 네이버 서버에서 제공한 정보만으로는 부족하여(쇼핑몰 도메인에 필요한 배송지나 기타 추가 사항을 입력해야지 되니깐.. id, name, 번호, 이메일 만으로는 부족) 추가로 정보를 입력하게끔 설계했고, 이후 로그인 시에는 소셜 인증만 진행하고 자동으로 로그인 될 수 있도록 설계함. (최초 로그인 시 회원가입 진행하고, 이후 로그인할 때 자동 로그인을 하도록)
※ 설계 의도
| 상황 | 처리 방식 |
| 기존 회원 | session 저장 -> 마이페이지 redirect |
| 최초 소셜 로그인 | 임시 회원 생성 -> 추가 정보 입력 페이지 이동 |
| 최초 이후 소셜 로그인 | 자동 로그인 |
5. OAuth2를 활용한 소셜로그인 흐름 (시퀀스 다이어그램)

상기 흐름도에 외부 서버가 표시가 안되어있지만 맨 왼쪽에 있다고 가정을 하면(최초 우리서버 naverLoginStart.java에서 로그인 요청 보내면, 네이버 서버 내부적으로 네이버 Token API(access_token 발급), 네이버 UserInfo API(사용자 정보 조회) 호출 진행하고 우리 서버로 callback 하는 흐름),
- 빨간색 흐름 : 이미 회원가입이 완료된 사용자의 경우임. 네이버 인증 완료 후 -> DB에서 기존 회원 정보 조회 -> session에 사용자 정보를 저장한 뒤 바로 마이페이지로 리다이렉트 되는 흐름이고 (항상 네이버 서버에서 인증하는 과정은 동일하게 거침)
- 보라색 흐름 : 최초 소셜 로그인 사용자 흐름임. DB 조회시 회원 정보 존재하지 않으면 -> 임시 회원가입 진행(insertSocialTempMember) 진행 후, 추가 정보 입력(socialJoin.jsp) 단계로 이동하도록 구현함. 마찬가지로 로그인 후 session에 저장함.
6. 마치며 (회고 + Spring Security OAuth2와 비교)
세미프로젝트에서 소셜 로그인을 구현할 당시에는 기능 구현에만 집중했기 때문에 실제 브라우저와 서버 사이에 어떤 HTTP 요청이 오갔는지 자세히 알지 못했음. 이번에 흐름을 뜯어보면서 302 Redirect가 단순 페이지 이동이 아니라 브라우저가 Location 헤더의 url로 다시 요청을 보내는 구조라는 것, state 값이 CSRF 방지를 위해 세션에 저장되고 callback에서 비교된다는 것, code -> token 교환이 브라우저를 거치지 않고 서버 간 직접 통신으로 처리된느 이유 등을 명확하게 이해할 수 있었음.
파이널(CIEL)에서는 Spring Security OAuth2를 사용했는데, 세미에서 직접 구현했던 authorize URL 생성, state 검증, code → token 교환, UserInfo 조회를 프레임워크가 전부 자동으로 처리해줌. 개발자가 직접 작성하는 부분은 CustomOAuth2UserService 하나뿐이고, 여기서 우리 서비스에 맞는 회원 처리 로직만 작성하면 됨. 세미에서 직접 짜봤기 때문에 프레임워크가 어떤 일을 대신 해주는지 체감할 수 있었음.(파이널에서는 yml파일 설정으로 모두 대체해줌)
Spring Security OAuth2가 대신 해주는 것들
① authorize URL 생성 + Redirect → 자동
세미에서는 직접 URL 만들고 302 Redirect 코드 작성했는데:
// 세미 (SISEON) - 직접 구현
String apiURL = "https://nid.naver.com/oauth2.0/authorize"
+ "?response_type=code"
+ "&client_id=" + clientId
+ "&redirect_uri=" + redirectURI
+ "&state=" + state;
super.setRedirect(true);
super.setViewPage(apiURL);
파이널 프로젝트 CIEL에서는 application.yml 설정만 해두면 /oauth2/authorization/naver 요청 시 프레임워크가 자동으로 URL 구성 + Redirect 처리함:
# application.yml
spring:
security:
oauth2:
client:
registration:
naver:
client-id: ${NAVER_CLIENT_ID}
client-secret: ${NAVER_CLIENT_SECRET}
redirect-uri: "{baseUrl}/login/oauth2/code/naver"
authorization-grant-type: authorization_code
scope: name, email, mobile
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
② state 생성 + 검증 → 자동
세미에서는 UUID 생성, 세션 저장, callback에서 비교까지 직접 다 짰는데:
// 세미 - 직접 구현
String state = UUID.randomUUID().toString();
session.setAttribute("NAVER_STATE", state);
// ... callback에서
String sessionState = (String) session.getAttribute("NAVER_STATE");
if (!sessionState.equals(state)) { /* 위조 차단 */ }
CIEL에서는 프레임워크가 내부적으로 동일한 로직을 수행함. 개발자가 state 관련 코드를 한 줄도 안써도 됨.
③ Authorization Code → Access Token 교환 → 자동
세미에서는 직접 HTTP 요청을 만들어서 보냈는데 :
// 세미 - 직접 구현
String tokenUrl = "https://nid.naver.com/oauth2.0/token"
+ "?grant_type=authorization_code"
+ "&client_id=" + clientId
+ "&client_secret=" + clientSecret
+ "&code=" + code
+ "&state=" + state;
String tokenResult = requestGET(tokenUrl);
CIEL에서는 callback URL(/login/oauth2/code/naver)로 요청이 들어오면 프레임워크가 자동으로 Token API 호출함. 개발자가 신경 쓸 필요 없음.
④ 사용자 정보 조회 → 자동
세미에서는 Authorization 헤더 세팅 + UserInfo API 직접 호출했는데:
// 세미 - 직접 구현
con.setRequestProperty("Authorization", "Bearer " + accessToken);
String userResult = requestGETWithToken(userInfoUrl, accessToken);
CIEL에서는 프레임워크가 자동으로 UserInfo API 호출하고, 결과를 OAuth2User 객체로 만들어서 넘겨줌.
⑤ 개발자는 OAuth2UserService 구현체 - 우리 서비스 회원 처리 로직만 직접 작성하면 됨.
프레임워크가 authorize URL 생성, state 검증, code -> token 교환, UserInfo 조회까지 전부 처리하고 나면, 마지막으로 개발자한테 OAuth2User 객체를 넘겨주기 때문에, 여기서부터 작성하면됨. 파이널의 경우 CustomOAuth2UserService에 작성되었음
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberService memberService;
private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 프레임워크가 이미 UserInfo API 호출해서 여기로 넘겨줌
OAuth2User oauth2User = delegate.loadUser(userRequest);
// 카카오 / 네이버 분기 처리
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.of(registrationId, oauth2User.getAttributes());
// 세미의 NaverCallback.java와 동일한 역할 → DB 조회 or 신규 생성
MemberDTO memberDto = memberService.findOrCreateSocialMember(
userInfo.getSocialProvider(),
userInfo.getProviderUserId(),
userInfo.getEmail(),
userInfo.getName());
// memberDto 담아서 반환
return new OAuth2MemberPrincipal(memberDto, oauth2User.getAttributes(), ...);
}
}
세미에서 직접 짰던 NaverCallback.java의 핵심 로직 - db 조회하고, 없으면 회원 생성하고, 분기 처리하는 부분 --> 그대로 loadUser() 안으로 들어온 것임. 하는 일은 동일하고, 그 앞단(state검증, token교환, UserInfo 조회)을 프레임워크가 대신 처리해주는 구조.
표로 정리하면,
| 처리 단계 | SISEON (세미, 직접 구현) | CIEL (파이널, Spring Security OAuth2) |
| authorize URL 생성 + Redirect | NaverLoginStart.java에서 직접 작성 | 프레임워크 자동 처리 |
| state 생성 + 검증 | 직접 작성 | 프레임워크 자동 처리 |
| code -> access_token 교환 | requestGET()으로 직접 HTTP 요청 | 프레임워크 자동 처리 |
| UserInfo API 호출 | requestGETWithToken()으로 직접 호출 | 프레임워크 자동 처리 |
| DB 조회 / 회원 생성 / 분기 처리 | NaverCallback.java | CustomOAuth2UserService.loadUser() |
정리하면, 핵심 비즈니스 로직(회원 처리- 운영하는 서비스에 맞는 회원 연결 로직이 있어야 함)는 개발자가 직접 작성해야됨. 프레임워크가 OAuth 프로토콜 처리만 대신 해줌.
+ 참고 : 소셜 로그인 접근 제한
네이버 개발자센터에서 애플리케이션 검수 미완료 상태라서 사전에 등록된 테스터 아이디로만 소셜 로그인이 가능함. 등록되지 않은 네이버 계정으로 로그인을 시도하면 네이버 서버에서 차단됨. 현재 CIEL 배포 환경의 소셜 로그인은 등록되지 않은 계정으로는 소셜 로그인이 동작하지 않음.