[JAVA] Expérimentons le flux d'octroi de code d'autorisation avec Spring Security OAuth-Partie 2: Créer une application pour le moment

Cette fois, dans la deuxième partie de «Découvrez le flux d'octroi de code d'autorisation avec Spring Security OAuth», pour le moment, créez une application qui authentifie et autorise l'API REST avec le flux d'octroi de code d'autorisation à l'aide de Spring Security OAuth + Spring Boot. Je vais essayer.

Exigences fonctionnelles de l'application

Les exigences fonctionnelles de l'application créée cette fois sont ...

Service Provider

Le fournisseur de services fournit une API REST qui gère (enregistre / met à jour / supprime / référence) les informations de tâche (titre, détails, date limite, indicateur d'achèvement, date / heure d'enregistrement, date / heure de mise à jour) pour chaque utilisateur et accède à l'API REST. Authentifie et autorise à l'aide du jeton d'accès émis par le flux d'octroi de code d'autorisation OAuth 2.0.

Je vais en faire une exigence. À l'origine, je voudrais préparer une interface utilisateur Web pour gérer les informations de tâche du côté du fournisseur de services également, mais j'omettrai l'explication car l'interface utilisateur Web du côté du fournisseur de services n'est pas au centre de cette entrée.

Resource Server

Créez l'API suivante en tant qu'API pour gérer les informations de tâche du propriétaire de la ressource.

Nom de l'API Présentation de l'API Portée pour autoriser l'accès
GET /api/tasks API pour obtenir une liste d'informations sur les tâches read
POST /api/tasks API pour enregistrer les informations de tâche write
GET /api/tasks/{id} API pour obtenir des informations sur les tâches read
PUT /api/tasks/{id} API pour mettre à jour les informations de tâche write
DELETE /api/tasks/{id} API pour supprimer les informations de tâche write

Authorization Server

