يشرح هذا المستند كيفية تنفيذ معالج معاودة الاتصال بتفويض OAuth 2.0 باستخدام خوادم Java من خلال نموذج تطبيق ويب يعرض مهام المستخدم باستخدام Google Tasks API. سيطلب نموذج التطبيق أولاً إذنًا للوصول إلى "مهام Google" لدى المستخدم، ثم سيعرض مهام المستخدم في قائمة المهام التلقائية.
الجمهور
هذا المستند مخصص للأشخاص الذين على دراية ببنية تطبيقات الويب Java وJ2EE. يُنصَح ببعض الإلمام بمسار تفويض OAuth 2.0.
المحتويات
من أجل الحصول على العديد من الخطوات لعيّنة تعمل بشكل كامل، تحتاج إلى:
- توضيح عمليات ربط Servicelet في ملف web.xml
- مصادقة المستخدمين في نظامك وطلب تفويض للوصول إلى "مهام Google"
- الاستماع إلى رمز التفويض من نقطة نهاية التفويض من Google
- استبدال رمز التفويض لإعادة التحميل والحصول على رمز الدخول
- قراءة مهام المستخدم وعرضها
تعريف تعيينات serlet في ملف web.xml
سنستخدم خدمتين بلغة سرليت في تطبيقنا:
- 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. في تلك الخدمة، يتم تنفيذ المهام التالية:
- للتحقق مما إذا كان المستخدم قد تمت مصادقته على النظام
- إذا لم تتم مصادقة المستخدم، ستتم إعادة توجيهه إلى صفحة المصادقة.
- إذا تمت مصادقة المستخدم، نتحقّق مما إذا كان لدينا رمز مميز للتحديث في مساحة تخزين البيانات، ويتعامل معه 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. 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 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;
}
}
/**
* 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
يتم أيضًا تخزين بيانات اعتماد OAuth 2.0 للتطبيق في ملف الخصائص. بدلاً من ذلك، يمكنك جعلها ثابتة في مكان ما في إحدى فئات جافا، على الرغم من أن فئة 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 of the right format (does not contains 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. بعد الدخول إلى وحدة التحكّم، عليك تنفيذ ما يلي:
- إنشاء مشروع أو اختياره
- يمكنك تفعيل واجهة برمجة تطبيقات "مهام Google" من خلال تبديل حالة واجهة برمجة تطبيقات "مهام Google" إلى تفعيل في قائمة الخدمات.
- ضمن الوصول إلى واجهة برمجة التطبيقات، أنشِئ معرِّف عميل 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 الخاص بـ OAuthCodeCallbackHandlerServlet:
تم تمييز التعليمة البرمجية المضافة إلى الملف أدناه، والشفرة الموجودة غير مفعّلة.
/** عنوان URL المطلوب إعادة توجيه المستخدم إليه بعد معالجة طلب معاودة الاتصال. ضع في اعتبارك
* حفظ هذا في ملف تعريف ارتباط قبل إعادة توجيه المستخدمين إلى صفحة
* عنوان URL التفويض إذا كان لديك عدة عناوين URL محتملة لإعادة توجيه الأشخاص إليها. */
public static Final String REDIRECT_URL = "/";
/** تنفيذ رمز DAO لرمز OAuth المميز. يمكنك إدخاله بدلاً من استخدام
* إعداد ثابت. كما نستخدم أداة بسيطة للذاكرة
* على سبيل المثال. قم بتغيير التنفيذ إلى استخدام نظام قاعدة البيانات الخاص بك. */
public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl();
// إنشاء عنوان URL للطلبات الواردة
String requestUrl = getOAuthCodeCallbackHandlerUrl(req);
// تبادل الرمز لرموز OAuth المميزة
AccessTokenResponse accessTokenResponse = exchangeCodeForAccessAndRefreshTokens(code[0],
requestUrl);
// الحصول على المستخدم الحالي
// يستخدم هذا خدمة مستخدم App Engine ولكن يجب عليك استبدال ذلك إلى
// تنفيذ المستخدم/تسجيل الدخول الخاص بك
UserService userService = UserServiceFound.getUserService();
String email = userService.getCurrentUser().getEmail();
// حفظ الرموز المميّزة
oauthTokenDao.saveKeys(accessTokenResponse, email);
resp.sendRedirect(REDIRECT_URL);
}
/**
* استبدال الرمز المحدّد برمز تبادلي ورمز مميّز لإعادة التحميل
*
* @param code: تم استرداد الرمز من خدمة التفويض.
* @param currentUrl عنوان URL لمعاودة الاتصال
* @param oauthProperties: يمثل الكائن الذي يحتوي على إعداد OAuth
* @return الكائن الذي يحتوي على رمزي الدخول والتحديث
* @throws IOException
*/
العام AccessTokenResponse exchangeCodeForAccessAndRefreshTokens(رمز السلسلة، السلسلة currentUrl)
throws IOException {
HttpTransport httpTransport = new NetHttpTransport();
Jacksonمصانع json = new Jacksonfactor();
// تحميل ملف إعداد oauth
OAuthProperties oauthProperties = new OAuthProperties();
إرجاع GooglePermissionCodeGrant(httpTransport, jsonFavorite, oauthProperties
.getClientId(), oauthProperties.getClientSecret(), code, currentUrl).execute();
}
}ملف OAuthCodeCallbackHandlerCertlet.javaملاحظة: يستخدم التنفيذ أعلاه بعض مكتبات App Engine، وتُستخدم هذه المكتبات للتبسيط. في حال كنت تطوِّر منصة أخرى، يمكنك إعادة تنفيذ واجهة UserService التي تعالج مصادقة المستخدم.
قراءة مهام المستخدم وعرضها
منح المستخدم التطبيق إذن الوصول إلى مهامه. يحتوي التطبيق على رمز إعادة تحميل يتم حفظه في مخزن البيانات ويمكن الوصول إليه من خلال OAuthTokenDao. يمكن الآن لخدمة PrintTaskListsTitlesServlet استخدام هذه الرموز المميزة للوصول إلى مهام المستخدم وعرضها:
تم تمييز التعليمة البرمجية المضافة إلى الملف أدناه، والشفرة الموجودة غير مفعّلة.