يوضّح هذا المستند كيفية تنفيذ معالج معاودة الاتصال الخاص بتفويض OAuth 2.0 باستخدام برامج Java النصية من خلال تطبيق ويب نموذجي يعرض مهام المستخدم باستخدام Google Tasks API. يطلب التطبيق النموذجي أولاً الحصول على إذن بالوصول إلى مهام المستخدم على Google، ثم يعرض مهام المستخدم في قائمة المهام التلقائية.
الجمهور
هذا المستند مخصّص للمستخدمين الذين لديهم معرفة بلغة Java وبنية تطبيقات الويب J2EE. ننصحك بمعرفة بعض المعلومات عن مسار الموافقة على الطلبات في OAuth 2.0.
المحتويات
للحصول على عيّنة تعمل بشكل كامل، يجب اتّخاذ عدة خطوات، وهي:
- تحديد عمليات ربط servlet في ملف web.xml
- مصادقة المستخدمين على نظامهم وطلب إذن بالوصول إلى "مهام Google"
- الاستماع إلى رمز التفويض من نقطة نهاية التفويض من Google
- تبديل رمز التفويض برمز تحديث ورمز دخول
- قراءة مهام المستخدم وعرضها
تحديد عمليات ربط الخادم الوهمي في ملف web.xml
يستخدم هذا التطبيق servletَين على النحو التالي:
- PrintTasksTitlesServlet (تم ربطه بـ
/): نقطة دخول التطبيق التي ستتعامل مع مصادقة المستخدم، وستعرض مهام المستخدم - OAuthCodeCallbackHandlerServlet (تم ربطه بـ
/oauth2callback): هو برنامج معالجة رد الاتصال OAuth 2.0 الذي يتعامل مع الرد من نقطة نهاية مصادقة OAuth
ملف web.xml التالي الذي يربط هذين العنصرَين بعناوين URL في تطبيقنا:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>PrintTasksTitles</servlet-name>
<servlet-class>com.google.oauthsample.PrintTasksTitlesServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PrintTasksTitles</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
<servlet-class>com.google.oauthsample.OAuthCodeCallbackHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>OAuthCodeCallbackHandlerServlet</servlet-name>
<url-pattern>/oauth2callback</url-pattern>
</servlet-mapping>
</web-app>مصادقة المستخدمين على نظامهم وطلب إذن بالوصول إلى مهامهم
يدخل المستخدم إلى التطبيق من خلال عنوان URL الأساسي "/" الذي يتم ربطه ببرنامج PrintTaskListsTitlesServlet servlet. في برنامج servlet هذا، يتم تنفيذ المهام التالية:
- تتحقّق هذه السمة مما إذا تمّت مصادقة المستخدم على النظام.
- إذا لم تتم مصادقة المستخدم، ستتم إعادة توجيهه إلى صفحة المصادقة.
- في حال مصادقة المستخدم، يتم التحقّق من توفّر رمز مميّز لإعادة التحميل في مساحة تخزين البيانات، ويتم التعامل مع ذلك من خلال
OAuthTokenDaoأدناه. إذا لم تكن الرموز المميزة متاحة في مساحة التخزين الخاصة بالمستخدم، يعني ذلك أنّ المستخدم لم يمنح التطبيق بعد إذن الوصول إلى مهامه. بعد ذلك، تتم إعادة توجيه المستخدم إلى نقطة نهاية التفويض في بروتوكول OAuth 2.0 من Google.
في ما يلي طريقة لتنفيذ ذلك:
package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Additionally, a * simple memory implementation is used as a mock. Change the implementation to * using the user's own user/login implementation. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If tokens are not available for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirects to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; } } /** * Construct the request's URL without the parameter part. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getFullRequestUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = req.getServletPath(); String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString; } }
ملاحظة: يستخدم التنفيذ السابق بعض مكتبات App Engine. ويتم استخدامها
لتبسيط العملية. إذا كنت تطوّر تطبيقًا لمنصة أخرى، عليك إعادة تنفيذ واجهة UserService التي تتعامل مع مصادقة المستخدم.
يستخدم التطبيق كائن الوصول إلى البيانات (DAO) للاحتفاظ برموز التفويض المميزة للمستخدم والوصول إليها.
يتم عرض واجهة OAuthTokenDao وتنفيذ وهمي (في الذاكرة) - OAuthTokenDaoMemoryImpl - يتم استخدامهما في هذا المثال، وذلك في الأمثلة التالية:
package com.google.oauthsample; import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse; /** * Allows easy storage and access of authorization tokens. */ public interface OAuthTokenDao { /** * Stores the given AccessTokenResponse using the {@code username}, the OAuth * {@code clientID} and the tokens scopes as keys. * * @param tokens The AccessTokenResponse to store * @param userName The userName associated wit the token */ public void saveKeys(AccessTokenResponse tokens, String userName); /** * Returns the AccessTokenResponse stored for the given username, clientId and * scopes. Returns {@code null} if there is no AccessTokenResponse for this * user and scopes. * * @param userName The username of which to get the stored AccessTokenResponse * @return The AccessTokenResponse of the given username */ public AccessTokenResponse getKeys(String userName); }
package com.google.oauthsample; import com.google.api.client.auth.oauth2.draft10.AccessTokenResponse; ... /** * Quick and Dirty memory implementation of {@link OAuthTokenDao} based on * HashMaps. */ public class OAuthTokenDaoMemoryImpl implements OAuthTokenDao { /** Object where all the Tokens will be stored */ private static Map<String, AccessTokenResponse> tokenPersistance = new HashMap<String, AccessTokenResponse>(); public void saveKeys(AccessTokenResponse tokens, String userName) { tokenPersistance.put(userName, tokens); } public AccessTokenResponse getKeys(String userName) { return tokenPersistance.get(userName); } }
يتم تخزين بيانات اعتماد OAuth 2.0 للتطبيق في ملف خصائص.
يمكنك بدلاً من ذلك تخزينها كقيمة ثابتة في مكان ما في إحدى فئات Java.
في ما يلي الفئة OAuthProperties وملف oauth.properties
المستخدَم في المثال:
package com.google.oauthsample; import ... /** * Object representation of an OAuth properties file. */ public class OAuthProperties { public static final String DEFAULT_OAUTH_PROPERTIES_FILE_NAME = "oauth.properties"; /** The OAuth 2.0 Client ID */ private String clientId; /** The OAuth 2.0 Client Secret */ private String clientSecret; /** The Google APIs scopes to access */ private String scopes; /** * Instantiates a new OauthProperties object reading its values from the * {@code OAUTH_PROPERTIES_FILE_NAME} properties file. * * @throws IOException IF there is an issue reading the {@code propertiesFile} * @throws OauthPropertiesFormatException If the given {@code propertiesFile} * is not of the right format (does not contains the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties() throws IOException { this(OAuthProperties.class.getResourceAsStream(DEFAULT_OAUTH_PROPERTIES_FILE_NAME)); } /** * Instantiates a new OAuthProperties object, reading its values from the given * properties file. * * @param propertiesFile the InputStream to read an OAuth Properties file. The * file should contain the keys {@code clientId}, {@code * clientSecret} and {@code scopes} * @throws IOException if there is an issue reading the {@code propertiesFile} * @throws OAuthPropertiesFormatException If the given {@code propertiesFile} * is not in the correct format (does not contain the keys {@code * clientId}, {@code clientSecret} and {@code scopes}) */ public OAuthProperties(InputStream propertiesFile) throws IOException { Properties oauthProperties = new Properties(); oauthProperties.load(propertiesFile); clientId = oauthProperties.getProperty("clientId"); clientSecret = oauthProperties.getProperty("clientSecret"); scopes = oauthProperties.getProperty("scopes"); if ((clientId == null) || (clientSecret == null) || (scopes == null)) { throw new OAuthPropertiesFormatException(); } } /** * @return the clientId */ public String getClientId() { return clientId; } /** * @return the clientSecret */ public String getClientSecret() { return clientSecret; } /** * @return the scopes */ public String getScopesAsString() { return scopes; } /** * Thrown when the OAuth properties file was not at the right format, i.e not * having the right properties names. */ @SuppressWarnings("serial") public class OAuthPropertiesFormatException extends RuntimeException { } }
يظهر ملف oauth.properties، الذي يحتوي على بيانات اعتماد OAuth 2.0 لتطبيقك، في المثال التالي.
عليك تغيير القيم في هذا الملف.
# Client ID and secret. They can be found in the APIs console. clientId=1234567890.apps.googleusercontent.com clientSecret=aBcDeFgHiJkLmNoPqRsTuVwXyZ # API scopes. Space separated. scopes=https://www.googleapis.com/auth/tasks
يحدّد معرّف عميل OAuth 2.0 وسر العميل التطبيق ويسمح لواجهة Tasks API بتطبيق الفلاتر وقواعد الحصص المحدّدة للتطبيق. يمكن العثور على معرّف العميل وسرّه في وحدة تحكّم Google APIs. بعد الوصول إلى وحدة التحكّم، على المستخدم إجراء ما يلي:
- إنشاء مشروع أو اختياره
- فعِّل Tasks API من خلال ضبط حالة Tasks API على مفعَّلة في قائمة الخدمات.
- ضمن الوصول إلى واجهة برمجة التطبيقات، أنشئ معرّف عميل OAuth 2.0 إذا لم يتم إنشاء معرّف بعد.
- تأكَّد من تسجيل عنوان URL لمعالج معاودة الاتصال برمز OAuth 2.0 الخاص بالمشروع أو إضافته إلى القائمة المسموح بها في معرّفات الموارد المنتظمة (URI) الخاصة بإعادة التوجيه. على سبيل المثال، في هذا المشروع النموذجي، على المستخدم تسجيل
https://www.example.com/oauth2callbackإذا كان تطبيق الويب معروضًا من النطاقhttps://www.example.com.
التعامل مع رمز التفويض من نقطة نهاية التفويض في Google
في حال لم يسبق للمستخدم أن منح التطبيق الإذن بالوصول إلى مهامه، وبالتالي تمت إعادة توجيهه إلى نقطة نهاية تفويض بروتوكول OAuth 2.0 من Google، سيظهر للمستخدم مربّع حوار تفويض من Google يطلب منه منح التطبيق الإذن بالوصول إلى مهامه:
بعد منح الإذن بالوصول أو رفضه، تتم إعادة توجيه المستخدم إلى معالج معاودة الاتصال برمز OAuth 2.0 الذي تم تحديده كعملية إعادة توجيه/معاودة اتصال عند إنشاء عنوان URL لمنح الإذن في Google:
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
.getScopesAsString()).build()يتولّى معالج معاودة الاتصال لرمز OAuth 2.0 - OAuthCodeCallbackHandlerServlet - عملية إعادة التوجيه من نقطة نهاية Google OAuth 2.0. هناك حالتان يجب التعامل معهما:
- منح المستخدم إذن الوصول: يتم تحليل الطلب للحصول على رمز OAuth 2.0 من مَعلمات عنوان URL.
- رفض المستخدم منح الإذن بالوصول: يتم عرض رسالة للمستخدم.
package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback"; public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the "error" URL parameter String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME); // Checking if there was an error such as the user denied access if (error != null && error.length > 0) { resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\"."); return; } // Getting the "code" URL parameter String[] code = req.getParameterValues(CODE_URL_PARAM_NAME); // Checking conditions on the "code" URL parameter if (code == null || code.length == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing"); return; } } /** * Construct the OAuth code callback handler URL. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = URL_MAPPING; String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo; } }
تبديل رمز التفويض برمز مميّز لإعادة التحميل ورمز دخول
بعد ذلك، يتبادل OAuthCodeCallbackHandlerServlet رمز Auth 2.0
مقابل رمز مميز لإعادة التحميل ورمز مميز للوصول، ويحفظه في مخزن البيانات، ثم يعيد توجيه المستخدم إلى عنوان URL الخاص بـ PrintTaskListsTitlesServlet:
يتم تمييز الرمز الذي تمت إضافته إلى الملف.
package com.google.oauthsample; import ... /** * Servlet handling the OAuth callback from the authentication service. We are * retrieving the OAuth code, then exchanging it for a refresh and an access * token and saving it. */ @SuppressWarnings("serial") public class OAuthCodeCallbackHandlerServlet extends HttpServlet { /** The name of the Oauth code URL parameter */ public static final String CODE_URL_PARAM_NAME = "code"; /** The name of the OAuth error URL parameter */ public static final String ERROR_URL_PARAM_NAME = "error"; /** The URL suffix of the servlet */ public static final String URL_MAPPING = "/oauth2callback"; /** The URL to redirect the user to after handling the callback. Consider * saving this in a cookie before redirecting users to the Google * authorization URL if you have multiple possible URL to redirect people to. */ public static final String REDIRECT_URL = "/"; /** The OAuth Token DAO implementation. Consider injecting it instead of using * a static initialization. Also we are using a simple memory implementation * as a mock. Change the implementation to using your database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the "error" URL parameter String[] error = req.getParameterValues(ERROR_URL_PARAM_NAME); // Checking if there was an error such as the user denied access if (error != null && error.length > 0) { resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "There was an error: \""+error[0]+"\"."); return; } // Getting the "code" URL parameter String[] code = req.getParameterValues(CODE_URL_PARAM_NAME); // Checking conditions on the "code" URL parameter if (code == null || code.length == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "The \"code\" URL parameter is missing"); return; } // Construct incoming request URL String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Exchange the code for OAuth tokens AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0], requestUrl); // Getting the current user // This is using App Engine's User Service, but the user should replace this // with their own user/login implementation UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // Save the tokens oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); } /** * Construct the OAuth code callback handler URL. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getOAuthCodeCallbackHandlerUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = URL_MAPPING; String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo; } /** * Exchanges the given code for an exchange and a refresh token. * * @param code The code gotten back from the authorization service * @param currentUrl The URL of the callback * @param oauthProperties The object containing the OAuth configuration * @return The object containing both an access and refresh token * @throws IOException */ public AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(String code, String currentUrl) throws IOException { HttpTransport httpTransport = new NetHttpTransport(); JacksonFactory jsonFactory = new JacksonFactory(); // Loading the oauth config file OAuthProperties oauthProperties = new OAuthProperties(); return new GoogleAuthorizationCodeGrant(httpTransport, jsonFactory, oauthProperties .getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute(); } }
ملاحظة: يستخدم التنفيذ السابق بعض مكتبات App Engine، والتي يتم استخدامها
لتبسيط العملية. إذا كنت تطوّر تطبيقًا لمنصة أخرى، عليك إعادة تنفيذ واجهة UserService التي تتعامل مع مصادقة المستخدم.
قراءة مهام المستخدم وعرضها
منح المستخدم التطبيق إذن الوصول إلى مهامه. يحتوي التطبيق على رمز مميز لإعادة التحميل
يتم حفظه في مخزن البيانات الذي يمكن الوصول إليه من خلال OAuthTokenDao. يمكن الآن أن تستخدم أداة معالجة الطلبات PrintTaskListsTitlesServlet هذه الرموز المميزة للوصول إلى مهام المستخدم وعرضها:
يتم تمييز الرمز المضاف إلى الملف.
package com.google.oauthsample; import ... /** * Simple sample Servlet which will display the tasks in the default task list of the user. */ @SuppressWarnings("serial") public class PrintTasksTitlesServlet extends HttpServlet { /** * The OAuth Token DAO implementation, used to persist the OAuth refresh token. * Consider injecting it instead of using a static initialization. Additionally, a * simple memory implementation is used as a mock. Change the implementation to * use your own database system. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Getting the current user // This is using App Engine's User Service but you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); // If the user is not logged-in it is redirected to the login service, then back to this page if (user == null) { resp.sendRedirect(userService.createLoginURL(getFullRequestUrl(req))); return; } // Checking if we already have tokens for this user in store AccessTokenResponse accessTokenResponse = oauthTokenDao.getKeys(user.getEmail()); // If we don't have tokens for this user if (accessTokenResponse == null) { OAuthProperties oauthProperties = new OAuthProperties(); // Redirect to the Google OAuth 2.0 authorization endpoint resp.sendRedirect(new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()); return; } // Prints the user's task list titles in the response resp.setContentType("text/plain"); resp.getWriter().append("Task Lists titles for user " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter()); } /** * Construct the request's URL without the parameter part. * * @param req the HttpRequest object * @return The constructed request's URL */ public static String getFullRequestUrl(HttpServletRequest req) { String scheme = req.getScheme() + "://"; String serverName = req.getServerName(); String serverPort = (req.getServerPort() == 80) ? "" : ":" + req.getServerPort(); String contextPath = req.getContextPath(); String servletPath = req.getServletPath(); String pathInfo = (req.getPathInfo() == null) ? "" : req.getPathInfo(); String queryString = (req.getQueryString() == null) ? "" : "?" + req.getQueryString(); return scheme + serverName + serverPort + contextPath + servletPath + pathInfo + queryString; } /** * Uses the Google Tasks API to retrieve a list of the user's tasks in the default * tasks list. * * @param accessTokenResponse The OAuth 2.0 AccessTokenResponse object * containing the access token and a refresh token. * @param output The output stream writer to write the task list titles to. * @return A list of the user's task titles in the default task list. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer output) throws IOException { // Initializing the Tasks service HttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = new JacksonFactory(); OAuthProperties oauthProperties = new OAuthProperties(); GoogleAccessProtectedResource accessProtectedResource = new GoogleAccessProtectedResource( accessTokenResponse.accessToken, transport, jsonFactory, oauthProperties.getClientId(), oauthProperties.getClientSecret(), accessTokenResponse.refreshToken); Tasks service = new Tasks(transport, accessProtectedResource, jsonFactory); // Using the initialized Tasks API service to query the list of tasks lists com.google.api.services.tasks.model.Tasks tasks = service.tasks.list("@default").execute(); for (Task task : tasks.items) { output.append(task.title + "\n"); } } }
يتم عرض مهام المستخدم على النحو التالي:
تطبيق نموذجي
يمكنك تنزيل الرمز البرمجي لهذا التطبيق النموذجي.