Nous fournissons les points de terminaison suivants (ci-après dénommés «points de terminaison d'autorisation») afin de permettre au propriétaire de la ressource d'accéder aux informations de tâche. Ce point de terminaison est fourni par Spring Security OAuth et n'a pas besoin d'être créé par le développeur.

Nom du point de terminaison Présentation du point de terminaison Conditions pour autoriser l'accès
GET /oauth/authorize Point final d'affichage de l'écran d'obtention de l'approbation du propriétaire de la ressource (ci-après dénommé «écran d'autorisation») Propriétaire de ressource authentifié
POST /oauth/authorize?user_oauth_approval Instruction d'autorisation du propriétaire de la ressource(Autoriser / Refuser)Subvention d'approbation reçue(Code d'autorisation)Point de terminaison pour l'émission Propriétaire de ressource authentifié

Créez les points de terminaison suivants (ci-après dénommés «points de terminaison de jeton») afin que le client émette des jetons d'accès en fonction de l'octroi d'autorisation (code d'autorisation) obtenu du propriétaire de la ressource. Ce point de terminaison est fourni par Spring Security OAuth et n'a pas besoin d'être créé par le développeur.

Nom du point de terminaison Présentation du point de terminaison Conditions pour autoriser l'accès
POST /oauth/token Subvention autorisée obtenue du propriétaire de la ressource(Code d'autorisation etc.)Point de terminaison pour l'émission de jetons d'accès basés sur Client authentifié

** Remarque: authentification / autorisation pour divers terminaux **

Dans cette entrée ... Nous authentifierons le propriétaire de la ressource et le client auprès de ces points de terminaison à l'aide de l'authentification de base fournie par Spring Security.

Client(Web UI)

Le client utilise l'API REST fournie par le fournisseur de services pour fournir une interface utilisateur Web (écran de liste des tâches et écran de détail des tâches) pour gérer les informations sur les tâches des propriétaires de ressources.

Nom du point de terminaison Présentation du point de terminaison Conditions pour autoriser l'accès
GET /tasks Affichage sur l'écran de la liste des tâches des informations de tâches acquises à partir du serveur de ressources Utilisateur authentifié
POST /tasks Créer des informations de tâche sur le serveur de ressources Utilisateur authentifié
GET /tasks/{id} Afficher les informations de tâche acquises à partir du serveur de ressources sur l'écran des détails de la tâche Utilisateur authentifié
POST /tasks/{id}?update Mettre à jour les informations de tâche gérées par le serveur de ressources Utilisateur authentifié
POST /tasks/{id}?delete Supprimer les informations de tâche gérées par le serveur de ressources Utilisateur authentifié

** Remarque: authentification / autorisation de l'interface utilisateur de gestion des tâches **

L'écran de gestion des tâches préparé côté client nécessite l'authentification utilisateur de l'application côté client, et l'authentification utilisateur est effectuée à l'aide de l'authentification de base fournie par Spring Security.

L'image d'écran et la transition d'écran sont les suivantes.

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

Configuration de l'application

Cette fois, nous allons créer deux applications Spring Boot, «client» et «fournisseur de services (serveur d'autorisation + serveur de ressources)», et créer une application qui authentifie et autorise l'API avec le flux d'octroi de code d'autorisation. En outre, la liaison du jeton d'accès et des informations d'authentification associées au jeton d'accès entre le serveur d'autorisation et le serveur de ressources doit être une liaison en mémoire sur l'application Web.

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

Warning:

L'application créée dans cette entrée s'exécute à l'aide de la communication HTTP, mais ... Dans le flux de protocole OAuth 2.0, ** Les propriétaires de ressources et les points de terminaison qui gèrent l'authentification client et les jetons d'accès doivent utiliser la communication HTTPS. **.

Note:

Les rôles du serveur d'autorisation et du serveur de ressources étant différents, je pense qu'il existe de nombreux cas où ils sont créés en tant qu'applications Web indépendantes avec le sentiment suivant, mais cette fois, en donnant la priorité à la simplicité de la configuration de l'application, du serveur d'autorisation et des ressources J'ai décidé de réaliser le serveur avec une seule application Spring Boot.

spring-security-oauth-std-app.png

Lors de la création d'un serveur d'autorisation et d'un serveur de ressources en tant qu'applications Web distinctes, un point est de sélectionner le jeton d'accès et les informations d'authentification associées au jeton d'accès entre les serveurs. De plus, nous prévoyons d'introduire la méthode de mise en œuvre lorsque le serveur d'autorisation et le serveur de ressources seront séparés de la prochaine fois.

Version vérifiée de l'opération

L'application créée dans cette entrée est vérifiée à l'aide des versions suivantes de la bibliothèque.

Créer un projet de développement

Maintenant, créons une application et expérimentons l'authentification / l'autorisation de l'API REST par le flux d'octroi de code d'autorisation. Tout d'abord, créons un projet de développement pour l'application Spring Boot. Voici un exemple de création d'un projet à partir de la ligne de commande, mais même si vous le générez avec SPRING INITIALIZR Web UI ou la fonction de votre IDE (bien sûr) D'ACCORD! !!

Puisque nous allons créer deux applications cette fois, nous allons créer un répertoire parent pour stocker ces applications.

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

Après avoir accédé au répertoire que vous avez créé, créez un projet de développement d'application Spring Boot pour le fournisseur de services et le client.

Créer un projet de développement pour un fournisseur de services

$ 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 -

Créer un projet de développement pour le client

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

Créer un projet pour build

Copiez les fichiers requis, tels que l'encapsuleur Maven, à partir de votre fournisseur de services ou projet de développement client.

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

Modifiez le fichier pom.xml copié dans les paramètres de la construction. Ici, le fournisseur de services et le client sont définis pour être gérés en tant que sous-modules. Cela vous permettra de créer Maven avec le fournisseur de services et le client.

$ 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>

Ajout de bibliothèques dépendantes au fournisseur de services

Ajoutez «Spring Security OAuth», «Jackson Extension Module for JSR 310» et «JTS Topology Suite» au fournisseur de services. ("JTS Topology Suite" est directement lié à cette entrée, mais je l'ai ajoutée car elle est utilisée par H2 Database et une erreur se produit lors de l'exécution.)

$ 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>

Ajouter des bibliothèques dépendantes au client

Ajoutez «Spring Security OAuth», «le module d'extension de Jackson pour JSR 310», «le module d'extension Thymeleaf pour JSR 310», «Webjars Locator» et «WebJar for Bootstrap» au client.

$ 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:

Pour "Webjars Locator" et "WebJar for Bootstrap", voir "Comprendre l'accès aux ressources statiques sur Spring MVC (+ Spring Boot) ) »...

  • [Accès aux ressources statiques sur Spring Boot-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9%E7% 94% A8-1)
  • [Accès aux ressources statiques à l'aide des fonctionnalités uniques de Spring MVC - Utilisation de WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9 % E7% 94% A8)

Est brièvement expliqué, alors jetez un œil si vous êtes intéressé

Vérifier la version de Maven

Effectuez une construction Mavne (package Maven) dans le répertoire où la construction pom.xml est stockée, et vérifiez la validité des paramètres pom.xml. Si vous voyez le journal suivant, la génération Mavne est réussie.

$ ./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] ------------------------------------------------------------------------

Créer un fournisseur de services

Tout d'abord, créez un fournisseur de services.

Paramètres du serveur du fournisseur de services

Comme indiqué dans la configuration de l'application au début, définissez le port du fournisseur de services sur «18081» et le chemin du contexte sur «/ provider». Veillez également à utiliser la base de données basée sur les fichiers H2 afin que les informations de tâche gérées par l'API REST ne disparaissent pas au redémarrage de l'application.

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


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

Note:

Lors de l'exécution du fournisseur de services et du client sur le même hôte (tel que localhost), si le chemin du contexte (par défaut est /) est le même, le cookie qui gère l'ID de session peut entrer en conflit et la session HTTP peut ne pas être gérée correctement. Par conséquent, si vous souhaitez l'exécuter dans l'environnement local, vous devez également définir context-path.

Configuration de la table

Créez une table pour stocker les informations sur les tâches.

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);

Créer un objet de domaine

Créez un objet de domaine contenant les informations de tâche gérées par l'API REST.

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

}

Créer un référentiel

Créez une classe Repository pour l'objet de domaine qui contient les informations de tâche. Dans cette entrée, nous implémenterons en utilisant la fonction d'accès aux données (JdbcOperations) fournie par Spring Framework sans utiliser O / R Mapper tel que JPA. (En fait ... utilisez NamedParameterJdbcOperations qui peut gérer les paramètres basés sur le nom)

Note:

Les beans pour JdbcOperations et NamedParameterJdbcOperations sont définis par le mécanisme Spring Boot AutoConfigure, le développeur n'a donc pas besoin de définir explicitement les beans.

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));
	}

}

