Build-Autorisierungs-Callback-Handler

In diesem Dokument wird erläutert, wie ein OAuth 2.0-Rückruf-Handler für die Autorisierung mithilfe von Java-Servlets über eine Beispiel-Webanwendung implementiert wird, in der die Aufgaben des Nutzers über die Google Tasks API angezeigt werden. Die Beispielanwendung fordert zuerst die Autorisierung für den Zugriff auf die Google Tasks-Aufgaben des Nutzers an. Die Aufgaben des Nutzers werden dann in der Standardaufgabenliste angezeigt.

Zielgruppe

Dieses Dokument richtet sich an Personen, die mit der Architektur von Java- und J2EE-Webanwendungen vertraut sind. Es werden grundlegende Kenntnisse über den OAuth 2.0-Autorisierungsablauf empfohlen.

Inhalt

Um eine solche voll funktionsfähige Stichprobe zu erhalten, sind mehrere Schritte erforderlich:

Servlet-Zuordnungen in der Datei web.xml deklarieren

Wir verwenden in unserer Anwendung 2 Servlets:

  • PrintTasksTitlesServlet (zu / zugeordnet): Der Einstiegspunkt der Anwendung, die die Nutzerauthentifizierung übernimmt und die Aufgaben des Nutzers anzeigt.
  • OAuthCodeCallbackHandlerServlet (zugeordnet zu /oauth2callback): Der OAuth 2.0-Callback, der die Antwort vom OAuth-Autorisierungsendpunkt verarbeitet.

Nachfolgend sehen Sie die Datei web.xml, die diese beiden Servlets URLs in unserer Anwendung zuordnet:

<?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>
Datei /WEB-INF/web.xml

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:

  • Überprü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 wird vom OAuthTokenDao unten verarbeitet. Wenn für den Nutzer kein Aktualisierungstoken im Speicher vorhanden ist, hat er der Anwendung noch keine Autorisierung für den Zugriff auf die Aufgaben erteilt. In diesem Fall wird der Nutzer zum OAuth 2.0-Autorisierungsendpunkt von Google weitergeleitet.
So implementieren Sie dies:

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;
  }
}
Datei PrintTasksTitlesServlet.java

Hinweis: Die obige Implementierung verwendet einige App Engine-Bibliotheken, die der Vereinfachung dienen. Wenn Sie für eine andere Plattform entwickeln, können Sie die Schnittstelle UserService, die die Nutzerauthentifizierung übernimmt, neu implementieren.

Die Anwendung verwendet einen DAO, um die Autorisierungstokens des Nutzers zu speichern und darauf zuzugreifen. Unten sehen Sie die Schnittstelle OAuthTokenDao und die fiktive (In-Memory)-Implementierung 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);
}
Datei OAuthTokenDao.java
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 tokenPersistance = new HashMap();

  public void saveKeys(AccessTokenResponse tokens, String userName) {
    tokenPersistance.put(userName, tokens);
  }

  public AccessTokenResponse getKeys(String userName) {
    return tokenPersistance.get(userName);
  }
}
Datei OAuthTokenDaoMemoryImpl.java

Außerdem werden die OAuth 2.0-Anmeldedaten für die Anwendung in einer Eigenschaftendatei gespeichert. Alternativ können Sie sie einfach als Konstante in einer Ihrer Java-Klassen verwenden. Im Beispiel werden jedoch die Klasse OAuthProperties und die Datei oauth.properties verwendet:

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 {
  }
}
Datei OAuthProperties.java

Die Datei oauth.properties enthält die OAuth 2.0-Anmeldedaten Ihrer Anwendung. Sie müssen die Werte unten selbst ändern.

# 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
Datei oauth.properties

Die OAuth 2.0-Client-ID und der Clientschlüssel identifizieren Ihre Anwendung und ermöglichen der Tasks API das Anwenden von Filtern und Kontingentregeln, die für Ihre Anwendung definiert sind. Die Client-ID und das Secret finden Sie in der Google APIs-Konsole. In der Konsole sind folgende Schritte erforderlich:

  • Erstellen Sie ein neues Projekt oder wählen Sie ein vorhandenes Projekt aus.
  • Aktivieren Sie die Tasks API, indem Sie den Status der Tasks API in der Liste der Dienste auf AN setzen.
  • Erstellen Sie unter API-Zugriff eine OAuth 2.0-Client-ID, falls noch keine vorhanden ist.
  • Achten Sie darauf, dass die URL des OAuth 2.0-Code-Callback-Handlers des Projekts unter Weiterleitungs-URIs registriert bzw. auf die Zulassungsliste gesetzt wird. In diesem Beispielprojekt müssten Sie beispielsweise https://www.example.com/oauth2callback registrieren, wenn Ihre Webanwendung von der Domain https://www.example.com bereitgestellt wird.

Weiterleitungs-URI in der APIs-Konsole
Weiterleitungs-URI in der APIs-Konsole

Warten Sie auf den Autorisierungscode vom Google-Autorisierungsendpunkt

Falls der Nutzer die Anwendung noch nicht für den Zugriff auf seine Aufgaben autorisiert hat und daher zum OAuth 2.0-Autorisierungsendpunkt von Google weitergeleitet wurde, wird dem Nutzer ein Autorisierungsdialogfeld von Google angezeigt, in dem der Nutzer aufgefordert wird, Ihrer Anwendung Zugriff auf die zugehörigen Aufgaben zu gewähren:

