들어가며
대부분의 회사나 조직은 직원과 고객 데이터베이스를 가지고 있습니다. 쓰리래빗츠 북을 도입하면 일부 데이터베이스를 이중으로 관리해야 하는 불편함에 직면합니다. 이 문제를 해결하기 위해서 쓰리래빗츠 북은 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
으로 리다이렉트합니다. 이 때 쿼리 문자열로 code
와 state
를 전달합니다.
보안을 위해서 미리 정해진 주소(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 값을 그대로 전달합니다.
구현할 때 다음을 참고합니다.
보안을 위해서 미리 정해진 주소(http://127.0.0.1:1975/r/oauth/auth)로 리다이렉트할 수도 있습니다.
state
값으로 중복 호출 및 코드 노출에 따른 보안 문제를 방지할 수 있습니다. OAuth 서버를 구현할 때 같은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
에 세미콜론을 구분자로 여러 개를 설정할 수 있습니다. 설정할 수 있는 권한은 다음과 같습니다.
admin
edtior
writer
reader
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사용자가 로그인했는지를 체크합니다. 개발 환경에 맞게 고칩니다.
12code
와 state
를 쿼리 문자열에 넣어서 리다이렉트합니다.
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이 아닌 http://127.0.0.1:1975/r/signon/login에서 로그인해야 합니다.
OAuth로 처음 로그인할 때 쓰리래빗츠 북에 자동으로 사용자가 만들어집니다. 라이선스를 초과하면 글 읽기 권한만 있는 사용자로 만들어 집니다.
OAuth로 로그인한 사용자는 내 프로파일 정보를 바꿀 수 없습니다.