[Из песочницы] Читаем Google-таблицы из web-приложения
Google-приложение
Создадим новый проект через google консоль.
Активируем Sheets API.
Чтобы использовать выбранный API, нужно создать учетные данные. Вызывать API будем из браузера.
Создадим идентификатор клиента OAuth 2 и зададим ограничения по URL. Указывать надо как продуктивные, так и разработческие url.
Окно запроса доступа тоже можно поднастроить, указав отображаемое имя, логотип и лицензию.
На выходе должен получиться файл учетных данных client_secrets.json. Готовый файл впоследствии необходимо разместить в ресурсах своего проекта.
Сценарии для авторизации
Идем дальше. Google Sheets API v4 поддерживает различные сценарии для авторизации с использованием авторизационного кода:
- Для web-серверных приложений. Необходимо реализовать пару определеных сервлетов и добавить их в свой web.xml
- Служебные учетные записи. Служебные учетные записи заводятся на google и предоставляют клиентам доступ к своим ресурсам, а не к ресурсам пользователя.
- Для установленных приложений. Пример работы с API из консольного приложения.
- Для клиентских приложений. В браузере формируем запрос на получение кода доступа, который потом можно обменять на токены.
- Для android.
Для нас подходят сценарии для web- и для клиентских приложений. Схема работы для них общая:
Первым делом необходимо запросить авторизационный ключ в google. Пользователю будет показана форма доступа. Получив код авторизации, его нужно обменять на токен доступа, без которого нельзя общаться с Google API. Последовательность действий можно понаблюдать в OAuth 2.0 песочнице.
Google oauth2 приложение
За основу web-приложения возьмем spring boot. Зависимости следующие:
com.google.oauth-client
google-oauth-client-java6
${google.oauth.client.version}
com.google.apis
google-api-services-oauth2
${google.oauth2.version}
com.google.apis
google-api-services-sheets
${google.sheets.version}
Создадим два сервиса. GoogleConnection будет загружать клиентские данные из локального файла и хранить идентификационные данные после их получения.
@Service
public class GoogleConnectionService implements GoogleConnection {
private static final String CLIENT_SECRETS = "/client_secrets.json";
// ..
@Override
public GoogleClientSecrets getClientSecrets() {
if (clientSecrets == null) {
try {
// load client secrets
InputStreamReader clientSecretsReader = new InputStreamReader(getSecretFile());
clientSecrets = GoogleClientSecrets.load(Global.JSON_FACTORY, clientSecretsReader);
} catch (IOException e) {
e.printStackTrace();
}
}
return clientSecrets;
}
@Override
public Credential getCredentials() {
return credential;
}
// ..
}
А GoogleSheets будет выполнять основную работу — считывать табличные данные.
@Service
public class GoogleSheetsService implements GoogleSheets {
private Sheets sheetsService = null;
@Override
public List> readTable(GoogleConnection connection) throws IOException {
Sheets service = getSheetsService(connection);
return readTable(service, spreadsheetId, sheetName);
}
private Sheets getSheetsService(GoogleConnection gc) throws IOException {
if (this.sheetsService == null) {
this.sheetsService = new Sheets.Builder(Global.HTTP_TRANSPORT, Global.JSON_FACTORY, gc.getCredentials())
.setApplicationName(appName).build();
}
return this.sheetsService;
}
}
Всю последовательность операций распределим между тремя контроллерами. Контроллер для авторизации.
@RestController
public class GoogleAuthorizationController {
@Autowired
private GoogleConnectionService connection;
@RequestMapping(value = "/ask", method = RequestMethod.GET)
public void ask(HttpServletResponse response) throws IOException {
// Step 1: Authorize --> ask for auth code
String url = new GoogleAuthorizationCodeRequestUrl(connection.getClientSecrets(), connection.getRedirectUrl(), Global.SCOPES).setApprovalPrompt("force").build();
response.sendRedirect(url);
}
}
Результатом его работы будет редирект на google для логина.
Затем запрос на доступ google приложения к пользовательским таблицам:
В случае успешной авторизации контроллер обратной связи обменяет код на токены и перенаправит на исходный url. За сам обмен отвечает класс GoogleAuthorizationCodeTokenRequest.
@RestController
public class GoogleCallbackController {
@Autowired
private GoogleConnectionService connection;
@RequestMapping(value = "/oauth2callback", method = RequestMethod.GET)
public void callback(@RequestParam("code") String code, HttpServletResponse response) throws IOException {
// Step 2: Exchange code --> access tocken
if (connection.exchangeCode(code)) {
response.sendRedirect(connection.getSourceUrl());
} else {
response.sendRedirect("/error");
}
}
}
И, собственно, рабочий контроллер, реализующий чтение табличных данных.
@RestController
public class GoogleSheetController {
@Autowired
private GoogleConnection connection;
@Autowired
private GoogleSheets sheetsService;
@RequestMapping(value = "/api/sheet", method = RequestMethod.GET)
public ResponseEntity>> read(HttpServletResponse response) throws IOException {
List> responseBody = sheetsService.readTable(connection);
return new ResponseEntity>>(responseBody, HttpStatus.OK);
}
}
Также нам потребуется интерцептор, чтобы невозможно было обратиться к рабочему контроллеру без аутентификации.
public class GoogleSheetsInterceptor implements HandlerInterceptor {
// ..
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj) throws Exception {
if (connection.getCredentials() == null) {
connection.setSourceUrl(request.getRequestURI());
response.sendRedirect("/ask");
return false;
}
return true;
}
}
По сравнению с API v3, каждая строка представляет собой список объектов.
private List> readTable(Sheets service, String spreadsheetId, String sheetName) throws IOException {
ValueRange table = service.spreadsheets().values().get(spreadsheetId, sheetName).execute();
List> values = table.getValues();
return values;
}
Используя нотацию A1 считываем постранично, а не блоками. Для этого добавим параметра id страницы и имя вкладки в настройки приложения.
google.spreadsheet.id=..
google.spreadsheet.sheet.name=..
Запускаем. Проверяем.
В результате должно сложиться представление о том, какие классы Google API служат для подключения к своим сервисам по OAuth 2.0 и как их можно использовать из web-приложения.
Spring sso приложение
Код приложения, отвечающий за работу с OAuth2 можно упростить за счет spring. Для этого подключим Spring Security OAuth.
org.springframework.security.oauth
spring-security-oauth2
Это нам позволит спрятать рутинные операции OAuth2 под капот и защитит наше приложение.
Перенесем пользовательские секреты в application.properties.
security.oauth2.client.client-id=Enter Client Id
security.oauth2.client.client-secret=Enter Client Secret
security.oauth2.client.accessTokenUri=https://accounts.google.com/o/oauth2/token
security.oauth2.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
security.oauth2.client.scope=openid,profile,https://www.googleapis.com/auth/spreadsheets
security.oauth2.resource.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
Подключив security к проекту, мы уже активировали базовую аутентификацию. Заменим её на что-то более подходящее.
Раз уж мы считываем данные из Google, пусть он и занимается аутентификацией пользователей для для нашего приложения. Для этого добавим всего лишь одну аннотацию @EnableOAuth2Sso.
@EnableOAuth2Sso
@SpringBootApplication
public class Application { //.. }
Будет создана и настроена точка аутентификации SSO. Нет необходимости переопределять WebSecurityConfigurerAdapter. Нам остаётся только задать пару параметров в конфигурации.
security.ignored=/
security.basic.enabled=false
security.oauth2.sso.login-path=/oauth2callback
В данном случае login-path должен соответствовать URI для редиректа, заданный в google проекте. А параметр scope должен содержать значение profile в том числе.
Дополнительные контроллеры и интерцептор больше не нужны. Теперь их работу будет выполнять spring.
Изменим класс GoogleConnection. Credential’s мы будем создавать используя авторизационный код, сохраненный после аутентификации в OAuth2 контексте. А клиентские данные будем брать из конфига приложения.
@Service
public class GoogleConnectionService implements GoogleConnection {
@Autowired
private OAuth2ClientContext oAuth2ClientContext;
private GoogleCredential googleCredentials = null;
// ..
@Override
public Credential getCredentials() {
if (googleCredentials == null) {
googleCredentials = new GoogleCredential.Builder()
.setTransport(Global.HTTP_TRANSPORT)
.setJsonFactory(Global.JSON_FACTORY)
.setClientSecrets(clientId, clientSecret)
.build()
.setAccessToken(response.getAccessToken())
.setFromTokenResponse(oAuth2ClientContext
.getAccessToken().getValue());
}
return googleCredentials;
}
}
Отображение данных в браузере, обработку ошибок, использование сессии, logout и прочие вещи оставим без рассмотрения. Их наличие и настройка будет зависеть от конкретных требований.
На этом все. Рабочие исходники есть на github’e. Разные подходы — по разным веткам.