Spring Security — пример REST-сервиса с авторизацией по протоколу OAuth2 через BitBucket и JWT

от автора

В предыдущей статье мы разработали простое защищенное веб приложение, в котором для аутентификации пользователей использовался протокол OAuth2 с Bitbucket в качестве сервера авторизации. Кому-то такая связка может показаться странной, но представьте, что мы разрабатываем CI (Continuous Integration) сервер и хотели бы иметь доступ к ресурсам пользователя в системе контроля версий. Например, по такому же принципу работает довольно известная CI платформа drone.io.

В предыдущем примере для авторизации запросов к серверу использовалась HTTP-сессия (и куки). Однако для реализации REST-сервиса данный способ авторизации не подходит, поскольку одним из требований REST архитектуры является отсутсвие состояния. В данной статье мы реализуем REST-сервис, авторизация запросов к которому будет осуществляться с помощью токена доступа (access token).

Немного теории

Аутентификация — это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя осуществляется путём сравнения введённого им логина/пароля с сохраненными данными.
Авторизация — это проверка прав пользователя на доступ к определенным ресурсам. Авторизация выполняется непосредственно при обращении пользователя к ресурсу.

Рассмотрим порядок работы двух вышеупомянутых способов авторизации запросов.
Авторизация запросов с помощью HTTP-сессии:

  • Пользователь проходит аутентификацию любым из способов.
  • На сервере создается HTTP-сессия и куки JSESSIONID, хранящий идентификатор сессии.
  • Куки JSESSIONID передается на клиент и сохраняется в браузере.
  • С каждым последующим запросом на сервер отправляется куки JSESSIONID.
  • Сервер находит соответствующую HTTP-сессию с информацией о текущем пользователе и определяет имеет ли пользователь права на выполнение данного вызова.
  • Для выполнения выхода из приложения необходимо удалить с сервера HTTP-сессию.

Авторизация запросов с помощью токена доступа:

  • Пользователь проходит аутентификацию любым из способов.
  • Сервер создает токен доступа, подписанный секретным ключом, а затем отправляет его клиенту. Токен содержит идентификатор пользователя и его роли.
  • Токен сохраняется на клиенте и передается на сервер с каждым последующим запросом. Как правило для передачи токена используетя HTTP заголовок Authorization.
  • Сервер сверяет подпись токена, извлекает из него идентификатор пользователя, его роли и определяет имеет ли пользователь права на выполнение данного вызова.
  • Для выполнения выхода из приложения достаточно просто удалить токен на клиенте без необходимости взаимодействия с сервером.

Распространенным форматом токена доступа в настоящее время является JSON Web Token (JWT). Токен в формате JWT содержит три блока, разделенных точками: заголовок (header), набор полей (payload) и сигнатуру (подпись). Первые два блока представлены в JSON-формате и закодированы в формат base64. Набор полей может состоять как из зарезервированных имен (iss, iat, exp), так и произвольных пар имя/значение. Подпись может генерироваться как при помощи симметричных, так и асимметричных алгоритмов шифрования.

Реализация

Мы реализуем REST-сервис, предоставляющий следующее API:

  • GET /auth/login — запустить процесс аутентификации пользователя.
  • POST /auth/token — запросить новую пару access/refresh токенов.
  • GET /api/repositories — получить список Bitbucket репозиториев текущего пользователя.


Высокоуровневая архитектура приложения.

Заметим, что поскольку приложение состоит из трех взаимодействующих компонентов, помимо того, что мы выполняем авторизацию запросов клиента к серверу, Bitbucket авторизует запросы сервера к нему. Мы не будем настраивать авторизацию методов по ролям, чтобы не делать пример сложнее. У нас есть только один API метод GET /api/repositories, вызывать который могут только аутентифицированные пользователи. Сервер может выполнять на Bitbucket любые операции, разрешенные при регистрации OAuth клиента.

Процесс регистрации OAuth клиента описан в предыдущей статье.

Для реализации мы будем использовать Spring Boot версии 2.2.2.RELEASE и Spring Security версии 5.2.1.RELEASE.

Переопределим AuthenticationEntryPoint.

