Authorization Code Grant 는 OAuth 2.0 인증 유형 중 하나로,
사용자가 권한 제공 기관에 연결하여 인증 받고 권한 부여에 동의하면 승인 코드를 발급해주는 유형이다.
해당 코드(Authorization code)를 통해 Access Token 을 받을 수 있으며
해당 Access Token 을 가지고 여러 서비스(구글 계정 정보 등)을 얻을 수 있다.
Authorization code 를 받는 이유?
웹 브라우저 <-> Provider ( Authorization Server ) 로 Access Token 을 받는 것보다
Client(Application) <-> Provider 로 Token 을 주는게 보안 상 더 좋다고 한다.
해당 포스트에서는 Google API Console + SpringBoot를 활용한 Authorization Code 방식 구현 코드를 작성했다.
키워드 : Intellij + SpringBoot + Maven + Java8 + Oauth 2.0 + Google Api Console
목적 : 코드 구현을 통한 Authorization Code Grant 흐름 파악 ( 누가 어떤 정보를 요청하고 누가 어떤 정보를 주고 받는 지 등 )
* Spring Security + Oauth2.0 구현이 아니다.
먼저, 사용할 Google API Console 등록 과정 및 용어 설명을 진행한다.
[1. Google Api Console 등록 과정]
(등록) 클라이언트가 Resource Server 를 이용하기 위해서 승인을 사전에 받는 과정이 필요하다.
서비스에 따라서 등록 방법은 다르지만 공통적으로
- Client ID
- Client Secret
- Authorized redirect URL
등은 사용되는 것 같다.
등록 과정은 다음과 같다.
- 프로젝트 생성
- 사용자 인증 정보 생성
-- 1.1 프로젝트 생성
Google API Console 접속 및 프로젝트 생성
console.developers.google.com/
-- 1.2 사용자 인증 정보 생성 (= OAuth Client ID 생성)
사용자 인증 정보 생성 > OAuth Client ID 생성 클릭 및 각 종 사항 기입
> OAuth 동의 화면 작성
- User Type : 외부
- 이름과 이메일 작성 후 Next & Finish
> 완료 후 사용자 인증정보 > OAuth Client ID 생성 다시 클릭
- 애플리케이션 유형 : 웹 프로젝트
- 승인된 리다이렉션 URI : Callback 에 사용할 URL 작성
(ex. http://localhost:8080/callback
* 만약 Postman 으로 테스트 할 경우, http://oauth.pstmn.io/v1/callback )
> Client ID & Client Secret 발급
-- 용어 정리
redirect uri : 서비스 제공자가 인가 요청에 대한 응답을 전달하는 URL (= Authorization code 를 돌려 받을 URL)
client id : 어떤 Client 가 OAuth 인증을 요청하는 지 Google 서버에서도 알아야 하기 때문에 필요
client secret : 비밀키, 해당 키를 통해 토큰을 생성
response_type : 어떤 OAuth 인증 방식을 사용할 것인가
- token : Implicit Grant 사용 시 사용
- code : Authorization Code Grant 사용 시 사용
state : 사이트 간 CSRF 공격을 막기 위해 애플리케이션에서 사용되는 예측 불가한 값으로 사용되는 것(애매)
access_type : 사용자가 브라우저를 나가도 Refresh Token을 받아서 쓸 수 있는 지 여부
- offline : Access Token 을 받을 때 Refresh Token 을 같이 받는다. (= 만료 시 갱신 필요한 경우 사용)
- online :
scope : 사용하고자 하는 API 서비스, 사용자 로그인 시 명시적으로 동의를 받는다.
즉, 사용자의 어떤 데이터와 어떤 작업에 대한 권한을 요청하는 지를 나타낸다.
if ) Access Token 이 반환되는 경우, Access Token의 유효 범위를 나타낸다.
token_type : 발급된 Access Token 의 타입 (보통 Bearer)
expires_in : Access Token 이 만료되기 까지 남은 시간 (초 단위)
해당 유효 기간에만 API 호출이 가능하다.
id_token : JWT(Json Web Token) 토큰 값으로 토큰 인증에 사용된다.
* Client 에서 서비스 제공 요청 시 -> 서버는 토큰 무결성 체크 & payload 정보를 사용해 데이터를 넘겨준다.
[2. 동작 과정 (=시나리오)]
* Resource Server / Authorization Server 역할은 Google 에서 수행한다.
1. 사용자(Resource Owner) 구글 로그인 수행(= 사용자의 승인)
2. 로그인 시, 구글(Authorization Server)에서 code 라는 파라미터를 붙여서 redirection_url 로 callback 수행
3. code + grant_type + redirection_url + client_id + client_secret 등을 담아서 구글에게 재 요청 (= Access Token 달라고 요청)
4. 구글(Authorization Server)은 해당 Access Token 을 비교
-> access_token + expires_in + scope + token_type + id_token 반환
5. 해당 access_token 을 가지고 "Google 계정 정보" 등 서비스를 요청
6. 구글(Resource Server)에서 해당 서비스 제공
[3. 구현]
-- 로그인 창
웹 브라우저 로그인 창을 만들기 위해 view로는 Thymeleaf templates 을 사용한다.
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<a href="
https://accounts.google.com/o/oauth2/v2/auth?
scope=https://www.googleapis.com/auth/cloud-identity.devices.lookup&
access_type=offline&
include_granted_scopes=true&
state=state_parameter_passthrough_value&
redirect_uri=<Redirect URL 기입>&
response_type=code&
client_id=<클라이언트 아이디 기입>
">Login with Google</a>
</body>
</html>
-- 구현 코드
* 테스트 용이므로, 하나의 클래스 안에 모두 작성했다.
설정 정보는 가져오기 나름이다.
//
private static final String CLIENT_ID = "";
private static final String CLIENT_SECRET = "";
private static final String REDIRECT_URI = "";
private static final String GRANT_TYPE = "";
private static final String ACCESS_TOKEN_URL = "";
첫 시작으로, localhost:port/로 접속 시 로그인 창이 뜨도록 설정한다.
/**
* Login with Google 1 : Authorization Code Grant
*/
@GetMapping("/")
public String login() {
return "login";
}
http://localhost:port/callback 이 Redirect URL 로 등록되어 있기 때문에 /callback 으로 code 값을 받는다.
GoogleSignCallback() 에서는 Access Token 을 얻기 / 사용자 정보 얻기 를 수행하도록 구현했다.
/**
* Callback 1 : Authorization Code Grant
*/
@RequestMapping(value = "/callback", method = RequestMethod.GET)
public String GoogleSignCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("## [REQUEST] code ={}", request.getParameter("code"));
try {
/* Access Token 얻기 */
String accessToken = getGoogleAccessToken(request, response);
/* 사용자 정보 얻기 */
getGoogleUserInfo(accessToken);
} catch (Exception e) {
log.error(e);
throw e;
}
/* 임시 - 로그인 성공 창 */
return "home";
}
code 값을 담아서 Access Token 을 달라고 요청한다.
/**
* Callback 1 : Authorization Code Grant
* Access Token 처리 (+획득)
*
* @param request
* @Param response
* @throws Exception
* @return Access Token 값
*/
public String getGoogleAccessToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
String code = request.getParameter("code");
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("code", code);
parameters.add("client_id", CLIENT_ID);
parameters.add("client_secret", CLIENT_SECRET);
parameters.add("redirect_uri", REDIRECT_URI);
parameters.add("grant_type", GRANT_TYPE);
HttpEntity<MultiValueMap<String, String>> rest_request = new HttpEntity<>(parameters, headers);
URI uri = URI.create(ACCESS_TOKEN_URL);
ResponseEntity<String> responseEntity;
responseEntity = restTemplate.postForEntity(uri, rest_request, String.class);
String bodys = responseEntity.getBody();
log.info("## BODYS = {}", bodys);
JsonParser jsonParser = new JsonParser();
JsonElement jsonElement = jsonParser.parse(bodys);
return jsonElement.getAsJsonObject().get("access_token").getAsString();
}
Access Token 을 가지고 사용하고자 하는 서비스를 제공해 달라고 요청한다.
* 이 코드에서는 사용자 계정 정보를 제공해 달라고 요청한다.
/**
* Callback 1 : Authorization Code Grant
* Google 사용자 계정 정보 가져오기
*
* @param accessToken 획득한 ACCESS TOKEN 값
* @throws
*/
public void getGoogleUserInfo(String accessToken) throws Exception {
//요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap 선언
HashMap<String, Object> googleUserInfo = new HashMap<>();
String reqURL = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + accessToken;
try {
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//요청에 필요한 Header에 포함될 내용
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
int responseCode = conn.getResponseCode();
log.info("## ResponseCode : {}", responseCode);
if (responseCode == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
String result = "";
while ((line = br.readLine()) != null) {
result += line;
}
JsonParser parser = new JsonParser();
log.info("## Result = {}", result);
JsonElement element = parser.parse(result);
String name = element.getAsJsonObject().get("name").getAsString();
String email = element.getAsJsonObject().get("email").getAsString();
String id = "GOOGLE_" + element.getAsJsonObject().get("id").getAsString();
googleUserInfo.put("name", name);
googleUserInfo.put("email", email);
googleUserInfo.put("id", id);
log.info("## Login Controller : {}", googleUserInfo);
}
} catch (Exception e) {
log.error(e);
throw e;
}
}
[추가 정보]
1. Access Token 만료 기간이 지나면 Refresh Token 값을 통해 갱신을 할 수 있다고 한다.
* Refresh Token 갱신에 대해서는 아직 해보지 않았다.
* Refresh Token 값은 Access Token 값 수신 시 같이 받는다.
2. Access Token 값을 얻기 위해 code 값을 전송해주면 provider 는 Access Token 을 전송해주고 해당 code 값을 폐기한다.
* 일회용 code
# 각 종 블로그 내용과 공홈 내용을 참고로 작성 & 정리 및 실습했습니다.
'Computer Science > Java' 카테고리의 다른 글
KeyCloak 외부 DB 연결 (PostgreSQL) (0) | 2020.12.06 |
---|---|
KeyCloak 설치 & 정리 (0) | 2020.12.06 |
# 전자정부 프레임워크 세부 적용 규칙 - Intellij + SpringBoot 적용 (0) | 2020.11.01 |
[intelliJ] 전자정부 프레임워크 구축(세팅)하기 (0) | 2020.10.14 |
[eclipse]전자정부 표준프레임워크 개발환경 구축하기 (0) | 2020.10.14 |