1. 직접 로그인 처리를 하지 않고 제 3자를 거치는 이유
로그인 처리를 직접 하는 경우, 다음과 같은 부담이 생긴다.
- 비밀번호를 안전하게 해싱 및 저장
- 사용자는 서비스마다 새롭게 회원가입
- 2단계 인증과 같은 보안 장치도 직접 구현
OAuth2는 위와 같은 부담을 신뢰할 수 있는 제 3자(ex. 구글, 네이버 등)에게 위임한다.
우리는 비밀번호를 아예 보지 않는 대신, 앞서 말한 제 3자가 증명해주는 내용을 받는다.
2. Authorization Code Flow - 리다이렉트 왕복 구조
1. "구글 로그인" 버튼 클릭
2. 서버가 사용자를 구글 로그인 페이지로 리다이렉트
3. 사용자가 구글 계정으로 로그인 + 권한 동의
4. 구글이 사용자를 다시 서버 콜백 URL로 리다이렉트 → 금방 만료되는 authorization code를 함께 붙여줌
5. 서버가 이 code를 들고 구글 서버에 직접 요청
6. 구글 토큰으로 교환
4~6번은 같은 리다이렉트처럼 보이지만, 완전히 다른 두 통신이 섞여 있다.
1. 구글 → 브라우저 → 서버
이 과정에서는 여전히 브라우저를 거친다.
사용자가 동의를 누르면 구글이 브라우저한테 리다이렉트 응답을 준다.
구글이 우리 서버로 뭔가를 직접 보내는 게 아니란 소리다.
구글 → 브라우저: "이 주소로 가"
https://oursite.com/login/oauth2/code/google?code=4/0AY0e-g7...&state=xyz
브라우저가 실제로 저 URL에 GET 요청을 보내고, 그 요청이 우리 서버에 도착한다.
즉, 1~3번은 전부 "구글이 브라우저를 시켜서 심부름을 보내는 방식"이다. (code는 심부름 URL의 쿼리 파라미터에 실려 온다.)
이 구간을 프론트 채널이라고 부른다. → "브라우저 주소창 / 히스토리에 노출되는 구간"
코드는 1회용인데다가 유효시간이 짧기 때문에 URL에 잠깐 노출되어도 큰 문제가 되지 않는다.
3. 서버 -> 구글
이제 브라우저를 거치지 않는 진짜 서버 간 통신이 시작된다.
서버 프로세스 자체가 구글의 토큰 발급 엔드포인트로 직접 HTTPS 요청을 보낸다.
우리 서버 → 구글: POST https://oauth2.googleapis.com/token
code=4/0AY0e-g7...
client_id=...
client_secret=... ← 여기가 핵심
redirect_uri=...
여기서 client_secret은 구글 콘솔에서 발급받은 값이다.
구글은 "code가 유효한지 + client_secret이 일치하는지"를 확인한 후 access_token / id_token을 응답으로 돌려준다.
이 구간을 백채널이라고 부르며, URL 히스토리나 프론트 로그에 전혀 남지 않는다. (브라우저를 거치지 않기 때문!)
정리하면 4번은 "구글이 브라우저 편으로 임시 교환권(code)만 보낸 것"
5~6번은 "교환권을 서버가 구글에게 직접 들고 가서 우리만 아는 비밀번호(client_secret)을 보여주고 진짜 토큰으로 바꿔온 것"이다.
3. code를 한 단계 거치는 이유?
처음부터 진짜 토큰을 브라우저 편으로 보내버리면 URL에 노출된 토큰을 누구나 쓸 수 있다.
하지만 이처럼 한 단계를 거치면 client_secret 없이는 무용지물이 되어버리기 때문에 노출돼도 안전하게 된다.
3. state 파라미터 - 리다이렉트 왕복을 노리는 공격
CSRF처럼, 리다이렉트를 여러번 오가는 경우 그 사이에 끼어들 여지도 생긴다.
(CSRF는 사이트 간 요청 위조를 뜻한다.)
1. 공격자가 자기 계정으로 로그인 흐름을 시작하여 authorization code를 미리 받아둔다.
2. 그 code가 담긴 콜백 URL을 피해자가 클릭하도록 유도한다.
3. 피해자의 브라우저가 그 URL로 요청을 보내면 서버가 "피해자가 로그인 시도를 했다"고 착각한다.
4. 결과적으로 피해자 계정이 공격자의 구글 계정과 연결된다.

