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:
visualização | |
/templates/home.html | |
/templates/hello.html | |
/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:
regra | código | |
acesso sem autenticação | | |
acesso somente com autenticação |
- 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:
O URL [/hello] será solicitado ao clicar no link. Este está protegido:
regra | código | |
acesso sem autenticação | | |
Acesso restrito a usuários autenticados |
É 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:
- 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]:
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:

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:
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:
- 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:
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:
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:
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:
![]() |
- 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:
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.
























































