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 encontram-se na pasta [support / chap-16]. O script SQL serve para gerar a base de dados necessária para os testes.

16.2. O papel do Spring Security numa aplicação Web

Vamos contextualizar o Spring Security no desenvolvimento de uma aplicação Web. Na maioria das vezes, esta será construída com base numa arquitetura multicamadas, como a seguinte:

  • a camada [Spring Security] concede acesso à camada [web] apenas aos utilizadores autorizados.

16.3. Um tutorial sobre o Spring Security

Vamos importar novamente um guia do Spring, seguindo os passos 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 analisar o seu ficheiro [pom.xml] para conhecer as 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 vistas Thymeleaf

  

A vista [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 baliza [<a>]. O valor [@{/hello}] irá gerar o caminho [<context>/hello], em que [context] é o contexto da aplicação 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 vista [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 baliza [<h1>]. Este 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 utilizador que está ligado;
  • linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da baliza [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], em que [context] é o contexto da aplicação 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 denominado (atributo name) _csrf;

A vista [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 baliza <div> só seja gerada se o URL, que apresenta a página de início de sessão, 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 apresenta a página de início de sessão, 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], em que <context> é o contexto da aplicação web;
  • linha 13: um campo de introdução de dados denominado [username];
  • linha 17: um campo de introdução de dados 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>

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

16.3.3. Configuração Spring 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] numa classe de configuração;
  • linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para redefinir alguns dos seus métodos;
  • linha 10: redefinição de um método da classe pai;
  • linhas 11-16: o método [addViewControllers] permite associar URL a vistas HTML. São feitas as seguintes associações:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

O sufixo [html] e a pasta [templates] são os valores predefinidos utilizados pelo Thymeleaf. Podem ser alterados através 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). Isto significa que o seu conteúdo estará na raiz do Classpath do projeto. Assim, 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] numa classe de configuração;
  • linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
  • linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para redefinir alguns dos seus métodos;
  • linha 12: redefinição de um método da classe pai;
  • linhas 13-16: o método [configure(HttpSecurity http)] é redefinido para definir os direitos de acesso aos diferentes URL da aplicação;
  • 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 apenas com autenticação
http.anyRequest().authenticated();
  • linha 15: define o método de autenticação. A autenticação é feita através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também está acessível a todos;
  • linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
  • linha 20: a autenticação é feita com utilizadores definidos de forma «estática» [auth.inMemoryAuthentication()]. Um utilizador é aqui definido com o nome de utilizador [user], a palavra-passe [password] e a função [USER]. É possível conceder os mesmos direitos a utilizadores 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 efetue a configuração que o programador não terá feito explicitamente;
  • linha 9: transforma a classe [Application] numa classe de configuração do Spring;
  • linha 10: solicita a análise da pasta da classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão assim detetadas, uma vez que 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 deparámos com este processo e sabemos que o servidor Tomcat incluído nas dependências Maven do projeto será iniciado e que o projeto será implementado nesse servidor. Vimos que quatro instâncias de URL eram geridas por [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.

16.3.6. Testes da aplicação

Comecemos por solicitar o URL [/], que é um dos quatro URL aceites. Está associado à vista [/templates/home.html]:

 

A URL solicitada, [/], está acessível a todos. Foi 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 quando se clicar no link. Este está protegido:

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

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acesso apenas para utilizadores autenticados
http.anyRequest().authenticated();

É necessário estar autenticado para o obter. O Spring Security irá então redirecionar o navegador do cliente para a página de autenticação. De acordo com a configuração apresentada, 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, surge um campo oculto que não consta na página original [login.html]. Foi o Thymeleaf que o adicionou. Este código, denominado CSRF (Cross Site Request Forgery), tem como objetivo eliminar uma falha de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que esta seja aceite;

Recordamos que apenas o utilizador user/password é reconhecido pelo Spring Security. Se introduzirmos outro valor 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] fez com que fosse exibida a tag:


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

