Le modèle M.V.C. de Spring 2/2

Utiliser des données en session

Il existe trois solutions pour travailler facilement avec des données stockées en session.

Gérer facilement les données en session

Voila un exemple de contrôleur qui travaille sur un compteur stocké en session et récupéré via les paramètres des méthodes :

package mybootapp.web;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;

@Controller
@RequestMapping("/counter")
public class CounterController {

    class CounterBean {
        int value = 0;
    }

    @RequestMapping(value = "/init")
    @ResponseBody
    public String init(HttpSession session) {
        var counter = new CounterBean();
        session.setAttribute("counter", counter);
        return String.format("int counter = %d\n", counter.value);
    }

    @RequestMapping(value = "/show")
    @ResponseBody
    public String show(@SessionAttribute(required = false) CounterBean counter) {
        if (counter == null) {
            return ("counter is null\n");
        }
        return String.format("counter = %d\n", counter.value);
    }

    @RequestMapping(value = "/inc")
    @ResponseBody
    public String incCounter(@SessionAttribute CounterBean counter) {
        counter.value++;
        return (show(counter));
    }

}

Utiliser la portée dans Spring

Il est facile de récupérer des données placées en session, mais Spring nous offre le moyen d'injecter directement dans nos contrôleurs des données de portée session.

Étape 1 : définissez un nouveau bean pour représenter l'utilisateur courant :

package mybootapp.web;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

import lombok.Data;

@Component
@SessionScope
@Data
public class User {

    private String name;

}

L'annotation Component indique que c'est un composant géré par Spring. L'annotation SessionScope donne la portée des instances (une par session). Les portées RequestScope et ApplicationScope sont également disponibles. Ce n'est pas directement une instance qui va être injectée, mais un proxy qui va sélectionner la bonne instance (dans la bonne session) en fonction du contexte.

Étape 2 : définissez un contrôleur qui utilise l'injection du bean User :

package mybootapp.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller()
@RequestMapping("/user")
public class UserController {

    protected final Log logger = LogFactory.getLog(getClass());

    @Autowired()
    User user;

    @ModelAttribute("user")
    public User newUser() {
        return user;
    }

    @RequestMapping(value = "/show")
    public String show() {
        logger.info("show user " + user);
        return "user";
    }

    @RequestMapping(value = "/login")
    public String login() {
        logger.info("login user " + user);
        user.setName("It's me");
        return "user";
    }

    @RequestMapping(value = "/logout")
    public String logout() {
        logger.info("logout user " + user);
        user.setName("Anonymous");
        return "user";
    }
}

Étape 3 : La vue :

<%@ include file="/WEB-INF/jsp/header.jsp"%>

<c:url var="login"  value="/user/login" />
<c:url var="logout" value="/user/logout" />
<c:url var="show"   value="/user/show" />

<div class="container">
    <h1>User</h1>

    <p>
    name : <c:out value="${user.name}" default="no name"/> | 
    <a href="${show}">Show</a> | <a href="${login}">Login</a> |
    <a href="${logout}">Logout</a>
    </p>
</div>

<%@ include file="/WEB-INF/jsp/footer.jsp"%>

Moralité : Le contrôleur (qui est un singleton exécuté par plusieurs threads) utilise le proxy pour sélectionner automatiquement l'instance du bean User qui correspond à la requête courante et à la session courante.

La liaison se fait par le thread. C'est le même thread qui traite toute la requête (Dispatcher, contrôleur, vue). Le thread courant est donc utilisé comme une sorte de variable globale qui permet de faire des liaisons implicites.

Placer des données en session

Une deuxième solution consiste à indiquer, dans le contrôleur, les instances du modèle que Spring devra placer dans la session. Reprenons le même exemple mais avec un utilisateur simple (pas annoté) :

package mybootapp.web;

import lombok.Data;

@Data
public class SimpleUser {

    private String name;

}

Nous pouvons maintenant définir un nouveau contrôleur :

package mybootapp.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller()
@RequestMapping("/simple-user")
@SessionAttributes("simpleUser")
public class SimpleUserController {

    protected final Log logger = LogFactory.getLog(getClass());

    @ModelAttribute("simpleUser")
    public SimpleUser newUser() {
        var user = new SimpleUser();
        logger.info("new user " + user);
        return user;
    }

    @RequestMapping(value = "/show")
    public String show(@ModelAttribute("simpleUser") SimpleUser user) {
        logger.info("show user " + user);
        return "simple-user";
    }

    @RequestMapping(value = "/login")
    public String login(//
            @ModelAttribute("simpleUser") SimpleUser user, //
            RedirectAttributes attributes) {
        logger.info("login user " + user);
        user.setName("It's me");
        attributes.addFlashAttribute("message", "Bienvenue !");
        return "redirect:show";
    }

    @RequestMapping(value = "/logout")
    public String logout(//
            @ModelAttribute("simpleUser") SimpleUser user, //
            RedirectAttributes attributes) {
        logger.info("logout user " + user);
        user.setName("Anonymous");
        attributes.addFlashAttribute("message", "Au revoir.");
        return "redirect:show";
    }
}