В стандартном веб-приложении, когда осуществляется обращение к защищенному ресурсу и в секьюрити контексте отсутствует объект Authentication, Spring Security перенаправляет пользователя на страницу аутентификации. Однако для REST-сервиса более подходящим поведением в этом случае было бы возвращать HTTP статус 401 (UNAUTHORIZED).

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());     } } 

Создадим login endpoint.

Для аутентификации пользователя мы по-прежнему используем OAuth2 с типом авторизации Authorization Code. Однако на предыдущем шаге мы заменили стандартный AuthenticationEntryPoint своей реализацией, поэтому нам нужен явный способ запустить процесс аутентификации. При отправке GET запроса по адресу /auth/login мы перенаправим пользователя на страницу аутентификации Bitbucket. Параметром этого метода будет URL обратного вызова, по которому мы возвратим токен доступа после успешной аутентификации.

Login endpoint

@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());     } } 

Переопределим AuthenticationSuccessHandler.

AuthenticationSuccessHandler вызывается после успешной аутентификации. Сгенерируем тут токен доступа, refresh токен и выполним редирект по адресу обратного вызова, который был передан в начале процесса аутентификации. Токен доступа вернем параметром GET запроса, а refresh токен в httpOnly куке. Что такое refresh токен разберем позже.

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);     } } 

Переопределим AuthenticationFailureHandler.

В случае, если пользователь не прошел аутентификацию, мы перенаправим его по адресу обратного вызова, который был передан в начале процесса аутентификации с параметром 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();     } } 

Создадим TokenAuthenticationFilter.

Задача этого фильтра извлечь токен доступа из заголовка Authorization в случае его наличия, провалидировать его и инициализировать секьюрити контекст.

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);     } } 

Создадим refresh token endpoint.

В целях безопасности время жизни токена доступа обычно делают небольшим. Тогда в случае его кражи злоумышленник не сможет пользоваться им бесконечно долго. Чтобы не заставлять пользователя выполнять вход в приложение снова и снова используется refresh токен. Он выдается сервером после успешной аутентификации вместе с токеном доступа и имеет большее время жизни. Используя его можно запросить новую пару токенов. Refresh токен рекомендуют хранить в httpOnly куке.

Refresh token endpoint

@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();         });     } } 

Переопределим AuthorizationRequestRepository.

Spring Security использует объект AuthorizationRequestRepository для хранения объектов OAuth2AuthorizationRequest на время процесса аутентификации. Реализацией по умолчанию является класс HttpSessionOAuth2AuthorizationRequestRepository, который использует HTTP-сессию в качестве хранилища. Т.к. наш сервис не должен хранить состояние, эта реализация нам не подходит. Реализуем свой класс, который будет использовать HTTP cookies.

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())));     } } 

Настроим Spring Security.

Соберем все проделанное выше вместе и настроим 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);     } } 

Создадим repositories endpoint.

То ради чего и нужна была аутентификация через OAuth2 и Bitbucket — возможность использовать Bitbucket API для доступа к своим ресурсам. Используем Bitbucket repositories API для получения списка репозиториев текущего пользователя.

Repositories endpoint

@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;     } } 

Тестирование

Для тестирования нам понадобится небольшой HTTP-сервер, на который будет отправлен токен доступа. Сначала попробуем вызвать repositories endpoint без токена доступа и убедимся, что в этом случае получим ошибку 401. Затем пройдем аутентификацию. Для этого запустим сервер и перейдем в браузере по адресу http://localhost:8080/auth/login. После того как мы введем логин/пароль, клиент получит токен и вызовет repositories endpoint еще раз. Затем будет запрошен новый токен и снова вызван repositories endpoint с новым токеном.

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);     } } 

Консольный вывод клиента.

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) 

Исходный код

Полный исходный код рассмотренного приложения находится на Github.

Ссылки

P.S.
Созданный нами REST-сервис работает по протоколу HTTP, чтобы не усложнять пример. Но поскольку токены у нас никак не шифруются, рекомендуется перейти на защищенный канал (HTTPS).

ссылка на оригинал статьи https://habr.com/ru/post/528410/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *