在Server-Side如何取得AccessToken?

先前介紹過Facebook的Authentication機制後,我們知道「一個應用程式要存取臉書上使用者基本資料以外的資源,就必須要先讓他認證,並取得一把授權鑰匙去操作Graph API。」,這把鑰匙就是AccessToken。但會因為不同種類的應用程式而使用不同授權方式,以一個WebApp來說,主要分為Client-SideServer-Side的認證。之前有介紹過大家5分鐘建立一個臉書應用程式,裡面使用到Javascript SDK幫你把Client-Side的認證授權流程都包裝在FB.login中。所以我不再另外介紹Client-Side該怎麼做,更多的內容可以參考Document。在這篇文章中要教你的,是透過Java做Server-Side認證去取得AccessToken。

首先看看Server-Side認證流程圖,來建立基礎概念:(取自Facebook Developers)

  • 使用者在進入應用程式頁面後,若尚未取得授權,應用程式會將瀏覽器導向臉書認證頁面。
  • 使用者允許應用程式存取的授權後,瀏覽器將攜帶一個授權碼(code)並導向應用程式頁面。(若使用者拒絕授權,應用程式應導向錯誤提示畫面或限制存取)
  • 應用程式取得授權碼(code)後,透過此授權碼(code)與臉書取得授權鑰匙(access_token)。(Authorization Code Grant)
  • 應用程式開始透過授權鑰匙(access_token)存取使用者在臉書上的資料。

我在2010年實作國軍登出倒數計時器時,開發臉書應用程式的文件並不夠完善。加上鮮少有人使用Java去開發臉書應用程式,網路上能找到的資料大都採用PHP開發,所以大都靠自己慢慢嘗試完成。隨著臉書教學文件原來越多,現在也有聽過某些遊戲是用Flash(Client)+Java(Server)實做。接下來讓我分享給大家: Server-Side認證授權的實做。在應用程式整個認證流程共包含四個頁面: index.jsp、authentication.jsp、error.jsp與working.jsp。

  • index.jsp: 確認session是否已存在AccessToken。存在就將瀏覽器導向working.jsp;不存在就導向authentication.jsp。
  • authentication.jsp: 根據code是否存在分為兩個動作: 存在就拿去交換AccessToken,最後再導向至index.jsp;不存在就與使用者請求授權以取得code,再透過callback回authentication.jsp去交換AccessToken。
  • error.jsp: 顯示錯誤訊息。
  • working.jsp: 提供應用程式功能。

index.jsp

很純粹的判斷session內是否有AccessToken去決定要導向authentication.jsp還是working.jsp。

<%@page import="org.tonylin.practice.facebook.web.SessionKeyEnum"%>
<% 
	if( session.getAttribute(SessionKeyEnum.ACCESS_TOKEN) == null ){
		response.sendRedirect("authentication.jsp");
	} else {
		response.sendRedirect("working.jsp");
	}
%>

authentication.jsp

首先會先確認是否有參數error_description傳入,主要是用來處理使用者拒絕授權的情況。當使用者拒絕授權後,會導向你所設定的REDIRECT_URI,並帶下面的參數,可依照需求選擇要拿哪一個參數來判斷。

YOUR_REDIRECT_URI?
    error_reason=user_denied
   &error=access_denied
   &error_description=The+user+denied+your+request.
   &state=YOUR_STATE_VALUE

接著我把主要認證流程分成兩部分並實作於FacebookAuthUtil類別:

  1. 請求使用者授權: 在呼叫requestAuthentication後,會先跳出授權的對話盒讓使用者授權。接著會帶著code參數導向傳入的REDIRECT_URI,也就是authentication.jsp。你也可以選擇將這些步驟做成不同的jsp或servlet。
  2. 使用code交換AccessToken: 呼叫getAccessToken去取得AccessToken,最後存入session並導向至應用程式首頁。這裡我偷懶直接使用Facebook App的URL。
<%@page import="org.tonylin.practice.facebook.web.SessionKeyEnum"%>
<%@page import="org.tonylin.util.web.ServerInfoUtil"%>
<%@page import="org.tonylin.practice.facebook.web.FacebookAuthUtil"%>
<%@page import="org.tonylin.practice.facebook.FacebookConfigProvider"%>
<%
	String error_description = request.getParameter("error_description");
	if( null != error_description ){
		response.sendRedirect("error.jsp?msg=" + error_description);
	} else {
		String code = request.getParameter("code");
		String appID = FacebookConfigProvider.getAppID();
		String appSecret = FacebookConfigProvider.getAppSecret();
		String appPermission = FacebookConfigProvider.getAppPermission();
		String currentURL = ServerInfoUtil.getRelatedURL(request, "authentication.jsp");
		if( null == code ){
			FacebookAuthUtil.requestAuthentication( appID, currentURL, appPermission, request, response);
		} else {
			try {
				String accessToken = FacebookAuthUtil.getAccessToken( appID, appSecret, 
						currentURL, request, response);
				session.setAttribute( SessionKeyEnum.ACCESS_TOKEN, accessToken);
				response.sendRedirect("http://apps.facebook.com/testingapfortony/");
			} catch(Exception e){
				response.sendRedirect("error.jsp?msg=" + e.getMessage());
			}
		}
	}
