티켓 예매 서비스 프로젝트에 카카오, 구글 소셜 로그인을 붙이기로 하면서 OAuth 2.0을 먼저 정리하게 되었다. 실제 백엔드 적용은 다음 글에서 다룬다.
이 글에서 다룰 개념은 다음과 같다.
OAuth 2.0
Resource Owner / Client / Authorization Server / Resource Server
Authorization Code Grant
Access Token / Refresh Token / scope
OpenID Connect
처음 접하면 "소셜 로그인 = OAuth 2.0"이라고 생각하기 쉽다. 그래서 OAuth 2.0을 로그인(인증) 기술로 오해하는 경우가 많다.
하지만 핵심 차이는 OAuth 2.0이 "누구인지 확인하는" 인증이 아니라 "무엇을 해도 되는지 허락하는" 인가 프레임워크라는 점이다.
간단히 정리하면
- OAuth 2.0 : 비밀번호를 넘기지 않고 제3자 앱에 제한된 접근 권한을 위임하는 인가 프레임워크 (RFC 6749)
- Authorization Code : 인가 코드를 먼저 받고, 백엔드가 그 코드를 토큰으로 교환하는 방식
- Access Token : 리소스 서버에 접근할 때 쓰는 자격 증명
- Refresh Token : Access Token을 다시 발급받기 위한 토큰
- OpenID Connect : OAuth 2.0 위에 "누구인지"를 얹은 인증 계층, ID Token(JWT)을 발급
상황별로는 다음과 같이 구분할 수 있다.
사용자 대신 외부 API를 호출하고 싶다 → OAuth 2.0 (Access Token)
사용자가 누구인지 확인하고 싶다 → OpenID Connect (ID Token)
백엔드 서버가 있는 웹 서비스 → Authorization Code Grant (+ PKCE)
OAuth 2.0
OAuth 2.0은 사용자의 비밀번호를 받지 않고도 제3자 애플리케이션에 사용자 리소스 접근 권한을 위임할 때 사용한다.
예를 들어 OAuth가 없다면 우리 백엔드가 사용자의 카카오 정보를 가져오려면 카카오 아이디와 비밀번호를 직접 받아야 한다. RFC 6749 서론은 이런 비밀번호 공유 방식의 문제를 다음처럼 정리한다.
- 제3자 앱이 사용자의 자격 증명을 저장해야 한다.
- 서버는 비밀번호 인증 방식을 계속 지원해야 한다.
- 제3자 앱이 필요 이상으로 넓은 접근 권한을 얻는다.
- 특정 제3자 앱의 권한만 골라서 철회할 수 없다.
- 제3자 앱 하나가 침해되면 사용자 비밀번호가 노출된다.
OAuth 2.0은 비밀번호 대신 인가 서버가 발급한 토큰을 건네는 방식으로 이 문제를 푼다. 우리 백엔드는 비밀번호를 볼 일이 없고, 받은 토큰으로 허락된 범위(scope)만 접근한다.
즉, OAuth 2.0은 "비밀번호 대신 토큰으로, 전부 대신 일부만" 위임하기 위한 표준이다.
Resource Owner / Client / Authorization Server / Resource Server
OAuth 2.0은 참여자를 네 가지 역할로 나눈다. 소셜 로그인 상황에 대응하면 다음과 같다.
| 역할 | RFC 6749 정의 | 소셜 로그인에서 |
|---|---|---|
Resource Owner |
보호된 리소스에 대한 접근을 허락할 수 있는 주체 | 로그인하는 사용자 |
Client |
Resource Owner를 대신해, 그 인가를 받아 리소스를 요청하는 애플리케이션 |
우리 티켓 예매 백엔드 |
Authorization Server |
Resource Owner를 인증하고 인가를 받은 뒤 Access Token을 발급하는 서버 |
카카오, 구글의 인가/토큰 서버 |
Resource Server |
보호된 리소스를 가지고 있고 Access Token으로 요청을 받는 서버 |
카카오, 구글의 사용자 정보 API |
즉, 소셜 로그인에서 우리 백엔드는 카카오, 구글에 권한을 빌리러 가는 Client다.
중요한 점은 백엔드 "서버"라는 이름 때문에 Authorization Server나 Resource Server로 착각하기 쉽다는 것이다.
Authorization Code Grant
Authorization Code Grant는 인가 코드를 먼저 받은 뒤, 백엔드가 이 코드를 Access Token으로 교환할 때 사용한다.
흐름은 다음과 같다. (인가 서버, API 서버는 카카오 쪽 서버다.)
1. 사용자 → 백엔드 : "카카오로 로그인" 클릭
2. 백엔드 → 인가 서버 : 인가 요청으로 리다이렉트 (client_id, redirect_uri, scope, state)
3. 사용자 → 인가 서버 : 로그인 후 동의
4. 인가 서버 → 백엔드 : redirect_uri로 code, state 전달
5. 백엔드 → 인가 서버 : code를 Access Token으로 교환 (서버 간 통신)
6. 백엔드 → API 서버 : Access Token으로 사용자 정보 조회
인가 요청은 브라우저 리다이렉트로 다음처럼 나간다.
GET https://kauth.kakao.com/oauth/authorize
?response_type=code
&client_id={REST_API_KEY}
&redirect_uri=https://ticketrun.example.com/login/oauth2/code/kakao
&scope={동의 항목}
&state={랜덤값}
사용자가 동의하면 redirect_uri로 code가 붙어 돌아온다. 백엔드는 이 code와 client_secret을 https://kauth.kakao.com/oauth/token으로 보내 토큰을 받는다.
그렇다면 왜 토큰을 바로 받지 않고 코드를 한 번 거칠까?
브라우저 주소창을 거치는 구간(흔히 프론트 채널이라 부른다)은 노출 위험이 크다. 그래서 이 구간에는 단독으로는 쓸모가 적은 code만 흘려보내고, 실제 토큰 교환은 client_secret을 가진 백엔드가 서버 간 통신으로 처리한다.
state는 돌아온 응답이 내가 보낸 요청의 응답인지 확인해 CSRF를 막는 용도다.
즉, 토큰과 client_secret은 브라우저에 드러나지 않고 백엔드 안에서만 다뤄진다.
다만, 예전에 쓰이던 Implicit Grant(토큰을 리다이렉트로 바로 받는 방식)와 Password Grant(비밀번호를 직접 받는 방식)는 아직 초안인 OAuth 2.1(draft-ietf-oauth-v2-1-16)에서는 명세에서 빠졌다. 대신 인가 코드 탈취를 막는 PKCE(RFC 7636)를 원칙적으로 요구하므로, 새로 구현한다면 Authorization Code Grant에 PKCE를 더하는 것이 권장되는 방향이다.
Access Token / Refresh Token / scope
Access Token은 Client가 Resource Server에 접근할 때 내미는 자격 증명이다. 요청은 다음처럼 Authorization 헤더에 Bearer 방식으로 실어 보낸다.
GET {카카오 사용자 정보 API}
Authorization: Bearer {ACCESS_TOKEN}
Bearer 토큰은 "가지고 있는 사람이 곧 권한자"인 토큰이다. 그래서 반드시 https로만 보낸다(RFC 6750).
Refresh Token은 Access Token이 만료됐을 때 사용자가 다시 로그인하지 않고 새 Access Token을 받는 데 쓴다.
scope는 토큰으로 할 수 있는 범위다. 예를 들어 닉네임만 동의받았다면 그 토큰으로는 이메일을 조회할 수 없다.
즉, Access Token은 짧게 쓰는 출입증, Refresh Token은 출입증 재발급용, scope는 출입 가능한 구역이다.
OpenID Connect
추가적으로 OpenID Connect를 알아보려고 한다! 소셜 로그인에서 실제로 필요한 것은 "이 사용자가 누구인지"이기 때문이다.
OpenID Connect(OIDC)는 OAuth 2.0 위에 인증 계층을 얹은 표준이다. 토큰 응답에 Access Token과 함께 사용자 신원 정보를 담은 ID Token(JWT)이 온다.
OAuth 2.0 → Access Token : "이 API를 호출해도 된다"
OIDC → ID Token : "이 사람은 누구다"
두 서비스의 지원 방식은 다음과 같다.
- 구글은 OIDC를 지원하며, ID Token을 받으려면
scope가openid로 시작해야 한다. - 카카오는 콘솔에서 OIDC를 활성화하면 토큰 응답에 ID Token을 함께 발급한다.
즉, 소셜 로그인의 "로그인"은 OAuth 2.0 자체가 아니라 그 위의 OIDC가 담당한다.
다만, 카카오는 Spring Security가 기본 제공하는 provider가 아니어서 구글보다 설정이 더 필요하다. 이 부분은 다음 글에서 다룬다.
정리
OAuth 2.0과 OIDC는 모두 비밀번호를 넘기지 않고 토큰으로 신뢰를 주고받는다는 공통점이 있다. 차이는 토큰이 "권한"을 말하는지, "신원"을 말하는지에 있다.
| 구분 | OAuth 2.0 |
OpenID Connect |
|---|---|---|
| 목적 | 인가 (접근 권한 위임) | 인증 (사용자 신원 확인) |
| 핵심 토큰 | Access Token |
ID Token (JWT) |
| 필요한 scope | 서비스별 scope |
openid 포함 |
| 소셜 로그인에서 | 사용자 정보 API 호출 | 로그인 사용자 식별 |
정리하면, 판단 기준은 다음과 같다.
비밀번호를 직접 받는다 → 하지 않는다 (OAuth 2.0을 쓰는 이유)
백엔드가 있는 웹 서비스 → Authorization Code Grant + PKCE
토큰 교환은 → 반드시 백엔드에서 client_secret과 함께
"누구인지"가 필요하다 → OIDC로 ID Token까지 받는다'Back-End > Spring' 카테고리의 다른 글
| [Spring Security] Spring Security는 왜 사용할까? (0) | 2026.10.23 |
|---|---|
| [Spring Boot] @EnableJpaAuditing이란? (0) | 2026.10.23 |
| [JPA] N+1 문제란 무엇일까? (0) | 2025.12.02 |
| @RequestParam, @ModelAttribute, @RequestBody는 언제 사용할까? (0) | 2025.11.24 |
| [JPA] @ManyToOne, @OneToMany 연관관계 정리 (0) | 2025.11.22 |