[JAVA] Lassen Sie uns den Berechtigungscode-Grant-Flow mit Spring Security OAuth-Teil 2: Erstellen einer App vorerst erleben

Erstellen Sie dieses Mal im zweiten Teil von "Erleben Sie den Berechtigungscode-Erteilungsfluss mit Spring Security OAuth" vorerst eine Anwendung, die die REST-API mit dem Berechtigungscode-Erteilungsfluss mithilfe von Spring Security OAuth + Spring Boot authentifiziert und autorisiert. Ich werde es versuchen.

Funktionsanforderungen der Anwendung

Die funktionalen Anforderungen der diesmal erstellten Anwendung sind ...

Service Provider

Der Dienstanbieter stellt eine REST-API bereit, die Aufgabeninformationen (Titel, Details, Stichtag, Abschlussflag, Registrierungsdatum / -zeit, Aktualisierungsdatum / -zeit) für jeden Benutzer verwaltet (registriert / aktualisiert / löscht / referenziert) und auf die REST-API zugreift. Authentifiziert und autorisiert mithilfe des vom OAuth 2.0-Berechtigungscode gewährten Zugriffstokens.

Ich werde es zur Voraussetzung machen. Ursprünglich möchte ich eine Web-Benutzeroberfläche für die Verwaltung von Aufgabeninformationen auch auf der Seite des Dienstanbieters vorbereiten, aber ich werde die Erklärung weglassen, da die Web-Benutzeroberfläche auf der Seite des Dienstanbieters nicht im Fokus dieses Eintrags steht.

Resource Server

Erstellen Sie die folgende API als API zum Verwalten von Aufgabeninformationen des Ressourcenbesitzers.

API-Name API-Übersicht Geltungsbereich, um den Zugriff zu ermöglichen
GET /api/tasks API, um eine Liste mit Aufgabeninformationen abzurufen read
POST /api/tasks API zum Registrieren von Aufgabeninformationen write
GET /api/tasks/{id} API zum Abrufen von Aufgabeninformationen read
PUT /api/tasks/{id} API zum Aktualisieren von Aufgabeninformationen write
DELETE /api/tasks/{id} API zum Löschen von Aufgabeninformationen write

Authorization Server

Wir stellen die folgenden Endpunkte bereit (im Folgenden als "Autorisierungsendpunkte" bezeichnet), damit der Ressourcenbesitzer auf Aufgabeninformationen zugreifen kann. Dieser Endpunkt wird von Spring Security OAuth bereitgestellt und muss nicht vom Entwickler erstellt werden.

Endpunktname Endpunktübersicht Bedingungen für den Zugang
GET /oauth/authorize Endpunkt zum Anzeigen des Bildschirms zum Einholen der Genehmigung durch den Ressourcenbesitzer (im Folgenden als "Autorisierungsbildschirm" bezeichnet) Authentifizierter Ressourcenbesitzer
POST /oauth/authorize?user_oauth_approval Autorisierungsanweisung vom Ressourcenbesitzer(Zulassen / Verweigern)Genehmigungszuschuss erhalten(Autorisierungscode)Endpunkt für die Ausgabe Authentifizierter Ressourcenbesitzer

Erstellen Sie die folgenden Endpunkte (im Folgenden als "Token-Endpunkte" bezeichnet), damit der Client Zugriffstoken basierend auf der vom Ressourcenbesitzer erhaltenen Autorisierungsgewährung (Autorisierungscode) ausstellen kann. Dieser Endpunkt wird von Spring Security OAuth bereitgestellt und muss nicht vom Entwickler erstellt werden.

Endpunktname Endpunktübersicht Bedingungen für den Zugang
POST /oauth/token Autorisierter Zuschuss vom Ressourcenbesitzer(Autorisierungscode usw.)Endpunkt für die Ausgabe von Zugriffstoken basierend auf Authentifizierter Client

** Hinweis: Authentifizierung / Autorisierung für verschiedene Endpunkte **

In diesem Eintrag ... Wir authentifizieren den Ressourcenbesitzer und den Client bei diesen Endpunkten mithilfe der von Spring Security bereitgestellten Standardauthentifizierung.

Client(Web UI)

Der Client verwendet die vom Dienstanbieter bereitgestellte REST-API, um eine Web-Benutzeroberfläche (Bildschirm mit Aufgabenliste und Bildschirm mit Aufgabendetails) zum Verwalten von Aufgabeninformationen von Ressourcenbesitzern bereitzustellen.

Endpunktname Endpunktübersicht Bedingungen für den Zugang
GET /tasks Zeigen Sie die vom Ressourcenserver erfassten Aufgabeninformationen auf dem Bildschirm mit der Aufgabenliste an Authentifizierter Nutzer
POST /tasks Erstellen Sie Aufgabeninformationen auf dem Ressourcenserver Authentifizierter Nutzer
GET /tasks/{id} Zeigen Sie die vom Ressourcenserver erfassten Aufgabeninformationen auf dem Bildschirm mit den Aufgabendetails an Authentifizierter Nutzer
POST /tasks/{id}?update Aktualisieren Sie die vom Ressourcenserver verwalteten Aufgabeninformationen Authentifizierter Nutzer
POST /tasks/{id}?delete Löschen Sie die vom Ressourcenserver verwalteten Aufgabeninformationen Authentifizierter Nutzer

** Hinweis: Authentifizierung / Autorisierung der Task-Management-Benutzeroberfläche **

Der auf der Clientseite vorbereitete Taskverwaltungsbildschirm erfordert die Benutzerauthentifizierung der Anwendung auf der Clientseite. Die Benutzerauthentifizierung wird mithilfe der von Spring Security bereitgestellten Standardauthentifizierung durchgeführt.

Das Bildschirmbild und der Bildschirmübergang sind wie folgt.

spring-security-oauth-1st-app-client.png

Anwendungskonfiguration

Dieses Mal erstellen wir zwei Spring Boot-Anwendungen, "Client" und "Dienstanbieter (Autorisierungsserver + Ressourcenserver)", und erstellen eine Anwendung, die die API mit dem Berechtigungscode-Grant-Flow authentifiziert und autorisiert. Darüber hinaus ist die Verknüpfung des Zugriffstokens und der mit dem Zugriffstoken verbundenen Authentifizierungsinformationen zwischen dem Autorisierungsserver und dem Ressourcenserver eine speicherinterne Verknüpfung in der Webanwendung.

spring-security-oauth-1st-app.png

Warning:

Die in diesem Eintrag erstellte Anwendung wird über HTTP-Kommunikation ausgeführt, aber ... Im OAuth 2.0-Protokollfluss müssen ** Ressourcenbesitzer und Endpunkte, die die Clientauthentifizierung und Zugriffstoken verwalten, die HTTPS-Kommunikation verwenden. **.

Note:

Da die Rollen des Autorisierungsservers und des Ressourcenservers unterschiedlich sind, gibt es meines Erachtens viele Fälle, in denen sie als unabhängige Webanwendungen mit dem folgenden Gefühl erstellt werden. Diesmal wird jedoch die Einfachheit der Anwendungskonfiguration, des Autorisierungsservers und der Ressourcen priorisiert Ich entschied mich, den Server mit einer Spring Boot-Anwendung zu realisieren.