Commentaire 1 : L'annotation @SessionAttributes permet d'indiquer quelles sont les instances du modèle qui doivent être placées en session. L'instance en question (simpleUser) est produite par la méthode newUser. Les méthodes traitants les requêtes peuvent récupérer cette instance pour la modifier.

Commentaire 2 : Je profite de cet exemple pour introduire les données flash qui sont destinées à être utilisées dans la requête suivante. C'est précisément le cas car, contrairement à la première version, les actions de /login et /logout renvoient une redirection vers /show.

Il ne reste plus qu'à définir la vue :

Fichier WEB-INF/jsp/simple-user.jsp
<%@ include file="/WEB-INF/jsp/header.jsp"%>

<c:url var="login"  value="/simple-user/login" />
<c:url var="logout" value="/simple-user/logout" />
<c:url var="show"   value="/simple-user/show" />

<div class="container">
    <h1>Simple User</h1>

    <c:if test="${message != null}">
        <div class="alert alert-success" role="alert">
            <c:out value="${message}" />
        </div>
    </c:if>

    <p>
        name :
        <c:out value="${simpleUser.name}" default="no name" />
        | <a href="${show}">Show</a> | <a href="${login}">Login</a> |
        <a href="${logout}">Logout</a>
    </p>
</div>

<%@ include file="/WEB-INF/jsp/footer.jsp"%>

Travail à faire : testez le bon fonctionnement de cet exemple.

Tester vos contrôleurs

Voila un exemple simple de test unitaire basé MockMvc qui permet de vérifier le bon fonctionnement des contrôleurs :

Travail à faire : Tester quelques possibilités des instances renvoyées par les méthodes statiques status(), view() et model().

Utiliser des intercepteurs

Vous pouvez très facilement installer des classes d'interception afin d'ajouter des opérations avant et après le traitement des requêtes. Définissez la classe suivante (elle vérifie l'adresse du client) :

package mybootapp.web;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class LoggerInterceptor implements HandlerInterceptor {

    private static Log log = LogFactory.getLog(LoggerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, //
            HttpServletResponse response, Object handler) throws Exception {
        var client = request.getRemoteAddr();
        log.info("Inside pre handle from " + client);
        switch (client) {
        case "127.0.0.1":
        case "0:0:0:0:0:0:0:1":
            return true;
        }
        response.getWriter().printf("Only 127.0.0.1");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, //
            HttpServletResponse response, //
            Object handler, //
            ModelAndView modelAndView) throws Exception {
        log.info("Inside post handle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, //
            HttpServletResponse response, Object handler, //
            Exception exception) throws Exception {
        log.info("Inside after completion");
    }
}

Pour installer cet intercepteur (il peut y avoir plusieurs), ajoutez la méthode ci-dessous à votre classe Starter :

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoggerInterceptor());
}

Travail à faire : Vérifiez que l'application n'est plus accessible à partir de l'adresse publique.

Gestion des erreurs

La récupération des erreurs est simplement réalisée par l'ajout d'un contrôleur spécifique dans lequel nous sommes capable de traiter plusieurs causes d'exception :

package mybootapp.web;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class ErrorControler implements ErrorController {

    @ResponseBody
    @ExceptionHandler({NullPointerException.class})
    public String handleNullPointerException(Exception e) {
        System.err.println("-- NullPointerException:");
        e.printStackTrace(System.err);
        return "Null Pointer Error";
    }

    @ResponseBody
    @ExceptionHandler
    public String handleOtherException(Exception e) {
        System.err.println("-- Other Exception:");
        e.printStackTrace(System.err);
        return "Other Error";
    }

}

Je ne rentre pas dans plus de détails, vous trouverez plus de détails sur cette page.

Très légère introduction aux API RESTfull

L'idée est simple :

Mise en place d'un controleur REST

Commencez par ajouter les dépendances pour Jackson (outils pour transformer une instance Java en Json et vice-versa) :

...
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <!-- <version>2.9.8</version> NOUVEAU -->
</dependency>
...

Créez ensuite une contrôleur REST (une calculatrice à pile) :

package mybootapp.web;

import java.util.Stack;

import javax.annotation.PostConstruct;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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;

@RestController
@RequestMapping("/calculator")
public class RestCalculator {

    protected final Log logger = LogFactory.getLog(getClass());
    private Stack<Integer> numbers = new Stack<>();

    @PostConstruct
    public void init() {
        numbers.push(100);
        numbers.push(200);
        numbers.push(300);
    }

    @GetMapping("/show")
    public Stack<Integer> show() {
        return numbers;
    }

    @GetMapping("/add")
    @ResponseStatus(HttpStatus.OK)
    public void add() {
        Integer val1 = numbers.pop();
        Integer val2 = numbers.pop();
        numbers.push(val1 + val2);
    }

