Este artigo é um guia sobre como configurar uma implementação do lado do servidor de JSON Web Token (JWT) - OAuth2 estrutura de autorização usando Spring Boot e Maven .
Uma compreensão inicial do OAuth2 é recomendada e pode ser obtida lendo o rascunho no link acima ou pesquisando informações úteis na web, como isto ou isto .
OAuth2 é uma estrutura de autorização que substitui a primeira versão OAuth, criada em 2006. Ele define os fluxos de autorização entre clientes e um ou mais serviços HTTP para obter acesso a recursos protegidos.
OAuth2 define as seguintes funções do lado do servidor:
JSON Web Token, ou JWT, é uma especificação para a representação de reivindicações a serem transferidas entre duas partes. As declarações são codificadas como um objeto JSON usado como carga útil de uma estrutura criptografada, permitindo que as declarações sejam assinadas ou criptografadas digitalmente.
A estrutura que contém pode ser JSON Web Signature (JWS) ou JSON Web Encryption (JWE).
O JWT pode ser escolhido como o formato para tokens de acesso e atualização usados dentro do protocolo OAuth2.
O OAuth2 e o JWT ganharam grande popularidade nos últimos anos devido aos seguintes recursos:
No entanto, OAuth2 e JWT nem sempre são a melhor escolha caso as seguintes considerações sejam importantes para o projeto:
Embora um dos principais recursos do OAuth2 seja a introdução de uma camada de autorização para separar o processo de autorização dos proprietários de recursos, por uma questão de simplicidade, o resultado do artigo é a construção de um único aplicativo que representa todos dono do recurso , servidor de autorização e servidor de recursos papéis. Por causa disso, a comunicação fluirá entre apenas duas entidades, o servidor e o cliente.
pontos de interrupção padrão de consultas de mídia css
Esta simplificação deve ajudar a focar no objetivo do artigo, ou seja, a configuração de tal sistema em um ambiente Spring Boot.
O fluxo simplificado é descrito abaixo:
Em primeiro lugar, uma breve introdução à pilha de tecnologia selecionada para este projeto.
c-corp vs s-corp
A ferramenta de gerenciamento de projeto de escolha é Maven , mas devido à simplicidade do projeto, não deve ser difícil mudar para outras ferramentas como Gradle .
Na continuação do artigo, enfocamos apenas os aspectos do Spring Security, mas todos os trechos de código são retirados de um aplicativo do lado do servidor totalmente funcional, cujo código-fonte está disponível em um repositório público junto com um cliente consumindo seus recursos REST.
Spring Security é uma estrutura que fornece serviços de segurança quase declarativos para aplicativos baseados em Spring. Suas raízes vêm do início do Spring e é organizado como um conjunto de módulos devido ao grande número de diferentes tecnologias de segurança cobertas.
Vamos dar uma olhada rápida na arquitetura Spring Security (um guia mais detalhado pode ser encontrado Aqui )
A segurança é principalmente sobre autenticação , ou seja, a verificação da identidade, e autorização , a concessão de direitos de acesso a recursos.
A segurança Spring oferece suporte a uma grande variedade de modelos de autenticação, fornecidos por terceiros ou implementados nativamente. Uma lista pode ser encontrada Aqui .
Em relação à autorização, três áreas principais são identificadas:
A interface básica é AuthenticationManager
que é responsável por fornecer um método de autenticação. O UserDetailsService
é a interface relacionada à coleta de informações do usuário, que pode ser implementada diretamente ou usada internamente no caso de métodos JDBC ou LDAP padrão.
A interface principal é AccessDecisionManager
; cujas implementações para todas as três áreas listadas acima delegam a uma cadeia de AccessDecisionVoter
. Cada instância da última interface representa uma associação entre um Authentication
(uma identidade de usuário, principal nomeada), um recurso e uma coleção de ConfigAttribute
, o conjunto de regras que descreve como o proprietário do recurso permitiu o acesso ao próprio recurso, talvez através do uso de funções de usuário.
A segurança de um aplicativo da web é implementada usando os elementos básicos descritos acima em uma cadeia de filtros de servlet e a classe WebSecurityConfigurerAdapter
é exposto como uma forma declarativa de expressar as regras de acesso do recurso.
javascript o que é um nó
A segurança do método é primeiro habilitada pela presença de @EnableGlobalMethodSecurity(securedEnabled = true)
anotação e, em seguida, pelo uso de um conjunto de anotações especializadas para aplicar a cada método a ser protegido, como @Secured
, @PreAuthorize
e @PostAuthorize
.
O Spring Boot adiciona a tudo isso uma coleção de configurações de aplicativos opinativos e bibliotecas de terceiros para facilitar o desenvolvimento enquanto mantém um padrão de alta qualidade.
Agora vamos passar ao problema original para configurar um aplicativo que implementa OAuth2 e JWT com Spring Boot.
Embora existam várias bibliotecas OAuth2 do lado do servidor no mundo Java (uma lista pode ser encontrada Aqui ), a implementação baseada em spring é a escolha natural, pois esperamos encontrá-la bem integrada na arquitetura Spring Security e, portanto, evitar a necessidade de lidar com muitos dos detalhes de baixo nível para seu uso.
Todas as dependências da biblioteca relacionadas à segurança são tratadas pelo Maven com a ajuda do Spring Boot, que é o único componente que requer uma versão explícita dentro do arquivo de configuração do maven pom.xml (ou seja, as versões da biblioteca são inferidas automaticamente pelo Maven escolhendo a versão mais atualizada compatível com a versão do Spring Boot inserida).
Encontre abaixo o trecho do arquivo de configuração do maven pom.xml contendo as dependências relacionadas à segurança do Spring Boot:
org.springframework.boot spring-boot-starter-security org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.0.RELEASE
O aplicativo atua como servidor de autorização / proprietário de recurso OAuth2 e como servidor de recurso.
Os recursos protegidos (como servidor de recursos) são publicados em /fogo/ caminho, enquanto o caminho de autenticação (como proprietário do recurso / servidor de autorização) é mapeado para / oauth / token , seguindo o padrão proposto.
Estrutura do aplicativo:
security
pacote contendo configuração de segurançaerrors
pacote contendo tratamento de errosusers
, glee
pacotes para recursos REST, incluindo modelo, repositório e controladorOs próximos parágrafos cobrem a configuração de cada uma das três funções OAuth2 mencionadas acima. As classes relacionadas estão dentro de security
pacote:
OAuthConfiguration
, estendendo AuthorizationServerConfigurerAdapter
ResourceServerConfiguration
, estendendo ResourceServerConfigurerAdapter
ServerSecurityConfig
, estendendo WebSecurityConfigurerAdapter
UserService
, implementando UserDetailsService
O comportamento do servidor de autorização é ativado pela presença de @EnableAuthorizationServer
anotação. Sua configuração é mesclada com aquela relacionada ao comportamento do proprietário do recurso e ambas estão contidas na classe AuthorizationServerConfigurerAdapter
.
tutorial de dinâmica de corpos rígidos
As configurações aplicadas aqui estão relacionadas a:
ClientDetailsServiceConfigurer
)inMemory
ou jdbc
métodosclientId
e clientSecret
(codificado com os atributos PasswordEncoder
bean escolhidos)accessTokenValiditySeconds
e refreshTokenValiditySeconds
atributosauthorizedGrantTypes
atributoscopes
métodoAuthorizationServerEndpointsConfigurer
)accessTokenConverter
UserDetailsService
e AuthenticationManager
interfaces para realizar autenticação (como proprietário do recurso)package net.reliqs.gleeometer.security; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; @Configuration @EnableAuthorizationServer public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; private final UserDetailsService userService; @Value('${jwt.clientId:glee-o-meter}') private String clientId; @Value('${jwt.client-secret:secret}') private String clientSecret; @Value('${jwt.signing-key:123}') private String jwtSigningKey; @Value('${jwt.accessTokenValidititySeconds:43200}') // 12 hours private int accessTokenValiditySeconds; @Value('${jwt.authorizedGrantTypes:password,authorization_code,refresh_token}') private String[] authorizedGrantTypes; @Value('${jwt.refreshTokenValiditySeconds:2592000}') // 30 days private int refreshTokenValiditySeconds; public OAuthConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.userService = userService; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(clientId) .secret(passwordEncoder.encode(clientSecret)) .accessTokenValiditySeconds(accessTokenValiditySeconds) .refreshTokenValiditySeconds(refreshTokenValiditySeconds) .authorizedGrantTypes(authorizedGrantTypes) .scopes('read', 'write') .resourceIds('api'); } @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) { endpoints .accessTokenConverter(accessTokenConverter()) .userDetailsService(userService) .authenticationManager(authenticationManager); } @Bean JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); return converter; } }
A próxima seção descreve a configuração a ser aplicada ao servidor de recursos.
O comportamento do servidor de recursos é ativado pelo uso de @EnableResourceServer
a anotação e sua configuração estão contidas na classe ResourceServerConfiguration
.
A única configuração necessária aqui é a definição da identificação do recurso para coincidir com o acesso do cliente definido na aula anterior.
package net.reliqs.gleeometer.security; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId('api'); } }
O último elemento de configuração é sobre a definição da segurança do aplicativo da web.
A configuração de segurança da Web do Spring está contida na classe ServerSecurityConfig
, ativada pelo uso de @EnableWebSecurity
anotação. O @EnableGlobalMethodSecurity
permite especificar a segurança no nível do método. Seu atributo proxyTargetClass
é definido para que funcione para os métodos de RestController
, porque os controladores geralmente são classes, não implementando nenhuma interface.
Ele define o seguinte:
authenticationProvider
passwordEncoder
HttpSecurity
AuthenticationEntryPoint
para lidar com mensagens de erro fora do manipulador de erros Spring REST padrão ResponseEntityExceptionHandler
package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler; import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final UserDetailsService userDetailsService; public ServerSecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, @Qualifier('userService') UserDetailsService userDetailsService) { this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; this.userDetailsService = userDetailsService; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(userDetailsService); return provider; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers('/api/signin/**').permitAll() .antMatchers('/api/glee/**').hasAnyAuthority('ADMIN', 'USER') .antMatchers('/api/users/**').hasAuthority('ADMIN') .antMatchers('/api/**').authenticated() .anyRequest().authenticated() .and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler()); } }
O extrato de código abaixo é sobre a implementação de UserDetailsService
interface para fornecer a autenticação do proprietário do recurso.
package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.users.User; import net.reliqs.gleeometer.users.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; @Service public class UserService implements UserDetailsService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException('User not found: ' + username)); GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name()); return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority)); } }
A próxima seção é sobre a descrição de uma implementação do controlador REST para ver como as restrições de segurança são mapeadas.
Dentro do controlador REST, podemos encontrar duas maneiras de aplicar o controle de acesso para cada método de recurso:
OAuth2Authentication
passado pelo Spring como um parâmetro@PreAuthorize
ou @PostAuthorize
anotaçõespackage net.reliqs.gleeometer.users; import lombok.extern.slf4j.Slf4j; import net.reliqs.gleeometer.errors.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.Size; import java.util.HashSet; @RestController @RequestMapping('/api/users') @Slf4j @Validated class UserController { private final UserRepository repository; private final PasswordEncoder passwordEncoder; UserController(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; this.passwordEncoder = passwordEncoder; } @GetMapping Page all(@PageableDefault(size = Integer.MAX_VALUE) Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmail(auth, pageable); } return repository.findAll(pageable); } @GetMapping('/search') Page search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmailContainsAndEmail(email, auth, pageable); } return repository.findByEmailContains(email, pageable); } @GetMapping('/findByEmail') @PreAuthorize('!hasAuthority('USER') || (authentication.principal == #email)') User findByEmail(@RequestParam String email, OAuth2Authentication authentication) { return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, 'email', email)); } @GetMapping('/{id}') @PostAuthorize('!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)') User one(@PathVariable Long id) { return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); } @PutMapping('/{id}') @PreAuthorize('!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)') void update(@PathVariable Long id, @Valid @RequestBody User res) { User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); res.setPassword(u.getPassword()); res.setGlee(u.getGlee()); repository.save(res); } @PostMapping @PreAuthorize('!hasAuthority('USER')') User create(@Valid @RequestBody User res) { return repository.save(res); } @DeleteMapping('/{id}') @PreAuthorize('!hasAuthority('USER')') void delete(@PathVariable Long id) { if (repository.existsById(id)) { repository.deleteById(id); } else { throw new EntityNotFoundException(User.class, 'id', id.toString()); } } @PutMapping('/{id}/changePassword') @PreAuthorize('!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)') void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) { User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, 'id', id.toString())); if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) { user.setPassword(passwordEncoder.encode(newPassword)); repository.save(user); } else { throw new ConstraintViolationException('old password doesn't match', new HashSet()); } } }
Spring Security e Spring Boot permitem configurar rapidamente um servidor de autorização / autenticação OAuth2 completo de uma maneira quase declarativa. A configuração pode ser ainda mais reduzida configurando as propriedades do cliente OAuth2 diretamente em application.properties/yml
arquivo, conforme explicado neste tutorial .
Todo o código-fonte está disponível neste repositório GitHub: spring-glee-o-meter . Um cliente Angular que consome os recursos publicados pode ser encontrado neste repositório GitHub: glee-o-meter .
python criar objeto com atributos
OAuth2 é uma estrutura de autorização para permitir que um aplicativo de terceiros obtenha acesso limitado a um serviço HTTP por meio do compartilhamento de um token de acesso. Sua especificação substitui e torna obsoleto o protocolo OAuth 1.0.
JWT significa JSON Web Token, uma especificação para a representação de reivindicações a serem transferidas entre duas partes. As declarações são codificadas como um objeto JSON usado como carga útil de uma estrutura criptografada que permite que as declarações sejam assinadas ou criptografadas digitalmente.
Spring Security é uma estrutura focada em fornecer autenticação e autorização para aplicativos baseados em Spring.
Spring Boot é uma visão opinativa da plataforma Spring e bibliotecas de terceiros que permite minimizar a configuração de aplicativos baseados em Spring, mantendo o nível de qualidade de nível de produção.