spring-security-oauth-std-app.png

Wenn Sie einen Autorisierungsserver und einen Ressourcenserver als separate Webanwendungen erstellen, müssen Sie unter anderem das Zugriffstoken und die mit dem Zugriffstoken verknüpften Authentifizierungsinformationen zwischen den Servern auswählen. Darüber hinaus planen wir die Einführung der Implementierungsmethode, wenn der Autorisierungsserver und der Ressourcenserver vom nächsten Mal getrennt werden.

Vom Betrieb verifizierte Version

Die in diesem Eintrag erstellte Anwendung wird anhand der folgenden Versionen der Bibliothek überprüft.

Entwicklungsprojekt erstellen

Lassen Sie uns nun tatsächlich eine Anwendung erstellen und die Authentifizierung / Autorisierung der REST-API anhand des Berechtigungscode-Grant-Flows erleben. Lassen Sie uns zunächst ein Entwicklungsprojekt für die Spring Boot-Anwendung erstellen. Hier ist ein Beispiel für das Erstellen eines Projekts über die Befehlszeile, aber auch wenn Sie es mit SPRING INITIALIZR Web UI oder der Funktion Ihrer IDE (natürlich) generieren. OK! !!

Da wir dieses Mal zwei Anwendungen erstellen, erstellen wir ein übergeordnetes Verzeichnis zum Speichern dieser Anwendungen.

$ mkdir spring-security-oauth-demo
$ cd spring-security-oauth-demo

Erstellen Sie nach dem Navigieren zu dem von Ihnen erstellten Verzeichnis ein Spring Boot-Anwendungsentwicklungsprojekt für den Dienstanbieter und den Client.

Erstellen Sie ein Entwicklungsprojekt für einen Dienstleister

$ curl -s https://start.spring.io/starter.tgz\
       -d name=service-provider\
       -d artifactId=service-provider\
       -d dependencies=web,jdbc,h2\
       -d baseDir=service-provider\
       | tar -xzvf -

Erstellen Sie ein Entwicklungsprojekt für den Client

$ curl -s https://start.spring.io/starter.tgz\
       -d name=client\
       -d artifactId=client\
       -d dependencies=thymeleaf\
       -d baseDir=client\
       | tar -xzvf -

Erstellen Sie ein Projekt zum Erstellen

Kopieren Sie die erforderlichen Dateien, z. B. den Maven-Wrapper, von Ihrem Dienstanbieter- oder Client-Entwicklungsprojekt.

$ cp -r client/.mvn .mvn
$ cp client/mvnw* .
$ cp client/.gitignore .
$ cp client/pom.xml .

Ändern Sie die kopierte pom.xml in die Einstellungen für den Build. Hier werden Dienstanbieter und Client als Submodule verwaltet. Auf diese Weise können Sie Maven zusammen mit dem Dienstanbieter und dem Client erstellen.

$ vi pom.xml

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>com.example</groupId>
        <artifactId>spring-security-oauth-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>pom</packaging>

        <name>spring-seucirty-oauth-demo</name>
        <description>Spring Security OAuth Demo project for Spring Boot</description>

        <modules>
                <module>service-provider</module>
                <module>client</module>
        </modules>

</project>

Abhängige Bibliotheken zum Dienstanbieter hinzugefügt

Fügen Sie dem Dienstanbieter "Spring Security OAuth", "Jackson Extension Module for JSR 310" und "JTS Topology Suite" hinzu. ("JTS Topology Suite" steht in direktem Zusammenhang mit diesem Eintrag, aber ich habe ihn hinzugefügt, da er von der H2-Datenbank verwendet wird und zur Laufzeit ein Fehler auftritt.)

$ vi service-provider/pom.xml

service-provider/pom.xml


<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
	<groupId>com.vividsolutions</groupId>
	<artifactId>jts</artifactId>
	<version>1.13</version>
	<scope>runtime</scope>
</dependency>

Fügen Sie dem Client abhängige Bibliotheken hinzu

Fügen Sie dem Client "Spring Security OAuth", "Jacksons Erweiterungsmodul für JSR 310", "Thymeleaf-Erweiterungsmodul für JSR 310", "Webjars Locator" und "WebJar for Bootstrap" zum Client hinzu.

$ vi client/pom.xml

client/pom.xml


<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.3.7-1</version>
</dependency>

Note:

Informationen zu "Webjars Locator" und "WebJar for Bootstrap" finden Sie unter "Grundlegendes zum Zugriff auf statische Ressourcen in Spring MVC (+ Spring Boot). ) ”...

  • [Zugriff auf statische Ressourcen in Spring Boot-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9%E7% 94% A8-1)
  • [Zugriff auf statische Ressourcen mithilfe der einzigartigen Funktionen von Spring MVC - Verwenden von WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9 % E7% 94% A8)

Wenn Sie interessiert sind, lesen Sie bitte die kurze Erklärung in>.

Überprüfen Sie den Maven-Build

Führen Sie einen Mavne-Build (Maven-Paket) in dem Verzeichnis durch, in dem der Build "pom.xml" gespeichert ist, und überprüfen Sie die Gültigkeit der Einstellungen "pom.xml". Wenn das folgende Protokoll angezeigt wird, ist der Mavne-Build erfolgreich.

