Spring Security: servicio REST de muestra con autenticación OAuth2 a través de BitBucket y JWT

En el artículo anterior , desarrollamos una aplicación web segura y simple que usaba OAuth2 para autenticar a los usuarios con Bitbucket como servidor de autorización. Para algunos, tal paquete puede parecer extraño, pero imagínense que estamos desarrollando un servidor CI (Integración continua) y nos gustaría tener acceso a los recursos del usuario en el sistema de control de versiones. Por ejemplo, la conocida plataforma de CI drone.io funciona con el mismo principio .



En el ejemplo anterior, se utilizó una sesión HTTP (y cookies) para autorizar solicitudes al servidor. Sin embargo, para la implementación de un servicio REST, este método de autorización no es adecuado, ya que uno de los requisitos de la arquitectura REST es la falta de estado. En este artículo implementaremos un servicio REST, cuya autorización de solicitudes se realizará mediante un token de acceso.



Un poco de teoría



La autenticación es el proceso de verificar las credenciales del usuario (inicio de sesión / contraseña). La autenticación del usuario se lleva a cabo comparando el nombre de usuario / contraseña ingresados ​​por él con los datos guardados.



La autorización es la verificación de los derechos de un usuario para acceder a ciertos recursos. La autorización se realiza directamente cuando el usuario accede al recurso.



Consideremos el orden de trabajo de los dos métodos mencionados anteriormente para autorizar solicitudes.

Autorizar solicitudes mediante una sesión HTTP:



  • El usuario está autenticado de cualquiera de las formas.
  • Se crea una sesión HTTP en el servidor y una cookie JSESSIONID que almacena el identificador de sesión.
  • La cookie JSESSIONID se transmite al cliente y se almacena en el navegador.
  • Con cada solicitud posterior, se envía una cookie JSESSIONID al servidor.
  • El servidor encuentra la sesión HTTP correspondiente con información sobre el usuario actual y determina si el usuario tiene permiso para realizar esta llamada.
  • Para salir de la aplicación, debe eliminar la sesión HTTP del servidor.


Autorizar solicitudes mediante un token de acceso:



  • El usuario está autenticado de cualquiera de las formas.
  • El servidor genera un token de acceso firmado con una clave privada y luego lo envía al cliente. El token contiene el identificador del usuario y su función.
  • El token se almacena en el cliente y se transmite al servidor con cada solicitud posterior. Normalmente, el encabezado HTTP de autorización se utiliza para transferir el token.
  • El servidor verifica la firma del token, extrae de él el ID de usuario, su función y determina si el usuario tiene derechos para realizar esta llamada.
  • Para salir de la aplicación, simplemente elimine el token en el cliente sin tener que interactuar con el servidor.


El JSON Web Token (JWT) es actualmente un formato de token de acceso común. Un token JWT contiene tres bloques, separados por puntos: encabezado, carga útil y firma. Los dos primeros bloques están en formato JSON y codificados en formato base64. El conjunto de campos puede constar de nombres reservados (iss, iat, exp) o pares arbitrarios de nombre / valor. La firma se puede generar utilizando algoritmos de cifrado simétricos y asimétricos.



Implementación



Implementaremos un servicio REST que brinda la siguiente API:



  • GET / auth / login: inicia el proceso de autenticación del usuario.
  • POST / auth / token: solicita un nuevo par de tokens de acceso / actualización.
  • GET / api / repositories: obtenga la lista de repositorios de Bitbucket del usuario actual.




Arquitectura de la aplicación de alto nivel



Tenga en cuenta que, dado que la aplicación consta de tres componentes que interactúan, además de autorizar las solicitudes del cliente al servidor, Bitbucket autoriza las solicitudes del servidor. No configuraremos la autorización de método por rol, para no complicar el ejemplo. Solo tenemos un método de API GET / api / repositories que solo pueden ser invocados por usuarios autenticados. El servidor puede realizar cualquier operación en Bitbucket que esté permitida por el registro OAuth del cliente.





El proceso para registrar un cliente OAuth se describe en el artículo anterior .



Para la implementación, usaremos Spring Boot versión 2.2.2.RELEASE y Spring Security versión 5.2.1.RELEASE.



Anular AuthenticationEntryPoint



En una aplicación web estándar, cuando se accede a un recurso seguro y no hay ningún objeto de autenticación en el contexto de seguridad, Spring Security redirigirá al usuario a la página de autenticación. Sin embargo, para un servicio REST, el comportamiento más apropiado en este caso sería devolver el estado HTTP 401 (NO AUTORIZADO).



