Skip to content

16. [Cours]: Proteger o acesso a um serviço web com o Spring Security

Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web / jSON seguro, cliente / servidor

16.1. Support

 

Os projetos deste capítulo estão na pasta [support / chap-16]. O script SQL serve para gerar o banco de dados necessário para os testes.

16.2. O papel do Spring Security em uma aplicação web

Vamos contextualizar o Spring Security no desenvolvimento de uma aplicação web. Na maioria das vezes, ela será construída sobre uma arquitetura multicamadas como a seguinte:

  • a camada [Spring Security] concede acesso à camada [web] apenas a usuários autorizados.

16.3. Um tutorial sobre o Spring Security

Vamos importar novamente um guia do Spring, seguindo as etapas 1 a 3 abaixo:

  

O projeto é composto pelos seguintes elementos:

  • na pasta [templates], encontram-se as páginas HTML do projeto;
  • [Application]: é a classe executável do projeto;
  • [MvcConfig]: é a classe de configuração do Spring MVC;
  • [WebSecurityConfig]: é a classe de configuração do Spring Security;

16.3.1. Configuração do Maven

O projeto [3] é um projeto Maven. Vamos examinar seu arquivo [pom.xml] para conhecer suas dependências:


<?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>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 10-14: o projeto é um projeto Spring Boot;
  • linhas 17-20: dependência do framework [Thymeleaf];
  • linhas 22-25: dependência do framework Spring Security;

16.3.2. As visualizações Thymeleaf

  

A visualização [home.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da tag [<a>]. O valor [@{/hello}] irá gerar o caminho [<context>/hello], onde [context] é o contexto do aplicativo web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

A visualização [hello.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • linha 9: O atributo [th:inline="text"] irá gerar o texto da tag [<h1>]. Esse texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] da consulta HTTP atual. Trata-se do nome do usuário conectado;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], em que [context] é o contexto do aplicativo web;

