In diesem Dokument wird beschrieben, wie Sie einen OAuth 2.0-Autorisierungs-Callback-Handler mit Java-Servlets in einer Beispiel-Webanwendung implementieren, die die Aufgaben des Nutzers mithilfe der Google Tasks API anzeigt. Die Beispielanwendung fordert zuerst die Autorisierung zum Zugriff auf die Google Tasks des Nutzers an und zeigt dann die Aufgaben des Nutzers in der Standardaufgabenliste an.
Zielgruppe
Dieses Dokument richtet sich an Personen, die mit der Architektur von Java- und J2EE-Webanwendungen vertraut sind. Grundkenntnisse zum OAuth 2.0-Autorisierungsablauf sind empfehlenswert.
Inhalt
Damit das Beispiel vollständig funktioniert, sind mehrere Schritte erforderlich:
- Servlet-Zuordnungen in der Datei „web.xml“ deklarieren
- Nutzer in Ihrem System authentifizieren und Autorisierung für den Zugriff auf Aufgaben anfordern
- Auf den Autorisierungscode vom Google-Autorisierungsendpunkt warten
- Autorisierungscode gegen Aktualisierungs- und Zugriffstoken eintauschen
- Aufgaben des Nutzers lesen und anzeigen
Servlet-Zuordnungen in der Datei „web.xml“ deklarieren
Wir verwenden in unserer Anwendung zwei Servlets:
- PrintTasksTitlesServlet (/ zugeordnet): Der Einstiegspunkt der Anwendung, der die Nutzerauthentifizierung verarbeitet und die Aufgaben des Nutzers anzeigt
- OAuthCodeCallbackHandlerServlet (zugeordnet zu /oauth2callback): Der OAuth 2.0-Callback, der die Antwort vom OAuth-Autorisierungsendpunkt verarbeitet
Unten sehen Sie die Datei web.xml, in der diese beiden Servlets den URLs in unserer Anwendung zugeordnet werden:
<?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>
Nutzer in Ihrem System authentifizieren und Autorisierung für den Zugriff auf die Aufgaben anfordern
Der Nutzer ruft die Anwendung über die Stamm-URL „/“ auf, die dem Servlet PrintTaskListsTitlesServlet zugeordnet ist. In diesem Servlet werden die folgenden Aufgaben ausgeführt:
- Prüft, ob der Nutzer im System authentifiziert ist
- Wenn der Nutzer nicht authentifiziert ist, wird er zur Authentifizierungsseite weitergeleitet.
- Wenn der Nutzer authentifiziert ist, prüfen wir, ob sich bereits ein Aktualisierungstoken in unserem Datenspeicher befindet. Dieser Vorgang wird unten vom OAuthTokenDao ausgeführt. Wenn für den Nutzer kein Aktualisierungstoken gespeichert ist, hat er der Anwendung noch nicht die Autorisierung zum Zugriff auf ihre Aufgaben erteilt. In diesem Fall wird der Nutzer zum OAuth 2.0-Autorisierungsendpunkt von Google weitergeleitet.
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; } }
Hinweis: In der obigen Implementierung werden einige App Engine-Bibliotheken verwendet, um die Sache zu vereinfachen. Wenn Sie für eine andere Plattform entwickeln, können Sie die Schnittstelle UserService, die für die Nutzerauthentifizierung zuständig ist, neu implementieren.
Die Anwendung verwendet eine DAO, um die Autorisierungstokens der Nutzer zu speichern und darauf zuzugreifen. Unten sehen Sie die Schnittstelle OAuthTokenDao und eine Mock-Implementierung (In-Memory) OAuthTokenDaoMemoryImpl, die in diesem Beispiel verwendet werden:
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); } }
Auch die OAuth 2.0-Anmeldedaten für die Anwendung werden in einer Properties-Datei gespeichert. Alternativ können Sie sie auch einfach irgendwo in einer Ihrer Java-Klassen als Konstante angeben. Hier sind die Klasse OAuthProperties und die Datei oauth.properties, die im Beispiel verwendet werden:
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 { } }
Unten sehen Sie die Datei oauth.properties mit den OAuth 2.0-Anmeldedaten Ihrer Anwendung. Sie müssen die Werte unten durch Ihre eigenen ersetzen.
# 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
Die OAuth 2.0-Client-ID und der Clientschlüssel identifizieren Ihre Anwendung und ermöglichen es der Tasks API, Filter und Kontingentregeln anzuwenden, die für Ihre Anwendung definiert sind. Die Client-ID und das Secret finden Sie in der Google APIs Console. In der Konsole müssen Sie Folgendes tun:
- Erstellen Sie ein neues Projekt oder wählen Sie ein vorhandenes Projekt aus.
- Aktivieren Sie die Tasks API, indem Sie in der Liste der Dienste den Status der Tasks API auf AKTIVIERT setzen.
- Erstellen Sie unter API-Zugriff eine OAuth 2.0-Client-ID, falls noch keine vorhanden ist.
- Die URL des OAuth 2.0-Code-Callback-Handlers des Projekts muss in den Weiterleitungs-URIs registriert bzw. auf die Zulassungsliste gesetzt sein. In diesem Beispielprojekt müssten Sie beispielsweise https://www.beispiel.de/oauth2callback registrieren, wenn Ihre Webanwendung über die Domain https://www.beispiel.de bereitgestellt wird.

