Spring Boot: SSO mit OAuth2 via GitHub oder Facebook

Nachdem wir in den letzten Artikeln zu OAuth2 umfangreiche Grundlagen geschaffen haben, soll es hier darum gehen, wie in einer eigenen Spring-Boot-Anwendung das Login via OAuth2 Single-Sign-On über einen fremden Authorization Server geregelt werden kann.

Dazu werden wir eine kleine CRM-artige Webanwendung mit Spring Boot entwickeln, bei der man Kundenobjekte ansehen und hinzufügen kann. Das Ganze soll allerdings erst nach Anmeldung möglich sein. Diese Anmeldung soll entweder über den GitHub-Account oder über einen Facebook-Account geschehen können.

Da sich das Ganze als nicht ganz trivial herausgestellt hat, gibt es dieses Mal den gesamten Source-Code weitestgehend ungekürzt im Artikel zum Kopieren und Einfügen.

Abhängigkeiten

Dieses Mal haben wir aufgrund der etwas höheren Komplexität einige Dependencies mehr als sonst. Hier die Liste im Maven-XML-Format:

<!-- Javascript Dependencies -->
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>2.1.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.2.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>
	<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>js-cookie</artifactId>
	<version>2.1.0</version>
</dependency>

<!-- Spring Boot -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Security -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth.boot</groupId>
	<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

<!-- JAXB Dependencies -->
<dependency>
	<groupId>javax.xml.bind</groupId>
	<artifactId>jaxb-api</artifactId>
	<version>2.2.11</version>
</dependency>
<dependency>
	<groupId>com.sun.xml.bind</groupId>
	<artifactId>jaxb-core</artifactId>
	<version>2.2.11</version>
</dependency>
<dependency>
	<groupId>com.sun.xml.bind</groupId>
	<artifactId>jaxb-impl</artifactId>
	<version>2.2.11</version>
</dependency>
<dependency>
	<groupId>javax.activation</groupId>
	<artifactId>activation</artifactId>
	<version>1.1.1</version>
</dependency>

Zunächst die JavaScript Dependencies. Da wären JQuery, Bootstrap und JS-Cookie. Wofür diese benötigt werden, kann weiter unten im Kapitel “Das Front-End” nachgelesen werden. Anschließend folgen die Dependencies von Spring Boot und Spring Security. Der Einfachheit halber haben wir hier die Starter-Dependencies für Spring Boot und Spring Security verwendet. Außerdem wird eine Dependency für die Verwendung von OAuth2 mit Spring Security verwendet.

Zum Schluss folgen noch einige Abhängigkeiten für JAXB, da dieses ja seit Java 9 nicht mehr im JDK enthalten ist. Da die hier verwendete Version von Spring Boot davon allerdings noch nichts zu wissen schien, liefern wir es manuell nach.

Damit hätten wir alle Dependencies kurz besprochen und können mit der Basisanwendung anfangen.

Die Basisanwendung

Beginnen wir mit der Entwicklung einer einfachen Spring-Boot-App für unsere Webanwendung. Zunächst das Customer-Objekt. Dieses soll die zwei Parameter id und companyName haben, sowie die entsprechenden Getter und Setter und equals/hashCode-Methoden. In etwa so:

public class Customer {
	private long id;
	private String companyName;

	public Customer() {	}
	
	public Customer(long id, String companyName) {
		this.id = id;
		this.companyName = companyName;
	}

	public long getId() {return id;}

	public void setId(long id) {this.id = id;}

	public String getCompanyName() {return companyName;}

	public void setCompanyName(String companyName) {
	   this.companyName = companyName;}

	public int hashCode() {...}

	public boolean equals(Object obj) {...}
}

Außerdem ein einfaches CustomerDao-Objekt, welches die Customer-Objekte in einem nicht-persistenten Speicher (eine lokale Liste) ablegt:

@Component
public class CustomerDao {
	private final List<Customer> customers = new ArrayList<Customer>();

	public List<Customer> getCustomers() {
		return customers;
	}

	public void addCustomer(Customer customer) {
		if (!customers.contains(customer)) {
			customers.add(customer);
		}
	}

