Open dfpinto opened 4 years ago
No STS: File / New / Maven Project Marque o checkbox "Create a simple project..." Preencha os campos: Group Id: nome do pacote, exemplo br.com.studyjava Artifect Id: nome do projeto, exemplo javanapratica Finish
1) Entre na página https://start.spring.io/ 2) Marque as opções: Project: Maven Language: Java Spring Boot: 2.3.1 3) Preencha os campos: Group: br.com.studyjava Artifact: javanapratica Packaging: jar Java: 8 4) Escolha as dependências: WEB, JPA, DEV TOOLS, Lombok, Spring Security, H2, MySql Driver 5) Clique no botão GENERATE 6) Descompacte o zip gerado em algum lugar do seu computador 7) No STS, clique em File / Import / Maven / Existing Maven Projects
1) ctrl + shift + F : formatar o texto 2) ctrl + D : deletar uma linha 3) ctrl + shift + O : importar dependências 4) ctrl + alt + up ou down : duplica a linha selecionada 5) sysout + ctrl + space : escreve System.out.println(); 6) main + ctrl + space: escreve public static void main(String[] args) {} 7) ctrl + 1 : declara uma variável do tipo informado Exemplo: new ArrayList<>(); [pressione ctrl + 1 em cima da instrução] A IDE dará a opção "Assign statement to new local variable". Selecione-a. 8) ctrl + space : autopreenchimento ou inclusão de alguma método em uma classe 9) ctrl + shift + / : comentário de bloco 10) ctrl + shift + \ : retira comentário de bloco 11) log + ctrl + space : escreve "private static final Logger log = LoggerFactory.getLogger(RestWebClientController.class);"
1) Inclua as dependências Maven a serem baixadas:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.12.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-entitymanager -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.4.12.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
</dependencies>
2) Configure o JPA no seu projeto por meio do arquivo persistence.xml Crie uma pasta "META-INF" a partir da pasta "resources" Dentro da pasta META-INF crie um arquivo "persistence.xml" Conteúdo do arquivo persistence.xml:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
#name = Apelido desejado; transaction-type = RESOURCE_LOCAL = significa que o controle da transação será manual;
<persistence-unit name="javanapratica-jpa" transaction-type="RESOURCE_LOCAL">
<properties>
#url do banco de dados desejado; neste caso é o mysql que está em localhost. Se o banco estivesse em um servidor usaríamos por exemplo jdbc:mysql://108.22.33.33/aulajpa?useSSL=false&serverTimezone=UTC
#dbjavanapratica é o nome do database criado no mysql
<property name="javax.persistence.jdbc.url"
value="jdbc:mysql://localhost/dbjavanapratica?useSSL=false&serverTimezone=UTC" />
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<!-- https://docs.jboss.org/hibernate/orm/5.4/javadocs/org/hibernate/dialect/package-summary.html -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect" />
</properties>
</persistence-unit>
</persistence>
Usando application.properties, application-dev.properties, application-homo.properties e application-prod.properties.
Servidor MySql Se vamos usar o banco de dados mysql será necessário um servidor. A nível de estudo sugiro usar o Xampp. Ele já vem com apache e mysql e é muito fácil usá-lo. Quando estiver executando seu projeto java inicie apenas o MySql no Xampp. Não há necessidade de iniciar o Apache, já que o Spring sobe o Tomcat por padrão.
Configurações do banco As configurações do banco de dados, como string de conexão, usuário e senha estarão em um profile do Spring chamados properties. Para configurar o nosso banco mysql usaremos o arquivo application-dev.properties
#datasource
spring.datasource.driver-class-name=com.mysql.jdbc.driver
spring.datasource.url=jdbc:mysql://localhost:3306/dbjwt?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
#jpa
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
Profile de desenvolvimento, teste e produção. Todo projeto teremos os ambientes de dev, homo e prod. Para separá-los usamos profiles, que nada mais são que os arquivos application.properties. Então para o ambiente de dev, podemos chamar de application-dev.properties, de homo application-homo.properties e produção application-prod.properties. Tudo que estiver em applicatin.properties será comum entre os outros. E como o Spring vai saber qual usar? Muito simples. Podemos usar o arquivo application.properties para isso, incluiremos a linha spring.profiles.active=dev.
Criando configurações específicas por profile
Crio uma classe e a anoto com @Configuration
e @Profile("dev")
.
Dessa forma todos os Beans que estiverem contidos nessa classe estarão disponíveis para uso, caso estejamos usando o profile "dev" em application.properties. Então, podemos chamar a classe de DevConfig e incluir, por exemplo, massa inicial de teste.
Crie um arquivo chamado data.sql com as instruções de insert das tabelas que irá usar e ponha em src/main/resources Exemplo: INSERT INTO USUARIO(nome, email, senha) VALUES('DIRLEY','dirley.figueredo@gmail.com','dirley');
https://stackoverflow.com/questions/2591098/how-to-parse-json-in-java
Veja a implementação em https://github.com/robsoncalixto/study-java/blob/master/ConsumindoServicoNaWebDeTresFormasDiferentes.zip.
O video abaixo explica em detalhes como implementar um WebClient. https://www.youtube.com/watch?v=Q1BjCuAQRrQ
Necessário adicionar as dependências abaixo:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectreactor</groupId>
<artifactId>reactor-spring</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
1. Inclua a dependência abaixo no pom.xml. O simples fato de incluirmos essa dependência, o spring bloqueará todos os acessos aos endpoints, pois esse é o padrão. Se você tentar acessar alguma uri, tomará o erro 401 unauthorized.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Crie uma classe que conterá as configurações de segurança. Nada melhor que chamá-la de SecucityConfigurations.
Precisamos, então, dizer pro spring que essa classe é de configuração e o que nela constar será lido e carregado pelo spring quando a aplicação iniciar. Para isso usamos a anotação @Configuration
. Também precisamos habilitar o modo de segurança e pra isso usamos a anotação @EnableWebSecutiry
. Por fim, estendemos nossa classe de WebSecurityConfigurerAdapter que nos fornecerá alguns métodos contendo comportamento padrão que iremos sobrescrever, adequando-os ao nosso contexto.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
...
}
2.1. Sobrescreva os métodos "configure". Ele é sobrecarregado três vezes e cada um tem uma responsabilidade distinta.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
// Configurações de autenticação
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
}
// Configurações de autorização / acesso
// Será nesse método que irei definir quais URIs (requests) terão acesso: o que será público, privado, qual método pode ser executado.
@Override
public void configure(HttpSecurity http) throws Exception {
}
// Configurações de recursos estáticos (imagens, css, js, etc)
@Override
public void configure(WebSecurity web) throws Exception {
}
}
2.2. Dando acesso a uma URI pública Consideremos que qualquer usuário poderá consultar todos os tópicos ou um específico.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
// Configurações de autenticação
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
}
// Configurações de autorização / acesso
@Override
public void configure(HttpSecurity http) throws Exception {
// Consultando todos os tópicos com /topicos
// Consultando um tópico específico com /topicos/1
http.authorizeRequest()
.antMatchers(HttpMethod.GET, "/topicos").permitAll()
.antMatchers(HttpMethod.GET, "/topicos/*").permitAll();
}
// Configurações de recursos estáticos (imagens, css, js, etc)
@Override
public void configure(WebSecurity web) throws Exception {
}
}
3. Restringindo o acesso a uma URI privada exigindo autenticação Para implementar uma autenticação é necessário login e senha do usuário e para isso criaremos uma classe chamada, adivinhe? Usuario. Então, teremos uma classe de domínio chamada Usuario, anotada como uma Entity. Porém o spring security precisa saber quem é a classe que conterá as credenciais de acesso e neste artigo vou explicar como fazê-lo implementando a interface UserDetails. Ao implementar essa interface obrigatoriamente teremos que cumprir o contrato que ela estabelece. Sabemos também que existem usuários ou atores de toda ordem: gestores, operadores, clientes, coordenadores, consultores etc. Para essa coleção de tipos de usuário damos o nome de perfis. E assim como criamos a classe Usuario implementando a interface UserDetails para que o spring possa reconhecer quem identifica as credenciais de um usuário, também faremos uma classe Perfil herdando da interface GrantedAuthority para que o spring reconheça quais são os perfis. Por fim, repare no método chamado formLogin() que usei na classe de configuração abaixo. Esse método fará com que o spring gere uma tela de login para nós. Entretanto, nós ainda não ensinamos ao spring o que fazer com o login e senha dessa tela. Vamos aprender a fazer isso no próximo item.
Precisamos implementar os métodos da interface UserDetails: // Lista ou coleção dos perfis do usuário. public Collection<? extends GrantedAuthority> getAuthorities() return this.perfis;
// Para que o spring saiba recuperar a senha de seu usuário passamos o atributo que a identifica. public String getPassword() return this.senha;
// Para que o spring saiba recuperar o username de seu usuário passamos o atributo que o identifica. public String getUsername() return this.email;
// Caso queira controlar expiração de contra, implemente este método, senão passe true. public boolean isAccountNonExpired() return true;
// Caso queira controlar bloqueio de contra, implemente este método, senão passe true. public boolean isAccountNonLocked() return true;
// Caso queira controlar expiração das credenciais, implemente este método, senão passe true. public boolean isCredentialsNonExpired() return true;
// Caso queira controlar se a contra está ou não habilitada, implemente este método, senão passe true. public boolean isEnabled() return true;
Precisamos implementar o método da interface GrantedAuthority: public String getAuthority() return this.nome;
package br.com.studyjava.models;
@Entity
public class Perfil implements GrantedAuthority {
private Long id;
private String nome;
...
@Override
public String getAuthority() {
return this.nome;
}
...
}
package br.com.studyjava.models;
@Entity
public class Usuario implements UserDetails {
private Long id;
private String senha;
private String email;
private String nome;
// Toda relação ToMany é Lazy e ao carregar o Usuário a JPA não carregará os perfis. Por isso precisamos dizer para a JPA que
// queremos que ela traga os perfis e por esta razão usamos o EAGER.
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "perfil_usuario",
joinColumns = @JoinColumn(name = "usuario_id"),
inverseJoinColumns = @JoinColumn(name = "perfil_id"))
private List<Perfil> perfis = new ArrayList<>();
...
@Override
public String getPassword() {
return this.senha;
}
@Override
public String getUsername() {
return this.email;
}
...
}
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
// Configurações de autenticação
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
}
// Configurações de autorização / acesso
@Override
public void configure(HttpSecurity http) throws Exception {
// Consultando todos os tópicos com /topicos
// Consultando um tópico específico com /topicos/1
// Qualquer outro request precisa de autenticação do usuário.
// Dizer para o spring montar uma tela de login.
http.authorizeRequest()
.antMatchers(HttpMethod.GET, "/topicos").permitAll()
.antMatchers(HttpMethod.GET, "/topicos/*").permitAll()
.anyRequest().authenticated()
.and().formLogin()
;
}
// Configurações de recursos estáticos (imagens, css, js, etc)
@Override
public void configure(WebSecurity web) throws Exception {
}
}
4. Autenticando o login do usuário No item anterior nós pedimos ao autenticador de acesso que apresentasse a tela de login em nossa aplicação. Essa tela é apresentada quando tentamos acessar uma URI não autorizada ou não pública. Agora iremos dizer para o spring como autenticar o login do usuário e pra isso iremos implementar o método configure(AuthenticationManagerBuilder auth). A classe AuthenticationManagerBuilder possui um método chamado userDetailService que recebe como parâmetro uma classe de serviço que implementa a lógica de autenticação que desejamos. Para isso, então, criaremos a nossa service e chamaremos de AutenticacaoService. E para que o spring security saiba que essa classe é a que contém a lógica de autenticação é necessário que implementemos a interface UserDetailService. Essa classe vai dispor o método loadUserByUsername que retorna um UserDetail e que iremos implementar buscando e verificando se o username existe na base de dados. Repare abaixo que o método loadUserByUsername recebe apenas o parâmetro username do login. E a senha? A senha pertence a classe Usuario que implementa a interface UserDetail que possui o método getPassword. Esse método será acionado pelo Spring através do processo de autenticação. Se não lembra, dê uma olhadinha no item 3. Para verificarmos se o usuário existe na base criaremos um repository, pois não é de responsabilidade do service. Então criamos a interface UsuarioRepository estendendo a classe JPARepository e usaremos o método findByEmail. ...
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
// Configurações de autenticação
@Autowired
private AutenticacaoService autenticacaoService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailService(autenticacaoService);
}
...
public class AutenticacaoService implementes UserDetailService {
@Autowired
private UsuarioRepository usuRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Usuario> usuario = usuRepo.findByEmail(username);
if(usuario.isPresent()){
return usuario.get();
}
throw new UsernameNotFoundException("Dados inválidos.");
}
}
...
public interface UsuarioRepository extends JPARepository<Usuario, Long> {
Usuario findByEmail(String email);
}
... 4.1 Criptografando a Senha Assim como o usuário, a senha também fica gravada no banco de dados, geralmente na tabela Usuario. Imagine, então, o login: user: rodrigo@gmail.com password: Rodrigo@2020 Qualquer um que conheça o mínimo de segurança sabe que a senha não pode ser gravada sem que seja submetida a alguma a um algorítimo de hash. O Spring fornece, através do método passwordEncoder, recurso para inputarmos nossa função de hash, porém o próprio spring security dispõe de uma classe para isso chamada BCryptPasswordEncoder. Então, veremos abaixo como usá-la em nosso login.
...
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailService(autenticacaoService).passwordEncoder(new BCryptPasswordEncoder());
}
...
Mas como iremos criptografar a senha que está gravada no banco como Rodrigo@2020? Claro que podemos fazer isso no método setPassword da classe Usuario, porém, a nível de teste, podemos simplesmente criar uma classe main chamando o método encode da classe BCryptPasswordEncoder.
public static void main(String[] args){
System.out.println(new BCryptPasswordEncoder().encoder("Rodrigo@2020"));
}
Por que autenticar via token? Por padrão o Spring Security armazena, no lado do servidor, sessões em memória para cada usuário logado, identificando-os de forma única, contendo suas informações de autenticação e devolvendo para o cliente / navegador, através de cookie. Por outro lado sabemos que uma das características do modelo Rest é ser stateless, ou seja, não armazena nada entre requisição e resposta. E por que isso é ruim? Porque se eu tiver zilhões de usuários ocuparão muita memória, porque ao escalar minha aplicação não será possível compartilhar essas sessões, porque se o servidor cair perderei essas sessões.
Usando token 1. Primeira coisa é baixar a dependência.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. Dizer para o spring security que não mais usaremos sessão. Lembra que existem 3 métodos configure de nossa classe SecurityConfigurations? Faremos isso na classe configure(HttpSecurity http). Vamos remover .and().formLogin() e incluir: .and().csrf().disable() .sessonManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
O que é CSRF? O cross-site request forgery, em português falsificação de solicitação entre sites, também conhecido como ataque de um clique ou montagem de sessão, é um tipo de exploit malicioso de um website, no qual comandos não autorizados são transmitidos a partir de um usuário em quem a aplicação web confia.
E para que serve o método sessonManagement...? É ele quem desabilita o uso de sessão pelo spring security. Repare que estamos dizendo para ele que a política de criação de sessão é stateless.
Como usaremos token automaticamente nossa API estará livre desse ataque.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
...
// Configurações de autorização / acesso
@Override
public void configure(HttpSecurity http) throws Exception {
// Consultando todos os tópicos com /topicos
// Consultando um tópico específico com /topicos/1
// Qualquer outro request precisa de autenticação do usuário.
http.authorizeRequest()
.antMatchers(HttpMethod.GET, "/topicos").permitAll()
.antMatchers(HttpMethod.GET, "/topicos/*").permitAll()
.anyRequest().authenticated()
//.and().formLogin()
.and().csrf().disable()
.sessonManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
...
3. Autenticando o login
Como removemos o método formLogin() perdemos não só a tela de login como o controlador dessa página gerenciado pelo spring. Quanto a tela, não é tarefa do backend. Essa tela será fornecida pelo frontend. Mas o controle para gerenciar o login nós teremos que criar e daremos o nome de AutenticacaoController, usando a URI /auth.
Não esqueça que se temos uma nova URI pública, temos que incluí-la em nosso configurador de acesso.
Para que possamos realizar a autenticação de forma programática no spring, precisaremos da classe AuthenticationManager. O problema é que, por algum motivo, essa classe não está disponível para ser injetada em nosso controller de autenticação. Porém, ela pode ser criada pelo método authenticationManager do nosso configurador de segurança SecurityConfigurations e é isso que faremos (dando ctrl + space e escolhendo esse método), incluindo @Bean
para que possamos injetá-la. Agora que já temos em nosso controller a classe AuthenticationManager injetada, usaremos o método authenticate para autenticarmos as credenciais do usuário. O problema é que esse método recebe o tipo UsernamePasswordAuthenticationToken. Então, precisamos converter o e-mail e senha do usuário nesse tipo e faremos criando o método converter na classe LoginForm.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
...
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// Configurações de autorização / acesso
@Override
public void configure(HttpSecurity http) throws Exception {
// Consultando todos os tópicos com /topicos
// Consultando um tópico específico com /topicos/1
// Qualquer outro request precisa de autenticação do usuário.
http.authorizeRequest()
.antMatchers(HttpMethod.GET, "/topicos").permitAll()
.antMatchers(HttpMethod.GET, "/topicos/*").permitAll()
.antMatchers(HttpMethod.POST, "/auth").permitAll()
.anyRequest().authenticated()
.and().csrf().disable()
.sessonManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
...
----------------------------------------------------------------------------------------------------------------------
package br.com.studyjava.controller;
@RestController
@RequestMapping("/auth")
public class AutenticacaoController {
@Autowired
private AuthenticationManager authManager;
@PostMapping
public ResponseEntity<?> autenticar(@RequestBody @Valid LoginForm form) {
UsernamePasswordAuthenticationToken dadosLogin = form.converter();
try {
Authentication authentication = authManager.authenticate(dadosLogin);
return ResponseEntity.ok().build();
} catch(AuthenticationException e) {
return ResponseEntity.badRequest().build();
}
}
}
------------------------------------------------------------------------------------------------------------------------
package br.com.studyjava.controller;
public class LoginForm {
private Sgring email;
private String senha;
// setter...
public UsernamePasswordAuthenticationToken converter {
return new UsernamePasswordAuthenticationToken(this.email, this.senha);
}
}
4. Gerando o token Agora que já autenticamos as credenciais do usuário já podemos gerar o token. Faremos isso no próprio controller AutenticacaoController. Para isso vamos criar um service chamado TokenService com o método gerarToken passando a variável authentication. Ele vai acionar a API da biblioteca jjwt que importamos no pom chamada Jwts. Precisamos setar diversos parâmetros que mostro abaixo mas antes é importante que dois desse parâmetros, o tempo de expiração e a secret, fiquem no arquivo application.properties. Podemos usar o node.js para gerar nossa secret: node -e "console.log(require('crypto').randomBytes(256).toString('base64'));" Exemplo:
# jwt
studyjava.jwt.secret=rm'!@N=Ke!~p8VTA2ZRK~nMDQX5Uvm!m'D&]{@Vr?G;2?XhbC:Qa#9#eMLN\}x3?JR3.2zr~v)gYF^8\:8>:XfB:Ww75N/emt9Yj[bQMNCWwW\J?N,nvH.<2\.r~w]*e~vgak)X"v8H`MH/7"2E`,^k@n<vE-wD3g9JWPy;CrY*.Kd2_D])=><D?YhBaSua5hW%{2]_FVXzb9`8FH^b[X3jzVER&:jw2<=c38=>L/zBq`}C6tT*cCSVC^c]-L}&/
studyjava.jwt.expiration=86400000
Explicando cada parâmetro do Jwts:
Jwts.builder()
// Quem fez a geração do token?
.setIssuer("API da equipe studyjava")
// Quem é o usuário autenticado que esse token pertence (id único)?
.setSubject(logado.getId().toString())
// Qual data o token foi gerado?
.setIssuedAt(hoje)
// Qual o tempo de expiração do token? Definir o tempo de expiração no arquivo application.properties.
.setExpiration(dataExpiracao)
// Qual o algoritmo a ser usado para criptografia o token e a chave secreta a ser usada?
// Definir o tempo de expiração e a secrect no arquivo application.properties.
.signWith(SignaturedAlgorithm.HS256, secret)
// Compacta e gera o token.
.compact()
;
Fonte java:
@RestController
@RequestMapping("/auth")
public class AutenticacaoController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private TokenService tokenService;
@PostMapping
public ResponseEntity<?> autenticar(@RequestBody @Valid LoginForm form) {
UsernamePasswordAuthenticationToken dadosLogin = form.converter();
try {
Authentication authentication = authManager.authenticate(dadosLogin);
String token = tokenService.gerarToken(authentication);
System.out.println(token);
return ResponseEntity.ok().build();
} catch(AuthenticationException e) {
return ResponseEntity.badRequest().build();
}
}
}
------------------------------------------------------------------------------------------------------------
@Service
public class TokenService {
@Value("${studyjava.jwt.expiration}")
private String expiration;
@Value("${studyjava.jwt.secrect}")
private String secret;
public String token gerarToken(Authentication authentication) {
Usuario logado = (Usuario)authentication.getPrincipal();
Date hoje = new Date();
Date dataExpiracao = new Date(hoje.getTime() + Long.parseLong(expiration));
return Jwts.builder()
.setIssuer("API da equipe studyjava")
.setSubject(logado.getId().toString())
.setIssuedAt(hoje)
.setExpiration(dataExpiracao)
.signWith(SignaturedAlgorithm.HS256, secret)
.compact()
;
}
}
5. Devolvendo o token para o cliente Para retornar o token gerado iremos criar a classe TokenDTO e, através do construtor, atribuiremos a ela o token e seu tipo e a retornaremos como resposta em nosso controller. Quem e como o token será armazenado é de responsabilidade do frontend.
package br.com.studyjava.dto;
public class TokenDTO {
private String token;
private String tipo;
public TokenDTO(String token, String tipo) {
this.token = token;
this.tipo = tipo;
}
// getters
}
-----------------------------------------------------------------------------------
@RestController
@RequestMapping("/auth")
public class AutenticacaoController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private TokenService tokenService;
@PostMapping
public ResponseEntity<?> autenticar(@RequestBody @Valid LoginForm form) {
UsernamePasswordAuthenticationToken dadosLogin = form.converter();
try {
Authentication authentication = authManager.authenticate(dadosLogin);
String token = tokenService.gerarToken(authentication);
return ResponseEntity.ok(new TokenDTO(token, "Bearer"));
} catch(AuthenticationException e) {
return ResponseEntity.badRequest().build();
}
}
}
6. Recebendo o token das requisições do cliente Antes de iniciarmos a leitura do token enviada pelo frontend, vamos repassar como isso tudo ocorre nos bastidores. Podemos deixar o spring security apresentar e controlar a tela de login ou, o que geralmente ocorre, construirmos nossa própria tela e controle. No primeiro caso basta acionar o método formLogin() em nosso contexto de configuração de segurança. Repare que neste caso não haverá um controlador visível. O próprio spring se encarrega de tratar o endpoint / ou /login, realizar a autenticação e responder para o cliente. Mas, como eu disse, geralmente criamos nossa própria tela de login e um controller para mapear a uri de login. Neste artigo usamos /auth. Feito essa introdução podemos apresentar o caminho ou fluxo do token. a) O cliente dispara uma requisição /auth do tipo POST incluindo no body o json com e-mail e senha. b) Nossa aplicação recebe essa requisição através do controller autenticar, valida o login no banco e retorna como resposta o token e o tipo de autenticação. c) O cliente recebe o token e o armazena em cookie, memória, session ou local storage, porque nas próximas requisições ele terá que enviá-lo para nossa API. d) Agora imaginemos que o cliente queira excluir um registro de nossa base. Para isso ele vai precisar nos enviar o token, pois esse tipo de operação é restrita. Se ele tentar enviar essa requisição sem informar o token, receberá um 403 - Forbidden. Então, para ele disparar essa requisição (ex: tipo DELETE na uri /topicos/2) ele tem que adicionar o cabeçalho authorization contendo a palavra Bearer +" " + token. e) Precisamos então receber esse token, revalidá-lo e seguir o fluxo da requisição. Para isso, precisamos interceptar a requisição de delete antes de cair em nosso controller autenticar. Para fazer isso, vamos criar um filtro que chamaremos de AutenticacaoViaTokenFilter e estendê-lo de OncePerRequestFilter, que é um filtro do spring chamado uma única vez a cada requisição e implementar seu método doFilterInternal. f) Neste método, vamos recuperar o token do cabeçalho (recuperarToken), validá-lo (isTokenValido), gravá-lo no contexto de segurança (autenticarToken) e dar seguimento ao fluxo da requisição através do doFilter.
Para validar o token, criaremos o método isTokenValido em nosso service TokenService. O problema é que não conseguimos injetar classes em filters. A solução será injetá-lo pelo construtor de nosso filtro, ou seja, quem quiser usá-lo, terá que passar nosso service TokenService.
Para que o spring considere o token enviado no contexto de segurança (leia-se colocar o usuário logado na memória), criaremos o método autenticarToken também em nosso service TokenService.
g) Nosso filtro AutenticacaoViaTokenFilter precisa ser registrado para ser usado pelo spring. Faremos isso em nossa classe Security Configuration, no método configure(HttpSecurity http). Usamos para isso o método addFilterBefore, pois o spring precisa saber a ordem de execução dos filtros. Informo para ele que deve executar nosso filtro AutenticacaoViaTokenFilter antes do UsernamePasswordAuthenticationFilter (filter usado internamento pelo spring).
// Crio meu filter.
package br.com.studyjava.config.security;
public class AutenticacaoViaTokenFilter extends OncePerRequestFilter {
private TokenService tokenService;
public AutenticacaoViaTokenFilter(TokenService tokenService){
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = recuparToken(request);
boolean valido = tokenService.isTokenValido(token);
if(valido){
tokenService.autenticarCliente(token);
}
filterChain.doFilter(request, response);
}
}
private String recuparToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if(token = null || token.isEmpty() || !token.startsWith("Bearer ")) {
return null;
}
return token.substring(7, token.length());
}
-------------------------------------------------------------------------------------------
// Registro meu filter no spring security.
package br.com.studyjava.config.security;
@EnableWebSecurity
@Configuration
public class SecurityConfigurations extends WebSecurityConfigurerAdapter{
...
@Autowired
private TokenService tokenService;
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return supoer.authenticationManager();
}
// Configurações de autorização / acesso
@Override
public void configure(HttpSecurity http) throws Exception {
// Consultando todos os tópicos com /topicos
// Consultando um tópico específico com /topicos/1
// Qualquer outro request precisa de autenticação do usuário.
http.authorizeRequest()
.antMatchers(HttpMethod.GET, "/topicos").permitAll()
.antMatchers(HttpMethod.GET, "/topicos/*").permitAll()
.antMatchers(HttpMethod.POST, "/auth").permitAll()
.anyRequest().authenticated()
.and().csrf().disable()
.sessonManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().doFilterBefore(new AutenticacaoViaTokenFilter(tokenService), UsernamePasswordAuthenticationFiler.class)
;
}
------------------------------------------------------------------------------------
// Criando nosso método para validar o token.
@Service
public class TokenService {
@Value("${studyjava.jwt.expiration}")
private String expiration;
@Value("${studyjava.jwt.secrect}")
private String secret;
@Autowired
private UsuarioRepository repoUsuario;
public String token gerarToken(Authentication authentication) {
Usuario logado = (Usuario)authentication.getPrincipal();
Date hoje = new Date();
Date dataExpiracao = new Date(hoje.getTime() + Long.parseLong(expiration));
return Jwts.builder()
.setIssuer("API da equipe studyjava")
.setSubject(logado.getId().toString())
.setIssuedAt(hoje)
.setExpiration(dataExpiracao)
.signWith(SignaturedAlgorithm.HS256, secret)
.compact()
;
}
public boolean isTokenValido(String token) {
try {
Jwts.parser().setSigningKey(this.secrect).parseClaimsJws(token);
return true;
}
catch(Exception e){
return false;
}
}
public void autenticarCliente(String token) {
Long idUsuario = tokenService.getIdUsuario(token);
Optional<Usuario> usuario = repoUsuario.findById(idUsuario);
if(usuario.IsPresent()) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(Usuario, null, usuario.get().getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
public String getCodUsuario(String token) {
Claims body = Jwts.parser().setSigningKey(this.secret).parseClaimsJws(token).getBody();
return body.getSubject();
}
}
7. Logoff Não é função do backend realizar o logoff quando estamos usando token, mas do frontend. Entretanto é possível implementar um blacklist de tokens inválidos. Poderíamos gravar essa lista no banco de dados ou no redis.
8. Consumindo minha API via aplicação javascript No exemplo abaixo os endereços das aplicações frontend que consumiriam meu serviço seriam localhost:3000 e localhost:3000.
@Override
public void addCorsMappings(CorsRegistry registry) {
//liberando app cliente 1
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "TRACE", "CONNECT");
//liberando app cliente 2
registry.addMapping("/topicos/**")
.allowedOrigins("http://localhost:4000")
.allowedMethods("GET", "OPTIONS", "HEAD", "TRACE", "CONNECT");
}
9. Como incluir e recuperar mais informações no token? Vamos incluir os perfis no token para evitar ida ao banco.
Eu só consegui realizar esse mapeamento de forma muito simples pela IDE do netbeans. Seguem os passos:
1. Baixe e instale o netbeans. https://netbeans.org/downloads/old/8.0/
2. Crie um maven project vazio. O netbeans abrirá 3 abas: Projetos, Arquivos e Serviços.
3. Crie uma nova conexão com o bd Clique na aba Serviços. Abra o item Banco de Dados. Abra o item Drivers. 3.1 Se já existir o driver MySql, então clique com botão direito em MySql e depois em Conectar Usando... 3.1.1 Vai abrir um popup geralmente preenchido com as configurações padrão. Se tiver usando o Xampp, então só vai precisar informar o nome do seu database. Exemplo: jdbc:mysql://localhost:3306/meudatabase?zeroDateTimeBehavior=convertToNull. Clique em próximo e depois em finalizar. 3.1.2 O Netbeans vai criar uma string de conexão que aparecerá como um item na aba Serviços. 3.2 Se não existir o driver MySql: 3.2.1 Baixe o drive do mysql em https://dev.mysql.com/downloads/connector/j/. Selecione no combo Operating System a opção Platform Idependent. Formato ZIP arquive. Descompacte-o na pasta desejada. 3.2.2 Clique com o botão direito em cima de Drivers e depois em Novo Driver. Uma tela popup irá abrir. Clique no botão adicionar e selecione o jar descompactado. 3.2.3 Siga os passos 3.1.
4. Mapeando uma tabela Clique com o botão direito no nome do projeto que criou. Clique em Novo e depois em Outros. Escolha a categoria Persistência. Escolha o tipo de arquivo "Classes de Entidade do Banco de Dados". Clique no botão próximo. Escolha a conexão do banco de dados. Quando clicar no combo, vai aparecer a string de conexão criada no item 3.1.2. Selecione-a. Aparecerá todas as tabelas disponíveis para seleção. Selecione uma ou todas as tabelas desejadas. Clique no botão próximo. Você poderá marcar ou desmarcar os checkbox disponíveis. Selecione a opção "lento" para o campo Extrair Associação. Clique no botão finalizar. Uma classe java com mesmo nome da tabela será criada.
PS: Contribuição dada pelo Amauri.
Tem uma outra forma também, porém bem antiga e não recomendo. Segue o link.
http://shengwangi.blogspot.com/2014/12/how-to-create-java-classes-from-tables.html
Geralmente usamos bean validation em nossas classes modelo (Entity): @NotEmpty @NotNull
etc.
Em nossos controladores, anotamos com @Valid
e dessa forma, quando a uri é acionada, o Spring acusará um erro, status 400. O problema é que o java vai apresentar esse erro com riqueza de detalhes e não é isso que queremos. O ideal seria mostrar um erro simples, com pouca informação, mas suficiente para o usuário.
Para isso, o Spring dispõe da anotação @RestControllerAdvice
que vai interceptar todos os erros ocorridos em nossos controladores.
Vamos ao passo a passo:
@RestControllerAdvice
.@RestControllerAdvice
public class ErroDeValidacaoHandler {
}
@ExceptionHandler
.
2.1. Passe como parâmetro da anotação as exceções oriundas da validação que deseja interceptar.
2.2. Para que o fluxo do erro permaneça com status 400 é necessário anotar o método com @ResponseStatus
. Se não o fizer, o Spring retornará o status 200.
2.3. O método precisa retornar a lista de erros.@RestControllerAdvice
public class ErroDeValidacaoHandler {
@ResponseStatus(code = HttpStatus._BAD_REQUEST_)
@ExceptionHandler(MethodArgumentNotValidException.class)
public List<ErroDeFormularioDTO> handle(MethodArgumentNotValidException exception) {
}
}
Crie uma classe para representar os erros do formulário. 3.1. Sugiro criar um classe Entity ou DTO para que representará os erros do formulário. Chamaremos essa classe de ErroDeFormularioDTO.
public class ErroDeFormularioDTO {
private String campo;
private String erro;
public ErroDeFormularioDTO(String campo, String erro) {
this.campo = campo;
this.erro = erro;
}
public String getCampo() {
return this.campo;
}
public String getErro() {
return this.erro;
}
}
@RestControllerAdvice public class ErroDeValidacaoHandler {
@ResponseStatus(code = HttpStatus._BAD_REQUEST_)
@ExceptionHandler(MethodArgumentNotValidException.class)
public List<ErroDeFormularioDTO> handle(MethodArgumentNotValidException exception) {
}
}
4. **Percorra a lista de erros interceptada pelo método handler e atribua a nossa lista ErroDeFormularioDTO;**
O parâmetro exception conterá a lista de erros interceptada.
Para recuperar a mensagem de erro iremos injetar a classe MessageSource do Sprint que tratar internacionalização.
@RestControllerAdvice public class ErroDeValidacaoHandler {
@Autowired
private MessageSource messageSource;
@ResponseStatus(code = HttpStatus._BAD_REQUEST_)
@ExceptionHandler(MethodArgumentNotValidException.class)
public List<ErroDeFormularioDTO> handle(MethodArgumentNotValidException exception) {
List<ErroDeFormularioDTO> dto = new ArrayList<>();
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErros();
fieldErros.forEach(e -> {
String mensagem = messageSource.getMessage(e, LocaleContextHolder.getLocale());
ErroDeFormularioDTO erro = new ErroDeFormularioDTO(e.getField(), mensagem);
dtp.add(erro);
}
return dto;
}
}
5. **Testando internacionalização.**
Se desejar testar sua aplicação usando um outro idioma, basta incluir o atributo _Accept-Language_ no _Header_ do request para _en-US_, por exemplo.
![Mensagem em portugues](https://user-images.githubusercontent.com/37200499/93718023-c556a700-fb4f-11ea-9993-9dc3f949b970.png)
![Mensagem em ingles 1](https://user-images.githubusercontent.com/37200499/93718038-d7d0e080-fb4f-11ea-964e-b4ff65f394f7.png)
![Mensagem em ingles 2](https://user-images.githubusercontent.com/37200499/93718042-db646780-fb4f-11ea-8a2b-29274ebc45e7.png)
Ferramentas e versões
Verificando a wsdl
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.13.1</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaLanguage>WSDL</schemaLanguage>
<generatePackage>deputados_ws</generatePackage>
<schemas>
<schema>
<url>https://www.camara.leg.br/SitCamaraWS/Deputados.asmx?wsdl</url>
</schema>
</schemas>
</configuration>
</plugin>
</plugins>
</build>
Depois de finalizado a configuração é preciso atualizar o projeto.
Quando atualizado será criada uma package dentro do projeto, o nome será o informado dentro do pom, na tag generatePackage e a estrutura final do projeto deve ficar como a imagem.
ClienteDeputadosWS .java
import java.net.URISyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.client.core.WebServiceMessageCallback;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;
import org.springframework.ws.soap.SoapMessage;
import deputados_ws.ObterDeputados;
import deputados_ws.ObterDeputadosResponse;
public class ClienteDeputadosWS extends WebServiceGatewaySupport{
private static final Logger log = LoggerFactory.getLogger(ClienteDeputadosWS.class);
public ObterDeputadosResponse getDeputados() throws URISyntaxException {
ObterDeputados request = new ObterDeputados();
log.info("Request Deputados");
return (ObterDeputadosResponse) getWebServiceTemplate().marshalSendAndReceive(request, new WebServiceMessageCallback() {
public void doWithMessage(WebServiceMessage message) {
((SoapMessage)message).setSoapAction("https://www.camara.gov.br/SitCamaraWS/Deputados/ObterDeputados");
}
});
}
}
ClienteConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
@Configuration
public class ClienteConfig {
@Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("deputados_ws");
return marshaller;
}
@Bean
public ClienteDeputadosWS deputadosCliente(Jaxb2Marshaller marshaller) {
ClienteDeputadosWS cliente = new ClienteDeputadosWS();
cliente.setDefaultUri("https://www.camara.leg.br/SitCamaraWS/Deputados.asmx");
cliente.setMarshaller(marshaller);
cliente.setUnmarshaller(marshaller);
return cliente;
}
}
executeCliente.java
import java.net.URISyntaxException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import deputados_ws.ObterDeputadosResponse.ObterDeputadosResult;
public class executeCliente {
public static void main(String[] args) throws URISyntaxException {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ClienteConfig.class)) {
ClienteDeputadosWS cliente = context.getBean(ClienteDeputadosWS.class);
ObterDeputadosResult response = cliente.getDeputados();
}
}
}
https://docs.spring.io/spring-ws/docs/2.2.x/reference/html/client.html
Usando banco de dados H2
1) Inclua no pom.xml:
2) Inclua em application.properties:
datasource
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:~/test
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.username=sa spring.datasource.password=
h2
spring.h2.console.enabled=true spring.h2.console.path=/h2-console
jpa
spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.hibernate.ddl-auto=none spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
3) Acessar: localhost:8080/h2-console