O código HTML gerado é o seguinte:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • linha 8: a tradução de Hello [[${#httpServletRequest.remoteUser}]]!;
  • linha 9: a tradução de @{/logout};
  • linha 11: um campo oculto chamado (atributo name) _csrf;

A visualização [login.html] é a seguinte:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • linha 9: o atributo [th:if="${param.error}"] faz com que a tag <div> só seja gerada se o URL, que exibe a página de login, contiver o parâmetro [error] (http://context/login?error);
  • linha 10: o atributo [th:if="${param.logout}"] faz com que a tag <div> só seja gerada se o URL, que exibe a página de login, contiver o parâmetro [logout] (http://context/login?logout);
  • linhas 11-23: um formulário HTML;
  • linha 11: o formulário será enviado para o URL [<context>/login], onde <context> é o contexto do aplicativo web;
  • linha 13: um campo de entrada chamado [username];
  • linha 17: um campo de entrada denominado [password];

O código HTML gerado é o seguinte:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>

        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

Observe-se, na linha 28, que o Thymeleaf adicionou um campo oculto chamado [_csrf].

16.3.3. Configuração do Spring para MVC

  

A classe [MvcConfig] configura o framework Spring MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • linha 7: a anotação [@Configuration] transforma a classe [MvcConfig] em uma classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para redefinir alguns de seus métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11 a 16: o método [addViewControllers] permite associar URL a visualizações HTML. São feitas as seguintes associações:
URL
visualização
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

O sufixo [html] e a pasta [templates] são os valores padrão utilizados pelo Thymeleaf. Eles podem ser alterados por meio da configuração. A pasta [templates] deve estar na raiz do Classpath do projeto:

Acima de [1], as pastas [java] e [resources] são ambas pastas de origem (source folders). Isso significa que seu conteúdo estará na raiz do Classpath do projeto. Portanto, no [2], as pastas [hello] e [templates] estarão na raiz do Classpath.

16.3.4. Configuração do Spring Security

  

A classe [WebSecurityConfig] configura o framework Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • linha 9: a anotação [@Configuration] transforma a classe [WebSecurityConfig] em uma classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] transforma a classe [WebSecurityConfig] em uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para redefinir alguns de seus métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13 a 16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso às diferentes URL do aplicativo;
  • linha 14: o método [http.authorizeRequests()] permite associar URL a direitos de acesso. São feitas as seguintes associações:
URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
acesso somente com autenticação
http.anyRequest().authenticated();
  • linha 15: define o método de autenticação. A autenticação é feita por meio de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também é acessível a todos;
  • linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gerencia os usuários;
  • linha 20: a autenticação é feita com usuários definidos de forma “estática” [auth.inMemoryAuthentication()]. Um usuário é definido aqui com o login [user], a senha [password] e a função [USER]. É possível conceder os mesmos direitos a usuários com a mesma função;

16.3.5. Classe executável

  

A classe [Application] é a seguinte:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

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

}
  • linha 8: a anotação [@EnableAutoConfiguration] solicita ao Spring Boot (linha 3) que realize a configuração que o desenvolvedor não terá feito explicitamente;
  • linha 9: transforma a classe [Application] em uma classe de configuração do Spring;
  • linha 10: solicita a varredura da pasta da classe [Application] para localizar componentes do Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, detectadas, pois possuem a anotação [@Configuration];
  • linha 13: o método [main] da classe executável;
  • linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparamos com esse processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto será implantado nele. Vimos que quatro instâncias de URL eram gerenciadas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

16.3.6. Testes da aplicação

Vamos começar solicitando o URL [/], que é um dos quatro URL aceitos. Ele está associado à visualização [/templates/home.html]:

 

A URL solicitada, [/], está acessível a todos. É por isso que a obtivemos. O link [here] é o seguinte:

Click <a href="/hello">here</a> to see a greeting.

O URL [/hello] será solicitado ao clicar no link. Este está protegido:

URL
regra
código
/, /home
acesso sem autenticação

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acesso restrito a usuários autenticados
http.anyRequest().authenticated();

É necessário estar autenticado para obtê-lo. O Spring Security redirecionará então o navegador do cliente para a página de autenticação. De acordo com a configuração exibida, trata-se da página URL [/login]. Esta página está acessível a todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Assim, obtemos [1]:

O código-fonte da página obtida é o seguinte:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • na linha 7, aparece um campo oculto que não está na página original [login.html]. Foi o Thymeleaf que o adicionou. Esse código, chamado CSRF (Cross Site Request Forgery), tem como objetivo eliminar uma falha de segurança. Esse token deve ser enviado de volta ao Spring Security junto com a autenticação para que esta seja aceita;

Lembramos que apenas o usuário user/password é reconhecido pelo Spring Security. Se inserirmos qualquer outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para a página URL [http://localhost:8080/login?error]. A presença do parâmetro [error] acionou a exibição da tag:


<div th:if="${param.error}">Invalid username and password.</div>

Agora, vamos inserir os valores esperados para usuário/senha [4]:

  • em [4], fazemos o login;
  • em [5], o Spring Security nos redireciona para URL [/hello], pois era URL que estávamos solicitando quando fomos redirecionados para a página de login. A identidade do usuário foi exibida na seguinte linha de [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

A página [5] exibe o seguinte formulário:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Ao clicar no botão [Sign Out], será executado um POST no URL [/logout]. Este, assim como o URL e o [/login], está acessível a todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Em nossa associação URL / visualizações, não definimos nada para o URL e o [/logout]. O que vai acontecer? Vamos testar:

  • no [6], clicamos no botão [Sign Out];
  • em [7], vemos que fomos redirecionados para o URL [http://localhost:8080/login?logout]. Foi o Spring Security que solicitou esse redirecionamento. A presença do parâmetro [logout] na página URL fez com que a seguinte linha fosse exibida na visualização:

<div th:if="${param.logout}">You have been logged out.</div>

16.3.7. Conclusão

No exemplo anterior, poderíamos ter escrito a aplicação web primeiro e, em seguida, implementado a segurança. O Spring Security não é intrusivo. É possível implementar a segurança em uma aplicação web já desenvolvida. Além disso, descobrimos os seguintes pontos:

  • é possível definir uma página de autenticação;
  • a autenticação deve ser acompanhada do token CSRF emitido pelo Spring Security;
  • se a autenticação falhar, o usuário é redirecionado para a página de autenticação, além disso, com um parâmetro “error” no token URL;
  • se a autenticação for bem-sucedida, o usuário é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se a página de autenticação for solicitada diretamente, sem passar por uma página intermediária, o Spring Security nos redireciona para o URL [/] (esse caso não foi apresentado);
  • o logout é realizado ao acessar a página URL [/logout] com um POST. O Spring Security nos redireciona então para a página de autenticação com o parâmetro logout no URL;

Todas essas conclusões se baseiam nos comportamentos padrão do Spring Security. Esses comportamentos podem ser alterados por meio de configuração, redefinindo determinados métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior não nos ajudará muito daqui para frente. De fato, vamos utilizar:

  • um banco de dados para armazenar os usuários, suas senhas e suas funções;
  • autenticação por cabeçalho HTTP;

Existem poucos tutoriais sobre o que queremos fazer aqui. A solução que será proposta é uma combinação de códigos encontrados aqui e ali.

16.4. Implementação da segurança no serviço web/json dos produtos

16.4.1. O banco de dados

O banco de dados [dbintrospringdata] está sendo atualizado para incluir os usuários, suas senhas e suas funções. Três novas tabelas são criadas:

Image

Tabela [USERS]: os usuários

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • IDENTITY: uma identidade descritiva do usuário;
  • LOGIN: o login do usuário;
  • PASSWORD: sua senha;

Na tabela USERS, as senhas não são armazenadas em texto simples:

 

O algoritmo que criptografa as senhas é o algoritmo BCRYPT.

Tabela [ROLES]: as funções

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • NAME: nome da função. Por padrão, o Spring Security espera nomes no formato ROLE_XX, por exemplo, ROLE_ADMIN ou ROLE_GUEST;
 

Tabela [USERS_ROLES]: tabela de junção USERS / ROLES

Um usuário pode ter várias funções, e uma função pode abranger vários usuários. Temos uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de controle de versão da linha;
  • USER_ID: identificador de um usuário;
  • ROLE_ID: identificador de uma função;
 

16.4.2. O projeto Eclipse

Criamos o seguinte projeto Eclipse:

1
  

  • em [1]: o novo projeto com os seguintes pacotes:
    • [spring.security.entities]: contém as entidades JPA correspondentes às três novas tabelas do banco de dados;
    • [spring.security.repositories]: contém os [repositories] do Spring Data associados às três novas tabelas;
    • [spring.security.dao]: contém um serviço baseado nos [repositories];
    • [spring.security.config]: contém a configuração do projeto e, em especial, a configuração dos acessos seguros ao serviço web;
    • [spring.security.boot]: contém a classe de inicialização do serviço web seguro;

16.4.3. A configuração do Maven

O novo projeto é um projeto Maven configurado pelo seguinte arquivo [pom.xml]:


<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>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-spring-security-server-01</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Logs do Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Teste do Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- plug-ins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • linhas 23-27: retoma-se o que já existe com o arquivo do serviço web / json analisado;
  • linhas 29-32: a dependência que traz as classes do Spring Security;
  • linhas 34-37: a biblioteca de logs;
  • linhas 39-42: a biblioteca que permite usar as anotações do Spring Boot;
  • linhas 44-48: a biblioteca necessária para os testes;

16.4.4. As novas entidades [JPA]

A camada JPA define três novas entidades:

  

A classe [User] é a imagem da tabela [USERS]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {

    // propriedades
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;

    // Construtor
    public User() {
    }

    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }

    // getters e setters
...
}
  • linha 11: a classe estende a classe [AbstractEntity], já utilizada para as outras entidades;

A classe [Role] é a representação da tabela [ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    // propriedades
    @Column(name="NAME")
    private String name;

    // construtores
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // getters e setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

A classe [UserRole] é a imagem da tabela [USERS_ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    // chaves estrangeiras
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // um UserRole faz referência a um User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // um UserRole faz referência a uma Função
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // construtores
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // getters e setters
...
}

  • linhas 22-24: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
  • linhas 27-29: definem a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];

16.4.5. As [repositories]

Cada uma das entidades JPA acima é gerenciada por um [repository] do Spring Data:

  

A interface [UserRepository] gerencia o acesso às entidades [User]:


package spring.security.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;
import spring.security.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // lista de funções de um usuário identificado por seu ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista de funções de um usuário identificado por seu login exclusivo
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // busca de um usuário por meio de seu login
    User findUserByLogin(String login);
}
  • linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 4);
  • linhas 12-13: o método [getRoles(User user)] permite obter todas as funções de um usuário identificado por seu [id]
  • linhas 16-17: o mesmo, mas para um usuário identificado por seu login/senha;
  • linha 20: para localizar um usuário por meio de seu login;

A interface [RoleRepository] gerencia os acessos às entidades [Role]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // busca de uma função pelo nome
    Role findRoleByName(String name);

}
  • linha 7: a interface [RoleRepository] estende a interface [CrudRepository];
  • linha 10: é possível pesquisar uma função pelo nome;

A interface [UserRoleRepository] gerencia os acessos às entidades [UserRole]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • linha 5: a interface [UserRoleRepository] limita-se a estender a interface [CrudRepository] sem adicionar novos métodos a ela;

16.4.6. As classes de gerenciamento de usuários e funções

  

O Spring Security exige a criação de uma classe que implemente a seguinte interface [UsersDetail]:

 

Essa interface é implementada aqui pela classe [AppUserDetails]:


package spring.security.dao;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // propriedades
    private User user;
    private UserRepository userRepository;

    // construtores
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------interface
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // getters e setters
    ...
}
  • linha 14: a classe [AppUserDetails] implementa a interface [UserDetails];
  • linhas 19-20: a classe encapsula um usuário (linha 19) e o repositório que permite obter os detalhes desse usuário (linha 20);
  • linhas 26-29: o construtor que instancia a classe com um usuário e seu repositório;
  • linhas 32-36: implementação do método [getAuthorities] da interface [UserDetails]. Ele deve construir uma coleção de elementos do tipo [GrantedAuthority] ou derivado. Aqui, utilizamos o tipo derivado [SimpleGrantedAuthority] (linha 36), que encapsula o nome de uma das funções do usuário da linha 19;
  • linhas 35-37: percorre-se a lista de funções do usuário da linha 19 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 42-44: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a senha do usuário da linha 19;
  • linhas 42-44: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o login do usuário da linha 19;
  • linhas 51-54: a conta do usuário nunca expira;
  • linhas 56-59: a conta do usuário nunca é bloqueada;
  • linhas 61-64: as credenciais do usuário nunca expiram;
  • linhas 66-69: a conta do usuário está sempre ativa;

O Spring Security também exige a existência de uma classe que implemente a interface [AppUserDetailsService]:

 

Essa interface é implementada pela seguinte classe [AppUserDetailsService]:


package spring.security.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // buscando o usuário pelo login
        User user = userRepository.findUserByLogin(login);
        // encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // retornamos os detalhes do usuário
        return new AppUserDetails(user, userRepository);
    }

}
  • linha 12: a classe será um componente Spring e, portanto, estará disponível em seu contexto;
  • linhas 15-16: o componente [UserRepository] será injetado aqui;
  • linhas 19-28: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o login do usuário;
  • linha 21: o usuário é pesquisado por meio de seu login;
  • linhas 23-25: se ele não for encontrado, uma exceção é lançada;
  • linha 27: um objeto [AppUserDetails] é criado e renderizado. Ele é, de fato, do tipo [UserDetails] (linha 19);