	public Customer getCustomerById(final long id) {
		for (Customer c : customers) {
			if (c.getId() == id) {
				return c;
			}
		}
		return null;
	}

	@PostConstruct
	public void init() {
		customers.add(new Customer(0, "Google"));
		customers.add(new Customer(1, "Apple"));
	}
}

Die Customer-Objekte können also ausgelesen (getCustomer()-Methode) und nach ID durchsucht werden (getCustomerById()-Methode). Neue Customer-Objekte können mit addCustomer() hinzugefügt werden. Initial gibt es die beiden Customer “Apple” und “Google” mit den CustomerIDs 0 und 1.

In Spring Boot werden sogenannte Controller-Objekte verwendet, um REST-artige Web-Endpunkte anzubieten. Dazu wird die Controller-Klasse mit @Controller annotiert. Für unsere Anwendung erstellen wir einen POST-Endpunkt, um ein neues Customer-Objekt hinzuzufügen (addCustomer()-Methode). Außerdem erstellen wir zwei GET-Endpunkte, um alle Customer (listCustomers()-Methode) oder nur den Customer korrespondierend zu einer bestimmten CustomerID (getCustomer()-Methode) zurückzugeben. Der Controller:

@Controller
public class CustomerController {
	@Autowired
	CustomerDao customerDao;

	@RequestMapping(value = "/customers", method = RequestMethod.GET)
	public ResponseEntity<List<Customer>> listCustomers() {
		return new ResponseEntity<List<Customer>>(customerDao.getCustomers(), HttpStatus.OK);
	}

	@RequestMapping(value = "/customer", method = RequestMethod.GET)
	public ResponseEntity<Customer> getCustomer(@RequestParam("customerId") Long customerId) {
		return new ResponseEntity<Customer>(customerDao.getCustomerById(customerId), HttpStatus.OK);
	}

