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.
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.
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.
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.
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.
L'application créée dans cette entrée est vérifiée à l'aide des versions suivantes de la bibliothèque.
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.
$ 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 -
$ curl -s https://start.spring.io/starter.tgz\
-d name=client\
-d artifactId=client\
-d dependencies=thymeleaf\
-d baseDir=client\
| tar -xzvf -
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>
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>
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é
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] ------------------------------------------------------------------------
Tout d'abord, créez un 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éfinircontext-path
.
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é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é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
etNamedParameterJdbcOperations
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é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érifions si l'API REST que vous avez créée fonctionne correctement.
Lancez l'application Spring Boot à l'aide du plug-in Maven fourni par Spring Boot.
$ ./mvnw -pl service-provider spring-boot:run
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
[]
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
.
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}
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}
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
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)
...
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
.
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.
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"}
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}
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.
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
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é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é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é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.
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>
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>
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! !!
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)
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 ".
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.
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)
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:
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".
É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.
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)
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é.
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é.
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.
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.
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.
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.
La version complète de l'application créée dans cette entrée est publiée dans le référentiel GitHub suivant.
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