OAuth는 'Open Authorization'의 줄임말로 '인가(Authorization)'를 위한 개방형 표준 프로토콜입니다.
이 OAuth 프로토콜을 이용하여 네이버나 카카오 또는 구글, 페이스북 등
Third-Party 프로그램의 사용 권한을 얻을 수 있습니다.
✅ OAuth 2.0 프로토콜 구성하는 4가지 역할
OAuth 프로그램은 4가지 역할을 다음과 같습니다.
☝ Authorization Server
- 인증 및 인가가 성공적으로 끝난 Client에게 액세스 토큰(Access Token)을 발급합니다.
☝ Resource Server
- 보호된 리소스를 호스팅 하는 서버입니다.
- 액세스 토큰(Access Token)을 사용하여 보호된 리소스 요청에 응답합니다.
☝ Client
- Resource Owner를 대신해 권한을 부여해 보호된 리소스 요청을 진행합니다.
☝ Resource Owner
- 보호된 리소스에 대해 액세스(access) 권한을 부여할 수 있는 entity입니다.
- Resource Owner가 사람인 경우 최종 사용자(end-user)라고 합니다.
위의 역할을 요약하면 'Authorization Server'와 'Resource Server'는
사용자가 이용하고자 하는 Third Party입니다.
네이버 로그인을 이용할 때 'Authorization Server'와 'Resource Server'의 주체는 네이버입니다.
Client는 운영하는 서비스의 주체입니다.
제가 서비스하는 사이트에서 네이버 로그인 기능을 제공하고 있다면
Client는 제가 운영하는 사이트가 됩니다.
Resource Owner는 네이버 로그인을 이용하려는 사용자입니다.
✅ Protocol Flow
OAuth 2.0 프로토콜의 흐름을 알아보겠습니다.
(A)
Client는 Resource Owner에게 권한(Authorization) 요청을 합니다.
권한 요청은 Client에게 직접 만들어지거나,
Authorization Server로부터 간접적으로 만들어질 수 있습니다.
(B)
Client는 권한을 부여받습니다.
해당 권한은 Authorization Server가 제공하는 형태나 Client의 요청에 따라 다른데
일반적으로 4가지 타입의 권한이 존재합니다.
다음은 4가지 타입을 간단히 정리한 표입니다.
Authorization Code | 권한 코드 방식입니다. Resource Owner에게 직접적으로 권한 요청을 하는 대신 Client는 Resoure Owner를 Resource Server로 안내하고 Resource Owner는 다시 Authorization Code를 사용해 Client로 돌아가게 합니다. Resource Owner는 Resoure Server로만 인증하기 때문에 Client에는 Resource Owner의 개인정보가 Client에 공유되지 않습니다. 따라서 보안적인 이점이 있습니다. 프로젝트에서 사용하려는 네이버나 카카오 로그인도 이 권한 부여 방식을 이용합니다. |
Implicit | 암시적 권한 부여 방식입니다. 해당 방식은 JavaScript와 같은 스크립트 언어를 사용하는 브라우저에 사용하는 방식입니다. 만약 BackEnd 서버가 존재하지 않는 경우 client_secret을 사용할 수 없습니다. (해당 정보를 따로 노출하지 않고 관리할 방법이 없습니다.) 이럴 때 사용하는 방식이 암시적 권한 부여 방식입니다. Client에게 Authorization Code와 같은 자격 증명이 발급되지 않고, 바로 Access Token이 발급되는 방식입니다. |
Resource Owner Password Credentials | Resource Owner 비밀번호 자격 증명 방식입니다. 해당 방식은 Resource Owner와 Client간의 높은 신뢰관계가 있을 때 사용하는 방식입니다. |
Client Credentials | Client 자격 증명 방식입니다. 해당 방식은 일반적으로 Client가 Resource Owner라 자체적으로 작업을 수행하거나 Authorization Server에 Client를 위한 제한된 리소스 접근 권한이 설정된 경우 사용합니다. |
(C)
Client는 Authorization Server로부터 인증을 받고 권한을 제공해
Access Token을 발급받습니다.
(D)
Authorization Server는 Client를 인증하고 권한이 유효한지 확인합니다.
권한이 유효하다면 Access Token을 발급합니다.
(E)
Client는 제한된 리소스를 Resource Server에 요청합니다.
이때 발급받은 Access Token을 이용합니다.
(F)
Resource Server는 Access Token이 유효한지 확인합니다.
Access Token이 유효하다면 제한된 리소스를 제공합니다.
✅ 요청 / 응답 예시
실제 네이버 로그인 API 명세를 통해
어떤 요청과 응답이 발생하는지 확인해보겠습니다.
로그인 인증 요청 URI는 다음과 같습니다.
https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=jyvqXeaVOVmV&redirect_uri=http%3A%2F%2Fservice.redirect.url%2Fredirect&state=hLiDdL2uhPtsftcU
각 요청 변수에 대한 설명은 다음과 같습니다.
해당 요청 API를 호출했을 때 사용자가 네이버로 로그인하지 않은 상태이면
네이버 로그인 화면으로 이동합니다.
사용자가 네이버에 로그인을 한 상태라면 기본 정보 제공 동의 확인 화면으로 이동합니다.
정보 제공 동의 과정이 완료되면 콜백 URL에 code값과 state 값이 URL 문자열로 전송됩니다.
해당 code 값은 Access Token을 발급하는 데 사용됩니다.
API 요청 성공 시 얻는 콜백 URL 모습은 다음과 같습니다.
http://콜백URL/redirect?code={code값}&state={state값}
콜백 URL로 발급받은 code는 Access Token을 발급받을 때 사용합니다.
Access Token을 발급받을 때 사용하는 필드 값은 다음과 같습니다.
접근 토큰 발급 요청 URL은 다음과 같습니다.
https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=jyvqXeaVOVmV&client_secret=527300A0_COq1_XV33cf&code=EIc5bFrl4RibFls1&state=9kgsGTfH4j7IyAkg
✅ 실제 프로젝트에 적용해보기
프로젝트에는 네이버와 카카오 로그인 기능을 적용할 예정입니다.
네이버와 카카오 로그인을 적용하기 앞서 네이버와 카카오의 개발자 센터에서
네이버와 카카오 로그인을 하기 위한 기본 설정을 완료해줍니다.
해당 설정 방법은 각각 다음 문서를 참고해주세요.
우선 OAuth 2.0을 사용하기 위해 다음과 같은 라이브러리를 추가합니다.
'Scribe Java'는 OAuth 프로토콜을 자바에서 사용하기 편하게 만들어주는 라이브러리입니다.
//OAuth 2.0 소셜 로그인
implementation 'com.github.scribejava:scribejava-apis:2.8.1'
다음으로는 소셜 로그인을 하기 위한 소셜 로그인 객체를 만들어줍니다.
해당 객체를 통해 AccessToken을 발급받거나 Authorization하기 위한 URL을 쉽게 생성할 수 있습니다.
//네이버
import com.github.scribejava.core.builder.api.DefaultApi20;
public class NaverLoginApi extends DefaultApi20 {
protected NaverLoginApi() {}
private static class InstanceHolder {
private static final NaverLoginApi INSTANCE = new NaverLoginApi();
}
public static NaverLoginApi instance() {
return InstanceHolder.INSTANCE;
}
@Override
public String getAccessTokenEndpoint() {
return "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code";
}
//로그인 처리 후 인증 페이지
@Override
protected String getAuthorizationBaseUrl() {
return "https://nid.naver.com/oauth2.0/authorize";
}
}
- getAccessTokenEndPoint() 메서드는 Access Token을 발급받을 때 사용하는 URL입니다.
- getAuthorizationBaseUrl() 메서드는 Authorization Server로부터 인증을 받을 때 사용하는 URL입니다.
다음으로 SocialService 인터페이스를 만들겠습니다.
SocialService 인터페이스는 다음과 같이 작성합니다.
public interface SocialService {
//콜백을 위한 url 생성
String getAuthorizationUrl(HttpSession session);
//access token 발급 받기
OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException;
//발급 받은 access token을 이용해 회원 정보 가져오기
default String getUserProfile(OAuth2AccessToken oauthToken) throws IOException {
return null;
}
//발급받은 토큰 삭제하기
void deleteAccessToken(String accessToken) throws IOException;
}
- getAuthorizationUrl은 네이버 로그인을 위한 인증 URL을 생성하는 메서드입니다.
- getAccessToken은 Access Token을 발급받기 위한 메서드입니다.
- getUserProfile 메서드는 Access Token을 이용해 회원 정보를 가져오는 메서드입니다.
- deleteAccessToken은 발급받은 Access Token을 삭제하는 메서드입니다.
이제 SocialService 인터페이스를 구현해보겠습니다.
네이버 로그인 클래스로 설명하겠습니다.
public class NaverServiceImpl implements SocialService {
private final MemberMapper memberMapper;
@Value("${naver.login.client.id}")
private String CLIENT_ID;
@Value("${naver.login.client.secret}")
private String CLIENT_SECRET;
@Value("${naver.login.redirect.url}")
private String REDIRECT_URI;
@Value("${naver.login.session.state}")
private String SESSION_STATE;
// 프로필 조회 API URL
@Value("${naver.login.profile.api.url}")
private String PROFILE_API_URL;
/**
* session에 데이터 저장
* @param session
* @param state
*/
private void setSession(HttpSession session, String state){
session.setAttribute(SESSION_STATE, state);
}
/**
* 세션에서 데이터 가져오기
* @param session
* @return String
*/
private String getSession(HttpSession session){
return (String) session.getAttribute(SESSION_STATE);
}
/**
* State 생성
* @return String
*/
public String generateState()
{
SecureRandom random = new SecureRandom();
return new BigInteger(130, random).toString(32);
}
/**
* 네이버 아이디로 인증 URL 생성
* @param session
* @return String
*/
@Override
public String getAuthorizationUrl(HttpSession session) {
// 세션 유효성 검증을 위하여 난수를 생성
String state = generateState();
// 생성한 난수 값을 session에 저장
setSession(session,state);
// Scribe에서 제공하는 인증 URL 생성 기능을 이용하여 네아로 인증 URL 생성
OAuth20Service oauthService = new ServiceBuilder()
.apiKey(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URI)
.state(state) //앞서 생성한 난수값을 인증 URL생성시 사용함
.build(NaverLoginApi.instance());
//인증 URL 리턴
return oauthService.getAuthorizationUrl();
}
/**
* 네이버 아이디로 Callback 처리 및 AccessToken 획득
* @param session
* @param code
* @param state
* @return OAuth2AccessToken
* @throws IOException
*/
@Override
public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException {
// Callback으로 전달받은 세선검증용 난수값과 세션에 저장되어있는 값이 일치하는지 확인
String sessionState = getSession(session);
if(StringUtils.pathEquals(sessionState, state)){
OAuth20Service oauthService = new ServiceBuilder()
.apiKey(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URI)
.state(state)
.build(NaverLoginApi.instance());
// Scribe에서 제공하는 AccessToken 획득 기능으로 네아로 Access Token을 획득
OAuth2AccessToken accessToken = oauthService.getAccessToken(code);
return accessToken;
}
return null;
}
/**
* Access Token을 이용하여 네이버 사용자 프로필 API를 호출
* @param oauthToken
* @return String
* @throws IOException
*/
@Override
public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException{
//OAuth20Service 객체 정보 설정
OAuth20Service oauthService = new ServiceBuilder()
.apiKey(CLIENT_ID)
.apiSecret(CLIENT_SECRET)
.callback(REDIRECT_URI)
.build(NaverLoginApi.instance());
//OAuthRequest 응답 객체 생성
OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService);
oauthService.signRequest(oauthToken, request);
//정보 전송
Response response = request.send();
return response.getBody();
}
/***
* 네이버 회원 정보 삭제
* @param accessToken
*/
@Override
public void deleteAccessToken(String accessToken) {
String deleteUrl =
"https://nid.naver.com/oauth2.0/token?grant_type=delete"
+ "&client_id=" + CLIENT_ID
+ "&client_secret=" + CLIENT_SECRET
+ "&access_token=" + accessToken
+ "&service_provider=NAVER";
//result : success
}
}
네이버 로그인에 사용되는 정보는 properties 파일에 따로 작성해줍니다.
properties의 경우 다음과 같이 작성합니다.
## Naver Settings
naver.login.client.id=발급받은 client id
naver.login.client.secret=발급받은 client secret
naver.login.redirect.url=지정한 redirect url 정보
naver.login.session.state=naver_oauth_state
naver.login.profile.api.url=https://openapi.naver.com/v1/nid/me
## Kakao Settings
kakao.login.client.id=발급받은 client id
kakao.login.client.secret=발급받은 client secret
kakao.login.redirect.url=지정한 redirect url 정보
kakao.login.session.state=kakao_oauth_state
kakao.login.profile.api.url=https://kapi.kakao.com/v2/user/me
각 메서드를 알아보겠습니다.
☝ setSession()
- 세션에 state값을 저장하는 메서드입니다.
- 사이트 간 요청 위조(cross-site request forgery) 공격을 방지하기 위해 state값을 생성합니다.
☝ getSession()
- 세션에서 state값을 가져오는 메서드입니다.
☝ generateState()
- state값을 난수로 생성하는 메서드입니다.
☝ getAuthorizationUrl()
- 네이버 로그인을 위한 권한 요청 URL을 생성하는 메서드입니다.
- state값을 생성하고 세션에 저장합니다.
- 생성한 state값과 client id, client secret, redirect uri 정보를 이용해
권한 요청 URL을 생성합니다.
☝ getAccessToken()
- Callback URL 정보를 통해 Access Token을 발급받는 메서드입니다.
- 생성한 state값과 client id, client secret, redirect uri 정보를 이용해
Access Token을 발급받습니다.
☝ getUserProfile()
- Access Token을 이용해 네이버 사용자의 프로필을 호출하는 메서드입니다.
- client id, client secret, redirect uri 정보와 발급받은 Access Token을 이용해
사용자 정보를 전달합니다.
☝ deleteAccessToken()
- 발급받은 Access Token을 삭제하는 메서드입니다.
Service 클래스 구현이 끝났습니다.
우선 로그인 페이지로 진입했을 때 권한 인증 URL을 생성해줍니다.
로그인 컨트롤러에 다음과 같은 코드를 추가해줍니다.
//네이보 로그인을 위한 콜백 url 생성
String naverAuthUrl = naverService.getAuthorizationUrl(session);
//model에 저장
model.addAttribute("naverUrl", naverAuthUrl);
로그인 페이지에는 다음과 같이 추가해줍니다.
현재 저는 Thymeleaf를 사용하고 있습니다.
<button th:onclick="'location.href=\''+ @{${naverUrl}} + '\''" class="login100-social-item">
<img style="width : 50px; height : 50px;" src="/img/네이버.png" alt="kakao"/>
</button>
이제 callback URL을 받을 컨트롤러를 작성해줍니다.
/**
* 네이버 로그인 성공시 callback호출 메서드
* @param model
* @param code
* @param state
* @param session
* @return String
* @throws IOException
* @throws ParseException
*/
@RequestMapping(value = "/naver/callback", method = { RequestMethod.GET, RequestMethod.POST })
public String callback(Model model,
@RequestParam String code,
@RequestParam String state,
HttpSession session) throws IOException, ParseException {
//토큰 발급
OAuth2AccessToken oauthToken;
oauthToken = naverService.getAccessToken(session, code, state);
if(oauthToken == null) {
throw new IOException();
}
//로그인 사용자 정보를 읽기
apiResult = naverService.getUserProfile(oauthToken); //String형식의 json데이터
//accessToken 가져오기
String accessToken = oauthToken.getAccessToken();
//.... 중략
}
SocialService를 구현했던 NaverServiceImpl 클래스를 통해 컨트롤러를 작성해줍니다.
다음과 같이 코드를 작성하면 네이버 로그인을 사용할 수 있습니다.
자세한 구현 내용과 카카오 구현 내용은 블로그에 따로 기술하지 않겠습니다.
해당 내용은 Github에서 코드를 통해 참조해주세요!
✅ 참조
'내가 공부하려고 올리는 > 스프링' 카테고리의 다른 글
Github Actions로 properties 파일 만들어서 배포하기 (0) | 2022.07.15 |
---|---|
스프링 시큐리티(Spring Security) CSRF 설정(AJAX, POST FORM) (0) | 2022.07.15 |
스프링 인터셉터(Interceptor) (0) | 2022.07.13 |
PathVariable, RequestParam, RequestBody(스프링에서 파라미터 받기) (0) | 2022.07.12 |
세션 저장소로 데이터베이스 사용하기(MySQL) (0) | 2022.07.12 |
댓글