RestAuthenticationEntryPoint
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}




Crear un punto final de inicio de sesión



Seguimos utilizando OAuth2 con el tipo de autorización de código de autorización para la autenticación de usuarios. Sin embargo, en el paso anterior, reemplazamos el AuthenticationEntryPoint estándar con nuestra propia implementación, por lo que necesitamos una forma explícita de iniciar el proceso de autenticación. Al enviar una solicitud GET a / auth / login, redirigiremos al usuario a la página de autenticación de Bitbucket. El parámetro de este método será la URL de devolución de llamada, donde devolveremos el token de acceso después de una autenticación exitosa.



Punto final de inicio de sesión
@Path("/auth")
public class AuthEndpoint extends EndpointBase {

...

    @GET
    @Path("/login")
    public Response authorize(@QueryParam(REDIRECT_URI) String redirectUri) {
        String authUri = "/oauth2/authorization/bitbucket";
        UriComponentsBuilder builder = fromPath(authUri).queryParam(REDIRECT_URI, redirectUri);
        return handle(() -> temporaryRedirect(builder.build().toUri()).build());
    }
}




Anular AuthenticationSuccessHandler



AuthenticationSuccessHandler se llama después de una autenticación exitosa. Generemos un token de acceso aquí, un token de actualización y redirigiremos a la dirección de devolución de llamada que se envió al comienzo del proceso de autenticación. Devolvemos el token de acceso con el parámetro de solicitud GET y el token de actualización en la cookie httpOnly. Analizaremos qué es un token de actualización más adelante.



ExampleAuthenticationSuccessHandler
public class ExampleAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final TokenService tokenService;

    private final AuthProperties authProperties;

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public ExampleAuthenticationSuccessHandler(
            TokenService tokenService,
            AuthProperties authProperties,
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.tokenService = requireNonNull(tokenService);
        this.authProperties = requireNonNull(authProperties);
        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("Logged in user {}", authentication.getPrincipal());
        super.onAuthenticationSuccess(request, response, authentication);
    }

    @Override
    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        Optional<String> redirectUri = getCookie(request, REDIRECT_URI).map(Cookie::getValue);

        if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
            throw new BadRequestException("Received unauthorized redirect URI.");
        }

        return UriComponentsBuilder.fromUriString(redirectUri.orElse(getDefaultTargetUrl()))
                .queryParam("token", tokenService.newAccessToken(toUserContext(authentication)))
                .build().toUriString();
    }

    @Override
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        redirectToTargetUrl(request, response, authentication);
    }

    private boolean isAuthorizedRedirectUri(String uri) {
        URI clientRedirectUri = URI.create(uri);
        return authProperties.getAuthorizedRedirectUris()
                .stream()
                .anyMatch(authorizedRedirectUri -> {
                    // Only validate host and port. Let the clients use different paths if they want to.
                    URI authorizedURI = URI.create(authorizedRedirectUri);
                    return authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
                            && authorizedURI.getPort() == clientRedirectUri.getPort();
                });
    }

    private TokenService.UserContext toUserContext(Authentication authentication) {
        ExampleOAuth2User principal = (ExampleOAuth2User) authentication.getPrincipal();
        return TokenService.UserContext.builder()
                .login(principal.getName())
                .name(principal.getFullName())
                .build();
    }

    private void addRefreshTokenCookie(HttpServletResponse response, Authentication authentication) {
        RefreshToken token = tokenService.newRefreshToken(toUserContext(authentication));
        addCookie(response, REFRESH_TOKEN, token.getId(), (int) token.getValiditySeconds());
    }

    private void redirectToTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        String targetUrl = determineTargetUrl(request, response, authentication);

        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
            return;
        }

        addRefreshTokenCookie(response, authentication);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}




Anular AuthenticationFailureHandler



En caso de que el usuario no esté autenticado, lo redirigiremos a la dirección de devolución de llamada que se pasó al comienzo del proceso de autenticación con el parámetro de error que contiene el texto del error.



ExampleAuthenticationFailureHandler
public class ExampleAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    public ExampleAuthenticationFailureHandler(
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.authorizationRequestRepository = requireNonNull(authorizationRequestRepository);
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        String targetUrl = getFailureUrl(request, exception);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    private String getFailureUrl(HttpServletRequest request, AuthenticationException exception) {
        String targetUrl = getCookie(request, Cookies.REDIRECT_URI)
                .map(Cookie::getValue)
                .orElse(("/"));

        return UriComponentsBuilder.fromUriString(targetUrl)
                .queryParam("error", exception.getLocalizedMessage())
                .build().toUriString();
    }
}




