前言:
去年有寫一篇 如何將live stream發佈至至YouTube,該篇其實已經有使用到oAuth2.0的授權認證機制,但該篇寫法僅適用於單機版本的應用程式,若想在Web Application整合oAuth2.0授權驗證機制,那麼便需要另一種作法,該作法是此篇要講述的重點,若完全沒有操作過YouTube Data API的人建議先看過該篇,因為有些前面講過的設定在這邊就不再贅述。
由於Google APIs是利用oAuth2.0來對user做驗證與授權,因此在開始進入本篇的重點之前,要先確保大家都了解何謂oAuth2.0。
oAuth2.0用最簡單白話的舉例來解釋,就是當我們的Web Application要提供user操作管理該名user在Youtube的影音服務時,我們的Web Application必須先取得該名user在Google平台上對我們Web Application的授權,因此需執行以下的流程:
1.驗證user的身分:在後面的實作會將user導到Google的user登入驗證頁面,通過驗證後,會詢問user是否同意授權我們的應用程式管理user的Youtube。
2.取得user的授權後,我們的Web Application才能開始針對user的Youtube做管理操作(在這邊的範例我們要將影音上傳至user在youtube上的頻道。
整個oAuth2.0就是一個驗證+授權的一連串流程整合在一起的機制,更詳盡的圖文說明請參考
Using OAuth 2.0 to Access Google APIs。
準備工作:
1.請先準備好一組OAuth2.0使用的憑證(Credential),若沒有憑證,請至Google Developers Console,然後如下圖:
新增OAuth2.0的憑證,進去後我是選擇"其他",理論上應該選擇"網路應用程式",但我想應該是都可以,各位自行試試看,重點是建立成功後所取得的 用戶端ID與用戶端密碼 ,這兩個值很重要,在之後的程式碼中會再使用到,請先記得有這件事情。
2.環境的準備:我的開發環境是 Eclipse 4.5 (Kepler),Tomcat7.0,JDK1.6,同時是使用Spring+Struts2+hibernate,這裡ssh不是重點,只有使用servlet也沒問題,各位只要使用習慣熟悉的環境即可。
3.相關Library的準備:一種比較快的方式是利用Google Plugin的工具來下載相關的Google API Lib,而在使用Google Plugin之前必須先安裝此Google Plugin,安裝方式如後:在Eclipse的選單選擇 Help>>Installed New Softwares,然後參考此頁貼入Direct plugin link 至 work with 中,將Google plugin 打勾,要看詳細的圖文說明可參考 Google Plugin for Eclipse;
另一種取得Google APIs Lib的方式是直接在 Supported Google APIs 找到我們要的 API項目,這裡我們要下載的是 YouTube Data API v3。
4.假設各位已經取得所需的YouTube Data API Lib,請將取得的jar檔加入到WEB-INF/lib底下,各位的lib底下應該最少要有下面這些jar檔:
開始程式實作與設定
1. Authorization Servlet的實作: 在這裡我們必須新增兩支Servlet分別去繼承 AbstractAuthorizationCodeServlet與AbstractAuthorizationCodeCallbackServlet ,並且覆寫其中特定的方法,讓我們的Web Application可以很容易的跟Google的身分驗證與授權流程串接起來,source code 如下:
底下將整個身分驗證流程在程式中的執行順序以及個人認為使用method的重點敘述如下:oAuth2.0用最簡單白話的舉例來解釋,就是當我們的Web Application要提供user操作管理該名user在Youtube的影音服務時,我們的Web Application必須先取得該名user在Google平台上對我們Web Application的授權,因此需執行以下的流程:
1.驗證user的身分:在後面的實作會將user導到Google的user登入驗證頁面,通過驗證後,會詢問user是否同意授權我們的應用程式管理user的Youtube。
2.取得user的授權後,我們的Web Application才能開始針對user的Youtube做管理操作(在這邊的範例我們要將影音上傳至user在youtube上的頻道。
整個oAuth2.0就是一個驗證+授權的一連串流程整合在一起的機制,更詳盡的圖文說明請參考
Using OAuth 2.0 to Access Google APIs。
準備工作:
1.請先準備好一組OAuth2.0使用的憑證(Credential),若沒有憑證,請至Google Developers Console,然後如下圖:
新增OAuth2.0的憑證,進去後我是選擇"其他",理論上應該選擇"網路應用程式",但我想應該是都可以,各位自行試試看,重點是建立成功後所取得的 用戶端ID與用戶端密碼 ,這兩個值很重要,在之後的程式碼中會再使用到,請先記得有這件事情。
2.環境的準備:我的開發環境是 Eclipse 4.5 (Kepler),Tomcat7.0,JDK1.6,同時是使用Spring+Struts2+hibernate,這裡ssh不是重點,只有使用servlet也沒問題,各位只要使用習慣熟悉的環境即可。
3.相關Library的準備:一種比較快的方式是利用Google Plugin的工具來下載相關的Google API Lib,而在使用Google Plugin之前必須先安裝此Google Plugin,安裝方式如後:在Eclipse的選單選擇 Help>>Installed New Softwares,然後參考此頁貼入Direct plugin link 至 work with 中,將Google plugin 打勾,要看詳細的圖文說明可參考 Google Plugin for Eclipse;
另一種取得Google APIs Lib的方式是直接在 Supported Google APIs 找到我們要的 API項目,這裡我們要下載的是 YouTube Data API v3。
4.假設各位已經取得所需的YouTube Data API Lib,請將取得的jar檔加入到WEB-INF/lib底下,各位的lib底下應該最少要有下面這些jar檔:
- google-api-client-1.20.0.jar
- google-api-client-servlet-1.20.0.jar
- google-oauth-client-1.20.0.jar
- google-oauth-client-servlet-1.20.0.jar
- google-http-client-1.20.0.jar
- commons-logging-1.1.1.jar
- gson-2.1.jar
- httpclient-4.0.1.jar
- httpcore-4.0.1.jar
- jackson-core-asl-1.9.11.jar
- jackson-core-2.1.3.jar
- jdo2-api-2.3-eb.jar
- jsr305-1.3.9.jar
- protobuf-java-2.4.1.jar
- transaction-api-1.1.jar
- xpp3-1.1.4c.jar (我的裡面沒有)
開始程式實作與設定
1. Authorization Servlet的實作: 在這裡我們必須新增兩支Servlet分別去繼承 AbstractAuthorizationCodeServlet與AbstractAuthorizationCodeCallbackServlet ,並且覆寫其中特定的方法,讓我們的Web Application可以很容易的跟Google的身分驗證與授權流程串接起來,source code 如下:
package com.gorilla.servlet; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; import com.google.api.client.auth.oauth2.BearerToken; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.auth.oauth2.StoredCredential; import com.google.api.client.extensions.servlet.auth.oauth2.AbstractAuthorizationCodeServlet; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; import com.google.api.client.http.BasicAuthentication; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.store.DataStore; import com.google.api.client.util.store.FileDataStoreFactory; public class ServletSample extends AbstractAuthorizationCodeServlet { public static AuthorizationCodeFlow flow; @Override public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException { Credential credential = this.getCredential();
if (credential != null) { if (credential.getExpiresInSeconds() < 0) //if the accessToken expired, refresh the accessToken using refreshToken if (credential.refreshToken()){ System.out.println("refresh the access token's expired date=" + new Date(credential.getExpirationTimeMilliseconds())); } req.getSession().setAttribute("credential", this.getCredential()); StringBuffer sb = new StringBuffer(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()) .append("/showContents.html"); try { response.sendRedirect(sb.toString()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } @Override protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException { // TODO Auto-generated method stub StringBuilder sb = new StringBuilder(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()) .append("/oauth2callback"); String redirectUri = sb.toString(); return redirectUri; } @Override protected String getUserId(HttpServletRequest req) throws ServletException, IOException { // TODO Auto-generated method stub return "root"; } @Override protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException { // TODO Auto-generated method stub String clientID = "xxxxxxxxxxxxxxxx-4hncs25gkhehaqhqqcsnirlli3p6p4i8.apps.googleusercontent.com"; String password = "meXotHAj2Pa46r0azuzKIAHI"; Listscopes = new ArrayList(); scopes.add("https://www.googleapis.com/auth/youtube"); String CREDENTIALS_DIRECTORY = ".oauth-credentials"; File file = new File(System.getProperty("user.home") + "/" + CREDENTIALS_DIRECTORY); FileDataStoreFactory fileDataStoreFactory = new FileDataStoreFactory(file); DataStore datastore = fileDataStoreFactory.getDataStore("youtube"); flow = new GoogleAuthorizationCodeFlow.Builder( new NetHttpTransport(), JacksonFactory.getDefaultInstance(), clientID, password, scopes).setDataStoreFactory( fileDataStoreFactory).setAccessType("offline").build(); return flow; } @Override protected void onAuthorization(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp, AuthorizationCodeRequestUrl authorizationUrl) throws javax.servlet.ServletException, IOException { Credential credential = flow.loadCredential("root"); if (credential != null) { StringBuilder sb = new StringBuilder(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()) .append("/showContents.html"); req.getSession().setAttribute("credential", credential); } else { super.onAuthorization(req, resp, authorizationUrl); } } }
1.getUserId: 在沒有做過任何身分驗證時,會從ServletSample(繼承AbstractAuthorizationCodeServlet)的getUserId() method開始, 因為我的應用程式只需要一個使用者來操作youtube API,因此在我的程式中我的getUserId始終是return "root",這邊應是要看個人應用程式的需求而定。
2. initializeFlow: 這個method最主要的目的就是產生授權流程物件,如程式碼中的靜態變數flow。在這邊我們需要找一個實體位置用來存放flow對應的Credential(憑證),存放Credential的用意是當使用者已經授權認證過後,該憑證資訊會存放在我們設定的實體位置,如此我們的Web Application在每次要執行使用者的授權驗證前,會先去該實體路徑檢查是否有特定使用者的授權憑證資訊,若有,則不需要再連結到Google去重新執行身分認證流程。 在產生flow物件時,就需要用到我們前面提到的 用戶端ID與用戶端密碼,如下面程式碼:
String clientID = "xxxxxxxxxxxx8-qpil7lurqnjvho7qphh5tfj3tru7vl3a.apps.googleusercontent.com"; String password = "_WJYpeI_d09UxNPkAxxxxxx"; Listscopes = new ArrayList(); scopes.add("https://www.googleapis.com/auth/youtube"); String CREDENTIALS_DIRECTORY = ".oauth-credentials"; //credential 資訊要存放的目錄 File file = new File(System.getProperty("user.home") + "/" + CREDENTIALS_DIRECTORY); FileDataStoreFactory fileDataStoreFactory = new FileDataStoreFactory(file); DataStore datastore = fileDataStoreFactory.getDataStore("youtube"); flow = new GoogleAuthorizationCodeFlow.Builder( 這邊注意要使用 GoogleAuthorizationCodeFlow來建立flow,並且設定accessType為offline,如此才有new NetHttpTransport(), JacksonFactory.getDefaultInstance(), clientID, password, scopes).setDataStoreFactory( fileDataStoreFactory).setAccessType("offline").build(); return flow;
refresh token可以更新access token,access token的存續期只有1小時,因此我在doGet()會特別去判定
驗證後,系統會將使用者重新導向到哪一個頁面,底下看一下我web.xml的設定:access token是否過去,若過去須執行token的更新,如第7點的程式內容 3.getRedirectUri:取得AuthorizationCodeFlow物件後,要設定當使用者通過Google的身分
<servlet> <servlet-name>oauth2callback</servlet-name> <servlet-class>com.gorilla.servlet.AuthorizationCodeCallbackServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>oauth2callback</servlet-name> <url-pattern>/oauth2callback</url-pattern> </servlet-mapping>我的AuthorizationCodeCallbackServlet是(必須)繼承上面所提到的AbstractAuthorizationCodeCallbackServlet,因此我的getRedirectUri()必須有如下的code:
sb.append("http://").append("127.0.0.1:" + req.getLocalPort()).append(req.getContextPath())
.append("/oauth2callback");
同時這邊要提醒,在你的GoogleDeveloperConsole的憑證畫面裡,也必須有如下的設定:
也就是你getRedirectUri 所return 的URL必須與你在console內設定的一致。
4.onAuthorization:這個method主要就是用來判斷AuthorizationCodeFlow物件是否能取得Credential物件,若能,則無須經過Google的身分認證流程,否則則進入Google的身分認證流程。當第一次執行程式時,一定取不到Credentail物件,則會進入如下的畫面:
5.若Google驗證程序成功,則流程會執行回呼Servlet,也就是在流程3提到的AuthorizationCodeCallbackServlet,首先會去執行它的initializeFlow(),這裡很重要的一點:
這個method回傳的AuthorizationCodeFlow物件是參考到ServletSample.flow物件,若不這樣參考,是取不到正確的Credential物件,程式如下:
@Override protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException { // TODO Auto-generated method stub return ServletSample.flow; }底下列出完整的AuthorizationCodeCallbackServlet程式碼:
package com.gorilla.servlet; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.auth.oauth2.StoredCredential; import com.google.api.client.extensions.servlet.auth.oauth2.AbstractAuthorizationCodeCallbackServlet; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.store.DataStore; import com.google.api.client.util.store.FileDataStoreFactory; public class AuthorizationCodeCallbackServlet extends AbstractAuthorizationCodeCallbackServlet { private static String firstPage = "" ; @Override protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential) throws ServletException, IOException { StringBuilder sb = new StringBuilder(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()) .append("/showContents.html"); req.getSession().setAttribute("credential", credential); resp.sendRedirect(sb.toString()); } @Override protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException { // TODO Auto-generated method stub StringBuilder sb = new StringBuilder(); sb.append("http://").append("127.0.0.1:" + req.getLocalPort()).append(req.getContextPath()) .append("/oauth2callback"); String redirectUri = sb.toString(); return redirectUri; } @Override protected String getUserId(HttpServletRequest req) throws ServletException, IOException { // TODO Auto-generated method stub return "root"; } @Override protected void onError(HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse) throws ServletException, IOException { // handle error String error = errorResponse.getError(); System.out.println(error); } @Override protected AuthorizationCodeFlow initializeFlow() throws ServletException, IOException { // TODO Auto-generated method stub return ServletSample.flow; } }6.接下來會再跑至getRedirectUri與getUserId等method,最後跑至onSuccess method,在這個method我先將Credential物件存至session中,再redirect至顯示youtube平台影音list的頁面,程式如下:
@Override protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential) throws ServletException, IOException { StringBuilder sb = new StringBuilder(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()).append("/showContents.html"); req.getSession().setAttribute("credential", credential); resp.sendRedirect(sb.toString()); }7.以上,基本上到第六點完成,就已經完成使用者第一次登入驗證的完整流程,接下來當你在執行同樣的程式,以我的程式來說,我只要輸入http://localhost:8080/ExMedia/authorize,又會再度進入驗證流程,但因為已經有執行過Google的身分驗證流程,且Credential資訊已經寫入指定的實體位置,因此再度執行時,會直接跳過SampleServlet 流程1~6的step,直接進入doGet ,如下列程式碼 :
@Override public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException { Credential credential = this.getCredential(); if (credential != null) { if (credential.getExpiresInSeconds() < 0) //if the accessToken expired, refresh the accessToken using refreshToken if (credential.refreshToken()){ System.out.println("refresh the access token's expired date=" + new Date(credential.getExpirationTimeMilliseconds())); } req.getSession().setAttribute("credential", this.getCredential()); StringBuffer sb = new StringBuffer(); sb.append("http://").append(req.getLocalAddr() + ":" + req.getLocalPort()).append(req.getContextPath()).append("/showContents.html"); try { response.sendRedirect(sb.toString()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }備註:
- OAuth2.0 的一些 Sevlet Sample Code(若要整合Web AP,建議看一看):
- Using OAuth2.0 with the Google API Client Library for Java
- OAuth2.0 and Google OAuth Client Library Java
- 有用的API查詢文件:
- google-oauth-java-client: 對於oauth-javaclient library內的類別有使用上的疑惑都可以從這個線上API Document上查到大部份的資訊
- 若你想透過呼叫 Google OAuth 2.0 endpoint 的方式來建立Web Server Application,可參考此篇Using OAuth2.0 for Web Server Applications。
除了setAccessType("offline"),还要setApprovalPrompt("force"),不然保存不下来refreshToken,后面也刷新不了accessToken。
回覆刪除好的,感謝你的提醒
回覆刪除