	@RequestMapping(value = "/customer", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	public ResponseEntity<String> addCustomer(Customer customer) {
		customerDao.addCustomer(customer);
		return new ResponseEntity<String>(HttpStatus.OK);
	}
}

So viel zu unserer Basis-Anwendung. All diese Funktionalität soll nur für eingeloggte Benutzer verfügbar sein. Das gehen wir als nächstes an. Außerdem soll der Login über die Authorization-Server von GitHub oder Facebook durchgeführt werden.

Die Hauptanwendung

Beginnen wir mit der Hauptklasse der Anwendung, die den Haupteinstiegspunkt des Java-Programms enthält:

@EnableOAuth2Client
@EnableAuthorizationServer
@SpringBootApplication
@RestController
public class App extends WebSecurityConfigurerAdapter {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/**").
		authorizeRequests().
		antMatchers("/", "/login**", "/webjars/**", "/error**")
		.permitAll()
		.anyRequest()
		.authenticated()
		.and().exceptionHandling()
		.authenticationEntryPoint(
			new LoginUrlAuthenticationEntryPoint("/"))
		.and().logout()
		.logoutSuccessUrl("/")
		.permitAll()
		.and().csrf()
		.csrfTokenRepository(
			CookieCsrfTokenRepository.withHttpOnlyFalse())
		.and().addFilterBefore(
			ssoFilter(), BasicAuthenticationFilter.class);
	}

	private Filter ssoFilter() { ... }
}

Zunächst die Annotationen. @EnableOAuth2Client aktiviert die grundlegende Funktionalität für einen OAuth2 Client mit Spring Security. @EnableAuthorizationServer aktiviert den Authorization-Endpunkt und den Token-Endpunkt für den Authorization Server in Spring Security. Die beiden Annotationen @SpringBootApplication und @RestController sind nicht spezifisch für die Security und annotieren die Klasse nur als Hauptklasse und als Klasse mit integrierten REST-Funktionalitäten, damit die @RequestMappings im nächsten Schritt funktionieren.

Nun zur komplizierteren configure()-Methode. Zunächst stellen wir eine allgemeine Regel für das URL-Pattern “/**” ein (Zeile 13). Für alle URLs wird zunächst einmal via authorizeRequests() die Möglichkeit der Autorisierung aktiviert (Zeile 14). Als nächstes wird der Zugriff auf die URLs “/”, “/login”, “/webjars/” und “/error” konfiguriert, damit später überhaupt eine Loginseite angezeigt werden kann. Dies geschieht über die permitAll()-Methode (Zeile 16). Außerdem wird der Zugriff für alle angemeldeten Benutzer erlaubt (Zeilen 17 und 18). Für die nicht angemeldeten Benutzer wird ein exceptionHandling() konfiguriert, welches auf die Hauptseite (“/”) mit dem Login weiterleitet (bis Zeile 21). Anschließend wird die Logout-URL als die gleiche Hauptseite unter “/” konfiguriert (bis Zeile 24). Zum Schluss wird noch das Cross-Site-Request-Forgery (CSRF)-Token aktiviert (Zeile 25-27) und der SSO-Filter für unsere Single-Sign-On-Provider gesetzt (Zeilen 28 und 29). Die ssoFilter()-Methode definieren wir später im Kapitel “Definition der SSO-Filter”.

OAuth2-Konfiguration in der application.yml

Für die Definition des SSO-Filters müssen zunächst die Authorization-Server der verknüpften Identity-Provider (Facebook und GitHub), sowie die grundlegende OAuth2-Konfiguration in der application.yml eingetragen werden:

facebook:
  client:
    clientId: client-id-of-your-oauth2-facebook-app
    clientSecret: client-secret-of-your-oauth2-facebook-app
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me
    
github:
  client:
    clientId: client-id-of-your-oauth2-github-app
    clientSecret: client-secret-of-your-oauth2-github-app
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user
    
security:
  oauth2:
    client:
      client-id: oauth2-testapp
      client-secret: oauth2-testsecret
      scope: read,write
      auto-approve-scopes: '.*'

Für einen erfolgreichen Anmeldeversuch bei einem fremden Authorization Server wird eine OAuth2-App bei dem entsprechenden Anbieter benötigt. Wie man diese erstellen kann, ist in einem vorherigen Blogartikel exemplarisch für GitHub erklärt. Der Prozess bei Facebook hat allerdings ganz ähnlich funktioniert.

In den oben gelisteten OAuth2-Konfigurationen müssen nun für jeden SSO-Provider, in unserem Fall also GitHub und Facebook, einige Daten hinterlegt werden. Dazu gehören Client-ID und ClientSecret der OAuth2-App. Außerdem werden die bereits bekannten OAuth2-URLs für den Erhalt des Authorization Codes und den Umtausch desselben in ein Access Token benötigt. Wenn das Access-Token im zu empfangenden JSON-Objekt (siehe OAuth2-Setup-Beispiel) nicht “token” oder “access_token” heißt, muss dies hier spezifiziert werden. So heißt dass Access Token beispielsweise bei Facebook oauth_token, daher muss in Zeile 7 “tokenName: oauth_token” spezifiziert werden. Dann muss noch die User-Info-Uri angegeben werden. Hier können später Benutzerdaten, wie etwa der Benutzername des angemeldeten Benutzers, abgerufen werden. Bei Facebook heißt diese …/me, während sie bei GitHub …/user heißt. Diese Schemas sind beide valide und üblich.

Der letzte Teil definiert die eigene OAuth2-Schnittstelle der Spring-Boot-Anwendung. Dazu wird unter client die OAuth2-App mit der Client-ID oauth2-testapp und dem Client-Secret oauth2-testsecret definiert. Sie hat als Permission-Scope read und write für alle Scopes (.*).

Verknüpfung mit der Hauptanwendung

Nun muss die Konfiguration noch mit der Hauptanwendung verknüpft werden. Dazu werden einige Methoden in die Hauptklasse eingefügt. Zunächst einmal die innere Klasse ClientResources:

class ClientResources {

	@NestedConfigurationProperty
	private AuthorizationCodeResourceDetails client = 
		new AuthorizationCodeResourceDetails();

	@NestedConfigurationProperty
	private ResourceServerProperties resource = 
		new ResourceServerProperties();

	public AuthorizationCodeResourceDetails getClient() {
		return client;
	}

	public ResourceServerProperties getResource() {
		return resource;
	}
}

Die Klasse ClientResources soll per @Bean jeweils einmal für GitHub und für Facebook befüllt werden. Sie wrappt einfach nur ein leeres AuthorizationCodeResourceDetails-Objekt und ein leeres ResourceServerProperties-Objekt. Hier die beiden dazugehörigen @Beans:

@Bean
@ConfigurationProperties("github")
public ClientResources github() {
	return new ClientResources();
}

@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
	return new ClientResources();
}

Als nächstes erfolgt die Definition des SSOFilter.

Definition der SSO-Filter

Die beiden ClientResources-Beans von oben werden für die im Kapitel “Die Hauptanwendung” ausgelassene ssoFilter()-Methode benötigt, welche für jede der beiden Beans einen SSO-Filter erstellt. Das folgende Snippet wird in die Hauptklasse eingefügt:

@Autowired
OAuth2ClientContext oauth2ClientContext;

private Filter ssoFilter() {
	CompositeFilter filter = new CompositeFilter();
	List<Filter> filters = new ArrayList<>();
	filters.add(ssoFilter(facebook(), "/login/facebook"));
	filters.add(ssoFilter(github(), "/login/github"));
	filter.setFilters(filters);
	return filter;
}

private Filter ssoFilter(ClientResources client, String path) {
	OAuth2ClientAuthenticationProcessingFilter filter = 
		new OAuth2ClientAuthenticationProcessingFilter(path);
	OAuth2RestTemplate template = 
		new OAuth2RestTemplate(client.getClient(), 
		oauth2ClientContext);
	filter.setRestTemplate(template);
	UserInfoTokenServices tokenServices = 
		new UserInfoTokenServices(client.getResource().
		getUserInfoUri(), client.getClient().getClientId());
	tokenServices.setRestTemplate(template);
	filter.setTokenServices(tokenServices);
	return filter;
}

In Zeile 5-9 wird ein neues CompositeFilter-Objekt erstellt, welches die beiden anschließend erstellten Objekte für Facebook und GitHub kombiniert. Die private ssoFilter()-Methode erzeugt nun aus dem übergebenen ClientResources-Objekt einen OAuth2ClientAuthenticationProcessingFilter, indem es die notwendigen Daten aus dem ClientResources-Objekt auspackt.

Schließlich werden noch die beiden Request-Methoden für die Ressourcenabfrage (/me und /user) benötigt (Zeilen 13-23), sowie ein weiteres ResourceServerConfiguration-Objekt, welches den Zugriff auf /me erlaubt (Zeile 8-9). Diese Methoden werden später vom Front-End aufgerufen und sollen Daten zum Benutzer, welche vorher von GitHub oder Facebook geladen wurden, zurückgeben. Der Inhalt der Methoden ist denkbar einfach:

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends 
	ResourceServerConfigurerAdapter {

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/me").authorizeRequests()
			.anyRequest().authenticated();
	}
}

@RequestMapping({ "/me" })
public Map<String, String> users(Principal principal) {
	Map<String, String> map = new LinkedHashMap<>();
	map.put("name", principal.getName());
	return map;
}

@RequestMapping({ "/user" })
public Principal user(Principal principal) {
	return principal;
}

Die gesamte Hauptklasse noch einmal im Überblick und zum Rauskopieren:

@EnableOAuth2Client
@SpringBootApplication
@EnableAuthorizationServer
@RestController
public class App extends WebSecurityConfigurerAdapter {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

	@Autowired
	OAuth2ClientContext oauth2ClientContext;

	@Configuration
	@EnableResourceServer
	protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
		@Override
		public void configure(HttpSecurity http) throws Exception {
			http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
		}
	}

	@RequestMapping({ "/me" })
	public Map<String, String> users(Principal principal) {
		Map<String, String> map = new LinkedHashMap<>();
		map.put("name", principal.getName());
		return map;
	}

	@RequestMapping({ "/user" })
	public Principal user(Principal principal) {
		return principal;
	}

	@Bean
	@ConfigurationProperties("github")
	public ClientResources github() {
		return new ClientResources();
	}

	@Bean
	@ConfigurationProperties("facebook")
	public ClientResources facebook() {
		return new ClientResources();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**", "/error**").permitAll().anyRequest().authenticated().and().exceptionHandling()
				.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout().logoutSuccessUrl("/").permitAll().and().csrf()
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
	}

	private Filter ssoFilter() {
		CompositeFilter filter = new CompositeFilter();
		List<Filter> filters = new ArrayList<>();
		filters.add(ssoFilter(facebook(), "/login/facebook"));
		filters.add(ssoFilter(github(), "/login/github"));
		filter.setFilters(filters);
		return filter;
	}

	private Filter ssoFilter(ClientResources client, String path) {
		OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
		OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
		filter.setRestTemplate(template);
		UserInfoTokenServices tokenServices = new UserInfoTokenServices(client.getResource().getUserInfoUri(), client.getClient().getClientId());
		tokenServices.setRestTemplate(template);
		filter.setTokenServices(tokenServices);
		return filter;
	}

	class ClientResources {

		@NestedConfigurationProperty
		private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

		@NestedConfigurationProperty
		private ResourceServerProperties resource = new ResourceServerProperties();

		public AuthorizationCodeResourceDetails getClient() {
			return client;
		}

		public ResourceServerProperties getResource() {
			return resource;
		}
	}
}

Das Front-End

Da es in diesem Artikel in erster Linie um OAuth2 und das Backend geht, sind Front-End und die dazugehörigen Erläuterungen einfach gehalten. Zunächst der Head der index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Auth2 SpringBoot Example</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width" />
<base href="/" />
<link rel="stylesheet" type="text/css"
	href="/webjars/bootstrap/css/bootstrap.min.css" />
<a href="/webjars/jquery/jquery.min.js">/webjars/jquery/jquery.min.js</a>
<a href="/webjars/bootstrap/js/bootstrap.min.js">/webjars/bootstrap/js/bootstrap.min.js</a>
<a href="/webjars/js-cookie/js.cookie.js">/webjars/js-cookie/js.cookie.js</a>
</head>

Im Head definieren wir den Titel der Seite, sowie einige Metainformationen. Außerdem lassen wir Skripte für Bootstrap, JQuery und JSCookie laden.

Als Nächstes beginnen wir mit dem Body.

<body>
	<h1 style="padding-left: 10px;">OAuth2 SpringBoot Example</h1>
	<div class="container"></div>

	<div style="padding-left: 20px">
		<h2>Social Login</h2>
		<div class="container unauthenticated">
			<div>
				Mit Facebook: <a href="/login/facebook">Hier Klicken</a>
			</div>

			<div>
				Mit GitHub: <a href="/login/github">Hier Klicken</a>
			</div>
		</div>
		<div class="container authenticated">
			<div class="container">
				Eingeloggt als: <span id="user"></span>
				<button class="btn btn-primary">Logout</button>
			</div>
			<br />
			<div class="container">
				<div>
					<a href="/customers">Alle Kunden zeigen</a>
				</div>
				<div>
					<a href="/customer?customerId=0">Kunde mit ID 0 zeigen</a>
				</div>

				<div>
					<a id="createCustomer" href="void(0);">Facebook-Testkunde erstellen</a>
				</div>
			</div>
		</div>
	</div>

Es fällt auf, dass den Divs unterschiedliche Klassen angehängt sind. Die Klasse “authenticated” markiert Divs, welche nur angemeldeten Benutzern angezeigt werden. Divs mit “unauthenticated” werden dagegen nur nicht angemeldeten Benutzern angezeigt. So ist beispielsweise der Container mit den Anmeldelinks nur für unangemeldete Benutzer sichtbar, während der Container mit dem Logout-Knopf und den Links für “Alle Kunden anzeigen“, etc. nur für angemeldete Benutzer sichtbar wird.

Nun schließen wir den Body mit dem folgenden Javascript ab:

		$.ajaxSetup({
			beforeSend : function(xhr, settings) {
				if (settings.type == 'POST' || settings.type == 'PUT'
						|| settings.type == 'DELETE') {
					if (!(/^http:.*/.test(settings.url) || /^https:.*/
							.test(settings.url))) {
						// Only send the token to relative URLs i.e. locally.
						xhr.setRequestHeader("X-XSRF-TOKEN", Cookies
								.get('XSRF-TOKEN'));
					}
				}
			}
		});

		$.get("/user", function(data) {
			if (data.userAuthentication != null) {
				$("#user").html(data.userAuthentication.details.name);
				$(".unauthenticated").hide()
				$(".authenticated").show()
			}
		});

		var logout = function() {
			$.post("/logout", function() {
				$("#user").html('');
				$(".unauthenticated").show();
				$(".authenticated").hide();
			})
			return true;
		};

		var customer = function() {
			$.ajax({
						type : "POST",
						contentType : "application/x-www-form-urlencoded; charset=utf-8",
						url : "/customer?id=3&companyName=Facebook",
						data : "",
						dataType : "x-www-form-urlencoded"
					});
			$("#createCustomer").hide();
		};