Crear TokenAuthenticationFilter



La tarea de este filtro es extraer el token de acceso del encabezado de autorización, si está presente, para validarlo e inicializar el contexto de seguridad.



TokenAuthenticationFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final UserService userService;

    private final TokenService tokenService;

    public TokenAuthenticationFilter(
            UserService userService, TokenService tokenService) {
        this.userService = requireNonNull(userService);
        this.tokenService = requireNonNull(tokenService);
    }

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException {
        try {
            Optional<String> jwtOpt = getJwtFromRequest(request);
            if (jwtOpt.isPresent()) {
                String jwt = jwtOpt.get();
                if (isNotEmpty(jwt) && tokenService.isValidAccessToken(jwt)) {
                    String login = tokenService.getUsername(jwt);
                    Optional<User> userOpt = userService.findByLogin(login);
                    if (userOpt.isPresent()) {
                        User user = userOpt.get();
                        ExampleOAuth2User oAuth2User = new ExampleOAuth2User(user);
                        OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oAuth2User, oAuth2User.getAuthorities(), oAuth2User.getProvider());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Could not set user authentication in security context", e);
        }

        chain.doFilter(request, response);
    }

    private Optional<String> getJwtFromRequest(HttpServletRequest request) {
        String token = request.getHeader(AUTHORIZATION);
        if (isNotEmpty(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        return Optional.ofNullable(token);
    }
}




Crear punto final de token de actualización



Por razones de seguridad, la vida útil del token de acceso suele ser pequeña. Entonces, si es robado, un atacante no podrá usarlo indefinidamente. Para no obligar al usuario a iniciar sesión en la aplicación una y otra vez, se utiliza el token de actualización. Lo emite el servidor después de una autenticación exitosa junto con el token de acceso y tiene una vida útil más larga. Al usarlo, puede solicitar un nuevo par de tokens. Se recomienda almacenar el token de actualización en la cookie httpOnly.



Actualizar el extremo del token
@Path("/auth")
public class AuthEndpoint extends EndpointBase {

...

    @POST
    @Path("/token")
    @Produces(APPLICATION_JSON)
    public Response refreshToken(@CookieParam(REFRESH_TOKEN) String refreshToken) {
        return handle(() -> {
            if (refreshToken == null) {
                throw new InvalidTokenException("Refresh token was not provided.");
            }
            RefreshToken oldRefreshToken = tokenService.findRefreshToken(refreshToken);
            if (oldRefreshToken == null || !tokenService.isValidRefreshToken(oldRefreshToken)) {
                throw new InvalidTokenException("Refresh token is not valid or expired.");
            }

            Map<String, String> result = new HashMap<>();
            result.put("token", tokenService.newAccessToken(of(oldRefreshToken.getUser())));

            RefreshToken newRefreshToken = newRefreshTokenFor(oldRefreshToken.getUser());
            return Response.ok(result).cookie(createRefreshTokenCookie(newRefreshToken)).build();
        });
    }
}




Anular AuthorizationRequestRepository



Spring Security utiliza el objeto AuthorizationRequestRepository para almacenar objetos OAuth2AuthorizationRequest durante el proceso de autenticación. La implementación predeterminada es la clase HttpSessionOAuth2AuthorizationRequestRepository, que usa la sesión HTTP como repositorio. Porque nuestro servicio no debe almacenar el estado, esta implementación no nos conviene. Implementemos nuestra propia clase que usará cookies HTTP.



HttpCookieOAuth2AuthorizationRequestRepository
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

    private static final int COOKIE_EXPIRE_SECONDS = 180;

    private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "OAUTH2-AUTH-REQUEST";

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        return getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
                .map(cookie -> deserialize(cookie, OAuth2AuthorizationRequest.class))
                .orElse(null);
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        if (authorizationRequest == null) {
            removeAuthorizationRequestCookies(request, response);
            return;
        }

        addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
        String redirectUriAfterLogin = request.getParameter(QueryParams.REDIRECT_URI);
        if (isNotBlank(redirectUriAfterLogin)) {
            addCookie(response, REDIRECT_URI, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);
        }
    }

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
        return loadAuthorizationRequest(request);
    }

    public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
        deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
        deleteCookie(request, response, REDIRECT_URI);
    }

    private static String serialize(Object object) {
        return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object));
    }

    @SuppressWarnings("SameParameterValue")
    private static <T> T deserialize(Cookie cookie, Class<T> clazz) {
        return clazz.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue())));
    }
}