    @PostMapping(value = "/put", consumes = "application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public String put(@RequestBody() Integer id) {
        numbers.push(id);
        logger.info(String.format("put %d", id));
        return "Ok";
    }

}

Testez l'API Rest avec des requêtes directes :

http://localhost:8081/calculator/show
http://localhost:8081/calculator/add
http://localhost:8081/calculator/show

La première donne les trois éléments de la pile. La second remplace les deux éléments de la tête de pile par leur addition. etc.

Vous pouvez ensuite déposer des données dans la pile en lancant une requete POST à l'aide de l'outil en ligne de commande curl :

Commandes à taper dans un shell
URL="http://localhost:8081/calculator/put"
curl -X POST -H "Content-Type: application/json" --data '222' $URL
curl -X POST -H "Content-Type: application/json" --data '333' $URL
curl http://localhost:8081/calculator/show

Une petite application REST

Nous allons maintenant créer un code javaScript coté client qui va interagir avec cette API REST. Commencez par le fichier javaScript suivant :

Fichier src/main/webapp/functions.js
function showStack() {
    var base = ($('<a href=".">')[0].href);
    $.ajax({
        type : 'GET',
        url : (base + "calculator/show"),
        data : '200',
        timeout : 3000,
        success : function(data) {
            $('#numbers').hide();
            $('#numbers').html("Stack: ");
            jQuery.each(data, function(i, val) {
                $("#numbers").append(" - ");
                $("#numbers").append(document.createTextNode(val));
            });
            $('#numbers').show();
        }
    });
}

function show() {
    showStack();
    $('#message').html("");
}

function add() {
    var base = ($('<a href=".">')[0].href);
    $.ajax({
        type : 'GET',
        url : (base + "calculator/add"),
        timeout : 3000,
        error : function() {
            $('#message').html('Addition impossible');
            showStack();
        },
        success : function(data) {
            $('#message').html('Addition réalisée');
            showStack();
        }
    });
}

function put() {
    var base = ($('<a href=".">')[0].href);
    var value = ($('#input').val());
    $.ajax({
        type : 'POST',
        url : (base + "calculator/put"),
        data : value,
        timeout : 3000,
        dataType : "json",
        contentType : "application/json",
        success : function(data) {
            showStack();
            $('#input').html("");
        },
        error : function() {
            showStack();
            $('#input').html("");
        }
    });
}

Ces fonctions JavaScript vont utiliser la méthode ajax de JQuery pour envoyer des requêtes asynchrones vers l'API REST.

Nous pouvons maintenant préparer une page JSP qui va produire une page HTML à destination d'un navigateur. La page chargée va utiliser JQuery et proposer une interface minimale pour lister les éléments de la pile, ajouter un nombre et calculer une addition.

Fichier src/main/webapp/rest-app.jsp
<%@ include file="/WEB-INF/jsp/header.jsp"%>

<c:url var="function" value="/functions.js" />

<div class="container">
        <script src="${function}"></script>
        <h1>Simple stack calculator (rest application)</h1>
        <p>
                <button onclick="show();">Show</button>
                <input id="input" size="10" />
                <button onclick="put();">put</button>
                <span> | </span>
                <button onclick="add();">+</button>
                <span> </span> <span style="color: blue;" id="message"></span>
        </p>
        <p id="numbers"></p>
</div>

<%@ include file="/WEB-INF/jsp/footer.jsp"%>

Travail à faire : prévoir l'opération de soustraction (fonction JavaScript, bouton html et requête /calculator/sub sur l'API REST.

Introduction à Spring security

Mise en place

Nous pouvons maintenant ajouter Spring Security : une couche des gestion de la sécurité. Pour ce faire, suivez les étapes ci-dessous :

Travail à faire :

Les tag de Spring Security

Protection CSRF :

Utiliser les données en BD

Objectif : utiliser notre BD pour représenter les utilisateurs authentifiés sur l'application.

package mybootapp.web;

import java.security.Principal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller()
@RequestMapping("/principal")
public class ShowPrincipal {

    protected final Log logger = LogFactory.getLog(getClass());

    @ResponseBody
    @RequestMapping("")
    public String show(Principal p) {
        logger.info("show user " + p);
        return p.toString();
    }

}

Contrôler les méthodes

Spring Security n'est pas seulement utile pour les contrôleurs Il est également capable de contrôler l'accès aux méthodes d'un service.

Définir ses conditions

Nous avons quelquefois besoin d'écrire le code de vérification d'un droit (par exemple pour savoir si un utilisateur a le droit d'agir sur une donnée précise). Nous devons, dans ce cas, définir un service de vérification :

package mybootapp.web.security;

import org.springframework.stereotype.Service;

@Service("securityChecker")
public class SecurityChecker {

    public boolean isOk(String userName) {
        return "aaa".equals(userName);
    }

}

Remarque : Ce service est trivial, mais nous pourrions faire des accès BD et des vérifications plus compliquées.

Travail à faire :