Die oberste Methode beforeSend ist teil der JQuery-API und spezifiert einen Default-Call für alle Ajax-Calls. In diesem Fall sollen alle Calls mit einem XSRF-Token ausgestattet werden, um Cross-Site-Request-Forgery zu unterbinden.

Anschließend wird der oben in Spring Boot definierte /user-Endpunkt aufgerufen. Sofern ein sinnvolles Resultat zurückkommt, wird der Name des Benutzers in das HTML-Element mit der id “user” eingefügt und die Container mit der Annotation “authenticated” und “unauthenticated” angezeigt bzw. ausgeblendet.

Als Nächstes wird die logout-Methode definiert. Sie wird vom Logout-Button (siehe HTML oben) aufgerufen und schickt eine POST-Anfrage an den /logout-Endpunkt des Spring-Boot-Backends. Anschließend werden wie nach dem Aufruf des /user-Endpunktes oben die entsprechenden Container aktualisiert.

Der letzte Script-Block enthält die clientseitige Logik, die nach einem Klick auf den “Facebook-Testkunde-erstellen”-Link eine POST-Anfrage ans Backend schickt, um den Kunden zu erstellen. Außerdem wird anschließend der Link ausgeblendet.

Die fertige Seite sieht zunächst wie folgt aus:

Loginseite der Beispielanwendung

