- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 12. 마치며
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 11. OAuth 서비스 심사 신청하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 10. 컨트롤러 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 9. Jersey로 RESTful API 서비스 제공하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 8. 프로세스 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 7. GitHub OAuth 서비스 신청 및 모듈 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 6. KAKAO OAuth 서비스 신청 및 모듈 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 5. Google OAuth 서비스 신청 및 모듈 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 4. NAVER OAuth 서비스 신청 및 모듈 구현하기
👀 [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 3. scribeJAVA로 OAuth2.0 인증 모듈 구현하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 2. 인증서버 설계하기
- [OAuth2.0] ScribeJAVA로 OAuth2.0 인증서버 구축하기 - 1. OAuth2.0이란?
Table of Contents
개요 🔗
OAuth 라이브러리인 scribeJAVA를 통해 인증 모듈을 구현해보자.
OAuth 인증 모듈 구현하기 🔗
이전 장에서도 언급했듯이, OAuth 인증 모듈은 그 공통된 특성으로 인해 추상 객체가 적합하다.
scribeJAVA 모듈을 사용하여 추상 객체를 구현한다.
scribeJAVA 적용하기 🔗
프로젝트에 scribeJAVA를 적용해보자.
GROOVY
0 | implementation group: 'com.github.scribejava', name: 'scribejava-apis', version: '8.3.1' |
build.gradle
의 dependencies에 위 의존성을 추가하는 것으로 scribeJAVA를 적용할 수 있다.
scribeJAVA 사용하기 🔗
본격적으로 scribeJAVA를 사용해보자.
scribeJAVA는 OAuth20Service
라는 객체를 중심으로 동작한다. OAuth20Service
객체를 통해 아래의 로직을 수행할 수 있다.
- 플랫폼 로그인 URL 생성
- 인가 코드를 Access, Refresh Token으로 교환
- Refresh Token을 통해 Access Token 갱신
- 기타 OAuth 관련 요청 생성
즉, OAuth 인증에 있어서 핵심이 되는 동작은 모두 이 OAuth20Service
객체를 중심으로 이루어진다.
OAuth20Service 객체 생성하기 🔗
OAuth20Service
객체를 생성한다. 필요한 요소는 아래와 같다.
구분 | 필수 여부 | 내용 |
---|---|---|
API Key | Y | API 키 |
API Secret Key | Y | API 시크릿 키 |
Callback URL | Y | 로그인 결과를 반환할 URL |
Scope | N (일부 플랫폼 Y) | 권한 |
API Key, API Secret Key, Callback URL은 기본적으로 반드시 필요하며, 몇몇 플랫폼의 경우 Scope도 필수 사양으로 지정하는 경우가 있다. 이 프로젝트에 적용할 플랫폼들 중 Google이 그렇다. 구글은 후술할 플랫폼 로그인 URL 생성 시 Scope를 지정하지 않으면 오류를 출력한다.
API Key, API Secret Key는 각 플랫폼에 OAuth 서비스 등록 시 부여해주며, Callback URL은 본인이 직접 정해서 입력하면 된다. 등록되지 않은 Callback URL로 로그인을 수행하면 오류가 출력된다. Callback URL은 여러개를 지정할 수 있다.
Callback URL의 등록 이유
Callback URL이 사전에 등록된 URL이 아닐경우 오류가 뜨는 이유는 보안 때문이다. 플랫폼 로그인 URL은 Callback URL이 URL 파라미터 형태로 입력되어있어서 탈취 및 변조가 매우 간단하다. 이러한 상황에서 Callback URL의 적절한 검증을 수행하지 않는다면 Callback URL을 임의 URL로 변경하여 code 혹은 Access Token을 탈취할 수 있게 된다.
각 플랫폼 별 OAuth 서비스를 등록하는 방법은 추후에 다루고, 일단 이런 부가적인 부분들은 준비가 됐다고 가정한다.
JAVA
0 | OAuth20Service service = new ServiceBuilder("{API_KEY}").apiSecret("{SECRET_KEY}").callback("{CALLBACK_URL}").build(this); |
이와 같이 생성할 수 있다. build(this)
에서 this
는 DefaultApi20
객체다. 아래는 OAuth20Service
객체의 메서드와 그 기능들이다.
구분 | 내용 |
---|---|
getAuthorizationUrl |
플랫폼 로그인 URL 반환 메서드 |
getAccessToken |
OAuth2AccessToken 반환 메서드 |
signRequest |
OAuth 요청 등록 메서드 |
execute |
등록된 요청 수행 메서드 |
이 프로젝트에선 위 4가지 용도만 알아도 무방하다.
AuthModule 생성하기 🔗
이제 본격적으로 모듈 객체를 구현해보자. 인증 모듈은 반드시 DefaultApi20
을 상속받아 구현해야한다.
JAVA
0 | abstract public class AuthModule extends DefaultApi20 |
1 | { |
2 | // 구현 예정 |
3 | } |
DefaultApi20
추상 객체는 두 개의 추상 메서드를 가지고 있다. 즉, 이를 상속하는 AuthModule
는 이 두 메서드를 구현해야할 책임이 있다.
getAccessTokenEndpoint
- 접근 토큰 요청 URL 반환 메서드getAuthorizationBaseUrl
- 인증 URL 반환 메서드
하지만 AuthModule
역시 추상 객체이므로, 이를 상속받아 사용할 하위 플랫폼 인증 모듈에게 이 책임을 위임할 수 있다. 즉, DefaultApi20
의 추상 메서드는 AuthModule
에서 구현하지 않고 이를 상속하는 하위 플랫폼 인증 모듈에서 구현할 것이다.
인증 모듈 추상 객체는 위와 같은 형태를 가진다. NAVER, Google 등 플랫폼별 인증 모듈은 위 AuthModule
을 상속받아 사용할 것이다. 인증 모듈의 핵심 동작은 대부분 OAuth20Service
객체로부터 이루어지니, AuthModule
을 상속할 때 반드시 관련 객체를 받도록 명시하는 것이 좋아보인다.
JAVA
0 | abstract public class AuthModule extends DefaultApi20 |
1 | { |
2 | protected OAuth20Service service; |
3 | |
4 | @Getter |
5 | protected String unique; |
6 | |
7 | protected AuthModule(ServiceBuilderOAuth20 serviceBuilder, String unique) |
8 | { |
9 | service = serviceBuilder.build(this); |
10 | |
11 | this.unique = unique; |
12 | } |
13 | } |
구분 | 데이터 형식 | 내용 |
---|---|---|
serviceBuilder | ServiceBuilderOAuth20 |
OAuth20Service 객체의 빌더 |
unique | String |
인증 모듈의 고유값. 플랫폼의 소문자 표기와 동일 (ex. NAVER -> naver 등) |
생성자와 멤버 변수를 위와 같이 지정한다. 전부 protected
접근 제어자를 가지므로, AuthModule
을 상속받은 객체에서만 생성자를 사용할 수 있으며, service
, unique
파라미터 역시 마찬가지다.
ServiceBuilderOAuth20
를 인수로 할당받아 생성자에서 OAuth20Service
로 빌드하여 멤버 변수 service
에 할당한다. AuthModule
상속받은 객체의 어느 곳에서든 service
와 unique
에 접근할 수 있다.
이 모듈에 기본적인 로직을 작성하고, 플랫폼마다 구현이 모두 다를 경우, 추상 메서드를 선언하여 이를 상속받는 객체가 이를 직접 구현하도록 위임한다.
인증 URL 반환 메서드 🔗
scribeJAVA를 통해 인증 URL 반환 메서드를 구현해보자. 예를 들어, 네이버 아이디로 로그인 버튼을 클릭하면 네이버 로그인 창이 뜰 것이다. 이 과정은 앞선 예시와 같은 플랫폼 로그인 창의 URL을 생성하는 것이다.
service
의 getAuthorizationUrl
메서드를 사용하는 것만으로 간단히 구현할 수 있다. ServiceBuilderOAuth20
에 입력했던 API Key, Secret Key, Callback URL을 토대로 플랫폼 인증 URL을 생성하여 반환한다.
JAVA
0 | public String getAuthorizationUrl(String state) |
1 | { |
2 | return service.getAuthorizationUrl(state); |
3 | } |
getAuthorizationBaseUrl
의 반환 URL을 기준으로 생성한다.
인수인 state
는 고유 상태값으로, 서버에서 임의의 UUID를 하나 생성해서 사용한다. 이는 보안을 위한 세션 체크용으로 사용하는 값이다.
접근 토큰 반환 메서드 🔗
이번엔 OAuth2AccessToken
접근 토큰 객체를 반환하는 메서드를 구현해보자. OAuth2AccessToken
는 Access, Refresh Token. 토큰 종류, 유효시간을 가지는 scribeJAVA의 객체다.
JAVA
0 | public OAuth2AccessToken getAccessToken(String code) throws IOException, ExecutionException, InterruptedException |
1 | { |
2 | return service.getAccessToken(code); |
3 | } |
마찬가지로 service
의 getAccessToken
메서드를 사용하여 간단히 구현할 수 있다. 인수인 code
를 Service Provider에 전달하면, Access, Refresh Token을 제공한다.
접근 토큰 갱신 및 반환 메서드 🔗
Access Token은 보안을 위해 만료시간이 굉장히 짧거나 세션 만료 시 같이 만료되는 것이 보통이다. 이 경우 원래대로라면 다시 인증을 받아야하고, 경우에 따라 사용자에게 플랫폼 로그인 수행을 다시 요구할 수도 있다.
대부분의 OAuth 플랫폼은 인증 시 Access Token과 함께 Refresh Token을 같이 제공한다.
구분 | 만료시간 | 인증 가능 여부 | 내용 |
---|---|---|---|
Access Token | 짧거나 일시적 | 가능 | 인증을 담당하는 토큰. Access Token 존재 자체만으로 사용자가 인증 정보를 제공한다고 가정한다. |
Refresh Token | 길거나 영구적 | 불가능 | Access Token을 재발급하는 토큰. Refresh Token 자체만으론 재발급 외의 유의미한 작업이 불가능하다. |
두 토큰의 차이는 위와 같다.
scribeJAVA에서 Refresh Token으로 Access Token을 재발급 받아보자.
JAVA
0 | public OAuth2AccessToken getRefreshAccessToken(String refresh) throws IOException |
1 | { |
2 | HashMap<String, String> params = new HashMap<>(); |
3 | params.put("client_id", service.getApiKey()); |
4 | params.put("client_secret", service.getApiSecret()); |
5 | params.put("refresh_token", refresh); |
6 | |
7 | StringBuilder builder = new StringBuilder(); |
8 | |
9 | for (Map.Entry<String, String> param : params.entrySet()) |
10 | { |
11 | builder.append("&").append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8)).append("=").append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8)); |
12 | } |
13 | |
14 | byte[] paramBytes = builder.toString().getBytes(StandardCharsets.UTF_8); |
15 | |
16 | URL url = new URL(getRefreshTokenEndpoint()); |
17 | |
18 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
19 | connection.setRequestMethod("POST"); |
20 | connection.setDoOutput(true); |
21 | connection.getOutputStream().write(paramBytes); |
22 | |
23 | BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); |
24 | |
25 | StringBuilder responseBuilder = new StringBuilder(); |
26 | String temp; |
27 | |
28 | while ((temp = reader.readLine()) != null) |
29 | { |
30 | responseBuilder.append(temp); |
31 | } |
32 | |
33 | reader.close(); |
34 | |
35 | ObjectMapper mapper = new ObjectMapper(); |
36 | |
37 | JsonNode node = mapper.readTree(responseBuilder.toString()); |
38 | |
39 | String access_token = node.get("access_token").textValue(); |
40 | String token_type = node.get("token_type").textValue(); |
41 | int expires_in = node.get("expires_in").intValue(); |
42 | |
43 | return new OAuth2AccessToken(access_token, token_type, expires_in, null, null, responseBuilder.toString()); |
44 | } |
45 | |
46 | @Override |
47 | public String getRefreshTokenEndpoint() |
48 | { |
49 | return Util.builder(getAccessTokenEndpoint(), "?grant_type=refresh_token"); |
50 | } |
Access Token 재발급 코드는 위와 같다. 원래 service.refreshAccessToken()
메서드가 있긴 한데, getRefreshTokenEndpoint
처리가 플랫폼별로 다른건지, 제대로 동작이 안 되는 것 같다. 필자는 그냥 HttpURLConnection
을 활용하여 OAuth2.0 스펙에 맞게끔 요청을 설계했다.
getRefreshTokenEndpoint
메서드는 Refresh Token 관련 작업을 수행할 때 베이스가 되는 URL을 반환하는 메서드다. getAccessTokenEndpoint
URL 뒤에 ?grant_type=refresh_token
파라미터를 붙여 반환한다.
getRefreshTokenEndpoint
이 반환하는 URL을 기준삼아 client_id
, client_secret
, refresh_token
을 파라미터로 담아 전송하여 응답을 받고, 여기서 필요한 내용을 추출하여 OAuth2AccessToken
객체를 생성하여 반환한다.
사용자 정보 응답 반환 메서드 🔗
Access Token을 성공적으로 받아왔다면, 이를 통해 사용자 정보를 불러올 수 있다.
JAVA
0 | public Response getUserInfo(String access) throws IOException, ExecutionException, InterruptedException |
1 | { |
2 | OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, getUserInfoEndPoint()); |
3 | service.signRequest(access, oAuthRequest); |
4 | |
5 | return service.execute(oAuthRequest); |
6 | } |
7 | |
8 | abstract protected String getUserInfoEndPoint(); |
9 | |
10 | abstract public UserInfoBean getUserInfoBean(String body) throws JsonProcessingException; |
총 세 가지 메서드를 정의해야하는데, 하나는 직접 구현이고 나머지 두 개는 추상 메서드다.
getUserInfo
메서드는 Access Token을 활용하여 해당 플랫폼의 UserInfo API로 요청을 보내 응답을 반환한다.
OAuthRequest
객체를 선언하여 요청 메서드와 대상 URL을 지정한다. 이후 service.signRequest
를 통해 service
에 요청을 등록하고, service.execute
로 해당 요청을 수행한다.
getUserInfoEndPoint
메서드는 각 플랫폼의 UserInfo API URL을 반환하는 추상 메서드로, 플랫폼마다 URL이 다르기 때문에 추상 객체로 선언하여 하위 플랫폼 인증 모듈에게 구현 책임을 위임한다.
getUserInfoBean
는 getUserInfo
의 응답을 받아 UserInfoBean
DTO로 변환하여 이를 반환하는 추상 메서드다. 참고로 UserInfoBean
은 scribeJAVA에 포함된 것이 아니라, 직접 작성한 DTO 객체다.
플랫폼마다 사용자 정보의 응답값이 다르기 때문에, 이를 적절히 대응하고 값을 반환하고자 설계된 메서드다. 이 역시 하위 플랫폼 인증 모듈에게 구현을 위임한다.
연동 해제 결과 반환 메서드 🔗
플랫폼으로부터 완전히 연동을 해제하는 메서드가 필요하다.
이는 단순히 로그아웃이 아니라, 플랫폼과 해당 사용자의 연결을 완전히 파기한다. 이 과정에서 사용자의 관련 데이터 및 정보 제공 동의 이력 역시 같이 파기된다.
연동 해제 후 다시 로그인을 하게 되면, 처음 로그인을 수행하는 것 처럼 약관과 정보 제공 동의를 다시 선택해야한다.
JAVA
0 | abstract public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException; |
연동 해제의 경우 OAuth의 범주에선 살짝 벗어나있어서, 이 역시도 동일한 인터페이스를 가지진 않는다. 각 플랫폼마다의 구현이 천차만별이므로 추상 메서드로 정의한다.
정보 제공 동의 갱신 URL 반환 메서드 🔗
서비스를 운영하다보면 사용자에게 요구할 정보가 변경되기도 한다.
만약 한참 잘 운영하다가 사용자에게 추가적인 정보를 받아야만 한다면? 정보를 호출해도 동의 내역 자체가 없으니 관련 정보는 얻을 수조차 없다.
이러한 상황에 대비해 정보 제공 동의를 갱신하는 기능이 필요하다.
JAVA
0 | abstract public String getUpdateAuthorizationUrl(String state); |
정보 제공 동의 갱신 역시 플랫폼마다 천차만별로 조금씩 다른 것 같다. 마찬가지로 추상 메서드로 정의하자.
API 객체 반환 메서드 🔗
마지막으로 scribeJAVA와 직접적인 관련은 없지만, API 관련 요소를 불러오기위한 메서드가 필요하다.
OAuth2.0을 사용하기 위해 필요한 3가지 요소는 API Key, API Secret Key, Callback URL이 필요하다. 이를 하드코딩해서 코드 안에 녹이는 건 그리 좋은 방법은 아니다.
플랫폼별 API 요소를 .properties
파일로 관리하여 WEB-INF/
아래에서 관리할 것이다.
WEB-INF의 특별함
Tomcat의WEB-INF
는 조금 특별하다. 기본적으로 대상 경로 아래의 모든 폴더 및 파일은 웹 형태로 접근이 가능하지만,WEB-INF
아래에 위치하는 폴더 및 파일은 배포 대상에서 제외되므로 접근할 수 없다. 하지만 파일 시스템엔 여전히 존재하고 있으므로, 이에 구애받지 않는 JAVA 등 Backend에서의 접근엔 지장이 없다.
API 키, 암호화 키 등 보안에 웹 서버 운영에 필요하면서도 각별한 보안이 요구되는 파일은WEB-INF
아래에 관리하는 것이 좋다.
이렇게 관리하고 gitignore
에서 각 플랫폼의 설정파일을 제외하면 GitHub에 올려도 해당 파일을 제외하고 올린다. 따라서 이렇게 코드를 오픈해도 API 유출을 막을 수 있다.
코드는 아래와 같다.
JAVA
0 | protected static ApiKeyBean getApiKeyBean(String platform) |
1 | { |
2 | ApiKeyBean apiKeyBean; |
3 | apiKeyBean = new ApiKeyBean(); |
4 | |
5 | // API 키 획득 시도 |
6 | try |
7 | { |
8 | HashMap<String, String> map = Util.getProperties(platform); |
9 | |
10 | apiKeyBean.setApi(map.get("api")); |
11 | apiKeyBean.setSecret(map.get("secret")); |
12 | apiKeyBean.setCallback(map.get("callback")); |
13 | } |
14 | |
15 | // 예외 |
16 | catch (Exception e) |
17 | { |
18 | e.printStackTrace(); |
19 | } |
20 | |
21 | return apiKeyBean; |
22 | } |
JAVA
0 | @Getter |
1 | @Setter |
2 | public class ApiKeyBean |
3 | { |
4 | // API 키 |
5 | private String api; |
6 | |
7 | // API SECRET 키 |
8 | private String secret; |
9 | |
10 | // 콜백 URL |
11 | private String callback; |
12 | } |
ApiKeyBean
은 직접 설계한 객체로, 위 코드와 같다. lombok
이 적용되어 있다.
JAVA는 .properties
파일을 읽어 key-value 형태의 HashMap으로 변환해주는 기능이 있다.
PROPERTIES
0 | api={API_KEY} |
1 | secret={SECRET_KEY} |
2 | callback={CALLBACK_URL} |
설정 파일은 위와 같다. 이 메서드를 활용하여 각 플랫폼별 API 설정파일을 불러오도록 구성한다.
AuthModule 전체 코드 🔗
JAVA
0 | package oauth.account.module; |
1 | |
2 | import com.fasterxml.jackson.core.JsonProcessingException; |
3 | import com.fasterxml.jackson.databind.JsonNode; |
4 | import com.fasterxml.jackson.databind.ObjectMapper; |
5 | import com.github.scribejava.core.builder.ServiceBuilderOAuth20; |
6 | import com.github.scribejava.core.builder.api.DefaultApi20; |
7 | import com.github.scribejava.core.model.OAuth2AccessToken; |
8 | import com.github.scribejava.core.model.OAuthRequest; |
9 | import com.github.scribejava.core.model.Response; |
10 | import com.github.scribejava.core.model.Verb; |
11 | import com.github.scribejava.core.oauth.AccessTokenRequestParams; |
12 | import com.github.scribejava.core.oauth.OAuth20Service; |
13 | import global.module.Util; |
14 | import lombok.Getter; |
15 | import oauth.account.bean.ApiKeyBean; |
16 | import oauth.account.bean.UserInfoBean; |
17 | |
18 | import java.io.BufferedReader; |
19 | import java.io.IOException; |
20 | import java.io.InputStreamReader; |
21 | import java.net.HttpURLConnection; |
22 | import java.net.URL; |
23 | import java.net.URLEncoder; |
24 | import java.nio.charset.StandardCharsets; |
25 | import java.util.HashMap; |
26 | import java.util.Map; |
27 | import java.util.concurrent.ExecutionException; |
28 | |
29 | /** |
30 | * 인증 모듈 추상 클래스 |
31 | * |
32 | * @author RWB |
33 | * @since 2021.09.29 Wed 23:30:47 |
34 | */ |
35 | abstract public class AuthModule extends DefaultApi20 |
36 | { |
37 | protected OAuth20Service service; |
38 | |
39 | @Getter |
40 | protected String unique; |
41 | |
42 | /** |
43 | * 생성자 메서드 |
44 | * |
45 | * @param serviceBuilder: [ServiceBuilderOAuth20] API 서비스 빌더 |
46 | * @param unique: [String] 유니크 키 |
47 | */ |
48 | protected AuthModule(ServiceBuilderOAuth20 serviceBuilder, String unique) |
49 | { |
50 | service = serviceBuilder.build(this); |
51 | |
52 | this.unique = unique; |
53 | } |
54 | |
55 | abstract protected String getUserInfoEndPoint(); |
56 | |
57 | abstract public UserInfoBean getUserInfoBean(String body) throws JsonProcessingException; |
58 | |
59 | abstract public boolean deleteInfo(String access) throws IOException, ExecutionException, InterruptedException; |
60 | |
61 | abstract public String getUpdateAuthorizationUrl(String state); |
62 | |
63 | /** |
64 | * 인증 URL 반환 메서드 |
65 | * |
66 | * @param state: [String] 고유 상태값 |
67 | * |
68 | * @return [String] 인증 URL |
69 | */ |
70 | public String getAuthorizationUrl(String state) |
71 | { |
72 | return service.getAuthorizationUrl(state); |
73 | } |
74 | |
75 | /** |
76 | * 접근 토큰 반환 메서드 |
77 | * |
78 | * @param code: [String] 인증 코드 |
79 | * |
80 | * @return [OAuth2AccessToken] 접근 토큰 |
81 | * |
82 | * @throws IOException 데이터 입출력 예외 |
83 | * @throws ExecutionException 실행 예외 |
84 | * @throws InterruptedException 인터럽트 예외 |
85 | */ |
86 | public OAuth2AccessToken getAccessToken(String code) throws IOException, ExecutionException, InterruptedException |
87 | { |
88 | return service.getAccessToken(code); |
89 | } |
90 | |
91 | /** |
92 | * 접근 토큰 반환 메서드 |
93 | * |
94 | * @param params: [AccessTokenRequestParams] AccessTokenRequestParams 객체 |
95 | * |
96 | * @return [OAuth2AccessToken] 접근 토큰 |
97 | * |
98 | * @throws IOException 데이터 입출력 예외 |
99 | * @throws ExecutionException 실행 예외 |
100 | * @throws InterruptedException 인터럽트 예외 |
101 | */ |
102 | public OAuth2AccessToken getAccessToken(AccessTokenRequestParams params) throws IOException, ExecutionException, InterruptedException |
103 | { |
104 | return service.getAccessToken(params); |
105 | } |
106 | |
107 | /** |
108 | * 접근 토큰 갱신 및 반환 메서드 |
109 | * |
110 | * @param refresh: [String] 리프레쉬 토큰 |
111 | * |
112 | * @return [OAuth2AccessToken] 접근 토큰 |
113 | * |
114 | * @throws IOException 데이터 입출력 예외 |
115 | */ |
116 | public OAuth2AccessToken getRefreshAccessToken(String refresh) throws IOException |
117 | { |
118 | HashMap<String, String> params = new HashMap<>(); |
119 | params.put("client_id", service.getApiKey()); |
120 | params.put("client_secret", service.getApiSecret()); |
121 | params.put("refresh_token", refresh); |
122 | |
123 | StringBuilder builder = new StringBuilder(); |
124 | |
125 | for (Map.Entry<String, String> param : params.entrySet()) |
126 | { |
127 | builder.append("&").append(URLEncoder.encode(param.getKey(), StandardCharsets.UTF_8)).append("=").append(URLEncoder.encode(param.getValue(), StandardCharsets.UTF_8)); |
128 | } |
129 | |
130 | byte[] paramBytes = builder.toString().getBytes(StandardCharsets.UTF_8); |
131 | |
132 | URL url = new URL(getRefreshTokenEndpoint()); |
133 | |
134 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); |
135 | connection.setRequestMethod("POST"); |
136 | connection.setDoOutput(true); |
137 | connection.getOutputStream().write(paramBytes); |
138 | |
139 | BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); |
140 | |
141 | StringBuilder responseBuilder = new StringBuilder(); |
142 | String temp; |
143 | |
144 | while ((temp = reader.readLine()) != null) |
145 | { |
146 | responseBuilder.append(temp); |
147 | } |
148 | |
149 | reader.close(); |
150 | |
151 | ObjectMapper mapper = new ObjectMapper(); |
152 | |
153 | JsonNode node = mapper.readTree(responseBuilder.toString()); |
154 | |
155 | String access_token = node.get("access_token").textValue(); |
156 | String token_type = node.get("token_type").textValue(); |
157 | int expires_in = node.get("expires_in").intValue(); |
158 | |
159 | return new OAuth2AccessToken(access_token, token_type, expires_in, refresh, null, responseBuilder.toString()); |
160 | } |
161 | |
162 | /** |
163 | * 사용자 정보 응답 반환 메서드 |
164 | * |
165 | * @param access: [String] 접근 토큰 |
166 | * |
167 | * @return [Response] 사용자 정보 응답 |
168 | * |
169 | * @throws IOException 데이터 입출력 예외 |
170 | * @throws ExecutionException 실행 예외 |
171 | * @throws InterruptedException 인터럽트 예외 |
172 | */ |
173 | public Response getUserInfo(String access) throws IOException, ExecutionException, InterruptedException |
174 | { |
175 | OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, getUserInfoEndPoint()); |
176 | service.signRequest(access, oAuthRequest); |
177 | |
178 | return service.execute(oAuthRequest); |
179 | } |
180 | |
181 | /** |
182 | * 접근 토큰 재발급 요청 URL 반환 메서드 |
183 | * |
184 | * @return [String] 접근 토큰 재발급 요청 URL |
185 | */ |
186 | @Override |
187 | public String getRefreshTokenEndpoint() |
188 | { |
189 | return Util.builder(getAccessTokenEndpoint(), "?grant_type=refresh_token"); |
190 | } |
191 | |
192 | /** |
193 | * API 키 객체 반환 메서드 |
194 | * |
195 | * @param platform: [String] 플랫폼 |
196 | * |
197 | * @return [ApiKeyBean] API 키 객체 |
198 | */ |
199 | protected static ApiKeyBean getApiKeyBean(String platform) |
200 | { |
201 | ApiKeyBean apiKeyBean; |
202 | apiKeyBean = new ApiKeyBean(); |
203 | |
204 | // API 키 획득 시도 |
205 | try |
206 | { |
207 | HashMap<String, String> map = Util.getProperties(platform); |
208 | |
209 | apiKeyBean.setApi(map.get("api")); |
210 | apiKeyBean.setSecret(map.get("secret")); |
211 | apiKeyBean.setCallback(map.get("callback")); |
212 | } |
213 | |
214 | // 예외 |
215 | catch (Exception e) |
216 | { |
217 | e.printStackTrace(); |
218 | } |
219 | |
220 | return apiKeyBean; |
221 | } |
222 | } |
전체 소스는 위와 같다.
중간에 한 번씩 사용되는 Util
객체는 해당 프로젝트에서 범용적으로 사용되는 메서드를 모아놓은 공통 모듈이다.
정리 🔗
scribeJAVA를 통해 인증 모듈의 원형이되는 추상 객체를 구현했다. 해당 모듈을 토대로 각 플랫폼별 인증 모듈을 확장하여 개발할 수 있을 것이다.
다음 장에서는 본격적으로 플랫폼별 OAuth 서비스 신청과 인증 모듈 구현에 대해 다룬다.
📆 작성일
2021-10-20 Wed 01:26:40
📚 카테고리
🏷️ 태그