Dialogfeld für die Autorisierung von Google
Dialogfeld für die Autorisierung von Google

Nachdem der Nutzer den Zugriff gewährt oder verweigert hat, wird er zurück zum OAuth 2.0-Code-Callback-Handler weitergeleitet, der beim Erstellen der Google-Autorisierungs-URL als Weiterleitung/Callback angegeben wurde:

new GoogleAuthorizationRequestUrl(oauthProperties.getClientId(),
      OAuthCodeCallbackHandlerServlet.getOAuthCodeCallbackHandlerUrl(req), oauthProperties
          .getScopesAsString()).build()

Der Callback-Handler für OAuth 2.0-Codes – OAuthCodeCallbackHandlerServlet – verarbeitet die Weiterleitung vom Google OAuth 2.0-Endpunkt. Es müssen zwei Fälle bearbeitet werden:

  • Der Nutzer hat Zugriff gewährt: parst die Anfrage, 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;
  }
}
Datei OAuthCodeCallbackHandlerServlet.java

Autorisierungscode gegen Aktualisierungs- und Zugriffstoken austauschen

Anschließend tauscht das OAuthCodeCallbackHandlerServlet den Auth 2.0-Code gegen eine Aktualisierung und ein Zugriffstoken aus, speichert ihn im Datenspeicher und leitet den Nutzer zurück zur URL PrintTaskListsTitlesServlet:

Der Code, der in die Datei unten eingefügt wird, ist in der Syntax hervorgehoben und der bereits vorhandene Code ist ausgegraut.

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";
/** Die URL, an die der Nutzer nach der Verarbeitung des Callbacks weitergeleitet wird. Wenn Sie mehrere mögliche URLs haben, sollten Sie dies in einem Cookie speichern, bevor Sie Nutzer zur Autorisierungs-URL von Google * weiterleiten. */ public static final String REDIRECT_URL = "/"; /** Die OAuth-Token-DAO-Implementierung. Fügen Sie sie gegebenenfalls ein, anstatt eine statische Initialisierung * zu verwenden. Außerdem wird eine einfache Speicherimplementierung * als Simulation verwendet. Ändern Sie die Implementierung so, dass Ihr Datenbanksystem verwendet wird. */ 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;
    }
// Erstelle eingehende Anfrage-URL String requestUrl = getOAuthCodeCallbackHandlerUrl(req); // Code für OAuth-Tokens austauschen AccessTokenResponse accessTokenResponse = ExchangeCodeForAccessAndRefreshTokens(code[0], // //E-Mail mit dem Nutzer zu aktualisieren, der Nutzer-/E-Mail-re-E-Mail-re-E-Mail-re-on-.
  /**
   * 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;
  }
oauth.
Datei OAuthCodeCallbackHandlerServlet.java

Hinweis: Die obige Implementierung verwendet einige App Engine-Bibliotheken, die der Vereinfachung dienen. Wenn Sie für eine andere Plattform entwickeln, können Sie die Schnittstelle UserService, die die Nutzerauthentifizierung übernimmt, neu implementieren.

Aufgaben des Nutzers lesen und anzeigen

Der Nutzer hat der Anwendung Zugriff auf ihre Aufgaben gewährt. Die Anwendung verfügt über ein Aktualisierungstoken, das im Datenspeicher gespeichert ist, auf den über OAuthTokenDao zugegriffen werden kann. Das Servlet PrintTaskListsTitlesServlet kann jetzt diese Tokens verwenden, um auf die Aufgaben des Nutzers zuzugreifen und sie anzuzeigen:

Der Code, der in die Datei unten eingefügt wird, ist in der Syntax hervorgehoben und der bereits vorhandene Code ist ausgegraut.

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;
    }
// Titel der Aufgabenliste des Nutzers in der Antwort ausgeben resp.setContentType("text/plain"); resp.getWriter().append("Aufgabenlistentitel für Nutzer " + 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;
  }
/** * Verwendet die Google Tasks-Liste der Standardnutzer, um eine Liste der Aufgaben der Standardnutzer abzurufen. * * * @param accessTokenResponse Das OAuth 2.0 AccessTokenResponse-Objekt * mit dem Zugriffstoken und einem Aktualisierungstoken * @param geben den Ausgabestream-Schreiber aus, wo die Titel der Aufgabenlisten wiederholt werden sollen * @return Eine Liste der Aufgabentitel der Nutzer in der Standardaufgabenliste. * @throws IOException */ public void printTasksTitles(AccessTokenResponse accessTokenResponse, Writer edition) wirft IOException { // Initializing the Tasks service HttpTransport transport = new NetHttpTransport() JsonFactory jsonFactory = new NetHttpFactory;"> OAuthProperties oauthResource.json.
Datei PrintTasksTitlesServlet.java

Der Nutzer wird mit seinen Aufgaben angezeigt:

Die Aufgaben der Nutzenden
Aufgaben des Nutzers

Beispielanwendung

Den Code für diese Beispielanwendung können Sie hier herunterladen. Schauen Sie mal rein.