Configuración de Spring Security



Juntemos todo lo anterior y configuremos Spring Security.



WebSecurityConfig
@Configuration
@EnableWebSecurity
public static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final ExampleOAuth2UserService userService;

    private final TokenAuthenticationFilter tokenAuthenticationFilter;

    private final AuthenticationFailureHandler authenticationFailureHandler;

    private final AuthenticationSuccessHandler authenticationSuccessHandler;

    private final HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository;

    @Autowired
    public WebSecurityConfig(
            ExampleOAuth2UserService userService,
            TokenAuthenticationFilter tokenAuthenticationFilter,
            AuthenticationFailureHandler authenticationFailureHandler,
            AuthenticationSuccessHandler authenticationSuccessHandler,
            HttpCookieOAuth2AuthorizationRequestRepository authorizationRequestRepository) {
        this.userService = userService;
        this.tokenAuthenticationFilter = tokenAuthenticationFilter;
        this.authenticationFailureHandler = authenticationFailureHandler;
        this.authenticationSuccessHandler = authenticationSuccessHandler;
        this.authorizationRequestRepository = authorizationRequestRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .formLogin().disable()
                .httpBasic().disable()
                .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))
                .exceptionHandling(eh -> eh
                        .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                )
                .authorizeRequests(authorizeRequests -> authorizeRequests
                        .antMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2Login -> oauth2Login
                        .failureHandler(authenticationFailureHandler)
                        .successHandler(authenticationSuccessHandler)
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService))
                        .authorizationEndpoint(authEndpoint -> authEndpoint.authorizationRequestRepository(authorizationRequestRepository))
                );

        http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}




Crear punto final de repositorios



Es por eso que se necesitaba la autenticación a través de OAuth2 y Bitbucket: la capacidad de usar la API de Bitbucket para acceder a sus recursos. Usamos la API de repositorios de Bitbucket para obtener una lista de los repositorios del usuario actual.



Punto final de repositorios
@Path("/api")
public class ApiEndpoint extends EndpointBase {

    @Autowired
    private BitbucketService bitbucketService;

    @GET
    @Path("/repositories")
    @Produces(APPLICATION_JSON)
    public List<Repository> getRepositories() {
        return handle(bitbucketService::getRepositories);
    }
}

public class BitbucketServiceImpl implements BitbucketService {

    private static final String BASE_URL = "https://api.bitbucket.org";

    private final Supplier<RestTemplate> restTemplate;

    public BitbucketServiceImpl(Supplier<RestTemplate> restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public List<Repository> getRepositories() {
        UriComponentsBuilder uriBuilder = fromHttpUrl(format("%s/2.0/repositories", BASE_URL));
        uriBuilder.queryParam("role", "member");

        ResponseEntity<BitbucketRepositoriesResponse> response = restTemplate.get().exchange(
                uriBuilder.toUriString(),
                HttpMethod.GET,
                new HttpEntity<>(new HttpHeadersBuilder()
                        .acceptJson()
                        .build()),
                BitbucketRepositoriesResponse.class);

        BitbucketRepositoriesResponse body = response.getBody();
        return body == null ? emptyList() : extractRepositories(body);
    }

    private List<Repository> extractRepositories(BitbucketRepositoriesResponse response) {
        return response.getValues() == null
                ? emptyList()
                : response.getValues().stream().map(BitbucketServiceImpl.this::convertRepository).collect(toList());
    }

    private Repository convertRepository(BitbucketRepository bbRepo) {
        Repository repo = new Repository();
        repo.setId(bbRepo.getUuid());
        repo.setFullName(bbRepo.getFullName());
        return repo;
    }
}




Pruebas



Para las pruebas, necesitamos un pequeño servidor HTTP al que enviar un token de acceso. Primero, intentemos llamar al punto final de los repositorios sin un token de acceso y asegurarnos de que obtengamos un error 401. Luego, nos autenticaremos. Para hacer esto, inicie el servidor y vaya al navegador en http: // localhost: 8080 / auth / login . Después de ingresar el nombre de usuario / contraseña, el cliente recibirá un token y volverá a llamar al punto final de los repositorios. Luego, se solicitará un nuevo token y se volverá a llamar al punto final de los repositorios con el nuevo token.



OAuth2JwtExampleClient
public class OAuth2JwtExampleClient {

