[Java] Spring Security — пример REST-сервиса с авторизацией по протоколу OAuth2 через BitBucket и JWT
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В предыдущей статье мы разработали простое защищенное веб приложение, в котором для аутентификации пользователей использовался протокол 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
SPL
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
SPL
@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
SPL
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
SPL
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
SPL
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
SPL
@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
SPL
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
SPL
@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
SPL
@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
SPL
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.
Ссылки
- OAuth — Википедия
- Spring Security Reference
- JSON Web Token (JWT)
- The OAuth 2.0 Authorization Framework
P.S.
Созданный нами REST-сервис работает по протоколу HTTP, чтобы не усложнять пример. Но поскольку токены у нас никак не шифруются, рекомендуется перейти на защищенный канал (HTTPS).
===========
Источник:
habr.com
===========
Похожие новости:
- [Oracle, Java, Карьера в IT-индустрии] Мой путь к получению Oracle Certified Associate и Oracle Certified Professional
- [Конференции] NX QA Meetup #14: (Не)адекватное code review автотестов и тестирование модуля расчета прав
- [Программирование, Java] Динамическое создание Spring Bean в рантайме (перевод)
- [Разработка веб-сайтов, CSS, JavaScript, Программирование, HTML] Современный стартовый HTML-шаблон
- [JavaScript, VueJS] CSR vs SSR для одностраничных приложений на VueJS
- [Тестирование IT-систем, JavaScript, Google Chrome] Автотесты на базе playwright и jest
- [Тестирование IT-систем, Java, Apache] Сбор данных и отправка в Apache Kafka
- [Java] Обеспечение границ компонент чистой архитектуры с помощью Spring Boot и ArchUnit (перевод)
- [Программирование, Java] Обработка исключений в контроллерах Spring
- [C#, Промышленное программирование] Как я при помощи Google сделал OPC2WEB клиент
Теги для поиска: #_java, #_spring_security, #_oauth_2.0, #_autentifikatsija (аутентификация), #_avtorizatsija (авторизация), #_jwt, #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:33
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В предыдущей статье мы разработали простое защищенное веб приложение, в котором для аутентификации пользователей использовался протокол OAuth2 с Bitbucket в качестве сервера авторизации. Кому-то такая связка может показаться странной, но представьте, что мы разрабатываем CI (Continuous Integration) сервер и хотели бы иметь доступ к ресурсам пользователя в системе контроля версий. Например, по такому же принципу работает довольно известная CI платформа drone.io. В предыдущем примере для авторизации запросов к серверу использовалась HTTP-сессия (и куки). Однако для реализации REST-сервиса данный способ авторизации не подходит, поскольку одним из требований REST архитектуры является отсутсвие состояния. В данной статье мы реализуем REST-сервис, авторизация запросов к которому будет осуществляться с помощью токена доступа (access token). Немного теории Аутентификация — это процесс проверки учётных данных пользователя (логин/пароль). Проверка подлинности пользователя осуществляется путём сравнения введённого им логина/пароля с сохраненными данными. Авторизация — это проверка прав пользователя на доступ к определенным ресурсам. Авторизация выполняется непосредственно при обращении пользователя к ресурсу. Рассмотрим порядок работы двух вышеупомянутых способов авторизации запросов. Авторизация запросов с помощью HTTP-сессии:
Авторизация запросов с помощью токена доступа:
Распространенным форматом токена доступа в настоящее время является JSON Web Token (JWT). Токен в формате JWT содержит три блока, разделенных точками: заголовок (header), набор полей (payload) и сигнатуру (подпись). Первые два блока представлены в JSON-формате и закодированы в формат base64. Набор полей может состоять как из зарезервированных имен (iss, iat, exp), так и произвольных пар имя/значение. Подпись может генерироваться как при помощи симметричных, так и асимметричных алгоритмов шифрования. Реализация Мы реализуем REST-сервис, предоставляющий следующее API:
Высокоуровневая архитектура приложения. Заметим, что поскольку приложение состоит из трех взаимодействующих компонентов, помимо того, что мы выполняем авторизацию запросов клиента к серверу, 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). RestAuthenticationEntryPointSPLpublic 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 endpointSPL@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 токен разберем позже. ExampleAuthenticationSuccessHandlerSPLpublic 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, содержащим текст ошибки. ExampleAuthenticationFailureHandlerSPLpublic 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 в случае его наличия, провалидировать его и инициализировать секьюрити контекст. TokenAuthenticationFilterSPLpublic 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 endpointSPL@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. HttpCookieOAuth2AuthorizationRequestRepositorySPLpublic 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. WebSecurityConfigSPL@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 endpointSPL@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 с новым токеном. OAuth2JwtExampleClientSPLpublic 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). =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:33
Часовой пояс: UTC + 5