$ ./mvnw package
...
[INFO] ------------------------------------------------------------------------
[INFO] Building spring-seucirty-oauth-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] service-provider ................................... SUCCESS [  3.899 s]
[INFO] client ............................................. SUCCESS [  2.697 s]
[INFO] spring-seucirty-oauth-demo ......................... SUCCESS [  0.000 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.088 s
[INFO] Finished at: 2017-02-25T09:05:35+09:00
[INFO] Final Memory: 29M/395M
[INFO] ------------------------------------------------------------------------

Erstellen eines Dienstanbieters

Erstellen Sie zunächst einen Dienstanbieter.

Servereinstellungen des Dienstanbieters

Setzen Sie, wie zu Beginn in der Anwendungskonfiguration beschrieben, den Service Provider-Port auf "18081" und den Kontextpfad auf "/ provider". Stellen Sie außerdem sicher, dass Sie die auf H2-Dateien basierende Datenbank verwenden, damit die von der REST-API verarbeiteten Aufgabeninformationen beim Neustart der Anwendung nicht verschwinden.

service-provider/src/main/resources/application.properties


server.port=18081
server.context-path=/provider
spring.datasource.url=jdbc:h2:~/.h2/service-provider

Note:

Wenn der Dienstanbieter und der Client auf demselben Host (z. B. localhost) ausgeführt werden und der Kontextpfad (Standard ist /) derselbe ist, kann das Cookie, das die Sitzungs-ID verwaltet, in Konflikt geraten und die HTTP-Sitzung wird möglicherweise nicht korrekt behandelt. Wenn Sie es daher in der lokalen Umgebung ausführen möchten, müssen Sie auch den Kontextpfad festlegen.

Tabelleneinrichtung

Erstellen Sie eine Tabelle zum Speichern von Aufgabeninformationen.

service-provider/src/main/resources/schema.sql


CREATE TABLE IF NOT EXISTS tasks (
  id IDENTITY PRIMARY KEY
  , username VARCHAR(255) NOT NULL
  , title TEXT NOT NULL
  , detail TEXT
  , deadline DATE
  , finished BOOLEAN NOT NULL DEFAULT FALSE
  , created_at DATETIME DEFAULT SYSTIMESTAMP
  , updated_at DATETIME DEFAULT SYSTIMESTAMP
  , version BIGINT DEFAULT 1
);

CREATE INDEX IF NOT EXISTS idx_tasks_username ON tasks(username);

Erstellen eines Domänenobjekts

Erstellen Sie ein Domänenobjekt, das die Aufgabeninformationen enthält, die von der REST-API verarbeitet werden.

service-provider/src/main/java/com/example/Task.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class Task {

	private long id;
	private String username;
	private String title;
	private String detail;
	private LocalDate deadline;
	private boolean finished;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;
	private long version;

	// getter/setter

}

Repository erstellen

Erstellen Sie eine Repository-Klasse für das Domänenobjekt, das die Aufgabeninformationen enthält. Dieser Eintrag verwendet keinen O / R-Mapper wie JPA, implementiert ihn jedoch mithilfe der vom Spring Framework bereitgestellten Datenzugriffsfunktion (Jdbc Operations). (Eigentlich ... benutze NamedParameterJdbcOperations, das namensbasierte Parameter verarbeiten kann)

Note:

Die Beans für JdbcOperations und NamedParameterJdbcOperations werden vom Spring Boot AutoConfigure-Mechanismus definiert, sodass der Entwickler die Beans nicht explizit definieren muss.

service-provider/src/main/java/com/example/TaskRepository.java


package com.example;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Repository
public class TaskRepository {
	private final NamedParameterJdbcOperations jdbcOperations;

	public TaskRepository(NamedParameterJdbcOperations jdbcOperations) {
		this.jdbcOperations = jdbcOperations;
	}

	public List<Task> findAll(String username) {
		return jdbcOperations.query(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE username = :username ORDER BY deadline DESC, id DESC",
				new MapSqlParameterSource("username", username), new BeanPropertyRowMapper<>(Task.class));
	}

	public Task findOne(long id) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
				new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
	}

	public void save(Task task) {
		if (task.getId() == null) {
			GeneratedKeyHolder holder = new GeneratedKeyHolder();
			jdbcOperations.update(
					"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
					new BeanPropertySqlParameterSource(task), holder);
			task.setId(holder.getKey().longValue());
		} else {
			jdbcOperations.update(
					"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
					new BeanPropertySqlParameterSource(task));
		}
	}

	public void remove(long id) {
		jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
	}

}

RestController erstellen (REST-API)

Erstellen Sie eine Controller-Klasse, die eine CRUD-Operation (REST-API) für Aufgabeninformationen bereitstellt. Wenn die Zieldaten bei Verwendung von "JdbcTemplate" nicht gefunden werden, tritt "EmptyResultDataAccessException" auf. Implementieren Sie daher zusätzlich zur Handler-Methode für die REST-API eine Ausnahmebehandlungsmethode für die Behandlung von "EmptyResultDataAccessException" und geben Sie 404 Not Found zurück. ..

service-provider/src/main/java/com/example/TaskRestController.java


package com.example;

import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.relativeTo;

import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Optional;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	private final TaskRepository repository;

	TaskRestController(TaskRepository repository) {
		this.repository = repository;
	}

	@GetMapping
	List<Task> getTasks(Principal principal) {
		return repository.findAll(extractUsername(principal));
	}

	@PostMapping
	ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
		task.setUsername(extractUsername(principal));
		repository.save(task);
		URI createdTaskUri = relativeTo(uriBuilder).withMethodCall(on(TaskRestController.class).getTask(task.getId()))
				.build().encode().toUri();
		return ResponseEntity.created(createdTaskUri).build();
	}

	@GetMapping("{id}")
	Task getTask(@PathVariable long id) {
		return repository.findOne(id);
	}

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task) {
		task.setId(id);
		repository.save(task);
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id) {
		repository.remove(id);
	}

	private String extractUsername(Principal principal) {
		return Optional.ofNullable(principal).map(Principal::getName).orElse("none");
	}

	@ExceptionHandler(EmptyResultDataAccessException.class)
	@ResponseStatus(HttpStatus.NOT_FOUND)
	void handleEmptyResultDataAccessException() {
		// NOP
	}

}

Überprüfung der REST-API-Operation

Lassen Sie uns überprüfen, ob die von Ihnen erstellte REST-API ordnungsgemäß funktioniert.

Starten Sie den Dienstanbieter

Starten Sie die Spring Boot-Anwendung mit dem von Spring Boot bereitgestellten Maven-Plugin.

$ ./mvnw -pl service-provider spring-boot:run

Aufruf der "API zur Erfassung von Aufgabeninformationslisten"

Nennen wir unschuldig die "API zur Erfassung von Aufgabeninformationslisten"! !!

$ $ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
WWW-Authenticate: Basic realm="Spring"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 01:53:04 GMT