    /**
     * Start client, then navigate to http://localhost:8080/auth/login.
     */
    public static void main(String[] args) throws Exception {
        AuthCallbackHandler authEndpoint = new AuthCallbackHandler(8081);
        authEndpoint.start(SOCKET_READ_TIMEOUT, true);

        HttpResponse response = getRepositories(null);
        assert (response.getStatusLine().getStatusCode() == SC_UNAUTHORIZED);

        Tokens tokens = authEndpoint.getTokens();
        System.out.println("Received tokens: " + tokens);
        response = getRepositories(tokens.getAccessToken());
        assert (response.getStatusLine().getStatusCode() == SC_OK);
        System.out.println("Repositories: " + IOUtils.toString(response.getEntity().getContent(), UTF_8));

        // emulate token usage - wait for some time until iat and exp attributes get updated
        // otherwise we will receive the same token
        Thread.sleep(5000);

        tokens = refreshToken(tokens.getRefreshToken());
        System.out.println("Refreshed tokens: " + tokens);

        // use refreshed token
        response = getRepositories(tokens.getAccessToken());
        assert (response.getStatusLine().getStatusCode() == SC_OK);
    }

    private static Tokens refreshToken(String refreshToken) throws IOException {
        BasicClientCookie cookie = new BasicClientCookie(REFRESH_TOKEN, refreshToken);
        cookie.setPath("/");
        cookie.setDomain("localhost");
        BasicCookieStore cookieStore = new BasicCookieStore();
        cookieStore.addCookie(cookie);

        HttpPost request = new HttpPost("http://localhost:8080/auth/token");
        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());

        HttpClient httpClient = HttpClientBuilder.create().setDefaultCookieStore(cookieStore).build();
        HttpResponse execute = httpClient.execute(request);

        Gson gson = new Gson();
        Type type = new TypeToken<Map<String, String>>() {
        }.getType();
        Map<String, String> response = gson.fromJson(IOUtils.toString(execute.getEntity().getContent(), UTF_8), type);

        Cookie refreshTokenCookie = cookieStore.getCookies().stream()
                .filter(c -> REFRESH_TOKEN.equals(c.getName()))
                .findAny()
                .orElseThrow(() -> new IOException("Refresh token cookie not found."));
        return Tokens.of(response.get("token"), refreshTokenCookie.getValue());
    }

    private static HttpResponse getRepositories(String accessToken) throws IOException {
        HttpClient httpClient = HttpClientBuilder.create().build();
        HttpGet request = new HttpGet("http://localhost:8080/api/repositories");
        request.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());
        if (accessToken != null) {
            request.setHeader(AUTHORIZATION, "Bearer " + accessToken);
        }
        return httpClient.execute(request);
    }
}




Salida de la consola del cliente.



Received tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDMxLCJleHAiOjE2MDU0NjY2MzF9.UuRYMdIxzc8ZFEI2z8fAgLz-LG_gDxaim25pMh9jNrDFK6YkEaDqDO8Huoav5JUB0bJyf1lTB0nNPaLLpOj4hw, refreshToken=BBF6dboG8tB4XozHqmZE5anXMHeNUncTVD8CLv2hkaU2KsfyqitlJpgkV4HrQqPk)

Repositories: [{"id":"{c7bb4165-92f1-4621-9039-bb1b6a74488e}","fullName":"test-namespace/test-repository1"},{"id":"{aa149604-c136-41e1-b7bd-3088fb73f1b2}","fullName":"test-namespace/test-repository2"}]

Refreshed tokens: Tokens(accessToken=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJldm9sdmVjaS10ZXN0a2l0IiwidXNlcm5hbWUiOiJFdm9sdmVDSSBUZXN0a2l0IiwiaWF0IjoxNjA1NDY2MDM2LCJleHAiOjE2MDU0NjY2MzZ9.oR2A_9k4fB7qpzxvV5QKY1eU_8aZMYEom-ngc4Kuc5omeGPWyclfqmiyQTpJW_cHOcXbY9S065AE_GKXFMbh_Q, refreshToken=mdc5sgmtiwLD1uryubd2WZNjNzSmc5UGo6JyyzsiYsBgOpeaY3yw3T3l8IKauKYQ)


Fuente



El código fuente completo de la aplicación revisada está en Github .



Enlaces





PD:

El servicio REST que creamos se ejecuta sobre el protocolo HTTP para no complicar el ejemplo. Pero dado que nuestros tokens no están encriptados de ninguna manera, se recomienda cambiar a un canal seguro (HTTPS).



All Articles