Agora, introduzamos os valores esperados para «user/password» [4]:

  • em [4], identificamo-nos;
  • em [5], o Spring Security redireciona-nos para o URL [/hello], pois era o URL que solicitámos quando fomos redirecionados para a página de início de sessão. A identidade do utilizador foi apresentada na seguinte linha de [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

A página [5] apresenta 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á efetuada uma ação POST na página URL [/logout]. Este, tal como o URL e o [/login], está acessível a todos:


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

Na nossa associação URL / vistas, não definimos nada para o URL e o [/logout]. O que irá acontecer? Vamos experimentar:

  • em [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 este redirecionamento. A presença do parâmetro [logout] no URL fez com que fosse apresentada a seguinte linha na vista:

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

16.3.7. Conclusão

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

  • é possível definir uma página de autenticação;
  • a autenticação deve ser acompanhada pelo token CSRF emitido pelo Spring Security;
  • se a autenticação falhar, o utilizador é redirecionado para a página de autenticação, com um parâmetro «error» adicional no token URL;
  • se a autenticação for bem-sucedida, o utilizador é redirecionado para a página solicitada no momento em que a autenticação ocorreu. Se se aceder diretamente à página de autenticação sem passar por uma página intermédia, o Spring Security redireciona-nos para o URL [/] (este caso não foi apresentado);
  • desautentificamo-nos ao aceder à página URL [/logout] com um POST. O Spring Security redireciona-nos então para a página de autenticação com o parâmetro «logout» no URL;

Todas estas conclusões baseiam-se nos comportamentos por predefinição do Spring Security. Estes comportamentos podem ser alterados através da configuração, redefinindo determinados métodos da classe [WebSecurityConfigurerAdapter].

O tutorial anterior será de pouca utilidade daqui em diante. Iremos, de facto, utilizar:

  • uma base de dados para armazenar os utilizadores, as suas palavras-passe e as suas funções;
  • uma autenticação por cabeçalho HTTP;

Existem poucos tutoriais sobre o que pretendemos fazer aqui. A solução que iremos propor é uma compilaçã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. A base de dados

A base de dados [dbintrospringdata] está a ser atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Surgem três novas tabelas:

Image

Tabela [USERS]: os utilizadores

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • IDENTITY: uma identidade descritiva do utilizador;
  • LOGIN: o nome de utilizador do utilizador;
  • PASSWORD: a sua palavra-passe;

Na tabela USERS, as palavras-passe não são armazenadas em texto simples:

 

O algoritmo que encripta as palavras-passe é o algoritmo BCRYPT.

Tabela [ROLES]: as funções

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • NAME: nome da função. Por predefiniçã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 utilizador pode ter várias funções, e uma função pode incluir vários utilizadores. Trata-se de uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].

  • ID: chave primária;
  • VERSION: coluna de controlo de versões da linha;
  • USER_ID: identificador de um utilizador;
  • 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 da base 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, nomeadamente, 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 ficheiro [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>
        <!-- Registos 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>
    <!-- plugins -->
    <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 fornece as classes do Spring Security;
  • linhas 34-37: a biblioteca de registos;
  • linhas 39-42: a biblioteca que permite utilizar 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 imagem 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 externas
    @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 utilizador
    @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 anteriores é gerida por um [repository] Spring Data:

  

A interface [UserRepository] gere 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 utilizador identificado pelo 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 utilizador identificado pelo seu nome de utilizador único
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // pesquisa de um utilizador através do seu nome de utilizador
    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 utilizador identificado pelo seu [id]
  • linhas 16-17: o mesmo, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
  • linha 20: para encontrar um utilizador através do seu nome de utilizador;

A interface [RoleRepository] gere 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> {

    // pesquisa de uma função através do seu nome
    Role findRoleByName(String name);

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

A interface [UserRoleRepository] gere 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 lhe adicionar novos métodos;

16.4.6. As classes de gestão de utilizadores e funções

  

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

 

Esta interface é aqui implementada 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 utilizador (linha 19) e o repositório que permite obter os detalhes desse utilizador (linha 20);
  • linhas 26-29: o construtor que instancia a classe com um utilizador e o seu repositório;
  • linhas 32-36: implementação do método [getAuthorities] da interface [UserDetails]. Este método 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 utilizador da linha 19;
  • linhas 35-37: percorre-se a lista de funções do utilizador da linha 19 para criar uma lista de elementos do tipo [SimpleGrantedAuthority];
  • linhas 42-44: implementam o método [getPassword] da interface [UserDetails]. Retorna-se a palavra-passe do utilizador da linha 19;
  • linhas 42-44: implementam o método [getUserName] da interface [UserDetails]. Retorna-se o nome de utilizador da linha 19;
  • linhas 51-54: a conta do utilizador nunca expira;
  • linhas 56-59: a conta do utilizador nunca é bloqueada;
  • linhas 61-64: as credenciais do utilizador nunca expiram;
  • linhas 66-69: a conta do utilizador está sempre ativa;

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

 

Esta 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 {
        // procura-se o utilizador pelo seu nome de utilizador
        User user = userRepository.findUserByLogin(login);
        // Encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // retornamos os detalhes do utilizador
        return new AppUserDetails(user, userRepository);
    }

}
  • linha 12: a classe será um componente Spring, pelo que estará disponível no 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 nome de utilizador;
  • linha 21: o utilizador é procurado através do seu nome de utilizador;
  • linhas 23-25: se não for encontrado, é lançada uma exceção;
  • linha 27: é criado e apresentado um objeto [AppUserDetails]. Este é, de facto, 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 entidades JPA. Existem as do projeto [intro-spring-data-01] e as do projeto do servidor seguro. Esta informação é o objeto do bean das linhas 16-19. Este bean redefine o bean com o 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 analisa as pastas indicadas na linha 1. Devido à redefinição do bean das linhas 14-17 no projeto seguro (linhas 16-19), a linha 8 acima passará agora a analisar as pastas ["spring.data.entities", "spring.security.entities"]. É importante referir que a classe importada na linha 10 da classe [spring.security.config.DaoConfig] deve incluir a anotação [@Configuration]; caso contrário, o fenómeno que acabou de ser explicado não funciona.

A classe [SecurityConfig] configura o aspeto de segurança do projeto. Já nos deparámos 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");
    }
}

