OAuth 2.0으로 사용자 관리하기

들어가며

대부분의 회사나 조직은 직원과 고객 데이터베이스를 가지고 있습니다. 쓰리래빗츠 북을 도입하면 일부 데이터베이스를 이중으로 관리해야 하는 불편함에 직면합니다. 이 문제를 해결하기 위해서 쓰리래빗츠 북은 OAuth 2.0으로 사용자를 관리하는 기능을 제공합니다.

OAuth 2.0이란?

OAuth 2.0은 여러 애플리케이션이 안전하게 인증 및 권한을 제어할 수 있도록 해주는 오픈 프로토콜입니다. OAuth를 구성하는 주요 요소는 다음과 같습니다.

인증 서버(Authorization Server)

로그인과 같은 사용자 인증을 처리하는 서버입니다. 직원이나 고객 데이터베이스에 접근할 수 있는 웹 사이트에 OAuth에 맞춰 필요한 기능을 추가해야 합니다.

클라이언트(쓰리래빗츠 북)

인증 서버로 로그인한 후에 사용할 수 있는 서비스를 말합니다. 쓰리래빗츠 북이 이에 해당합니다.

웹 브라우저

웹 브라우저 리다이렉트로 인증 서버와 쓰리래빗츠 북을 연결합니다.

OAuth에서는 인증 서버와 자원 서버(Resource Server)를 분리해서 설명합니다. 자원 서버는 사용자 프로파일 등을 제공하는 역활을 하는데 쓰리래빗츠 북에 OAuth를 적용할 때 인증 서버와 자원 서버를 분리할 필요가 없기 때문에 인증과 자원 제공을 모두 인증 서버에서 처리하는 것으로 가정합니다.

쓰리래빗츠 북에 OAuth 2.0을 적용하면 다음과 같이 사용자 인증을 처리합니다.

1

웹 브라우저에서 쓰리래빗츠 북을 호출합니다.

외부에 공개한 문서에는 바로 접근할 수 있습니다.

2

인증 과정을 거치지 않았다면 쓰리래빗츠 북은 인증 서버로 리다이렉트합니다. 이 때 쿼리 문자열로 다음 파라미터를 전달합니다.

redirect_uri

인증이 성공한 후 웹 브라우저 리다이렉트로 이동할 쓰리래빗츠 북 주소

state

중복 호출을 방지하기 위한 장치입니다. 인증이 성공한 후 이 값을 쿼리 문자열로 다시 보냅니다.

3

인증 서버에서 인증에 성공하면 쓰리래빗츠 북으로부터 받은 redirect_uri으로 리다이렉트합니다. 이 때 쿼리 문자열로 codestate를 전달합니다.

