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": "사용자 이메일 주소",
  "locale": "ko",
  "roles": "사용자 권한",
  "groups": "사용자가 속한 그룹"
}

locale은 쓰리래빗츠 북 3.0.21 버전부터 지원합니다. locale에는 en 또는 ko를 설정할 수 있습니다.

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 적용에 따라 알아야 하는 사항

로그아웃

쓰리래빗츠 북에서 로그아웃을 해도 자동으로 다시 로그인을 합니다. OAuth 서버에는 여전히 로그인되어 있기 때문입니다. 이를 방지하려면 시작 스트립트에 3rabbitz.logout_redirect_path 옵션을 설정합니다. 자세한 방법은 시작 스크립트 옵션 설정을 참고합니다.

이 옵션에 로그아웃 이후에 이동할 URL을 설정합니다. 일반적으로 이 URL에서 OAuth 서버 로그아웃을 처리합니다.

기타