16.4.7. A configuração do projeto

O projeto é configurado por duas classes:

A classe [DaoConfig] configura a camada [DAO] introduzida pelo novo projeto:


package spring.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {

    // constantes
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}
  • linha 10: importa-se a classe de configuração [spring.data.config.DaoConfig] do projeto [intro-spring-data-01], que implementa a camada [DAO] de produtos e categorias;
  • linha 8: indicam-se as pastas do projeto atual que contêm os componentes Spring Data [repositories];
  • linha 9: indicam-se as pastas do projeto atual que contêm componentes Spring relativos à camada [DAO];
  • linha 14: indicam-se as pastas que contêm as entidades JPA. Existem as do projeto [intro-spring-data-01] e as do projeto do servidor seguro. Essa informação é objeto do bean das linhas 16 a 19. Esse bean redefine o bean de mesmo nome do projeto [intro-spring-data-01]:

    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
    
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
}

Na camada [DAO], a linha 8 verifica as pastas indicadas na linha 1. Devido à redefinição do bean das linhas 14 a 17 no projeto seguro (linhas 16 a 19), a linha 8 acima passará a verificar as pastas ["spring.data.entities", "spring.security.entities"]. É importante observar que a classe importada na linha 10 da classe [spring.security.config.DaoConfig] deve conter a anotação [@Configuration]; caso contrário, o fenômeno que acabamos de explicar não funcionará.