Vamos seguir 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 aos diferentes URL do serviço web;
  • linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os utilizadores e as 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 palavra-passe é encriptada pelo algoritmo de hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // aplicação segura?
        if (activateSecurity) {
            // a palavra-passe é transmitida através do 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 a aplicação
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • linha 16: para ativar os elementos do Spring Security;
  • linha 17: adicionam-se os componentes Spring do pacote [spring.security.service];
  • linha 18: importam-se os beans da camada [DAO] que acabámos de apresentar, bem como os do servidor web / jSON não seguro;
  • linhas 21-22: é injetada a classe [AppUserDetails], que concede acesso aos utilizadores da aplicação;
  • linha 25: um valor booleano que protege (true) ou não (false) a aplicação web;
  • linhas 27-32: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe como parâmetro um tipo [AuthenticationManagerBuilder]. Este parâmetro é complementado com duas informações (linha 38):
    • uma referência ao serviço [appUserDetailsService] da linha 22, que dá acesso aos utilizadores registados. Note-se aqui que o facto de estarem registados numa base de dados não é mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, ...
    • o tipo de encriptação utilizado para a palavra-passe. Recorde-se aqui que utilizámos 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 predefinição, o Spring Security geria um token CSRF (Cross Site Request Forgery) que o utilizador que pretendesse autenticar-se tinha de reenviar ao servidor. Aqui, este mecanismo está desativado. Isto, aliado ao valor booleano (isSecured=false), permite utilizar a aplicação web sem segurança;
  • linha 41: ativa-se 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 cadeia «login:password» através do algoritmo Base64. Por exemplo, a codificação Base64 da cadeia admin:admin é YWRtaW46YWRtaW4=. Assim, o utilizador com o nome de utilizador [admin] e a palavra-passe [admin] enviará o seguinte cabeçalho HTTP para se autenticar:

Authorization:Basic YWRtaW46YWRtaW4=
  • linhas 46-48: indicam que todos os URL do serviço web estão acessíveis aos utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador que não tenha essa função não pode aceder ao serviço web;
  • linha 50: no modo [session], um utilizador que se tenha autenticado uma vez não precisa de o fazer nos acessos seguintes. Aqui, desativamos este modo, pelo que o utilizador terá de se autenticar em cada acesso;

16.4.8. Testes da camada [DAO]

  

Em primeiro lugar, criamos uma classe executável [CreateUser] capaz de criar um utilizador 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, palavra-passe roleName

        // são necessários três parâmetros
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // recuperam-se 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, criamo-lo
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // o utilizador já existe?
        User user = userRepository.findUserByLogin(login);
        // Se não existir, criamo-lo
        if (user == null) {
            // a palavra-passe é hashada com o bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // guardamos o utilizador
            user = userRepository.save(new User(login, login, crypt));
            // criamos a relação com a função
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // o utilizador já existe — tem 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));
            }
        }

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

}
  • linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, a sua palavra-passe e a 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 utilizador;
  • linha 34: verifica-se se a função já existe;
  • linhas 36-38: se não for o caso, cria-se na base de dados. Terá um nome do tipo [ROLE_XX];
  • linha 40: verifica-se se o nome de utilizador já existe;
  • linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
  • linha 44: encripta-se a palavra-passe. Aqui, utiliza-se a classe [BCrypt] do Spring Security (linha 4). Por isso, são necessários os ficheiros deste framework. O ficheiro [pom.xml] inclui esta dependência:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • linha 46: o utilizador é guardado na base de dados;
  • linha 48: assim como a relação que o liga à sua função;
  • linhas 51-57: caso em que o login já exista – verifica-se então se, entre as suas funções, já se encontra a função que se pretende atribuir-lhe;
  • linhas 59-61: se a função procurada não for encontrada, cria-se uma linha na tabela [USERS_ROLES] para associar o utilizador à sua função;
  • não se previu a ocorrência de eventuais exceções. Trata-se de uma classe de suporte para criar rapidamente um utilizador com uma função.

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

