این سند نحوه پیادهسازی یک کنترلکننده بازخوانی مجوز OAuth 2.0 را با استفاده از سرورهای جاوا از طریق یک برنامه وب نمونه توضیح میدهد که وظایف کاربر را با استفاده از Google Tasks API نمایش میدهد. برنامه نمونه ابتدا مجوز دسترسی به Google Tasks کاربر را درخواست می کند و سپس وظایف کاربر را در لیست وظایف پیش فرض نمایش می دهد.
مخاطب
این سند برای افرادی که با معماری برنامه های وب جاوا و J2EE آشنا هستند طراحی شده است. مقداری آگاهی از جریان مجوز OAuth 2.0 توصیه می شود.
مطالب
برای داشتن چنین نمونه کاملاً کارآمد چندین مرحله لازم است، شما باید:
- نگاشت servlet را در فایل web.xml اعلام کنید
- کاربران سیستم خود را احراز هویت کنید و برای دسترسی به Tasks آن مجوز درخواست کنید
- به کد مجوز از نقطه پایانی مجوز Google گوش دهید
- کد مجوز را برای بازخوانی و رمز دسترسی مبادله کنید
- وظایف کاربر را بخوانید و نمایش دهید
نگاشت servlet را در فایل web.xml اعلام کنید
ما از 2 سرولت در برنامه خود استفاده خواهیم کرد:
- PrintTasksTitlesServlet (نگاشت به / ): نقطه ورود برنامه است که احراز هویت کاربر را مدیریت می کند و وظایف کاربر را نمایش می دهد.
- OAuthCodeCallbackHandlerServlet (نگاشت به /oauth2callback ): پاسخ تماس OAuth 2.0 که پاسخ از نقطه پایانی مجوز OAuth را کنترل می کند
در زیر فایل web.xml است که این 2 سرولت را به 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 وظایف زیر انجام می شود:
- بررسی می کند که آیا کاربر در سیستم احراز هویت شده است یا خیر
- اگر کاربر احراز هویت نشده باشد، به صفحه احراز هویت هدایت می شود
- اگر کاربر احراز هویت شده باشد، بررسی میکنیم که آیا نشانهی تازهسازی از قبل در ذخیرهسازی دادههایمان وجود دارد - که توسط OAuthTokenDao زیر مدیریت میشود. اگر نشانههای تازهسازی برای کاربر وجود نداشته باشد، به این معنی است که کاربر هنوز مجوز برنامه را برای دسترسی به وظایف خود اعطا نکرده است. در این صورت کاربر به نقطه پایانی OAuth 2.0 Google's Authorization هدایت می شود.
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<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 برای برنامه در یک فایل خواص ذخیره می شود. از طرف دیگر، می توانید آنها را به عنوان یک ثابت در جایی در یکی از کلاس های جاوا خود داشته باشید، اگرچه در اینجا کلاس 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 Client ID و Client راز برنامه شما را شناسایی می کند و به Tasks API اجازه می دهد فیلترها و قوانین سهمیه ای را که برای برنامه شما تعریف شده است اعمال کند. شناسه مشتری و راز را می توان در کنسول APIs Google پیدا کرد. هنگامی که روی کنسول قرار می گیرید باید:
- ایجاد یا انتخاب یک پروژه
- Tasks API را با تغییر وضعیت Tasks API روی ON در لیست خدمات فعال کنید.
- اگر شناسه مشتری OAuth 2.0 هنوز ایجاد نشده باشد، تحت API Access ایجاد کنید.
- اطمینان حاصل کنید که نشانی وب کنترلکننده پاسخ تماس کد OAuth 2.0 پروژه در URIهای تغییر مسیر ثبت/فهرست شده است. به عنوان مثال، در این پروژه نمونه، اگر برنامه وب شما از دامنه https://www.example.com ارائه می شود، باید https://www.example.com/oauth2callback را ثبت کنید.
به کد مجوز از نقطه پایانی مجوز 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 مدیریت میکند. 2 مورد برای رسیدگی وجود دارد:
- کاربر اجازه دسترسی داده است: درخواست دریافت کد 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";
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; }
توجه: پیاده سازی فوق از برخی کتابخانه های 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. 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; }
کاربر با وظایف خود نمایش داده می شود:
نمونه برنامه
کد این نمونه برنامه را می توانید از اینجا دانلود کنید. با خیال راحت آن را بررسی کنید.