%>
  • 註: ServerInfoUtil是我用來處理與WebServer相關的Utility。

FacebookAuthUtil.java

requestAuthentication

這method的目的就是要取得使用者授權,基本上就是照著臉書所需要的資訊照做。需特別注意的的有幾點:

  1. state: 用來防止Cross-site Request Forgery攻擊。這裡我是透過隨機產生一個數字並存入session中,在getAccessToken用來確認是否為同一個使用者存取的操作。
  2. callBackURL: 用來讓臉書攜帶code回來以交換AccessToken。
  3. response的javascript: 一般臉書應用程式都是使用Canvas的做法,這段javascript是確保將你整個頁面導向認證畫面而不是只有iframe內容。
public static void requestAuthentication(String appId, String callBackURL, String aPermission,
		HttpServletRequest request, HttpServletResponse response)
		throws IOException {
	String state = String.valueOf(RandomUtils.nextLong());
	request.getSession().setAttribute(SessionKeyEnum.STATE, state);
 
	StringBuffer authURLSB = new StringBuffer(
			"https://www.facebook.com/dialog/oauth?client_id=");
	authURLSB.append(appId);
	authURLSB.append("&redirect_uri=");
	authURLSB.append(callBackURL);
	authURLSB.append("&scope=");
	authURLSB.append(aPermission);
	authURLSB.append("&state=");
	authURLSB.append(state);
 
	PrintWriter pw = response.getWriter();
	pw.println("<html>");
	pw.println("<script type=\"text/javascript\">");
	pw.println("window.open ('" + authURLSB.toString() + "','_top')");
	pw.println("</script>");
	pw.println("</html>");
}

getAccessToken

這method的目的就是取得AccessToken。首先會確認state是否與requestAuthentication中的相同,並確認有沒有任何錯誤訊息,如果有問題都會丟FacebookAuthException給caller處理。否則就會使用HttpClient去請求authURLSB這個url以交換AccessToken回來(callBackURL必須與之前的相同)。透過HttpClient請求後的Response會像下面的格式,我只取出=之後的內容回傳使用。

access_token=xxxxxxxxxxxxxx
public static String getAccessToken(String appId, String appSecret,
		String callBackURL, HttpServletRequest request,
		HttpServletResponse response) throws FacebookAuthException {
	String state = request.getParameter("state");
	String error_description = request.getParameter("error_description");
	String previousState = (String) request.getSession().getAttribute(
			SessionKeyEnum.STATE);
 
	if (error_description != null) {
		throw new FacebookAuthException("You need to allow the permission.");
	} else if (state == null || previousState == null || !state.equals(previousState)) {
		throw new FacebookAuthException("State is incorrect.");
	} else {
		String code = request.getParameter("code");
		StringBuffer authURLSB = new StringBuffer(
				"https://graph.facebook.com/oauth/access_token?client_id=");
		authURLSB.append(appId);
		authURLSB.append("&redirect_uri=");
		authURLSB.append(callBackURL);
		authURLSB.append("&client_secret=");
		authURLSB.append(appSecret);
		authURLSB.append("&code=");
		authURLSB.append(code);
		try {
			String content = URLConnectionUtil.getWebContent(authURLSB
					.toString());
			String accessToken = content.split("=")[1];
			return accessToken;
		} catch (Exception e) {
			throw new FacebookAuthException("Get AccessToken failed", e);
		}
	}
}

* 註: URLConnectionUtil是我用來處理與HttpClient相關的Utility,getWebContent就只是建立與URL的連線並讀出裡面的內容。

這篇教學中,已將精隨交給大家。我知道裡面還隱藏了一些實作細節,但欲知詳情…嘿嘿嘿。另外,我試圖將這部份程式碼做的可以重複使用,FacebookAuthUtil就是為了達到這個目的。至於能不能只呼叫一個method就取得AccessToken呢? 目前我只有Client-Side有能力做到而已。隨著你的臉書應用程式越多,隨著臉書的改變,如果你能改一隻程式就套到每一個應用程式上,何樂而不為?
認證流程除了文章中所提到的外,還要注意AccessToken是否有過期,之後會再撰文告訴大家。(偷偷告訴大家我是用Struts2的interceptor去處理的)

友藏內心獨白: 這樣寫會不會讓人太難懂?