Auf den Autorisierungscode vom Google-Autorisierungsendpunkt warten
Wenn der Nutzer die Anwendung noch nicht zum Zugriff auf seine Aufgaben autorisiert hat und daher zum OAuth 2.0-Autorisierungsendpunkt von Google weitergeleitet wurde, wird ihm ein Autorisierungsdialogfeld von Google angezeigt, in dem er aufgefordert wird, Ihrer Anwendung Zugriff auf seine Aufgaben zu gewähren:

Nachdem der Zugriff gewährt oder abgelehnt wurde, wird der Nutzer zum OAuth 2.0-Code-Callback-Handler zurückgeleitet, der beim Erstellen der Google-Autorisierungs-URL als Weiterleitung/Callback angegeben wurde:
new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(), OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties .getScopesAsString()).build()
Der OAuth 2.0-Code-Callback-Handler OAuthCodeCallbackHandlerServlet verarbeitet die Weiterleitung vom Google OAuth 2.0-Endpunkt. Es gibt zwei Fälle:
- Der Nutzer hat Zugriff gewährt: Die Anfrage wird analysiert, um den OAuth 2.0-Code aus den URL-Parametern abzurufen.
- Der Nutzer hat den Zugriff verweigert: Dem Nutzer wird eine Nachricht angezeigt.
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; } }
Autorisierungscode gegen Aktualisierungs- und Zugriffstoken eintauschen
Anschließend tauscht das OAuthCodeCallbackHandlerServlet den Auth 2.0-Code gegen ein Aktualisierungs- und Zugriffstoken aus, speichert es im Datenspeicher und leitet den Nutzer zurück zur URL PrintTaskListsTitlesServlet:
Der der Datei unten hinzugefügte Code ist syntaxfarbig hervorgehoben, der bereits vorhandene Code ist ausgegraut.
/** Die URL, zu der der Nutzer nach der Verarbeitung des Callbacks weitergeleitet werden soll. Du kannst diese Informationen in einem Cookie speichern, bevor du Nutzer zur Autorisierungs-URL von Google weiterleitest, wenn du mehrere URLs hast, zu denen Nutzer weitergeleitet werden können. */ public static final String REDIRECT_URL = "/"; /** Die OAuth-Token-DAO-Implementierung. Sie können sie einschleusen, anstatt eine statische Initialisierung zu verwenden. Außerdem verwenden wir eine einfache Speicherimplementierung als Mock. Ändern Sie die Implementierung so, dass Ihr Datenbanksystem verwendet wird. */ public static OAuthTokenDao oauthTokenDao = new OAuthTokenDaoMemoryImpl(); 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";// 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 you should replace this to // your own user/login implementation UserService userService = UserServiceFactory.getUserService(); String email = userService.getCurrentUser().getEmail(); // Save the tokens oauthTokenDao.saveKeys(accessTokenResponse, email); resp.sendRedirect(REDIRECT_URL); } 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; }/** * Tauschen Sie den angegebenen Code gegen ein Exchange- und ein Aktualisierungstoken aus. * * @param code Der vom Autorisierungsdienst zurückgegebene Code * @param currentUrl Die URL des Callbacks * @param oauthProperties Das Objekt mit der OAuth-Konfiguration * @return Das Objekt mit einem Zugriffs- und einem Aktualisierungstoken * @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(); } } /** * 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; }Datei „OAuthCodeCallbackHandlerServlet.java“Hinweis: In der obigen Implementierung werden einige App Engine-Bibliotheken verwendet, um die Sache zu vereinfachen. Wenn Sie für eine andere Plattform entwickeln, können Sie die Schnittstelle UserService, die für die Nutzerauthentifizierung zuständig ist, neu implementieren.
Aufgaben des Nutzers lesen und anzeigen
Der Nutzer hat der Anwendung Zugriff auf seine Aufgaben gewährt. Die Anwendung hat ein Aktualisierungstoken, das im Datenspeicher gespeichert wird, auf den über den OAuthTokenDao zugegriffen werden kann. Das Servlet PrintTaskListsTitlesServlet kann jetzt diese Tokens verwenden, um auf die Aufgaben des Nutzers zuzugreifen und sie anzuzeigen:
Der der Datei unten hinzugefügte Code ist syntaxfarbig hervorgehoben, der bereits vorhandene Code ist ausgegraut.
// Titel der Aufgabenlisten des Nutzers in der Antwort ausgeben resp.setContentType("text/plain"); resp.getWriter().append("Titel der Aufgabenlisten für Nutzer " + user.getEmail() + ":\n\n"); printTasksTitles(accessTokenResponse, resp.getWriter()); 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; }/** * Ruft mit der Google Tasks API eine Liste der Aufgaben der Nutzer in der Standardaufgabenliste ab. * * @param accessTokenResponse Das OAuth 2.0-AccessTokenResponse-Objekt mit dem Zugriffs- und dem Aktualisierungstoken. * @param output der Outputstream-Writer, in den die Titel der Aufgabenlisten geschrieben werden sollen * @return Eine Liste der Aufgabentitel der Nutzer in der Standardaufgabenliste. * @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"); } } } } /** * 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; }Datei „PrintTasksTitlesServlet.java“Dem Nutzer werden seine Aufgaben angezeigt:
Die Aufgaben der NutzerBeispielanwendung
Den Code für diese Beispielanwendung können Sie hier herunterladen. Sie können sich das gerne ansehen.