Das Feld für den Namen des Benutzers sowie die Links zum Anschauen und Hinzufügen von Kunden sind wie erwartet zunächst ausgeblendet. Nach einem Klick auf “Hier Klicken” neben GitHub erscheint, sofern der Benutzer noch nicht auf GitHub angemeldet ist, die folgende Abfrage:

Anmelde-Prompt für GitHub

Nachdem der Benutzer hier seine Daten eingegeben und auf Sign in gedrückt hat, wird er mit seinem Authorization-Code automatisch an unseren Server weitergeleitet. Der Authorization Code wird dann vom OAuth2-Modul in Spring Security automatisch im Hintergrund entgegengenommen und in ein Access-Token umgetauscht. Schließlich landet der Benutzer auf der folgenden Seite:

Seite nach erfolgreicher Anmeldung

Wie in der Abbildung erkennbar, wurde der Klarname des Benutzers (im Beispiel “Steffen Jacobs”) von der GitHub-API abgefragt und hier angezeigt. Außerdem werden die drei Optionen zum Anzeigen und Erstellen der Kunden für unsere Basisanwendung angezeigt. Die Login-Links wurden ausgeblendet.

Ein Klick auf “Alle Kunden zeigen” leitet auf /customers um, wo dann die aktuell existierenden Kunden angezeigt werden:

/customers Endpunkt nach Klick auf “Alle Kunden anzeigen”

Mit einem Klick auf “Logout” kann sich der Benutzer wieder aus der Testanwendung ausloggen. Seine Login-Session beim Authorization Provider, etwa GitHub, bleibt jedoch bestehen. Nach einem erneuten Klick auf den Anmeldelink wird der Benutzer nicht erneut nach Passwort und Benutzername gefragt, sondern wird gleich an die Hauptseite zurückgeleitet und angemeldet.

Short URL for this post: https://wp.me/p4nxik-3gy
Steffen Jacobs

About Steffen Jacobs

Java Consultant & Developer at Orientation in Objects GmbH. Follow me on Twitter and find me on LinkedIn and Xing. Some of the source code associated with the blog articles can be found on GitHub.
This entry was posted in Security, Web as a Platform and tagged , , , , , , . Bookmark the permalink.

Leave a Reply