前言
先前介紹過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)
- 使用者在進入應用程式頁面後,若尚未取得授權,應用程式會將瀏覽器導向臉書認證頁面。
- 使用者允許應用程式存取的授權後,瀏覽器將攜帶一個授權碼(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類別:
- 請求使用者授權: 在呼叫requestAuthentication後,會先跳出授權的對話盒讓使用者授權。接著會帶著code參數導向傳入的REDIRECT_URI,也就是authentication.jsp。你也可以選擇將這些步驟做成不同的jsp或servlet。
- 使用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()); } } } %>
FacebookAuthUtil.java
requestAuthentication
- state: 用來防止Cross-site Request Forgery攻擊。這裡我是透過隨機產生一個數字並存入session中,在getAccessToken用來確認是否為同一個使用者存取的操作。
- callBackURL: 用來讓臉書攜帶code回來以交換AccessToken。
- 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
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); } } }
留言
張貼留言