Google Developers網站現在對如何設置OAuth 2.0進行了很好的描述 。 但是,事實證明,配置一個真實的示例如何完成它是一個挑戰,因此我想記錄下我學到的東西。
教程場景在上一個教程中,我創建的項目說明了如何訪問用戶的Google Docs文件列表。 在本教程中,我做了一些改動,而是使用YouTube的API來顯示用戶喜歡的視頻的列表。 訪問用戶的收藏夾確實需要使用OAuth進行身份驗證,因此這是一個很好的測試。
入門 (可以在此處找到本教程的Eclipse項目)。
您必須做的第一件事是遵循Google官方文檔中概述的有關使用OAuth 2.0的步驟。 由于我正在創建一個Web應用程序,因此您將需要遵循這些文檔中名為“ Web服務器應用程序”的部分。 此外,我之前討論的用于設置Google App Engine的步驟仍然很重要,因此我將直接跳入代碼并跳過這些設置步驟。
(注意:可以在這里找到Eclipse項目-我再次選擇不使用Maven,以使那些沒有安裝Maven或在Maven方面有豐富知識的人保持簡單。)
應用程序流程非常簡單(假設是首次使用用戶):
- 用戶訪問Web應用程序時(假設您正在使用GAE開發人員模擬器在http:// localhost:8888在本地運行該Web應用程序),則他們必須首先使用其gmail或Google域帳戶登錄到Google。
- 登錄后,用戶將被重定向到一個簡單的JSP頁面,該頁面具有指向其YouTube最喜歡的視頻的鏈接。
- 當點擊鏈接時,servlet將啟動OAuth流程以獲取對其YouTube帳戶的訪問權限。 此過程的第一部分將重定向到Google Page,該頁面會提示他們是否要授予應用程序訪問權限。
- 假設用戶回答肯定,將顯示10個帶有鏈接的收藏夾列表。
- 如果他們單擊鏈接,將加載視頻。
這是前3頁流程的描述:

這是最后兩頁(假設用戶單擊了給定的鏈接):

盡管此示例特定于YouTube,但相同的通用原則也適用于訪問任何基于Google的云服務,例如Google +,Google Drive,Docs等。它們創建此類集成的關鍵推動因素顯然是OAuth,因此讓我們來看一下該過程有效。
OAuth 2.0處理流程
對于剛開始學習這項技術的新開發人員而言,使用OAuth可能會有些不知所措。 其背后的主要前提是允許用戶有選擇地確定他們希望外部應用程序可以訪問哪些“私有”資源,例如我們正在為本教程開發的資源。 通過使用OAuth,用戶可以避免與第三方共享其登錄憑據,而可以簡單地向該第三方授予訪問其某些信息的權限。
為了實現此功能,需要將用戶導航到其私人數據所在的源(在本例中為YouTube)。 然后,他們可以允許或拒絕訪問請求。 如果他們允許,則私有數據(YouTube)的源將一次性授權代碼返回給第三方應用程序。 由于每次需要訪問權限時用戶都必須授予訪問權限是一件很麻煩的事情,因此可以玩一個額外的通話,它將“以舊換新”他們的單次使用授權更長的時間。 我們為本教程開發的Web應用程序的總體流程如下所示。
OAuth流程

進行的第一步是確定用戶是否已經使用其gmail或Google Domain帳戶登錄到Google。 盡管不直接與OAuth流程綁定,但使用戶能夠使用其Google帳戶登錄非常方便,而不是要求用戶登錄您的網站。 這是對Google的第一個標注。 然后,一旦登錄,應用程序將確定用戶是否具有授予OAuth權限的本地帳戶設置。 如果他們是第一次登錄,則不會。 在這種情況下,將啟動OAuth流程。
該過程的第一步是向OAuth提供程序(在本例中為Google YouTube)指定請求訪問的“范圍”。 由于Google提供了很多服務,因此它們具有很多范圍。 您可以使用其OAuth 2.0沙箱輕松確定。
當您啟動OAuth流程時,會向他們提供您想要訪問的范圍,以及Google為您提供的OAuth客戶端憑據(這些步驟實際上是支持OAuth的任何提供程序所通用的)。 為了我們的目的,我們正在尋求訪問該用戶的YouTube帳戶的權限,因此Google提供的范圍是:https://gdata.youtube.com/。
如果最終用戶授予對由范圍標識的資源的訪問權限,則Google會將授權碼發回給應用程序。 這是在servlet中捕獲的。 由于返回的代碼只是“一次性”代碼,因此將其交換為運行時間更長的訪問令牌(和相關的刷新令牌)。 上面的步驟由標題為“請求訪問和刷新令牌”的活動/框表示。
一旦配備了訪問令牌,應用程序便可以通過將API調用與令牌一起放置來訪問用戶的私有數據。 如果一切順利,API將返回結果。
這不是一個可怕的復雜過程,它只涉及幾個步驟。 讓我們看一些具體的實現細節,首先從servlet過濾器開始,該過濾器確定用戶是否已經登錄Google和/或已授予OAuth訪問權限。
授權過濾器
讓我們看一下AuthorizationFilter的前幾行(要了解如何將其配置為過濾器,請參閱web.xml文件)。
public void doFilter(ServletRequest req, ServletResponse res,FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;HttpSession session = request.getSession();if not present, add credential store to servlet contextif (session.getServletContext().getAttribute(Constant.GOOG_CREDENTIAL_STORE) == null) {LOGGER.fine('Adding credential store to context ' + credentialStore);session.getServletContext().setAttribute(Constant.GOOG_CREDENTIAL_STORE, credentialStore);}if google user isn't in session, add itif (session.getAttribute(Constant.AUTH_USER_ID) == null) {LOGGER.fine('Add user to session');UserService userService = UserServiceFactory.getUserService();User user = userService.getCurrentUser();session.setAttribute(Constant.AUTH_USER_ID, user.getUserId());session.setAttribute(Constant.AUTH_USER_NICKNAME, user.getNickname());if not running on app engine prod, hard-code my email address for testingif (SystemProperty.environment.value() == SystemProperty.Environment.Value.Production) {session.setAttribute(Constant.AUTH_USER_EMAIL, user.getEmail());} else {session.setAttribute(Constant.AUTH_USER_EMAIL, 'jeffdavisco@gmail.com');}}
前幾行簡單地將通用servlet請求和響應轉換為它們對應的Http等效項-這是必需的,因為我們要訪問HTTP會話。 下一步是確定Servlet上下文中是否存在CredentialStore。 正如我們將看到的,它用于存儲用戶的憑據,因此在后續的servlet中隨時可以使用它很方便。 當我們使用以下命令檢查用戶是否已經在會話中時,事情就開始了:
if (session.getAttribute(Constant.AUTH_USER_ID) == null) {
如果沒有,我們將使用Google的UserService類獲取其Google登錄憑據。 這是GAE用戶可用來獲取用戶的Google用戶ID,電子郵件和昵稱的幫助程序類。 從UserService獲得此信息后,我們將在會話中存儲一些用戶的詳細信息。
目前,我們還沒有對OAuth做任何事情,但是在接下來的代碼行系列中會有所改變:
嘗試{
Utils.getActiveCredential(request,credentialStore); } catch(NoRefreshTokenException e1){ //如果輸入了該catch塊,則需要執行oauth流程 LOGGER.info('未找到用戶–授權URL為:'+ e1.getAuthorizationUrl()); response.sendRedirect(e1.getAuthorizationUrl()); }
大多數OAuth處理都使用一個稱為Utils的幫助程序類。 在這種情況下,我們將調用靜態方法getActiveCredential()。 稍后我們將看到,如果以前沒有為用戶捕獲OAuth憑據,則此方法將返回NoRefreshTokenException。 作為自定義例外,它將返回URL值,該URL值用于將用戶重定向到Google以尋求OAuth批準。
讓我們更詳細地了解getActiveCredential()方法,因為這是管理許多OAuth處理的地方。
public static Credential getActiveCredential(HttpServletRequest request, CredentialStore credentialStore) throws NoRefreshTokenException {String userId = (String) request.getSession().getAttribute(Constant.AUTH_USER_ID);Credential credential = null;try {if (userId != null) {credential = getStoredCredential(userId, credentialStore);}if ((credential == null || credential.getRefreshToken() == null) && request.getParameter('code') != null) {credential = exchangeCode(request.getParameter('code'));LOGGER.fine('Credential access token is: ' + credential.getAccessToken());if (credential != null) {if (credential.getRefreshToken() != null) {credentialStore.store(userId, credential);}}}if (credential == null || credential.getRefreshToken() == null) {String email = (String) request.getSession().getAttribute(Constant.AUTH_USER_EMAIL);String authorizationUrl = getAuthorizationUrl(email, request);throw new NoRefreshTokenException(authorizationUrl);}} catch (CodeExchangeException e) {e.printStackTrace();} return credential;}
我們要做的第一件事是從會話中獲取Google userId(如果沒有填充,則無法做到這一點)。 接下來,我們嘗試使用Utils靜態方法getStoredCredential()從CredentialStore獲取用戶的OAuth憑據(以相同名稱存儲在Google類中)。 如果找不到該用戶的憑據,則調用名為getAuthorizationUrl()的Utils方法。 如下所示,此方法用于構造瀏覽器重定向到的URL,該URL用于提示用戶授權訪問其私有數據(該URL由Google提供,因為它將詢問用戶批準)。
private static String getAuthorizationUrl(String emailAddress, HttpServletRequest request) {GoogleAuthorizationCodeRequestUrl urlBuilder = null;try {urlBuilder = new GoogleAuthorizationCodeRequestUrl(getClientCredential().getWeb().getClientId(),Constant.OATH_CALLBACK,Constant.SCOPES).setAccessType('offline').setApprovalPrompt('force');} catch (IOException e) {TODO Auto-generated catch blocke.printStackTrace();}urlBuilder.set('state', request.getRequestURI());if (emailAddress != null) {urlBuilder.set('user_id', emailAddress);}return urlBuilder.build();}
如您所見,此方法正在使用稱為GoogleAuthorizationCodeRequestUrl的類(來自Google)。 它使用您注冊使用OAuth時由Google提供的OAuth客戶端憑據來構造HTTP調用(巧合的是,這些憑據存儲在一個名為client_secrets.json的文件中。其他參數包括OAuth請求的范圍和URL如果獲得用戶的批準,該用戶將被重定向回該URL。該URL是您在注冊Google的OAuth訪問權限時指定的URL:

現在,如果用戶已經授予OAuth訪問權限,則getActiveCredential()方法將改為從CredentialStore獲取憑據。
回到接收OAuth憑據結果的URL(在本例中為http:// localhost:8888 / authSub),您可能想知道Google如何發布到該內部專用地址? 好吧,實際上是用戶的瀏覽器發回了結果,因此在這種情況下,本地主機就可以很好地解決問題。 讓我們看一下用于處理此回調的名為OAuth2Callback的servlet(有關如何完成authSub的servlet映射的信息,請參見web.xml)。
public class OAuth2Callback extends HttpServlet {private static final long serialVersionUID = 1L;private final static Logger LOGGER = Logger.getLogger(OAuth2Callback.class.getName());public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {StringBuffer fullUrlBuf = request.getRequestURL();Credential credential = null;if (request.getQueryString() != null) {fullUrlBuf.append('?').append(request.getQueryString());}LOGGER.info('requestURL is: ' + fullUrlBuf);AuthorizationCodeResponseUrl authResponse = new AuthorizationCodeResponseUrl(fullUrlBuf.toString());check for user-denied errorif (authResponse.getError() != null) {LOGGER.info('User-denied access');} else {LOGGER.info('User granted oauth access');String authCode = authResponse.getCode();request.getSession().setAttribute('code', authCode);response.sendRedirect(authResponse.getState());}}}
該課程最重要的收獲是:
AuthorizationCodeResponseUrl authResponse =新的AuthorizationCodeResponseUrl(fullUrlBuf.toString()); Google提供了AuthorizationCodeResponseUrl類,以方便分析OAuth請求的結果。 如果該類的getError()方法不為null,則意味著用戶拒絕了該請求。 如果它為null(表示用戶已批準該請求),則使用方法調用getCode()來檢索一次性授權碼。 此代碼值放置在用戶的會話中,并且在重定向到用戶的目標URL(通過過濾器)后調用Utils.getActiveCredential()時,它將將該授權代碼交換為長期訪問權限并使用電話: 憑證= exchangeCode((String)request.getSession()。getAttribute('code')); 接下來顯示Utils.exchangeCode()方法:
public static Credential exchangeCode(String authorizationCode)throws CodeExchangeException {try {GoogleTokenResponse response = new GoogleAuthorizationCodeTokenRequest(new NetHttpTransport(), Constant.JSON_FACTORY, Utils.getClientCredential().getWeb().getClientId(), Utils.getClientCredential().getWeb().getClientSecret(),authorizationCode, Constant.OATH_CALLBACK).execute();return Utils.buildEmptyCredential().setFromTokenResponse(response);} catch (IOException e) {e.printStackTrace();throw new CodeExchangeException();}}
此方法還使用稱為GoogleAuthorizationCodeTokenRequest的Google類,該類用于調用Google以將一次性OAuth授權代碼交換為較長時間的訪問令牌。
現在,我們已經(最終)獲得了YouTube API所需的訪問令牌,我們準備向用戶顯示其視頻收藏夾中的10個。
調用YouTube API服務
有了訪問令牌,我們現在可以繼續向用戶顯示其收藏夾列表。 為此,調用了一個名為“ FavoritesServlet”的servlet。 它將調用YouTube API,通過Jackson將生成的JSON-C格式解析為一些本地Java類,然后將結果發送到JSP頁面進行處理。 這是servlet:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {LOGGER.fine('Running FavoritesServlet');Credential credential = Utils.getStoredCredential((String) request.getSession().getAttribute(Constant.AUTH_USER_ID), (CredentialStore) request.getSession().getServletContext().getAttribute(Constant.GOOG_CREDENTIAL_STORE));VideoFeed feed = null;if the request fails, it's likely because access token is expired - we'll refreshtry {LOGGER.fine('Using access token: ' + credential.getAccessToken());feed = YouTube.fetchFavs(credential.getAccessToken());} catch (Exception e) {LOGGER.fine('Refreshing credentials');credential.refreshToken();credential = Utils.refreshToken(request, credential);GoogleCredential googleCredential = Utils.refreshCredentials(credential);LOGGER.fine('Using refreshed access token: ' + credential.getAccessToken());retry feed = YouTube.fetchFavs(credential.getAccessToken());} LOGGER.fine('Video feed results are: ' + feed);request.setAttribute(Constant.VIDEO_FAVS, feed);RequestDispatcher dispatcher = getServletContext().getRequestDispatcher('htmllistVids.jsp');dispatcher.forward(request, response); }
由于這篇文章主要是關于OAuth流程的,因此我不會過多介紹API調用的放置方式,但是最重要的代碼行是:feed = YouTube.fetchFavs(credential.getAccessToken()); feed是VideoFeed的實例。 如您所見,另一個名為YouTube的幫助程序類用于執行繁重的工作。 為了總結一下,我將展示fetchFavs()方法。
public static VideoFeed fetchFavs(String accessToken) throws IOException, HttpResponseException {HttpTransport transport = new NetHttpTransport();final JsonFactory jsonFactory = new JacksonFactory();HttpRequestFactory factory = transport.createRequestFactory(new HttpRequestInitializer() {@Overridepublic void initialize(HttpRequest request) {set the parserJsonCParser parser = new JsonCParser(jsonFactory);request.addParser(parser);set up the Google headersGoogleHeaders headers = new GoogleHeaders();headers.setApplicationName('YouTube Favorites1.0');headers.gdataVersion = '2';request.setHeaders(headers);}});build the YouTube URLYouTubeUrl url = new YouTubeUrl(Constant.GOOGLE_YOUTUBE_FEED);url.maxResults = 10;url.access_token = accessToken;build the HTTP GET requestHttpRequest request = factory.buildGetRequest(url);HttpResponse response = request.execute();execute the request and the parse video feedVideoFeed feed = response.parseAs(VideoFeed.class);return feed;}
它使用稱為HttpRequestFactory的Google類構造對YouTube的出站HTTP API調用。 由于我們使用的是GAE,因此我們只能使用哪些類來發出此類請求。 注意代碼行:
url.access_token = accessToken;
那就是我們使用通過OAuth流程獲取的訪問令牌的地方。
因此,雖然需要花費大量代碼才能使OAuth內容正常運行,但是一旦到位,您就可以準備調用各種Google API服務來進行滾動!
參考:我們的JCG合作伙伴 Jeff Davis在Jeff's SOA Ruminations博客上為Google Services進行了認證,第2部分 。
翻譯自: https://www.javacodegeeks.com/2012/06/google-services-authentication-in-app_20.html