이를 `state` 파라미터가 막아준다.
1. 구글로 리다이렉트하는 시점에서 서버가 임의의 랜덤 값을 만들어 `state`에 실어 보낸댜.
2. 이 값을 세션 등에 잠깐 저장해 둔다.
3. 구글이 콜백으로 돌아올 때 같은 `state` 값을 그대로 되돌려준다.
4. 서버는 이 값이 저장해 둔 값과 일치하는지 확인한다. → 일치하지 않으면 요청 거부
즉, `state`는 "이 콜백이 방금 내가 시작한 그 로그인 시도의 응답이 맞다"는 것을 증명하는 값이다.
Spring security의 `oauth2Login()`은 이 `state` 생성/검증을 자동으로 처리해주기 때문에 빠지면 안 되는 핵심 로직이다.
4. OAuth2 만으로는 로그인이 되지 않는다.
OAuth2는 "권한 위임을 위한 관리 표준"이다. "로그인"을 위한 기능이 아니다.
예를 들면, "이 앱이 구글 캘린더에 접근해도 된다"를 증명하는 용도인 것이다.
또한, `access_token`은 애초에 "구글 API를 대신 호출해도 좋다"는 허가증일 뿐이지, "이 사람이 누구다"를 알려주는 신분증이 아니다.
따라서 로그인 성공을 보장하기 위해 OAuth2 위에 신원 확인 계층을 얹은 것이 OpenID Connect(OIDC)이다.
- OIDC는 `access_token`과 별개로 `id_token`을 함께 내려준다.
- `id_token`은 "이 사용자가 누구인지"를 담은, 구글이 서명한 JWT이다.
- 로그인 용도로 쓸 때는 이 id_token을 신뢰해야 한다.
5. Spring Security에서의 자동화
Spring Security의 `oauth2Login()`이 "리다이렉트 왕복 + `state` 검증 + 토큰 교환 과정"을 통째로 대신 처리해준다.
`SecurityConfig`에서 다음과 같이 설정한다.
http.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(u -> u.oidcUserService(customOidcUserService))
.successHandler(oAuth2SuccessHandler)
.failureHandler(oAuth2FailureHandler)
);
- `userInfoEndpoint`: 토큰을 받은 뒤 사용자 정보를 우리 시스템의 사용자 객체로 변환하는 과정이며, "이 사용자를 들일지" 판단하는 로직도 보통 여기에 넣음
- `successHandler`: 로그인 성공 후 어디로 리다이렉트할지, 우리 쪽 토큰을 어떻게 발급할지 결정
- `failureHandler`: 로그인 실패 시 어디로 리다이렉트 할 지 결정
실무에서는 다음과 같은 부분을 조심해야 한다:
`application.yml`의 `scope`에 `openid`를 빼먹으면 OIDC를 차지 않고 일반 OAuth2 경로로 빠진다.
그렇게 되면 `OidcUser` 타입으로 캐스팅하는 코드가 전부 깨지게 된다.
6. OAuth2/OIDC의 한계
OAuth2/OIDC가 증명해주는 건 "이 사람이 이 구글 계정의 주인이다."까지이다.
"이 사람이 우리 서비스를 쓸 자격이 있는가"는 별개의 질문이기 때문에, 직접 판단해야 한다.
'JAVA' 카테고리의 다른 글
| #3. Arrays 메소드 (0) | 2021.09.05 |
|---|---|
| #2. ArrayList (0) | 2021.08.31 |
| #1. 정규식 (0) | 2021.08.30 |