{"timestamp":1487987584305,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/provider/api/tasks"}

Anscheinend ist eine Standardauthentifizierung erforderlich. Dies liegt daran, dass bei der automatischen Konfiguration von Spring Boot eine Klasse von Spring Security gefunden wird ... Standardmäßig müssen alle Anforderungspfade (/ **) über eine Standardauthentifizierung verfügen. Nachdem wir nun den Betrieb der REST-API überprüfen möchten, deaktivieren wir die Standardauthentifizierung einmal. Die für Spring Boot erforderliche Standardauthentifizierung kann durch Hinzufügen der Einstellung "security.basic.enabled = false" deaktiviert werden.

service-provider/src/main/resources/application.properties


security.basic.enabled=false

Warning:

** Bitte aktivieren Sie die Standardauthentifizierung, nachdem Sie den Betrieb der REST-API überprüft haben! !! ** ** **

Sie können jetzt eine leere Liste von Aufgaben erhalten, indem Sie auf die REST-API zugreifen, nachdem Sie die Standardauthentifizierung deaktiviert haben.

$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:01:57 GMT

[]

Aufruf der "API zur Erstellung von Aufgabeninformationen"

Unmittelbar nach dem Start werden keine Aufgabeninformationen registriert. Rufen Sie daher die "API zur Erstellung von Aufgabeninformationen" auf, um Aufgabeninformationen zu erstellen.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201 
Location: http://localhost:18081/provider/api/tasks/1
Content-Length: 0
Date: Sat, 25 Feb 2017 02:17:37 GMT

Wenn die Aufgabeninformationen erfolgreich erstellt wurden, wird die URL für den Zugriff auf die erstellten Aufgabeninformationen in der Kopfzeile "Standort" festgelegt.

Aufruf der "Task Information Acquisition API"

Lassen Sie uns die erstellten Aufgabeninformationen abrufen, indem Sie die "API zur Erfassung von Aufgabeninformationen" aufrufen (Zugriff auf die im Header "Standort" festgelegte URL).

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:29:12 GMT

{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":[2017,2,28],"finished":false,"createdAt":[2017,2,25,11,17,37,671000000],"updatedAt":[2017,2,25,11,17,37,671000000],"version":1}

Ich konnte einige Aufgabeninformationen abrufen, aber ... Das Format von Datum und Uhrzeit ist etwas enttäuschend. Versuchen wir also, den formatierten Wert auszugeben. Wenn Sie das formatierte Datum / die formatierte Uhrzeit ausgeben möchten, setzen Sie spring.jackson.serialization.write-date-as-timestamps = false.

service-provider/src/main/resources/application.properties


spring.jackson.serialization.write-dates-as-timestamps=false

Wenn Sie nach dem Festlegen von "spring.jackson.serialization.write-date-as-timestamps = false" erneut darauf zugreifen, können Sie sehen, dass es so formatiert ist.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:31:13 GMT

{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:17:37.671","version":1}

Aufruf von "Update Task Information API"

Da es eine große Sache ist ... Ich rufe die "Task Information Update API" auf, um die Task Information zu aktualisieren.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -H "Content-Type: application/json" -X PUT -d '{"title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31"}'
HTTP/1.1 204 
Date: Sat, 25 Feb 2017 02:33:58 GMT

Wenn Sie die aktualisierten Aufgabeninformationen erhalten, können Sie bestätigen, dass sie korrekt aktualisiert wurden.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:34:21 GMT

{"id":1,"username":"none","title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:33:58.51","version":2}

Aufruf von "Task Information API löschen"

Rufen Sie die letzte "API mit Aufgabeninformationen löschen" auf und versuchen Sie, die erstellten Aufgabeninformationen zu löschen.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -X DELETE
HTTP/1.1 204 
Date: Sat, 25 Feb 2017 02:35:20 GMT

Wenn Sie versuchen, die gelöschten Aufgabeninformationen abzurufen, wird ein Clientfehler (404: Nicht gefunden) angezeigt, der Sie darüber informiert, dass keine Zieldaten vorhanden sind.

$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 404 
Content-Length: 0
Date: Sat, 25 Feb 2017 02:35:37 GMT

Einrichtung des Autorisierungsservers

Weisen Sie der Konfigurationsklasse "@ EnableAuthorizationServer" zu, definieren Sie die für die OAuth-Authentifizierung / Autorisierung erforderlichen Beans und veröffentlichen Sie den "Autorisierungsendpunkt" und den "Tokenendpunkt" auf dem Autorisierungsserver.

service-provider/src/main/java/com/example/AuthorizationServerConfiguration.java


package com.example;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration {
}

Mit der Standardeinstellung von Spring Boot werden die folgenden Werte beim Start zufällig zugewiesen. Legen Sie daher einen festen Wert fest.

service-provider/src/main/resources/application.properties


security.user.password=password
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret

Geben Sie außerdem den Bereich und den Gewährungstyp an, die der Standardclient verarbeiten kann. "Passowrd: Anmeldeinformationen für das Kennwort des Ressourcenbesitzers" ist zunächst nicht erforderlich, wird jedoch angegeben, um den Betrieb des Autorisierungsservers und des Ressourcenservers mithilfe der CUI (Befehl cURL) zu überprüfen.

service-provider/src/main/resources/application.properties


security.oauth2.client.scope=read,write
security.oauth2.client.authorized-grant-types=authorization_code,password

Wenn Sie den Dienstanbieter in diesem Status starten, wird das folgende Protokoll ausgegeben, und Sie können sehen, dass der Endpunkt für OAuth auf dem Autorisierungsserver veröffentlicht wurde.

...
2017-02-25 13:41:48.269  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-02-25 13:41:48.270  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-02-25 13:41:48.271  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2017-02-25 13:41:48.271  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2017-02-25 13:41:48.272  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
2017-02-25 13:41:48.273  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
2017-02-25 13:41:48.290  INFO 75893 --- [           main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
...

Einrichtung des Ressourcenservers

Weisen Sie der Konfigurationsklasse "@ EnableAuthorizationServer" zu, definieren Sie die für die OAuth-Authentifizierung / Autorisierung erforderlichen Beans und überschreiben Sie "configure (HttpSecurity)" von "ResourceServerConfigurerAdapter", um die Autorisierungseinstellungen für die REST-API zu konfigurieren.

service-provider/src/main/java/com/example/ResourceServerConfiguration.java


package com.example;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/api/**")
			.authorizeRequests()
			.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
			.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
			.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
			.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')");
	}
}

Durch Vornehmen der obigen Einstellungen kann die OAuth-Authentifizierung / Autorisierung auf Anforderungen unter "/ api" angewendet werden.

Berechtigungs- / Ressourcenserver-Betriebsprüfung

Nachdem Sie den Autorisierungsserver und den Ressourcenserver eingerichtet haben, greifen wir auf den Ressourcenserver zu.

$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 09:39:55 GMT

{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

Irgendwie ist ein Fehler aufgetreten. Anzeigen der Details des Fehlers ... Ein Authentifizierungsfehler (401 Unauthorized) ist aufgetreten, und es wurde benachrichtigt, dass eine Authentifizierung mit dem Bearer-Token von OAuth erforderlich ist.

Zugriffstoken erhalten

Der letztendliche Zweck dieses Eintrags besteht darin, zu erläutern, wie Sie mit dem "Autorisierungscode-Grant" ein Zugriffstoken erhalten und auf die REST-API zugreifen können. Zunächst jedoch ... "Ressourcen", mit denen Sie auf einfache Weise ein Zugriffstoken erhalten können. Lassen Sie uns ein Zugriffstoken erhalten, um mithilfe von "Besitzerkennwortanmeldeinformationen" auf die Aufgabeninformationen zuzugreifen.

$ curl -D - -s -u client:secret http://localhost:18081/provider/oauth/token -X POST -d grant_type=password -d username=user -d password=password
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:08:39 GMT

{"access_token":"d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097","token_type":"bearer","expires_in":43138,"scope":"read write"}

Zugriff auf den Ressourcenserver

Geben Sie das erhaltene Zugriffstoken im Header "Authorization" an, um erneut auf den Ressourcenserver zuzugreifen.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:11:17 GMT

[]

Da es eine große Sache ist ... Lassen Sie uns eine neue Aufgabe erstellen und die erstellten Aufgabeninformationen abrufen.

$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097" -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:18081/provider/api/tasks/6
Content-Length: 0
Date: Sat, 25 Feb 2017 15:24:40 GMT

$ curl -D - -s http://localhost:18081/provider/api/tasks/6 -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 15:26:13 GMT

{"id":6,"username":"user","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-26T00:24:40.843","updatedAt":"2017-02-26T00:24:40.843","version":1}

Client erstellen (Web-Benutzeroberfläche)

Nachdem wir den Dienstanbieter (Autorisierungsserver und Ressourcenserver) erstellt haben, erstellen wir eine Web-Benutzeroberfläche zum Bearbeiten der vom Ressourcenserver verwalteten Aufgabeninformationen.

Client-Server-Einstellungen

Setzen Sie, wie zu Beginn in der Anwendungskonfiguration beschrieben, den Cleint-Port auf "18080" und den Kontextpfad auf "/ client".

client/src/main/resources/application.properties


server.port=18080
server.context-path=/client

In dieser Anwendung verwendet die Benutzerauthentifizierung auf der Clientseite die von Spring Boot eingerichtete Standardauthentifizierung. Wenn es sich um die Standardoperation handelt, ändert sich das Kennwort des Standardbenutzers bei jedem Start. Korrigieren Sie daher das Kennwort.

client/src/main/resources/application.properties


security.user.password=password

Client-Setup

Durch Zuweisen von "@ EnableOAuth2Client" zur Konfigurationsklasse, einer Komponente, die Zugriffstoken verwaltet ("OAuth2ClientContext") und einer Komponente, die den Benutzeragenten (Browser) zum Autorisierungsserver führt, um die Autorisierung vom Ressourcenbesitzer ("OAuth2ClientContext") zu erhalten. Bean-Definition wie "OAuth2ClientContextFilter"). Erstellen Sie außerdem eine Bean-Definition von "RestTemplate" ("OAuth2RestTemplate"), die für OAuth erweitert wurde. Bei Verwendung von "OAuth2RestTemplate" muss die Anwendung keine OAuth-bezogenen Prozesse kennen (z. B. den Prozess des Erwerbs eines Zugriffstokens vom Autorisierungsserver), und die REST-API kann auf dieselbe Weise aufgerufen werden, wie wenn keine Authentifizierung / Autorisierung durch OAuth durchgeführt wird. Sie werden in der Lage sein.

client/src/main/java/com/example/ClientConfiguration.java


package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;

@EnableOAuth2Client
@Configuration
public class ClientConfiguration {

	@Bean
	OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context, OAuth2ProtectedResourceDetails details) {
		return new OAuth2RestTemplate(details, context);
	}

}

Fügen Sie außerdem OAuth-bezogene Einstellungen hinzu (API-URL, Autorisierungsserver-URL, Client-Informationen).

client/src/main/resources/application.properties


#API-URL
api.url=http://localhost:18081/provider/api

#Endpunkt-URL des Autorisierungsservers
auth.url=http://localhost:18081/provider/oauth
security.oauth2.client.access-token-uri=${auth.url}/token
security.oauth2.client.user-authorization-uri=${auth.url}/authorize

#Client-Informationseinstellungen
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.scope=read,write

Note:

Standardmäßig sind in Spring Boot AutoConfigure Beans definiert, um mithilfe des "Berechtigungscode-Erteilungsflusses" auf den Ressourcenserver zuzugreifen.

Erstellen eines Domänenobjekts

Erstellen Sie ein Domänenobjekt, das Aufgabeninformationen enthält, mit denen Sie über die REST-API arbeiten. (Kopieren Sie die beim Erstellen des Dienstanbieters erstellte Task-Klasse auf die Clientseite.)

client/src/main/java/com/example/Task.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class Task {

	private long id;
	private String username;
	private String title;
	private String detail;
	private LocalDate deadline;
	private boolean finished;
	private LocalDateTime createdAt;
	private LocalDateTime updatedAt;
	private long version;

	// getter/setter

}

Repository erstellen

Erstellen Sie eine Repository-Klasse für das Domänenobjekt, das die Aufgabeninformationen enthält. Diese Klasse verwendet die von Spring Security OAuth bereitgestellte Methode der Schnittstellenimplementierungsklasse "RestOperations" ("OAuth2RestTemplate"), um auf die auf dem Ressourcenserver verwalteten Aufgabeninformationen zuzugreifen.

client/src/main/java/com/example/TaskRepository.java


package com.example;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import org.springframework.web.client.RestOperations;

@Repository
public class TaskRepository {

	private final RestOperations restOperations;
	private final String resourcesUrl;
	private final String resourceUrlTemplate;

	TaskRepository(RestOperations restOperations,
			@Value("${api.url}/tasks") String resourcesUrl) {
		this.restOperations = restOperations;
		this.resourcesUrl = resourcesUrl;
		this.resourceUrlTemplate = resourcesUrl + "/{id}";
	}

	public List<Task> findAll() {
		return Arrays.asList(restOperations.getForObject(resourcesUrl, Task[].class));
	}

	public Task findOne(long id) {
		return restOperations.getForObject(resourceUrlTemplate, Task.class, id);
	}

	public void save(Task task) {
		if (task.getId() == null) {
			restOperations.postForLocation(resourcesUrl, task);
		} else {
			restOperations.put(resourceUrlTemplate, task, task.getId());
		}
	}

	public void remove(long id) {
		restOperations.delete(resourceUrlTemplate, id);
	}

}

Controller erstellen

Erstellen Sie eine Controller-Klasse, die eine Web-Benutzeroberfläche zum Ausführen von CRUD-Operationen für Aufgabeninformationen bereitstellt.

client/src/main/java/com/example/TaskController.java


package com.example;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.beans.BeanUtils;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@RequestMapping("/tasks")
@Controller
public class TaskController {

	private final TaskRepository repository;

	TaskController(TaskRepository repository) {
		this.repository = repository;
	}

	@ModelAttribute
	TaskForm setUpForm() {
		return new TaskForm();
	}

	@GetMapping
	String list(Model model) {
		List<Task> taskList = repository.findAll();
		model.addAttribute(taskList);
		return "task/list";
	}

	@PostMapping
	String create(@Validated TaskForm form, BindingResult bindingResult, Model model) {
		if (bindingResult.hasErrors()) {
			return list(model);
		}
		Task task = new Task();
		BeanUtils.copyProperties(form, task);
		repository.save(task);
		return "redirect:/tasks";
	}

	@GetMapping("{id}")
	String detail(@PathVariable long id, TaskForm form, Model model) {
		Task task = repository.findOne(id);
		BeanUtils.copyProperties(task, form);
		model.addAttribute(task);
		return "task/detail";
	}

	@PostMapping(path = "{id}", params = "update")
	String update(@PathVariable long id, @Validated TaskForm form, BindingResult bindingResult,
			Model model, RedirectAttributes redirectAttributes) {
		if (bindingResult.hasErrors()) {
			return "task/detail";
		}
		Task task = new Task();
		BeanUtils.copyProperties(form, task);
		repository.save(task);
		redirectAttributes.addAttribute("id", id);
		return "redirect:/tasks/{id}";
	}

	@PostMapping(path = "{id}", params = "delete")
	String delete(@PathVariable long id) {
		repository.remove(id);
		return "redirect:/tasks";
	}

	static class TaskForm {
		private static final String DATE_TIME_FORMAT = "uuuu-MM-dd HH:mm:ss";

		private Long id;
		@NotEmpty private String title;
		private String detail;
		@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate deadline;
		private boolean finished;
		@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime createdAt;
		@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime updatedAt;
		private long version;

		public Long getId() {
			return id;
		}

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

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}

		public String getDetail() {
			return detail;
		}

		public void setDetail(String detail) {
			this.detail = detail;
		}

		public LocalDate getDeadline() {
			return deadline;
		}

		public void setDeadline(LocalDate deadline) {
			this.deadline = deadline;
		}

		public boolean isFinished() {
			return finished;
		}

		public void setFinished(boolean finished) {
			this.finished = finished;
		}

		public LocalDateTime getCreatedAt() {
			return createdAt;
		}

		public void setCreatedAt(LocalDateTime createdAt) {
			this.createdAt = createdAt;
		}

		public LocalDateTime getUpdatedAt() {
			return updatedAt;
		}

		public void setUpdatedAt(LocalDateTime updatedAt) {
			this.updatedAt = updatedAt;
		}

		public long getVersion() {
			return version;
		}

		public void setVersion(long version) {
			this.version = version;
		}
	}

}

Note:

Das Registrierungsdatum und die Registrierungszeit (createdAt) sowie das Aktualisierungsdatum und die Aktualisierungszeit ( updateAt) sind keine Eingabeelemente und sollten nicht als Formularelemente beibehalten werden. Es ist jedoch auch möglich, bei jedem Eingabeprüfungsfehler Aufgabeninformationen vom Ressourcenserver abzurufen. Es ist subtil, also etwas rau, aber dieses Mal werde ich es in das Formularelement aufnehmen.

Erstellen eines Aufgabenlistenbildschirms

Es zeigt die vom Ressourcenserver erhaltene Aufgabenliste an und bietet eine Web-Benutzeroberfläche zum Erstellen neuer Aufgabeninformationen.

client/src/main/resources/templates/task/list.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Task List</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
          th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
    <style type="text/css">
        .strike {
            text-decoration: line-through;
        }
    </style>
</head>
<body>

<div class="container">

    <h1>Task List</h1>

    <div id="taskForm">
        <form action="list.html" method="post" class="form-horizontal"
              th:action="@{/tasks}" th:object="${taskForm}">
            <div class="form-group">
                <label for="title" class="col-sm-1 control-label">Title</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="title" th:field="*{title}"/>
                    <span class="text-error" th:errors="*{title}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-1 control-label">Detail</label>
                <div class="col-sm-10">
                    <textarea class="form-control" id="detail" th:field="*{detail}">
                    </textarea>
                    <span class="text-error" th:errors="*{detail}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-1 control-label">Deadline</label>
                <div class="col-sm-4">
                    <input type="date" class="form-control" id="detail" th:field="*{deadline}"/>
                    <span class="text-error" th:errors="*{deadline}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-1 col-sm-10">
                    <button type="submit" class="btn btn-default">Create</button>
                </div>
            </div>
        </form>
    </div>

    <table id="todoList" class="table table-hover" th:if="${not #lists.isEmpty(taskList)}">

        <tr>
            <th>#</th>
            <th>Title</th>
            <th>Deadline</th>
            <th>Created Datetime</th>
        </tr>

        <tr th:each="task : ${taskList}">
            <td th:text="${taskStat.count}">1</td>
            <td>
                <span th:class="${task.finished} ? 'strike'">
                    <a href="detail.html"
                       th:text="${task.title}" th:href="@{/tasks/{id}(id=${task.id})}">
                        Create Sample Application
                    </a>
                </span>
            </td>
            <td>
                <span th:text="${#temporals.format(task.deadline,'uuuu-MM-dd')}" th:if="${task.deadline != null}">2017-02-28</span>
            </td>
            <td>
                <span th:text="${#temporals.format(task.createdAt,'uuuu-MM-dd HH:mm.ss')}">2017-02-27 15:17:02</span>
            </td>
        </tr>

    </table>

</div>
</body>
</html>

Erstellen eines Aufgabendetails-Bildschirms

Bietet eine Web-Benutzeroberfläche zum Anzeigen, Aktualisieren und Löschen von Aufgabeninformationen, die vom Ressourcenserver erfasst wurden.

client/src/main/resources/templates/task/detail.html


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Task Detail</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
          th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
</head>
<body>

<div class="container">

    <h1>Task Detail</h1>

    <div id="taskForm">
        <form action="detail.html" method="post" class="form-horizontal"
              th:action="@{/tasks/{id}(id=*{id})}" th:object="${taskForm}">
            <div class="form-group">
                <label for="title" class="col-sm-2 control-label">Title</label>
                <div class="col-sm-10">
                    <input class="form-control" id="title" value="Create Sample Application" th:field="*{title}"/>
                    <span class="text-error" th:errors="*{title}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-2 control-label">Detail</label>
                <div class="col-sm-10">
                    <textarea class="form-control" id="detail" th:field="*{detail}">
                    </textarea>
                    <span class="text-error" th:errors="*{detail}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="detail" class="col-sm-2 control-label">Deadline</label>
                <div class="col-sm-4">
                    <input type="date" class="form-control" id="detail" value="2017-03-10" th:field="*{deadline}"/>
                    <span class="text-error" th:errors="*{deadline}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <label for="finished" class="col-sm-2 control-label">Finished ?</label>
                <div class="col-sm-2">
                    <input type="checkbox" id="finished" th:field="*{finished}"/>
                </div>
            </div>
            <div class="form-group">
                <label for="createdAt" class="col-sm-2 control-label">Created Datetime</label>
                <div class="col-sm-4">
                    <input id="createdAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{createdAt}" readonly="readonly"/>
                </div>
            </div>
            <div class="form-group">
                <label for="updatedAt" class="col-sm-2 control-label">Updated Datetime</label>
                <div class="col-sm-4">
                    <input id="updatedAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{updatedAt}" readonly="readonly"/>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-2 col-sm-10">
                    <input type="hidden" th:field="*{version}"/>
                    <button type="submit" class="btn btn-default" name="update">Update</button>
                    <button type="submit" class="btn btn-default" name="delete">Delete</button>
                </div>
            </div>
        </form>
    </div>

    <hr/>

    <a href="list.html" class="btn btn-default" th:href="@{/tasks}">Task List</a>

</div>
</body>
</html>

Erleben Sie den Ablauf des Berechtigungscodes

Nachdem Sie die Dienstanbieter- und Client-Anwendungen erstellt haben, können Sie die Anwendungen tatsächlich verwenden und den Ablauf der Berechtigungscode-Gewährung erleben! !!

Programm starten

Starten Sie zunächst die Dienstanbieter- und Clientanwendungen.

$ ./mvnw -pl service-provider spring-boot:run
...
2017-02-27 00:29:12.820  INFO 78931 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-02-27 00:29:12.867  INFO 78931 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18081 (http)
2017-02-27 00:29:12.872  INFO 78931 --- [           main] com.example.ServiceProviderApplication   : Started ServiceProviderApplication in 3.247 seconds (JVM running for 5.825)
$ ./mvnw -pl client spring-boot:run
...
2017-02-27 00:29:49.282  INFO 78940 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-02-27 00:29:49.337  INFO 78940 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18080 (http)
2017-02-27 00:29:49.344  INFO 78940 --- [           main] com.example.ClientApplication            : Started ClientApplication in 3.033 seconds (JVM running for 6.17)

Anzeige des Aufgabenlistenbildschirms

Geben Sie http: // localhost: 18080 / client /asks in die Adressleiste Ihres Browsers ein, um den Bildschirm mit der Aufgabenliste anzuzeigen. Beim erstmaligen Zugriff ist zunächst eine Benutzerauthentifizierung (Standardauthentifizierung) auf der Clientseite erforderlich. Geben Sie daher im Dialogfeld den Benutzernamen ("Benutzer") und das Kennwort ("Kennwort") ein und klicken Sie auf die Schaltfläche "Anmelden".

spring-security-oauth-1st-app-client-auth.png

Wenn die Benutzerauthentifizierung auf der Clientseite erfolgreich ist, werden Sie zum vom Dienstanbieter bereitgestellten Autorisierungsendpunkt (/ oauth / authorize) umgeleitet, um die Autorisierungsgewährung (Autorisierungscode) vom Ressourcenbesitzer zu erhalten. Beim ersten Zugriff ist auf der Seite des Dienstanbieters eine Authentifizierung des Ressourcenbesitzers (Standardauthentifizierung) erforderlich. Geben Sie daher im Dialogfeld den Benutzernamen ("Benutzer") und das Kennwort ("Kennwort") ein und klicken Sie auf "Anmelden". Bitte.

spring-security-oauth-1st-app-provider-auth.png

Wenn die Authentifizierung des Ressourcenbesitzers auf der Seite des Dienstanbieters erfolgreich ist, wird der Bildschirm (Autorisierungsbildschirm) zum Abrufen der Autorisierung des Ressourcenbesitzers für den vom Client angeforderten Bereich angezeigt. Wählen Sie daher für jeden Bereich Zulassen / Verweigern aus und wählen Sie " Klicken Sie auf die Schaltfläche "Autorisieren". (Bitte erlauben Sie alle Bereiche hier)

spring-security-oauth-1st-app-provider-authorize.png

Note:

In diesem Eintrag wird der von Spring Security OAuth bereitgestellte Autorisierungsbildschirm verwendet. Ich denke jedoch, dass Anpassungen in der tatsächlichen Anwendungsentwicklung üblich sind. Daher möchte ich die Anpassungsmethode ab dem nächsten Mal einführen.

Nach der Autorisierung durch den Ressourcenbesitzer wird der Bildschirm mit der Aufgabenliste angezeigt: lachend:

spring-security-oauth-1st-app-client-list.png

Aufgabenregistrierung

Ich weiß jedoch nicht, ob die Aufgabeninformationen ordnungsgemäß vom Ressourcenserver abgerufen werden können, da die Aufgabe nicht registriert ist ...: heat_smile: Also ... Als nächstes registrieren wir die Aufgabe über die Web-Benutzeroberfläche. Geben Sie die Aufgabeninformationen in das Eingabeformular auf dem Bildschirm mit der Aufgabenliste ein und klicken Sie auf die Schaltfläche "Erstellen".

spring-security-oauth-1st-app-client-create.png spring-security-oauth-1st-app-client-list-after-created.png

Bildschirm mit Aufgabendetails anzeigen

Da in der Aufgabenliste nur Titel, Frist und Erstellungsdatum angezeigt werden, zeigen wir den Bildschirm mit den Aufgabendetails an und überprüfen die detaillierten Informationen der Aufgabe. Da der Titel der Aufgabenliste ein Link ist, klicken Sie auf den Titel (Link) der Aufgabe, die Sie anzeigen möchten.

spring-security-oauth-1st-app-client-detail.png

Aufgaben aktualisieren / löschen

Um eine Aufgabe zu aktualisieren oder zu löschen, klicken Sie im Bildschirm mit den Aufgabendetails auf die Schaltfläche "Aktualisieren" oder "Löschen". Hier wird die Frist auf "15.03.2017" aktualisiert. (Außerdem wird das Löschen weggelassen.)

spring-security-oauth-1st-app-client-update.png spring-security-oauth-1st-app-client-detail-after-updated.png

Versuchen Sie, Schreibbereiche abzulehnen (einige Bereiche).

Starten Sie den Dienstanbieter neu ("Strg + C" + "./mvnw spring-boot: run") und zeigen Sie den Bildschirm mit der Aufgabenliste erneut an, um die Zugriffstoken- und Autorisierungsinformationen zu zerstören. Dann ... wird der Autorisierungsbildschirm des Dienstanbieters angezeigt und der Zugriff auf den Schreibbereich wird verweigert.

spring-security-oauth-1st-app-provider-reject-write.png

Als ich versuchte, eine neue Aufgabe auf dem Bildschirm mit der Aufgabenliste zu erstellen ... Die Fehlermeldung "Unzureichender Bereich für diese Ressource" wurde angezeigt und der Aufruf der "API zur Aufgabenerstellung" wurde abgelehnt.

spring-security-oauth-1st-app-client-reject-write.png

Versuchen Sie, alle Bereiche abzulehnen

Starten Sie den Dienstanbieter neu ("Strg + C" + "./mvnw spring-boot: run") und zeigen Sie den Bildschirm mit der Aufgabenliste erneut an, um die Zugriffstoken- und Autorisierungsinformationen zu zerstören. Dann ... wird der Autorisierungsbildschirm des Dienstanbieters angezeigt und der Zugriff auf alle Bereiche wird verweigert. Dann ... Fügen Sie anstelle des "Autorisierungscodes" einen Parameter hinzu, um zu benachrichtigen, dass der Ressourcenbesitzer den Zugriff verweigert hat, und leiten Sie auf die Seite auf der Clientseite um.

spring-security-oauth-1st-app-client-reject-all.png

** [Super wichtig] Beseitigen Sie die Sicherheitsprobleme der REST-API! !! ** ** **

Tatsächlich hat die bisher erstellte Anwendung (REST-API) ein ernstes Sicherheitsproblem. Wissen Sie, wo das Problem liegt? Es ist ... wie ... ** Sie können auf die Aufgabeninformationen anderer verweisen und diese aktualisieren **: schreien :: schreien :: schreien:

Es gibt zwei Möglichkeiten, um dieses Problem zu lösen.

Welche Methode Sie wählen, hängt von Ihren Sicherheitsanforderungen ab. Ersteres verhält sich wie bei einer nicht vorhandenen Aufgabe (404 nicht gefunden), während sich letzteres wie 403 Verboten verhält. Daher muss aufgezeichnet werden, dass ein nicht autorisierter Zugriff vorhanden war In einigen Fällen halte ich es für besser, die letztere Methode zu verwenden.

Fügen Sie der Bedingung in SQL einen Benutzernamen hinzu

Wenn Sie den Bedingungen für den Zugriff auf Aufgabeninformationen (SQL-Bedingungen) einen Benutzernamen hinzufügen möchten, fügen Sie den Benutzernamen zum Argument der Repository-Methode hinzu und fügen Sie den Benutzernamen zu den SQL-Bedingungen hinzu. Hier wird zum Aktualisieren und Löschen von Prozessen durch Aufrufen der Methode "findOne" überprüft, ob der Prozess für die eigenen Aufgabeninformationen bestimmt ist.

@Transactional
@Repository
public class TaskRepository {
	// ...

	public Task findOne(long id, String username) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id AND username = :username",
				new MapSqlParameterSource("id", id).addValue("username", username), new BeanPropertyRowMapper<>(Task.class)); //★★★ Korrektur
	}

	public void save(Task task, String username) {
		if (task.getId() == null) {
			GeneratedKeyHolder holder = new GeneratedKeyHolder();
			jdbcOperations.update(
					"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
					new BeanPropertySqlParameterSource(task), holder);
			task.setId(holder.getKey().longValue());
		} else {
			findOne(task.getId(), username); //★★★ Hinzugefügt
			jdbcOperations.update(
					"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
					new BeanPropertySqlParameterSource(task));
		}
	}

	public void remove(long id, String username) {
		findOne(id, username); //★★★ Hinzugefügt
		jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
	}

}

Wenn das Argument der Repository-Methode geändert wird, wird auch die Controller-Klasse geändert.

@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	// ...

	@PostMapping
	ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
		task.setUsername(extractUsername(principal));
		repository.save(task, task.getUsername()); //★★★ Korrektur
		URI createdTaskUri = relativeTo(uriBuilder)
				.withMethodCall(on(TaskRestController.class).getTask(task.getId(), principal)).build().encode().toUri(); //★★★ Korrektur
		return ResponseEntity.created(createdTaskUri).build();
	}

	@GetMapping("{id}")
	Task getTask(@PathVariable long id, Principal principal) {
		return repository.findOne(id, extractUsername(principal)); //★★★ Korrektur
	}

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task, Principal principal) {
		task.setId(id);
		repository.save(task, extractUsername(principal)); //★★★ Korrektur
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id, Principal principal) {
		repository.remove(id, extractUsername(principal)); //★★★ Korrektur
	}

	// ...

}

Beim Zugriff auf die Aufgabeninformationen einer anderen Person tritt ein Fehler (404 nicht gefunden) auf, wenn die Ressource nicht vorhanden ist (siehe unten).

spring-security-oauth-1st-app-client-notfound-other-owner.png

Führen Sie eine Eigentümerprüfung durch

Wenn Sie überprüfen möchten, ob der Eigentümer der Aufgabeninformationen und der Benutzer der Anmeldeinformationen übereinstimmen, können Sie den von Spring Security bereitgestellten Methodensicherheitsmechanismus schneller verwenden.

Gewähren Sie der Konfigurationsklasse zunächst "@ EnableGlobalMethodSecurity", um den Methodensicherheitsmechanismus zu aktivieren.

service-provider/src/main/java/com/example/ResourceServerConfiguration.java


@EnableGlobalMethodSecurity(prePostEnabled = true) //★★★ Hinzugefügt
@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	// ...
}

Stellen Sie als Nächstes die Berechtigung so ein, dass nur der Ressourcenbesitzer auf die Methode zugreifen kann, mit der Aufgabeninformationen erfasst werden.

service-provider/src/main/java/com/example/TaskRepository.java


@Transactional
@Repository
public class TaskRepository {
	// ...
	@PostAuthorize("returnObject.username == authentication.name") //★★★ Hinzugefügt
	public Task findOne(long id) {
		return jdbcOperations.queryForObject(
				"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
				new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
	}
	// ...
}

Ändern Sie es schließlich so, dass beim Aktualisieren oder Löschen die Methode "findOne" aufgerufen wird.

service-provider/src/main/java/com/example/TaskRestController.java


@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {

	// ...

	@PutMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void putTask(@PathVariable long id, @RequestBody Task task) {
		repository.findOne(id); //★★★ Hinzugefügt
		task.setId(id);
		repository.save(task);
	}

	@DeleteMapping("{id}")
	@ResponseStatus(HttpStatus.NO_CONTENT)
	void deleteTask(@PathVariable long id) {
		repository.findOne(id); //★★★ Hinzugefügt
		repository.remove(id);
	}

	// ...

}

Der Zugriff auf die Aufgabeninformationen einer anderen Person führt zu einem Autorisierungsfehler (403 Verboten), wie unten gezeigt.

spring-security-oauth-1st-app-client-reject-other-owner.png

Vollständige Version der Anwendung

Die vollständige Version der in diesem Eintrag erstellten Anwendung wird im folgenden GitHub-Repository veröffentlicht.

Zusammenfassung

Die Erklärung ist etwas lang, aber ... Ich habe eine Anwendung erstellt, die die REST-API mithilfe des Berechtigungscode-Grant-Flows authentifiziert und autorisiert. Dieses Mal bestand der Zweck darin, den Berechtigungscode-Erteilungsfluss mit Spring Security OAuth + Spring Boot zu erfahren (zu berühren). Nutzen Sie also den AutoConfigure-Mechanismus von Spring Boot (Standardoperation) und wenden Sie ihn an. Ich habe versucht zu machen. In der eigentlichen Anwendungsentwicklung ist es jedoch üblich, Benutzer- (Ressourcenbesitzer) und Clientinformationen in einer Datenbank usw. zu verwalten, und Zugriffstoken und Autorisierungsinformationen werden auch in der Datenbank verwaltet, anstatt im Speicher der Anwendung verwaltet zu werden. In vielen Fällen ist es erforderlich, es dauerhaft zu machen. Darüber hinaus kann es Fälle geben, in denen der Dienstanbieter mehrere Ressourcen anstelle eines Typs verarbeitet, und Fälle, in denen der Client auch auf die Ressourcen mehrerer Dienstanbieter zugreift, sodass die Anwendung dem tatsächlichen Betrieb nur mit den diesmal eingeführten Inhalten standhalten kann. Die Realität ist, dass es schwierig ist, sich zu entwickeln. Also ... Ab dem nächsten Mal möchte ich die Architektur von Spring Security OAuth erläutern und vorstellen, wie Anwendungen mithilfe der Erweiterungspunkte von Spring Boot, Spring Security und Spring Security OAuth entwickelt werden.

Bis zum nächsten Mal! !!

Recommended Posts

Lassen Sie uns den Berechtigungscode-Grant-Flow mit Spring Security OAuth-Teil 2: Erstellen einer App vorerst erleben
Lassen Sie uns den Ablauf der Erteilung des Autorisierungscodes mit Spring Security OAuth-Part 1: Review of OAuth 2.0 erleben
Erstellen einer App und erstmaliges Bereitstellen mit heroku
Versuchen Sie vorerst, Spring Cloud Config auszuführen
Hallo Welt mit Ruby-Erweiterungsbibliothek vorerst
Erstellen Sie eine App mit Spring Boot 2
Erstellen Sie eine App mit Spring Boot
Zertifizierung / Autorisierung mit Spring Security & Thymeleaf
Spring AOP zum ersten Mal
[Erstes Java] Machen Sie etwas, das vorerst mit Intellij funktioniert
Quellcode zum Finden einer normalen orthogonalen Basis mit der Orthogonalisierungsmethode von Gram-Schmidt