Création de RestController (API REST)

Créez une classe Controller qui fournit une opération CRUD (API REST) pour les informations de tâche. Etant donné que ʻEmptyResultDataAccessException se produit lorsque les données cibles ne sont pas trouvées lors de l'utilisation de JdbcTemplate, en plus de la méthode Handler pour l'API REST, implémentez une méthode de gestion des exceptions pour gérer ʻEmptyResultDataAccessException et renvoyer 404 Not Found. ..

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
	}

}

Vérification du fonctionnement de l'API REST

Vérifions si l'API REST que vous avez créée fonctionne correctement.

Lancer le fournisseur de services

Lancez l'application Spring Boot à l'aide du plug-in Maven fourni par Spring Boot.

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

Appel "API d'acquisition de liste d'informations de tâches"

Appelons innocemment "l'API d'acquisition de liste d'informations sur les tâches"! !!

$ $ 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"}

Apparemment, une authentification de base est requise. En effet, lorsque la configuration automatique de Spring Boot trouve une classe de Spring Security ... Par défaut, tous les chemins de requête (/ **) doivent avoir une authentification de base. Maintenant que nous voulons vérifier le fonctionnement de l'API REST, désactivons une fois l'authentification de base. L'authentification de base requise par Spring Boot peut être désactivée en ajoutant le paramètre security.basic.enabled = false.

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


security.basic.enabled=false

Warning:

** Veuillez activer l'authentification de base après avoir vérifié le fonctionnement de l'API REST! !! ** **

Vous pouvez maintenant obtenir une liste vide de tâches en accédant à l'API REST après avoir désactivé l'authentification de base.

$ 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

[]

Appel de "l'API de création d'informations sur les tâches"

Immédiatement après le démarrage, aucune information de tâche n'est enregistrée, appelons donc «l'API de création d'informations de tâche» pour créer des informations de tâche.

$ 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

Si les informations de tâche sont créées avec succès, l'URL pour accéder aux informations de tâche créées sera définie dans l'en-tête Location.

Appel "API d'acquisition d'informations sur les tâches"

Obtenons les informations de tâche créées en appelant "l'API d'acquisition d'informations de tâche" (en accédant à l'URL définie dans l'en-tête Location).

$ 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}

