Neste artigo, consideraremos uma maneira de autorizar usuários em aplicativos Web no Spring Boot usando o protocolo OAuth2 usando um servidor de autorização externo (usando o exemplo de Bitbucket).O que queremos obter
Suponha que estamos desenvolvendo um aplicativo Web seguro que tenha acesso aos recursos do usuário em um servidor externo, por exemplo, um sistema de integração contínua, e o Bitbucket atue como um servidor externo, e não desejaríamos armazenar o nome de usuário e a senha do usuário no sistema externo. Ou seja, precisamos autorizar o usuário através do Bitbucket para obter acesso à sua conta e recursos, além disso, verifique se ele é usuário de nosso aplicativo e faça com que ele não nos revele suas credenciais do Bitbucket. Gostaria de saber como fazer isso - bem-vindo ao gato.OAuth2
OAuth2 é um protocolo de autorização que permite fornecer a terceiros acesso limitado aos recursos protegidos de um usuário sem precisar fornecer a ela (terceiros) um nome de usuário e senha.OAuth2 define quatro funções:- Proprietário do recurso
- Servidor de recursos
- Servidor de autorização
- Cliente (aplicativo)
Um proprietário de recurso é um usuário que usa um aplicativo cliente e permite que ele acesse sua conta hospedada em um servidor de recursos. O acesso de aplicativos à conta é limitado pelas permissões concedidas.O servidor de recursos hospeda contas de usuário seguras.O servidor de autorização autentica o proprietário do recurso e emite tokens de acesso. O servidor de autorização pode ser simultaneamente um servidor de recursos.Um aplicativo cliente é um aplicativo que deseja acessar a conta e os recursos de um usuário.No nosso caso, o aplicativo cliente é o aplicativo que estamos desenvolvendo e o Bitbucket será um servidor de autorização e um servidor de recursos.O OAuth2 suporta quatro tipos de autorização: código de autorização, implícito, credenciais de senha do proprietário do recurso e credenciais do cliente. Não consideraremos todos eles, estamos interessados no tipo de código de autorização. O tipo de código de autorização é otimizado para aplicativos de servidor nos quais o código fonte não está disponível ao público e o código secreto do cliente pode ser mantido em sigilo. Esse tipo funciona com base no redirecionamento, ou seja, o usuário será redirecionado ao servidor de autorização para confirmar sua identidade e permitir que o aplicativo use sua conta.O processo de autorização por meio do código de autorização consiste em uma sequência de duas solicitações:- Pedido de Autorização
- Solicitação de token de acesso
Uma solicitação de autorização é usada para confirmar a identidade do usuário, bem como solicitar autorização do nosso aplicativo ao usuário. Esta solicitação é uma solicitação GET com os seguintes parâmetros:- response_type - o valor deve ser igual ao código
- client_id - valor obtido ao registrar um cliente no provedor OAuth2
- redirect_uri - URL para o qual o usuário será redirecionado após a autorização
- escopo - um parâmetro opcional que indica qual nível de acesso é solicitado
- state - sequência gerada aleatoriamente para verificar a resposta
Exemplo de solicitação:GET https://server.example.com/authorize?response_type=code&client_id=CLIENT_ID&state=xyz&redirect_uri=REDIRECT_URI
Se o usuário confirmar sua identidade e permitir que o aplicativo acesse seus recursos, o agente do usuário será redirecionado para a URL de retorno de chamada especificada durante o registro do cliente com o código de parâmetro adicional que contém o código de autorização e o parâmetro state com o valor passado na solicitação.Exemplo de retorno de chamada:GET https://client.example.com/cb?code=AUTH_CODE_HERE&state=xyz
Uma solicitação de código de acesso é usada para trocar o código de autorização recebido por um código de acesso aos recursos do usuário. Esta solicitação é uma solicitação POST com os seguintes parâmetros:- grant_type - o valor deve ser permission_code
- code - código de autorização obtido na etapa anterior
- redirect_uri - deve corresponder ao URL especificado na etapa anterior
- client_id - valor obtido ao registrar um cliente no provedor OAuth2
- client_secret - valor obtido ao registrar um cliente com o provedor OAuth2
Exemplo de solicitação:POST https://server.example.com/token
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
A resposta do servidor contém o código de acesso e sua vida útil:{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
Todo esse processo já está automatizado no Spring Security e não precisamos nos preocupar com sua implementação.Cadastro de cliente
Primeiro, registraremos nosso aplicativo como cliente no Bitbucket para obter uma chave (Chave) e um código de acesso (Segredo do Cliente).
Digite o nome do cliente e o URL de retorno de chamada. Depois, observamos o que estará disponível para esse cliente.
Os valores Key e ClientSecret obtidos são armazenados em application.properties:client_id=ZJsdANfWkJ7jcktw2x
client_secret=28uUrJ9m43svbkcnXVNj8qeBjFtd8jaD
Configurar Spring Security
Em seguida, vamos configurar o Spring Security. Para o OAuth2 funcionar, você deve criar um objeto ClientRegistration. ClientRegistration armazena informações sobre o cliente registrado com o provedor OAuth2. Aqui precisamos do client_id e client_secret obtidos na etapa anterior. Como pode haver vários objetos ClientRegistration em geral, o Spring Security usa o objeto ClientRegistrationRepository para armazená-los e acessá-los. Vamos criar também. Também indicamos que apenas um usuário autorizado pode chamar qualquer solicitação e redefinir o UserService com sua implementação.SecurityConfig.java@Configuration
public class SecurityConfig {
@Value("${client_id}")
private String clientId;
@Value("${client_secret}")
private String clientSecret;
@Bean
public ClientRegistration clientRegistration() {
return ClientRegistration
.withRegistrationId("bitbucket")
.clientId(clientId)
.clientSecret(clientSecret)
.userNameAttributeName("username")
.clientAuthenticationMethod(BASIC)
.authorizationGrantType(AUTHORIZATION_CODE)
.userInfoUri("https://api.bitbucket.org/2.0/user")
.tokenUri("https://bitbucket.org/site/oauth2/access_token")
.authorizationUri("https://bitbucket.org/site/oauth2/authorize")
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.build();
}
@Bean
@Autowired
public MyOAuth2UserService oAuth2userService(UserService userService) {
return new MyOAuth2UserService(userService);
}
@Bean
@Autowired
public ClientRegistrationRepository clientRegistrationRepository(List<ClientRegistration> registrations) {
return new InMemoryClientRegistrationRepository(registrations);
}
@Configuration
@EnableWebSecurity
public static class AuthConfig extends WebSecurityConfigurerAdapter {
private final MyOAuth2UserService userService;
@Autowired
public AuthConfig(MyOAuth2UserService userService) {
this.userService = userService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login -> oauth2Login
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService))
);
}
}
}
Customização UserService
O Spring Security não apenas implementa completamente o processo de autorização, mas também oferece a capacidade de personalizá-lo. Por exemplo, a possibilidade de personalizar uma solicitação de autorização , solicitar um código de acesso , bem como a possibilidade de pós-processamento personalizado de uma resposta a uma solicitação de código de acesso . Após a autorização bem-sucedida, o Spring Security usa o UserInfo Endpoint para recuperar atributos do usuário do servidor de autorização. Em particular, a implementação da interface OAuth2UserService é usada para isso.Vamos criar nossa própria implementação deste serviço, para que, após a autorização do usuário no servidor de autorização, verifiquemos adicionalmente se ele é usuário do nosso aplicativo ou registre-o se o registro estiver aberto a todos. Por padrão, o Spring Security usa a implementação de DefaultOAuth2UserService. Ele formará a base da nossa implementação.MyOAuth2UserService.javapublic class MyOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<Map<String, Object>>() {
};
private final UserService userService;
private final RestOperations restOperations;
private final Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
public MyOAuth2UserService(UserService userService) {
this.userService = requireNonNull(userService);
this.restOperations = createRestTemplate();
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
checkNotNull(userRequest, "userRequest cannot be null");
if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE, "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + registrationId, null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
ResponseEntity<Map<String, Object>> response;
try {
response = this.restOperations.exchange(requestEntityConverter.convert(userRequest), PARAMETERIZED_RESPONSE_TYPE);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
StringBuilder errorDetails = new StringBuilder();
errorDetails.append("Error details: [");
errorDetails.append("UserInfo Uri: ").append(
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
if (oauth2Error.getDescription() != null) {
errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
}
errorDetails.append("]");
oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
Map<String, Object> userAttributes = emptyIfNull(response.getBody());
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
for (String authority : userRequest.getAccessToken().getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
User user = findOrCreate(userAttributes);
userAttributes.put(MyOAuth2User.ID_ATTR, user.getId());
return new MyOAuth2User(userNameAttributeName, userAttributes, authorities);
}
private User findOrCreate(Map<String, Object> userAttributes) {
String login = (String) userAttributes.get("username");
String username = (String) userAttributes.get("display_name");
Optional<User> userOpt = userService.findByLogin(login);
if (!userOpt.isPresent()) {
User user = new User();
user.setLogin(login);
user.setName(username);
return userService.create(user);
}
return userOpt.get();
}
private RestTemplate createRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
return restTemplate;
}
}
Ponto final de teste
É hora de criar um ponto de extremidade para testar a integridade do que acabamos de fazer. Nosso endpoint consistirá em apenas uma solicitação, que receberá o usuário atual.WelcomeEndpoint.java@Path("/")
public class WelcomeEndpoint {
@Autowired
private UserService userService;
@GET
public String welcome() {
User currentUser = getCurrentUser();
return String.format("Welcome, %s! (user id: %s, user login: %s)",
currentUser.getName(), currentUser.getId(), currentUser.getLogin());
}
public User getCurrentUser() {
return userService.findByLogin(getAuthenticatedUser().getName()).orElseThrow(() -> new RuntimeException("No user logged in."));
}
private Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
private MyOAuth2User getAuthenticatedUser() {
return (MyOAuth2User) getAuthentication().getPrincipal();
}
}
Exame de saúde
Iniciamos o aplicativo e vamos para o endereço http: // localhost: 8080 e vemos que fomos redirecionados para o site Bitbucket para confirmar nossa conta. Digite o nome de usuário e a senha.
Agora, precisamos permitir que nosso aplicativo acesse nossa conta e recursos.
A saudação do usuário contém os dois atributos do servidor de autorização e o ID do usuário em nosso aplicativo.
Fonte
O código fonte completo para este aplicativo está no Github .Referências