跳到主要內容

Facebook - 在Server-Side如何取得AccessToken?

前言

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

Server-Side認證流程

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

  1. 使用者在進入應用程式頁面後,若尚未取得授權,應用程式會將瀏覽器導向臉書認證頁面。
  2. 使用者允許應用程式存取的授權後,瀏覽器將攜帶一個授權碼(code)並導向應用程式頁面。(若使用者拒絕授權,應用程式應導向錯誤提示畫面或限制存取)
  3. 應用程式取得授權碼(code)後,透過此授權碼(code)與臉書取得授權鑰匙(access_token)。(Authorization Code Grant)
  4. 應用程式開始透過授權鑰匙(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的連線並讀出裡面的內容。

Summary

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

Reference

留言

這個網誌中的熱門文章

Show NIC selection when setting the network command with the device option

 Problem  在answer file中設定網卡名稱後,安裝時會停在以下畫面: 所使用的command參數如下: network --onboot = yes --bootproto =dhcp --ipv6 =auto --device =eth1 Diagnostic Result 這樣的參數,以前試驗過是可以安裝完成的。因此在發生這個問題後,我檢查了它的debug console: 從console得知,eth1可能是沒有連接網路線或者是網路太慢而導致的問題。後來和Ivy再三確認,有問題的是有接網路線的網卡,且問題是發生在activate階段: Solution 我想既然有retry應該就有次數或者timeout限制,因此發現在Anaconda的說明文件中( link ),有提到dhcptimeout這個boot參數。看了一些人的使用範例,應該是可以直接串在isolinux.cfg中,如下: default linux ksdevice = link ip =dhcp ks =cdrom: / ks.cfg dhcptimeout = 90 然而我在RHEL/CentOS 6.7與6.8試驗後都無效。 因此我就拿了顯示的錯誤字串,問問Google大師,想找一下Anaconda source code來看一下。最後找到別人根據Anaconda code修改的版本: link ,關鍵在於setupIfaceStruct函式中的setupIfaceStruct與readNetConfig: setupIfaceStruct: 會在dhcp時設定dhcptimeout。 readNetConfig: 在writeEnabledNetInfo將timeout寫入dhclient config中;在wait_for_iface_activation內會根據timeout做retry。 再來從log與code可以得知,它讀取的檔案是answer file而不是boot command line。因此我接下來的測試,就是在answer file的network command上加入dhcptimeout: network --onboot = yes --bootproto =dhcp --ipv6 =auto --device =eth1 --dhcptimeo

解決RobotFramework從3.1.2升級到3.2.2之後,Choose File突然會整個Hand住的問題

考慮到自動測試環境的維護,我們很久以前就使用java去執行robot framework。前陣子開始處理從3.1.2升級到3.2.2的事情,主要先把明確的runtime語法錯誤與deprecate item處理好,這部分內容可以參考: link 。 直到最近才發現,透過SeleniumLibrary執行Choose File去上傳檔案的動作,會導致測試案例timeout。本篇文章主要分享心路歷程與解決方法,我也送了一條issue給robot framework: link 。 我的環境如下: RobotFramework: 3.2.2 Selenium: 3.141.0 SeleniumLibrary: 3.3.1 Remote Selenium Version: selenium-server-standalone-3.141.59 首先並非所有Choose File的動作都會hang住,有些測試案例是可以執行的,但是上傳一個作業系統ISO檔案一定會發生問題。後來我透過wireshark去比對新舊版本的上傳動作,因為我使用 Remote Selenium ,所以Selenium會先把檔案透過REST API發送到Remote Selenium Server上。從下圖我們可以發現,在3.2.2的最後一個TCP封包,比3.1.2大概少了500個bytes。 於是就開始了我trace code之路。包含SeleniumLibrary產生要送給Remote Selenium Server的request內容,還有HTTP Content-Length的計算,我都確認過沒有問題。 最後發現問題是出在socket API的使用上,就是下圖的這支code: 最後發現可能因為開始使用nio的方式送資料,但沒處理到尚未送完的資料內容,而導致發生問題。加一個loop去做計算就可以解決了。 最後我有把解法提供給robot framework官方,在他們出新的版本之前,我是將改完的_socket.py放在我們自己的Lib底下,好讓我們測試可以正常進行。(shutil.py應該也是為了解某個bug而產生的樣子..)

PostgreSQL - Unattended installation on windows

Introduction 要將別人軟體包裝到自己軟體中,不可或缺的東西就是Unattended installation。以Unattended installation來說,我們可以選擇透過Installer的silent mode安裝,也可以透過把目標軟體做成portable的版本。本篇文章分享這兩種方法,教導大家如何將PostgreSQL透過Unattended installation方式安裝到目標系統成為service。 Note. 本篇以PostgreSQL 10.7為例。 Install with installer Tips 安裝程式或反安裝程式的參數,除了可以直接上官網搜尋Installation User Guide以外,也可以直接使用help參數查詢: postgresql- 10.7 - 2 -windows-x64.exe --help Windows安裝程式主要有EnterpriseDB與BigSQL兩種。BigSQL版本安裝元件是透過網路下載且支援參數不如EnterpriseDB版本多,以我們需求來說,我們傾向於使用EnterpriseDB版本。接下來分享給大家安裝與反安裝方法。 Installation @ echo off set INSTALL_DIR =C:\postgres10 set INSTALLER =postgresql- 10.7 - 2 -windows-x64.exe   rem options for installation set SSMDB_SERVICE =postgresql- 10 set MODE =--unattendedmodeui none --mode unattended   set DB_PASSWD =--superpassword postgres set DB_PORT =--serverport 5432   set SERVICE_NAME =--servicename % SSMDB_SERVICE %   set PREFIX =--prefix "%INSTALL_DIR%" set DATA_DIR =--datadir "%INSTALL_DIR%\data"   set OPTIONS =