J'ai pu obtenir des informations sur la tâche, mais ... Le format de la date et de l'heure est un peu décevant, alors essayons de sortir la valeur formatée. Si vous souhaitez afficher la date / heure formatée, définissez spring.jackson.serialization.write-dates-as-timestamps = false.

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


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

Si vous y accédez à nouveau après avoir défini spring.jackson.serialization.write-dates-as-timestamps = false, vous pouvez voir qu'il est formaté comme ça.

$ 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}

Appel de "Update Task Information API"

Puisque c'est un gros problème ... j'appellerai l '"API de mise à jour des informations de tâche" pour mettre à jour les informations de tâche.

$ 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

Si vous obtenez les informations de tâche mises à jour, vous pouvez confirmer qu'elles ont été correctement mises à jour.

$ 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}

Appel de "Supprimer l'API des informations de tâche"

Appelez la dernière "API de suppression des informations de tâche" et essayez de supprimer les informations de tâche créées.

$ 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

Si vous essayez d'obtenir les informations de la tâche supprimée, vous obtiendrez une erreur client (404: introuvable) vous informant qu'il n'y a pas de données cibles.

$ 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

Configuration du serveur d'autorisation

Affectez @ EnableAuthorizationServer à la classe de configuration, définissez les beans requis pour l'authentification / l'autorisation OAuth et publiez le" point de terminaison d'autorisation "et le" point de terminaison de jeton "sur le serveur d'autorisation.

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 {
}

De plus, avec la valeur par défaut de Spring Boot, les valeurs suivantes sont attribuées de manière aléatoire au démarrage, définissez donc une valeur fixe.

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


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

En outre, spécifiez la portée et le type d'octroi que le client par défaut peut gérer. "Passowrd: informations d'identification du mot de passe du propriétaire de la ressource" n'est pas nécessaire en premier lieu, mais spécifiez-le pour vérifier le fonctionnement du serveur d'autorisation et du serveur de ressources à l'aide de CUI (commande cURL).

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


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

Si vous démarrez le fournisseur de services dans cet état, le journal suivant est généré et vous pouvez voir que le point de terminaison pour OAuth a été publié sur le serveur d'autorisation.

...
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)
...

Configuration du serveur de ressources

Affectez @ EnableAuthorizationServer à la classe de configuration, définissez les beans requis pour l'authentification / autorisation OAuth et remplacez configure (HttpSecurity) ʻof ResourceServerConfigurerAdapter` pour configurer les paramètres d'autorisation pour l'API REST.

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')");
	}
}

En définissant les paramètres ci-dessus, l'authentification / autorisation OAuth peut être appliquée aux demandes sous / api.

Vérification du fonctionnement du serveur d'autorisation / de ressources

Maintenant que vous avez configuré le serveur d'autorisation et le serveur de ressources, accédons au serveur de ressources.

$ 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"}

Une erreur s'est produite d'une manière ou d'une autre. Examen des détails de l'erreur ... Une erreur d'authentification (401 non autorisée) s'est produite et il a été informé qu'une authentification avec le jeton de support d'OAuth est requise.

Obtenez un jeton d'accès

Le but ultime de cette entrée est d'expliquer comment utiliser le "code d'autorisation" pour obtenir un jeton d'accès et accéder à l'API REST, mais avant tout ... des "ressources" qui vous permettent d'obtenir facilement un jeton d'accès. Obtenons un jeton d'accès pour accéder aux informations de la tâche à l'aide de "Identifiants de mot de passe du propriétaire".

$ 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"}

Accès au serveur de ressources

Spécifiez le jeton d'accès obtenu dans l'en-tête "Authorization" pour accéder à nouveau au serveur de ressources.

$ 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

[]

Puisque c'est un gros problème ... Créons une nouvelle tâche et récupérons les informations sur la tâche créée.

$ 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}

Création d'un client (interface utilisateur Web)