Tabela [USERS]

Tabela

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 utilizador [admin]
        User user = userRepository.findUserByLogin("admin");
        // verifica-se se a sua palavra-passe é [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // verifica-se a função de administrador / administrador
        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 utilizador [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // verifica-se se a sua palavra-passe é [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 — apresenta 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. São apresentados todos os utilizadores com as respetivas funções;
  • linhas 46-56: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o repositório [UserRepository];
  • linha 51: [admin] é a palavra-passe em texto simples. Na base, está encriptada de acordo com o algoritmo BCrypt. O método [BCrypt.checkpw] permite verificar se a palavra-passe em texto simples, uma vez encriptada, é efetivamente igual à que se encontra na base;
  • linhas 58-69: verifica-se se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN], utilizando o serviço [appUserDetailsService];

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

----------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]. Teremos de especificar o cabeçalho de autenticação HTTP:

Authorization:Basic code

onde [code] é o código Base64 da cadeia [login:password]. Para gerar este código, pode utilizar-se 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: nome de utilizador e palavra-passe
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // recuperam-se os dois argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // codifica-se a cadeia
        byte[] data = Base64.encode(chaîne.getBytes());
        // exibe a sua codificação Base64
        System.out.println(new String(data));
    }

}

Se executarmos este 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 cadeia [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 devolve uma resposta jSON;

Consegue-se, de facto, 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 experimentar o utilizador user / user. Este 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. É diferente da anterior, que era [401 Unauthorized]. Desta vez, o utilizador autenticou-se corretamente, mas não possui direitos suficientes para aceder ao URL;

O nosso serviço web seguro está agora operacional.

16.4.10. Um URL de autenticação

  

Vamos criar um URL que nos permitirá saber se um utilizador está autorizado ou não a aceder ao serviço web. Para tal, criamos o novo controlador MVC [AuthenticateController] seguinte:


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. Como tal, expõe URL;
  • linha 22: expõe o URL [/authenticate];
  • linha 23: o resultado do método será enviado diretamente para o cliente;
  • linhas 26-27: o método limita-se a devolver um objeto [Response] vazio, mas com um [status] igual a 0, indicando que não houve qualquer erro;

Para que serve este URL? Quando quisermos simplesmente autenticar um utilizador, iremos solicitá-lo. Vimos que, se a camada de segurança não aceitar esse utilizador, ela devolve uma exceção. Eis um exemplo;

Com o utilizador [admin:admin]:

Recebemos uma resposta vazia, mas não há exceção.

Com o utilizador [user:user]:

Ocorreu uma exceção.

16.4.11. Conclusão

A adição das classes necessárias ao Spring Security foi possível sem alterações no projeto web/json original. Este caso muito favorável deve-se ao facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Teríamos até podido colocá-las numa base de dados separada. Noutros 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á criámos um cliente para o serviço web / jSON não seguro:

Vamos agora criar um cliente programado para o serviço web seguro:

Duplicamos o projeto já criado, [intro-webjson-client], num novo projeto, [intro-spring-security-client-01]:

  

16.5.1. A classe [AbstractDao]

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

Authorization:Basic YWRtaW46YWRtaW4=

Isto é feito da seguinte forma:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

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

    // pedido genérico
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL para contacto

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

Esta classe é a seguinte:

  

package spring.security.client.entities;

public class User {

    // características
    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 então 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) {
        // o utilizador e a sua palavra-passe são codificados 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())));
    }

    // pedido genérico
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL - contactar
        // jsonPost: o valor jSON a enviar
        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á a ser 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 utilizador [user] não for nulo, então adiciona-se o cabeçalho de autenticação. A encriptação Base64 do utilizador e da sua palavra-passe é assegurada pelo método [getBase64] das linhas 25-29. É importante ter em conta que este método utiliza uma classe [Base64] pertencente ao JDK 1.8.
  • Para além 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);

    // eliminaçã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);

    // eliminaçã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: adicionámos o método [authenticate(User user)] para autenticar um utilizador. Este método lança uma exceção se o utilizador não tiver permissão para aceder ao 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 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 a respetiva categoria)
        try {
            // mapeadores jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // pedido
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // erro?
            if (response.getStatus() != 0) {
                // é lançada 1 exceção
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // retorna 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] é alterada 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;

    // utilizadores
    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 a base de dados antes de cada teste
        log("Vidage de la base de données", 1);
        // esvazia-se a tabela [CATEGORIES] — em cadeia, a tabela [PRODUITS] será esvaziada
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // preenchem-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 cadeia, 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-26, são criados três utilizadores:
    • o utilizador [admin] tem acesso ao URL do serviço web, teste linhas 96-104;
    • o utilizador [user] existe, mas não está autorizado a utilizar os URL do serviço web, teste nas linhas 71-81;
    • o utilizador [unknown] não existe, teste nas linhas 83-93;
  • os métodos de teste são os 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, como primeiro parâmetro, o utilizador [admin], que tem permissão para utilizar os URL;

O teste é bem-sucedido, mas verifica-se que é mais lento do que com o serviço web não seguro. A securização de uma aplicação aumenta significativamente os 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();
        // aplicação segura?
        if (activateSecurity) {
            // a palavra-passe é transmitida através do 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 a aplicação
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todas as URL
                    .hasRole("ADMIN");
            // sem sessão
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

A linha 17 tem um custo. Obriga o utilizador a autenticar-se em cada acesso. Se a colocarmos em comentário, a duração do teste JUnit anterior passa de 10,57 segundos para 4,21 segundos, isto porque o utilizador [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 volta a verificar a palavra-passe do utilizador). Com um serviço web não seguro, a duração do teste JUnit desce para 2,33 segundos.