A classe [SecurityConfig] configura o aspecto de segurança do projeto. Já nos deparamos com uma classe de configuração do Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremos o mesmo procedimento:

  • linha 11: definir uma classe que estenda a classe [WebSecurityConfigurerAdapter];
  • linha 13: definir um método [configure(HttpSecurity http)] que define os direitos de acesso às diferentes URL do serviço web;
  • linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os usuários e suas funções;

A classe [SecurityConfig] será a seguinte:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;

@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // proteção
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // a autenticação é feita pelo bean [appUserDetailsService]
        // a senha é criptografada pelo algoritmo de hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicativo seguro?
        if (activateSecurity) {
            // a senha é transmitida pelo cabeçalho Authorization: Basic xxxx
            http.httpBasic();
            // o método HTTP OPTIONS deve ser autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // apenas a função ADMIN pode utilizar o aplicativo
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todos os URL
                    .hasRole("ADMIN");
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 16: para ativar os componentes do Spring Security;
  • linha 17: adicionamos os componentes Spring do pacote [spring.security.service];
  • linha 18: importam-se os beans da camada [DAO] que acabamos de apresentar, bem como os do servidor web / jSON não seguro;
  • linhas 21-22: a classe [AppUserDetails], que dá acesso aos usuários do aplicativo, é injetada;
  • linha 25: um booleano que protege (true) ou não (false) o aplicativo web;
  • linhas 27-32: o método [configure(HttpSecurity http)] define os usuários e suas funções. Ele recebe como parâmetro um tipo [AuthenticationManagerBuilder]. Esse parâmetro é complementado com duas informações (linha 38):
    • uma referência ao serviço [appUserDetailsService] da linha 22, que dá acesso aos usuários registrados. Observe-se aqui que o fato de eles estarem registrados em um banco de dados não é mencionado. Portanto, eles poderiam estar em um cache, fornecidos por um serviço web, etc.
    • o tipo de criptografia utilizado para a senha. Vale lembrar que utilizamos o algoritmo BCrypt;
  • linhas 34-52: o método [configure(HttpSecurity http)] define os direitos de acesso aos URL do serviço web;
  • linha 37: vimos no projeto de introdução que, por padrão, o Spring Security gerencia um token CSRF (Cross Site Request Forgery) que o usuário que deseja se autenticar deve enviar de volta ao servidor. Aqui, esse mecanismo está desativado. Isso, aliado ao valor booleano (isSecured=false), permite utilizar o aplicativo web sem segurança;
  • linha 41: ativamos o modo de autenticação por cabeçalho HTTP. O cliente deverá enviar o seguinte cabeçalho HTTP:
Authorization:Basic code

onde “code” é a codificação da sequência “login:password” pelo algoritmo Base64. Por exemplo, a codificação Base64 da sequência admin:admin é YWRtaW46YWRtaW4=. Portanto, o usuário com login [admin] e senha [admin] enviará o cabeçalho HTTP a seguir para se autenticar:

Authorization:Basic YWRtaW46YWRtaW4=
  • linhas 46-48: indicam que todos os URL do serviço web estão acessíveis aos usuários com a função [ROLE_ADMIN]. Isso significa que um usuário que não possua essa função não pode acessar o serviço web;
  • linha 50: no modo [session], um usuário que já se autenticou uma vez não precisa fazer isso novamente nos acessos seguintes. Aqui, desativamos esse modo; portanto, o usuário deverá se autenticar a cada acesso;

16.4.8. Testes da camada [DAO]

  

Primeiramente, criamos uma classe executável [CreateUser] capaz de criar um usuário com uma função:


package sprin.security.tests;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // sintaxe: login senha roleName

        // são necessários três parâmetros
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // recuperamos os parâmetros
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contexto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // a função já existe?
        Role role = roleRepository.findRoleByName(roleName);
        // se não existir, ele é criado
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // o usuário já existe?
        User user = userRepository.findUserByLogin(login);
        // se não existir, criá-lo-emos
        if (user == null) {
            // a senha é hashada com bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // salvamos o usuário
            user = userRepository.save(new User(login, login, crypt));
            // criamos a associação com a função
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // o usuário já existe — ele possui a função solicitada?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // se não for encontrado, cria-se a relação com a função
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // fechamento do contexto Spring
        context.close();
        // fim
        System.out.println("Travail terminé...");
    }

}
  • linha 17: a classe espera três argumentos que definem um usuário: seu login, sua senha e sua função;
  • linhas 25-27: os três parâmetros são recuperados;
  • linha 29: o contexto Spring é construído a partir da classe de configuração [AppConfig];
  • linhas 30-32: recuperam-se as referências das três instâncias de [Repository] que podem ser úteis para criar o usuário;
  • linha 34: verifica-se se a função já existe;
  • linhas 36-38: se não for o caso, ele é criado no banco de dados. Ele terá um nome do tipo [ROLE_XX];
  • linha 40: verifica-se se o login já existe;
  • linhas 42-49: se o login não existir, criamo-lo no banco de dados;
  • linha 44: criptografamos a senha. Aqui, utilizamos a classe [BCrypt] do Spring Security (linha 4). Portanto, precisamos dos arquivos desse framework. O arquivo [pom.xml] inclui essa dependência:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • linha 46: o usuário é armazenado no banco de dados;
  • linha 48: assim como a relação que o vincula à sua função;
  • linhas 51-57: caso o login já exista – verifica-se, então, se entre suas funções já se encontra a função que se deseja atribuir a ele;
  • linhas 59-61: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para vincular o usuário à sua função;
  • não há proteção contra possíveis exceções. Trata-se de uma classe de suporte para criar rapidamente um usuário com uma função.