Maintenant que nous avons créé le fournisseur de services (serveur d'autorisation et serveur de ressources), nous allons créer une interface Web pour manipuler les informations de tâche gérées par le serveur de ressources.

Paramètres du serveur client

Comme introduit dans la configuration de l'application au début, définissez le port Cleint sur «18080» et le chemin de contexte sur «/ client».

client/src/main/resources/application.properties


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

Dans cette application, l'authentification utilisateur côté client utilise l'authentification de base configurée par Spring Boot. S'il s'agit de l'opération par défaut, le mot de passe de l'utilisateur par défaut changera chaque fois qu'il est démarré, donc corrigez le mot de passe.

client/src/main/resources/application.properties


security.user.password=password

Configuration du client

En attribuant @ EnableOAuth2Client à la classe de configuration, un composant qui gère les jetons d'accès (ʻOAuth2ClientContext) et un composant qui guide l'agent utilisateur (navigateur) vers le serveur d'autorisation pour obtenir l'autorisation du propriétaire de la ressource (ʻOAuth2ClientContext) ʻOAuth2ClientContextFilter) etc. est défini comme un bean. De plus, créez une définition Bean de RestTemplate (ʻOAuth2RestTemplate) étendue pour OAuth. ʻOAuth2RestTemplate` élimine le besoin pour l'application de connaître les processus liés à OAuth (comme le processus d'acquisition d'un jeton d'accès à partir d'un serveur d'autorisation), et appelle l'API REST de la même manière que lorsque l'authentification / l'autorisation par OAuth n'est pas effectuée. Vous pourrez.

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);
	}

}

En outre, ajoutez des paramètres liés à OAuth (URL d'API, URL du serveur d'autorisation, informations sur le client).

client/src/main/resources/application.properties


#URL de l'API
api.url=http://localhost:18081/provider/api

#URL du point de terminaison du serveur d'autorisation
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

#Paramètres des informations client
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.scope=read,write

Note:

Par défaut dans Spring Boot AutoConfigure, les beans sont définis pour accéder au serveur de ressources à l'aide du "flux d'octroi de code d'autorisation".

Créer un objet de domaine

Créez un objet de domaine contenant les informations de tâche avec lesquelles vous travaillez via l'API REST. (Copiez la classe Task créée lors de la création du fournisseur de services côté client)

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

}

Créer un référentiel

Créez une classe Repository pour l'objet de domaine qui contient les informations de tâche. Cette classe utilise la méthode de la classe d'implémentation d'interface RestOperations (ʻOAuth2RestTemplate`) fournie par Spring Security OAuth pour accéder aux informations de tâche gérées sur le serveur de ressources.

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);
	}

}

Créer un contrôleur

Créez une classe Controller qui fournit une interface utilisateur Web pour effectuer des opérations CRUD sur les informations de tâche.

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:

La date d'enregistrement (createdAt) et la date de mise à jour (ʻupdatedAt`) ne sont pas des éléments d'entrée et ne doivent pas être conservées en tant qu'éléments de formulaire, mais il est également possible d'obtenir des informations sur les tâches du serveur de ressources à chaque fois qu'une erreur de vérification d'entrée se produit. C'est subtil, donc c'est un peu rugueux, mais cette fois je vais l'inclure dans l'élément de formulaire.

Créer un écran de liste de tâches

Il affiche la liste des tâches obtenue à partir du serveur de ressources et fournit une interface utilisateur Web pour créer de nouvelles informations sur les tâches.

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>

Créer un écran de détails de tâche

Fournit une interface utilisateur Web pour afficher, mettre à jour et supprimer les informations de tâche acquises à partir du serveur de ressources.

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>

Découvrez le flux d'octroi de code d'autorisation

Maintenant que vous avez créé les applications du fournisseur de services et du client, utilisons réellement les applications et expérimentons le flux d'octroi de code d'autorisation! !!

Lancer l'application

Tout d'abord, lancez les applications du fournisseur de services et du client.

$ ./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)

Affichage de l'écran de la liste des tâches

Entrez http: // localhost: 18080 / client / tasks dans la barre d'adresse de votre navigateur pour afficher l'écran de la liste des tâches. Lors de l'accès pour la première fois, l'authentification de l'utilisateur (authentification de base) côté client est requise en premier, donc entrez le nom d'utilisateur (ʻuser) et le mot de passe (password`) dans la boîte de dialogue et appuyez sur le bouton" Connexion ".

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