보안을 위해서 미리 정해진 주소(http://127.0.0.1:1975/r/oauth/auth)로 리다이렉트할 수도 있습니다.

4

쓰리래빗츠 북은 code를 파라미터로 인증 서버에 토큰을 요청합니다.

웹 브라우저를 거치지 않고 쓰리래빗츠 북가 인증 서버를 직접 호출합니다.

5

인증 서버에서 받은 토큰을 파라미터로 쓰리래빗츠 북은 인증 서버에 사용자 프로파일 정보를 요청합니다.

웹 브라우저를 거치지 않고 쓰리래빗츠 북가 인증 서버를 직접 호출합니다.

토큰 대신에 바로 사용자 프로파일 정보를 요청하고 받는 것이 낫아 보입니다. 하지만 OAuth는 인증뿐만 아니라 다양한 서비스나 자원에 접근할 수 있는 프레임워크입니다. 예를 들어 주소록이나 사진 목록과 같은 것들을 요청하는데 사용할 수 있습니다. 따라서 토큰을 가져오는 것과 서비스(사용자 프로파일 정보)를 요청하는 것이 분리되어 있습니다.

쓰리래빗츠 북 OAuth URL 설정

사용자를 인증하는 인증 서버 URL을 쓰리래빗츠 북에 설정합니다.

1

<관리 | 환경 설정 | API> 메뉴로 이동합니다. 1<API 변경> 링크를 클릭합니다.

2

OAuth 서버 URL을 모두 입력한 후 저장합니다.

네트워크 보안을 위해서 HTTPS를 사용하는 것을 권장합니다.

인증 서버 구현하기

쓰리래빗츠 북에 설정한 OAuth URL 기능을 구현합니다. 구현해야 하는 URL은 세 개입니다.

직원 또는 고객 데이터베이스에 접근할 수 있는 기존 웹 사이트(애플리케이션)에 이 기능을 추가합니다.

OAuth 서버 URL

사용자를 인증하는 URL입니다. 사용자가 로그인하지 않았다면 로그인 페이지로 이동시킵니다. 사용자가 인증에 성공하면 웹 브라우저 리다이렉트로 인증 코드를 쓰리래빗츠 북으로 전달합니다.

OAuth 서버 토큰 URL

인증 서버로 받은 인증 코드로 쓰리래빗츠 북이 접근 토큰을 가져오는 URL입니다. 이 때는 웹 브라우저를 거치지 않고 쓰리래빗츠 북이 인증 서버를 직접 호출합니다.

OAuth 서버 사용자 프로파일 URL

인증 서버로 받은 인증 토큰으로 쓰리래빗츠 북이 사용자 정보를 가져오는 URL입니다. 이 때는 웹 브라우저를 거치지 않고 쓰리래빗츠 북이 인증 서버를 직접 호출합니다.

OAuth 서버 URL 구현

쓰리래빗츠 북은 웹 브라우저 리다이렉트로 다음 파라미터와 함께 인증 서버를 호출합니다.

redirect_uri

인증이 성공한 후 웹 브라우저 리다이렉트로 이동할 쓰리래빗츠 북 주소

state

중복 호출을 방지하기 위한 장치입니다. 인증이 성공한 후 이 값을 쿼리 문자열로 다시 보냅니다.

사용자가 인정 서버 로그인에 성공하면 redirect_uri로 리다이렉트합니다. 이 때 다음을 쿼리 문자열로 함께 보내야 합니다.

code

코드 문자열입니다. 특별한 포멧은 없습니다.

state

쓰리래빗츠 북으로부터 받은 state 값을 그대로 전달합니다.

구현할 때 다음을 참고합니다.

OAuth 서버 토큰 URL 구현

쓰리래빗츠 북은 인증 서버로 다음 파라미터를 보냅니다.

code

앞 단계에서 받은 코드 값입니다.

인증 서버는 JSON 형식으로 결과를 반환해야 합니다.

Content-Type: application/json; charset=UTF-8

JSON 형식은 다음과 같습니다.

{
  "access_token": "접근 토큰",
  "expires_in": 60
}

expires_in은 접근 토큰 유효 기간으로 초를 단위로 합니다.

OAuth 서버 사용자 프로파일 URL 구현

쓰리래빗츠 북은 인증 서버로 다음 파라미터를 보냅니다.

access_token

앞 단계에서 받은 토큰 값입니다.

인증 서버는 토큰 값에 맞는 사용자 정보를 쓰리래빗츠 북으로 반환해야 합니다. 이 때 지켜야하는 형식은 다음과 같습니다.

Content-Type: application/json; charset=UTF-8

다음을 반환합니다.

{
  "id": "사용자 아이디",
  "name": "사용자 이름",
  "email": "사용자 이메일 주소",
  "roles": "사용자 권한",
  "groups": "사용자가 속한 그룹"
}

roles에 세미콜론을 구분자로 여러 개를 설정할 수 있습니다. 설정할 수 있는 권한은 다음과 같습니다.

roles에 세미콜론을 구분자로 여러 개를 설정할 수 있습니다.

"roles": "admin;writer"

writer를 설정했다면 reader는 설정할 필요가 없습니다.

쓰리래빗츠 북에서 권한을 설정하려면 roles에 3rabbitz를 입력합니다.

"roles": "3rabbitz"

groups에 세미콜론을 구분자로 여러 그룹 아이디를 설정할 수 있습니다. 그룹 아이디에 대한 설명은 쓰리래빗츠 북 사용자 가이드 - 그룹 관리를 참고합니다.

"groups": "marketing;support"

쓰리래빗츠 북에서 그룹을 설정하려면 groups에 3rabbitz를 입력합니다.

"groups": "3rabbitz"

JSP 구현 예제

프로그래밍 언어와 개발 환경에 따라서 인증 서버를 구현하는 방법이 달라집니다. 인증 서버를 구현할 때 참고할 수 있는 자바와 JSP로 구현한 예제입니다.

OAuthQueue.java

코드와 토큰과 사용자 아이디를 연결시켜주는 자바 클래스입니다.

package com.threerabbitz.oauth;

import java.util.LinkedList;
import java.util.UUID;

public class OAuthQueue {

    private static final int MAX_SIZE = 10000;

    private static OAuthQueue instance = new OAuthQueue();

    public static OAuthQueue getInstance() {
        return instance;
    }

    private LinkedList<OAuthInfo> queue = new LinkedList<OAuthInfo>();

    public synchronized String getCode(String user) {
        if (user == null) {
            throw new IllegalArgumentException("user_cannot_be_null");
        }
        OAuthInfo result = new OAuthInfo(user);
        queue.add(result);
        if (queue.size() > MAX_SIZE) {
            queue.removeFirst();
        }
        return result.code;
    }

    public synchronized String getToken(String code) {
        if (code == null) {
            throw new IllegalArgumentException("code_cannot_be_null");
        }
        for (int i = queue.size() - 1; i > -1; i--) {
            OAuthInfo info = queue.get(i);
            if (info.code.equals(code)) {
                return info.token;
            }
        }
        return null;
    }

    public synchronized String getUser(String token) {
        if (token == null) {
            throw new IllegalArgumentException("token_cannot_be_null");
        }
        for (int i = queue.size() - 1; i > -1; i--) {
            OAuthInfo info = queue.get(i);
            if (info.token.equals(token)) {
                return info.user;
            }
        }
        return null;
    }

    static private class OAuthInfo {
        String code = UUID.randomUUID().toString();
        String token = UUID.randomUUID().toString();
        String user;

        OAuthInfo(String user) {
            this.user = user;
        }
    }

}

8인증 데이터 캐싱 크기를 10,000개로 제한합니다.

12싱글톤 패턴을 적용했습니다.

56코드, 토큰, 사용자 아이디를 담고 있는 OAuthInfo 클래스를 선언합니다.

57java.util.UUID 클래스로 코드와 토큰 값을 만듭니다.

oauth.jsp

로그인 여부를 체크하고 웹 브라우저 리다이렉트로 쓰리래빗츠 북에 코드를 전달하는 JSP 파일입니다.

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

<%@ page import="com.threerabbitz.base.domain.User" %>
<%@ page import="com.threerabbitz.oauth.OAuthQueue" %>

<%
  String redirectUri = request.getParameter("redirect_uri");
  String state = request.getParameter("state");
  User user = (User) session.getAttribute("user");
  if (user != null) {
    String code = OAuthQueue.getInstance().getCode(user.getUserid());
    response.sendRedirect(redirectUri + "?code=" + code + "&state=" + state);
  } else {
    String path = "/oauth.jsp?redirect_uri=" + redirectUri + "&state=" + state;
    session.setAttribute("path", path);
    request.getRequestDispatcher("/login.jsp").forward(request, response);
  }
%>

10사용자가 로그인했는지를 체크합니다. 개발 환경에 맞게 고칩니다.

12codestate를 쿼리 문자열에 넣어서 리다이렉트합니다.

15로그인 이후에 이동할 페이지를 설정합니다. 개발 환경에 맞게 고칩니다.

16로그인 페이지로 이동합니다. 개발 환경에 맞게 고칩니다.

oauth_token.jsp

코드(code)를 받아 토큰을 반환하는 JSP 파일입니다.

<%@ page contentType="application/json; charset=UTF-8" pageEncoding="UTF-8" %>

<%@ page import="com.threerabbitz.oauth.OAuthQueue" %>

<%
  String code = request.getParameter("code");
  String token = OAuthQueue.getInstance().getToken(code);
  if (token != null) {
    out.print("{");
    out.print("\"access_token\": \"" + token + "\",");
    out.print("\"expires_in\": 60");
    out.print("}");
  }
%>

1Content-Type은 application/json; charset=UTF-8입니다.

11expires_in 속성으로 유효 기간을 설정합니다. 단위는 초입니다.

oauth_user_profile.jsp

토큰(access_token)을 받아 사용자 정보를 반환하는 JSP 파일입니다.

<%@ page contentType="application/json; charset=UTF-8" pageEncoding="UTF-8" %>

<%@ page import="com.threerabbitz.base.domain.User" %>
<%@ page import="com.threerabbitz.base.domain.Users" %>
<%@ page import="com.threerabbitz.oauth.OAuthQueue" %>

<%
  String token = request.getParameter("access_token");
  User user = Users.find(OAuthQueue.getInstance().getUser(token));
  if (user != null) {
    out.print("{");
    out.print("\"id\": \"" + user.getUserid() + "\",");
    out.print("\"name\": \"" + user.getName() + "\",");
    out.print("\"email\": \"" + user.getEmail() + "\",");
    out.print("\"roles\": \"3rabbitz\",");
    out.print("\"groups\": \"3rabbitz\"");
    out.print("}");
  }
%>

1Content-Type은 application/json; charset=UTF-8입니다.

9사용자 아이디로 사용자 정보를 찾습니다. 실제 환경에 맞게 고칩니다.

OAuth 적용에 따라 알아야 하는 사항