Ao executar a classe com os argumentos [x x guest], obtêm-se, no banco de dados, os seguintes resultados:

Tabela [USERS]

Tabela [ROLES]

 

Tabela [USERS_ROLES]

 

Consideremos agora a segunda classe [UsersTest], que é um teste JUnit:

  

package spring.security.tests;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // mapeador jSON
    private ObjectMapper mapper = new ObjectMapper();

    @Test
    public void findAllUsersWithTheirRoles() throws JsonProcessingException {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // recupera-se o usuário [admin]
        User user = userRepository.findUserByLogin("admin");
        // verifica-se se a senha dele é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // verifica-se a função de admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // recuperamos o usuário [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // verifica-se se a senha dele é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // verifica-se a função de admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // método utilitário — exibe os elementos de uma coleção
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • linhas 37-44: teste visual. Exibimos todos os usuários com suas funções;
  • linhas 46-56: verifica-se se o usuário [admin] possui a senha [admin] e a função [ROLE_ADMIN] utilizando o repositório [UserRepository];
  • linha 51: [admin] é a senha em texto simples. Na base, ela está criptografada de acordo com o algoritmo BCrypt. O método [BCrypt.checkpw] permite verificar se a senha em texto simples, uma vez criptografada, é de fato igual à que está na base;
  • linhas 58-69: verifica-se se o usuário [admin] possui a senha [admin] e a função [ROLE_ADMIN] utilizando o serviço [appUserDetailsService];

A execução dos testes é bem-sucedida com os seguintes logs:

----------Utilisateur [{"id":14,"version":0,"identity":"admin","login":"admin","password":"$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG"}]
Roles :
{"id":6,"version":0,"name":"ROLE_ADMIN"}

----------Utilisateur [{"id":15,"version":0,"identity":"user","login":"user","password":"$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq"}]
Roles :
{"id":7,"version":0,"name":"ROLE_USER"}

----------Utilisateur [{"id":16,"version":0,"identity":"guest","login":"guest","password":"$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

----------Utilisateur [{"id":17,"version":0,"identity":"x","login":"x","password":"$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

16.4.9. Testes do serviço web

Vamos testar o serviço web com o cliente Chrome [Advanced Rest Client]. Precisaremos especificar o cabeçalho de autenticação HTTP:

Authorization:Basic code

onde [code] é o código Base64 da sequência [login:password]. Para gerar esse código, pode-se usar o seguinte programa:

  

package spring.security.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // são esperados dois argumentos: login e senha
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // recupera-se os dois argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // codifica a string
        byte[] data = Base64.encode(chaîne.getBytes());
        // exibe sua codificação Base64
        System.out.println(new String(data));
    }

}

Se executarmos esse programa com os dois argumentos [admin admin]:

  

obtemos o seguinte resultado:

YWRtaW46YWRtaW4=

Agora que sabemos como gerar o cabeçalho de autenticação HTTP, iniciamos o serviço web seguro e, em seguida, com o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os produtos:

  • em [1], solicitamos o URL das categorias;
  • em [2], com o método GET;
  • em [3], fornecemos o cabeçalho HTTP da autenticação. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da sequência [admin:admin];
  • em [4], enviamos o comando HTTP;

A resposta do servidor é a seguinte:

  • em [1], o cabeçalho de autenticação HTTP;
  • em [2], o servidor retorna uma resposta jSON;

Conseguimos obter a lista de categorias:

 

Vamos agora tentar uma solicitação HTTP com um cabeçalho de autenticação incorreto. A resposta é então a seguinte:

  • em [1]: o cabeçalho de autenticação HTTP;

Obtemos a seguinte resposta:

  • em [2]: a resposta do serviço web;

Agora, vamos tentar com o usuário user / user. Ele existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [user user]:

  

obtemos o seguinte resultado:

dXNlcjp1c2Vy
  • em [1]: o cabeçalho de autenticação HTTP está incorreto;
  • em [2]: a resposta do serviço web. Ela é diferente da anterior, que era [401 Unauthorized]. Desta vez, o usuário se autenticou corretamente, mas não possui permissões suficientes para acessar o URL;

Nosso serviço web seguro já está operacional.

16.4.10. Um URL de autenticação

  

Vamos criar um URL que nos permitirá saber se um usuário está autorizado ou não a acessar o serviço web. Para isso, criamos o novo controlador MVC [AuthenticateController] a seguir:


package spring.security.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.webjson.models.Response;

@Controller
public class AuthenticateController {

    // dependências do Spring
    @Autowired
    private ApplicationContext context;

    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        // resposta jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
    }

}
  • linha 15: a classe [AuthenticateController] é um controlador Spring. Nessa função, ela expõe URL;
  • linha 22: expõe o URL [/authenticate];
  • linha 23: o resultado do método será enviado diretamente ao cliente;
  • linhas 26-27: o método se limita a retornar um objeto [Response] vazio, mas com um [status] igual a 0, indicando que não houve erro;

Para que serve esse URL? Quando quisermos simplesmente autenticar um usuário, vamos solicitá-lo. Vimos que, se a camada de segurança não aceitar esse usuário, ela retorna uma exceção. Aqui está um exemplo;

Com o usuário [admin:admin]:

Recebemos uma resposta vazia, mas sem exceção.

Com o usuário [user:user]:

Ocorreu uma exceção.

16.4.11. Conclusão

A adição das classes necessárias ao Spring Security pôde ser realizada sem alterações no projeto web/json original. Esse cenário muito favorável decorre do fato de que as três tabelas adicionadas ao banco de dados são independentes das tabelas existentes. Elas poderiam até mesmo ter sido colocadas em um banco de dados separado. Em outros casos, as tabelas adicionadas podem ter relações com as tabelas existentes. Nesse caso, é necessário modificar as entidades JPA, o que geralmente afeta todas as camadas do projeto.

16.5. Um cliente programado para o serviço web / jSON seguro

Já escrevemos um cliente para o serviço web / jSON não seguro:

Agora, vamos criar um cliente programado para o serviço web seguro:

Vamos duplicar o projeto já criado, [intro-webjson-client], em um novo projeto, [intro-spring-security-client-01]:

  

16.5.1. A classe [AbstractDao]

A classe [AbstractDao] garante a comunicação HTTP com o servidor web / jSON seguro. Como acabamos de ver, nessa comunicação HTTP, o cliente deve agora enviar um cabeçalho de autenticação, por exemplo:

Authorization:Basic YWRtaW46YWRtaW4=

Isso é feito da seguinte maneira:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

    // dados
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // solicitação genérica
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL para contato

  • linha 15: o método genérico [getResponse], responsável pela comunicação HTTP com o serviço web seguro, agora aceita como primeiro parâmetro o usuário que solicita um URL. A classe [User] é a seguinte:

Esta classe é a seguinte:

  

package spring.security.client.entities;

public class User {

    // propriedades
    private String login;
    private String password;

    // construtor
    public User() {
    }

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // getters e setters
...
}

O método [getResponse] passa a ser o seguinte:


package spring.security.client.dao;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;

import spring.security.client.entities.User;

public abstract class AbstractDao {

    // dados
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    private String getBase64(User user) {
        // codifica o usuário e sua senha em base 64 — requer Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // solicitação genérica
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL – entre em contato
        // jsonPost: o valor jSON a ser enviado
        try {
            // execução da solicitação
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }

            // a consulta está sendo executada
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • linhas 42-44, 49-51: se o usuário [user] não for nulo, então adiciona-se o cabeçalho de autenticação. A codificação Base64 do usuário e de sua senha é realizada pelo método [getBase64] nas linhas 25-29. É importante observar que esse método utiliza uma classe [Base64] pertencente ao JDK 1.8.
  • Fora das linhas anteriores, o código permanece inalterado;

16.5.2. A interface [IDao]

Todos os métodos da interface [IDao] recebem um parâmetro adicional [User user]:

  

package spring.security.client.dao;

import java.util.List;

import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;

public interface IDaoClient {

    // autenticação
    public void authenticate(User user);

    // inserção de uma lista de produtos
    public List<Produit> addProduits(User user, List<Produit> produits);

    // exclusão de todos os produtos
    public void deleteAllProduits(User user);

    // atualização de uma lista de produtos
    public List<Produit> updateProduits(User user, List<Produit> produits);

    // obter todos os produtos
    public List<Produit> getAllProduits(User user);

    // inserção de uma lista de categorias
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // exclusão de todas as categorias
    public void deleteAllCategories(User user);

    // atualização de uma lista de categorias
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // obter todas as categorias
    public List<Categorie> getAllCategories(User user);

    // um produto específico
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);

    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);

    public Produit getProduitByNameWithCategorie(User user, String nom);

    public Produit getProduitByNameWithoutCategorie(User user, String nom);

    // uma categoria específica
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);

    public Categorie getCategorieByNameWithProduits(User user, String nom);

    public Categorie getCategorieByNameWithoutProduits(User user, String nom);

}
  • linha 12: adicionamos o método [authenticate(User user)] para autenticar um usuário. Ele lança uma exceção se o usuário não tiver permissão para acessar o URL [/authenticate] do serviço web;

16.5.3. A classe [Dao]

Todos os métodos da classe [Dao] recebem um parâmetro adicional [User user], que eles passam para o método genérico [getResponse] da classe [AbstractDao]. Aqui estão dois exemplos:


// autenticação
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- adicionar produtos (sem suas categorias)
        try {
            // mapeadores jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // solicitação
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // erro?
            if (response.getStatus() != 0) {
                // lançamos 1 exceção
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // retornando o corpo da resposta do servidor
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. Testes unitários da classe [Dao]

A classe [Test01] de testes unitários da classe [Dao] é modificada da seguinte forma:

  

package client.tests.junit;

...

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // contexto Spring
    @Autowired
    private ApplicationContext context;
    // camada [DAO]
    @Autowired
    private IDaoClient dao;

    // usuários
    static private User admin;
    static private User user;
    static private User unknown;

    @BeforeClass
    public static void init() {
        admin = new User("admin", "admin");
        user = new User("user", "user");
        unknown = new User("x", "y");
    }

    @Before
    public void cleanAndFill() {
        // limpa-se o banco de dados antes de cada teste
        log("Vidage de la base de données", 1);
        // esvazia-se a tabela [CATEGORIES] — em cascata, a tabela [PRODUITS] será esvaziada
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // preenche-se as tabelas
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // adição da categoria — por efeito em cascata, os produtos também serão inseridos
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lista de categorias
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // lista de produtos
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // algumas verificações
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }

    @Test()
    public void checkUserUnknown() {
        ServiceException se = null;
        try {
            dao.authenticate(unknown);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
    }

    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • Durante a inicialização da classe de teste, nas linhas 21 a 26, são criados três usuários:
    • o usuário [admin] tem acesso ao URL do serviço web, teste das linhas 96 a 104;
    • o usuário [user] existe, mas não está autorizado a usar o URL do serviço web, teste nas linhas 71 a 81;
    • o usuário [unknown] não existe, teste nas linhas 83-93;
  • os métodos de teste são os mesmos já vistos para o serviço web não seguro, com a diferença de que os métodos da interface [IDaoClient] são chamados com o usuário [admin] como primeiro parâmetro, o qual tem permissão para utilizar os URL;

O teste é aprovado, mas é possível observar que ele é mais lento do que com o serviço web não seguro. A segurança de um aplicativo aumenta significativamente seus tempos de resposta. É possível observar um fator importante no desempenho do serviço web seguro: na classe [AppConfig], que o configura, escrevemos:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicativo seguro?
        if (activateSecurity) {
            // a senha é transmitida pelo cabeçalho Authorization: Basic xxxx
            http.httpBasic();
            // o método HTTP OPTIONS deve ser autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // apenas a função ADMIN pode utilizar o aplicativo
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

A linha 17 tem um custo. Ela obriga o usuário a se autenticar a cada acesso. Se a colocarmos entre comentários, a duração do teste JUnit anterior passa de 10,57 segundos para 4,21 segundos, isso porque o usuário [admin] só se autentica no primeiro teste e não nos seguintes (mesmo que o cabeçalho de autenticação HTTP seja enviado pelo cliente, o servidor não verifica novamente a senha do usuário). Com um serviço web não seguro, a duração do teste JUnit cai para 2,33 segundos.