Si l'authentification de l'utilisateur côté client réussit, vous serez redirigé vers le point de terminaison d'autorisation (/ oauth / authorize) fourni par le fournisseur de services pour obtenir l'octroi d'autorisation (code d'autorisation) du propriétaire de la ressource. Lors du premier accès, l'authentification du propriétaire de la ressource (authentification de base) est requise du côté du fournisseur de services, entrez donc le nom d'utilisateur (ʻuser) et le mot de passe (password`) dans la boîte de dialogue et appuyez sur le bouton" Connexion ". S'il vous plaît.

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

Si l'authentification du propriétaire de la ressource côté fournisseur de services est réussie, l'écran (écran d'autorisation) pour obtenir l'autorisation du propriétaire de la ressource pour l'étendue demandée par le client sera affiché, alors sélectionnez autoriser / refuser pour chaque étendue et sélectionnez " Cliquez sur le bouton "Autoriser". (Veuillez autoriser toutes les portées ici)

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

Note:

Cette entrée utilise l'écran d'autorisation fourni par Spring Security OAuth, mais je pense qu'il est courant de personnaliser dans le développement d'application réel, je voudrais donc présenter la méthode de personnalisation à partir de la prochaine fois.

Après autorisation du propriétaire de la ressource, l'écran de la liste des tâches s'affiche: en riant:

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

Enregistrement des tâches

Cependant ... je ne sais pas si les informations sur la tâche peuvent être correctement obtenues à partir du serveur de ressources car la tâche n'est pas enregistrée ...: sweat_smile: Alors ... Ensuite, enregistrons la tâche à l'aide de l'interface utilisateur Web. Entrez les informations de la tâche dans le formulaire de saisie sur l'écran de la liste des tâches et cliquez sur le bouton "Créer".

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

Afficher l'écran des détails de la tâche

Étant donné que la liste des tâches n'affiche rien d'autre que le titre, l'échéance et la date de création, affichons l'écran des détails de la tâche et vérifions les informations détaillées de la tâche. Le titre de la liste des tâches étant un lien, cliquez sur le titre (lien) de la tâche que vous souhaitez afficher.

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

Mettre à jour / supprimer des tâches

Pour mettre à jour ou supprimer une tâche, cliquez sur le bouton «Mettre à jour» ou «Supprimer» sur l'écran des détails de la tâche. Ici, la date limite est mise à jour au "15/03/2017". (De plus, la suppression est omise)

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

Essayez de rejeter les étendues d'écriture (certaines étendues)

Pour détruire le jeton d'accès et les informations d'autorisation, redémarrez le fournisseur de services ("Ctrl + C" + "./mvnw spring-boot: run") et affichez à nouveau l'écran de la liste des tâches. Ensuite ... L'écran d'autorisation du fournisseur de services sera affiché et l'accès à l'étendue d'écriture sera refusé.

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

Lorsque j'ai essayé de créer une nouvelle tâche sur l'écran de la liste des tâches ... Le message d'erreur «Portée insuffisante pour cette ressource» a été affiché et l'appel à «l'API de création de tâche» a été rejeté.

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

Essayez de rejeter toutes les portées

Pour détruire le jeton d'accès et les informations d'autorisation, redémarrez le fournisseur de services ("Ctrl + C" + "./mvnw spring-boot: run") et affichez à nouveau l'écran de la liste des tâches. Ensuite ... L'écran d'autorisation du fournisseur de services s'affichera et l'accès à toutes les étendues sera refusé. Ensuite ... Au lieu du "code d'autorisation", ajoutez un paramètre pour notifier que le propriétaire de la ressource a refusé l'accès et redirigez vers la page côté client.

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

** [Super Important] Éliminez les problèmes de sécurité de l'API REST! !! ** **

En fait, l'application (API REST) créée jusqu'à présent a un sérieux problème de sécurité. Savez-vous quel est le problème? C'est ... comment ... ** Vous pouvez vous référer et mettre à jour les informations de tâche des autres **: scream :: scream :: scream:

Il existe deux manières de résoudre ce problème.

La méthode que vous choisissez dépend de vos exigences en matière de sécurité. Le premier se comporte de la même manière que lors d'une opération sur une tâche qui n'existe pas (404 Not Found), tandis que le second se comporte comme un accès refusé (403 Forbidden), il est donc nécessaire d'enregistrer qu'il y a eu un accès non autorisé. Dans certains cas, je pense qu'il vaut mieux utiliser cette dernière méthode.

Ajouter un nom d'utilisateur à la condition dans SQL

Si vous souhaitez ajouter un nom d'utilisateur aux conditions d'accès aux informations de tâche (conditions SQL), ajoutez le nom d'utilisateur à l'argument de la méthode Repository et ajoutez le nom d'utilisateur aux conditions SQL. Ici, pour les processus de mise à jour et de suppression, l'appel de la méthode findOne est utilisé pour vérifier si le processus est destiné aux informations de tâche qui lui sont propres.

@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)); //★★★ Correction
	}

	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); //★★★ Ajouté
			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); //★★★ Ajouté
		jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
	}

}

Lorsque l'argument de la méthode Repository est modifié, la classe Controller est également modifiée.

@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()); //★★★ Correction
		URI createdTaskUri = relativeTo(uriBuilder)
				.withMethodCall(on(TaskRestController.class).getTask(task.getId(), principal)).build().encode().toUri(); //★★★ Correction
		return ResponseEntity.created(createdTaskUri).build();
	}

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

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

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

	// ...

}

Lors de l'accès aux informations de tâche d'une autre personne, une erreur (404 Not Found) se produit lorsque la ressource n'existe pas comme indiqué ci-dessous.

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

Effectuer la vérification du propriétaire

Si vous souhaitez vérifier si le propriétaire des informations de tâche et l'utilisateur des informations d'identification correspondent, il serait plus rapide d'utiliser le mécanisme de sécurité de méthode fourni par Spring Security.

Tout d'abord, accordez @ EnableGlobalMethodSecurity à la classe de configuration pour activer le mécanisme de sécurité de la méthode.

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


@EnableGlobalMethodSecurity(prePostEnabled = true) //★★★ Ajouté
@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
	// ...
}

Ensuite, définissez l'autorisation de sorte que seul le propriétaire de la ressource puisse accéder à la méthode qui acquiert les informations de tâche.

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


@Transactional
@Repository
public class TaskRepository {
	// ...
	@PostAuthorize("returnObject.username == authentication.name") //★★★ Ajouté
	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));
	}
	// ...
}

Enfin, modifiez-le pour appeler la méthode findOne lors de la mise à jour ou de la suppression.

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); //★★★ Ajouté
		task.setId(id);
		repository.save(task);
	}

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

	// ...

}

L'accès aux informations de tâche de quelqu'un d'autre entraînera une erreur d'autorisation (403 interdit) comme indiqué ci-dessous.

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

Version complète de l'application

La version complète de l'application créée dans cette entrée est publiée dans le référentiel GitHub suivant.

Résumé

L'explication est un peu longue, mais ... j'ai fait une application qui authentifie et autorise l'API REST en utilisant le flux d'octroi de code d'autorisation. Cette fois, le but était de vous permettre de découvrir (toucher) le flux d'octroi de code d'autorisation avec Spring Security OAuth + Spring Boot, alors tirez le meilleur parti du mécanisme de configuration automatique de Spring Boot (opération par défaut) et appliquez-le. J'ai essayé de faire. Cependant ... Dans le développement d'une application réelle, il est courant de gérer les informations utilisateur (propriétaire de la ressource) et client dans une base de données, etc., et les jetons d'accès et les informations d'autorisation sont également gérés dans la base de données au lieu d'être gérés dans la mémoire de l'application. Dans de nombreux cas, il est nécessaire de le rendre permanent. En outre, il peut y avoir des cas où le fournisseur de services gère plusieurs ressources au lieu d'un type, et des cas où le client accède également aux ressources de plusieurs fournisseurs de services, de sorte que l'application peut supporter le fonctionnement réel avec uniquement le contenu introduit cette fois. La réalité est qu'elle est difficile à développer. Donc ... À partir de la prochaine fois, j'aimerais expliquer l'architecture de Spring Security OAuth et vous présenter comment développer des applications en utilisant les points d'extension de Spring Boot, Spring Security et Spring Security OAuth.

À la prochaine! !!

Recommended Posts

Expérimentons le flux d'octroi de code d'autorisation avec Spring Security OAuth-Partie 2: Créer une application pour le moment
Expérimentons le flux d'octroi de code d'autorisation avec Spring Security OAuth-Part 1: Review of OAuth 2.0
Créer une application et la déployer pour la première fois avec heroku
Essayez d'exécuter Spring Cloud Config pour le moment
Hello World avec la bibliothèque d'extension Ruby pour le moment
Créez une application avec Spring Boot 2
Créez une application avec Spring Boot
Certification / autorisation avec Spring Security & Thymeleaf
Spring AOP pour la première fois
[First Java] Créez quelque chose qui fonctionne avec Intellij pour le moment
Code source pour trouver une base orthogonale normale avec la méthode d'orthogonalisation de Gram-Schmidt