Автор оригинала: Sampada Wagde.
1. Обзор
В этом уроке мы продолжим изучение потока кода авторизации OAuth2, который мы начали собирать в нашей предыдущей статье и мы сосредоточимся на том, как обрабатывать токен обновления в приложении Angular. Мы также будем использовать прокси-сервер Zuul.
Мы будем использовать стек OAuth в Spring Security 5. Если вы хотите использовать устаревший стек OAuth Spring Security, ознакомьтесь с этой предыдущей статьей: OAuth2 для API Spring REST – Обработайте токен обновления в AngularJS (устаревший стек OAuth)
2. Истечение срока действия токена доступа
Во-первых, помните, что клиент получал маркер доступа с помощью типа предоставления кода авторизации в два этапа. На первом этапе мы получаем код авторизации . И на втором этапе мы фактически получаем токен доступа .
Наш токен доступа хранится в файле cookie, срок действия которого истекает в зависимости от того, когда истекает срок действия самого Токена:
var expireDate = new Date().getTime() + (1000 * token.expires_in); Cookie.set("access_token", token.access_token, expireDate);
Важно понимать, что сам файл cookie используется только для хранения и он не управляет ничем другим в потоке OAuth2. Например, браузер никогда не будет автоматически отправлять файлы cookie на сервер с запросами, поэтому мы здесь защищены.
Но обратите внимание, как мы на самом деле определяем эту функцию retrieveToken() для получения маркера доступа:
retrieveToken(code) { let params = new URLSearchParams(); params.append('grant_type','authorization_code'); params.append('client_id', this.clientId); params.append('client_secret', 'newClientSecret'); params.append('redirect_uri', this.redirectUri); params.append('code',code); let headers = new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'}); this._http.post('http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token', params.toString(), { headers: headers }) .subscribe( data => this.saveToken(data), err => alert('Invalid Credentials')); }
Мы отправляем секрет клиента в params , что на самом деле не является безопасным способом справиться с этим. Давайте посмотрим, как мы можем избежать этого.
3. Доверенное лицо
Итак, теперь у нас будет прокси-сервер Zuul, работающий в интерфейсном приложении и в основном расположенный между интерфейсным клиентом и сервером авторизации . Вся конфиденциальная информация будет обрабатываться на этом уровне.
Клиент переднего плана теперь будет размещен в качестве загрузочного приложения, чтобы мы могли легко подключиться к нашему встроенному прокси-серверу Zuul с помощью стартера Spring Cloud Zuul.
Если вы хотите ознакомиться с основами Zuul, быстро прочтите основную статью Zuul .
Теперь давайте настроим маршруты прокси-сервера :
zuul: routes: auth/code: path: /auth/code/** sensitiveHeaders: url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth auth/token: path: /auth/token/** sensitiveHeaders: url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token auth/refresh: path: /auth/refresh/** sensitiveHeaders: url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token auth/redirect: path: /auth/redirect/** sensitiveHeaders: url: http://localhost:8089/ auth/resources: path: /auth/resources/** sensitiveHeaders: url: http://localhost:8083/auth/resources/
Мы настроили маршруты для обработки следующих:
- auth/code – получите код авторизации и сохраните его в файле cookie
- auth/redirect – обработка перенаправления на страницу входа сервера авторизации
- auth/resources – сопоставление с соответствующим путем Сервера авторизации для ресурсов страницы входа в систему ( css и js )
- auth/token – получите токен доступа, удалите refresh_token из полезной нагрузки и сохраните его в файле cookie
- auth/refresh – получите токен обновления, удалите его из полезной нагрузки и сохраните в файле cookie
Что интересно, так это то, что мы проксируем трафик только на сервер авторизации, а не на что-либо еще. Нам действительно нужен прокси-сервер только тогда, когда клиент получает новые токены.
Далее давайте рассмотрим все это по очереди.
4. Получите код с помощью предварительного фильтра Zuul
Первое использование прокси – сервера просто-мы настраиваем запрос на получение кода авторизации:
@Component public class CustomPreZuulFilter extends ZuulFilter { @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest req = ctx.getRequest(); String requestURI = req.getRequestURI(); if (requestURI.contains("auth/code")) { Mapparams = ctx.getRequestQueryParams(); if (params == null) { params = Maps.newHashMap(); } params.put("response_type", Lists.newArrayList(new String[] { "code" })); params.put("scope", Lists.newArrayList(new String[] { "read" })); params.put("client_id", Lists.newArrayList(new String[] { CLIENT_ID })); params.put("redirect_uri", Lists.newArrayList(new String[] { REDIRECT_URL })); ctx.setRequestQueryParams(params); } return null; } @Override public boolean shouldFilter() { boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext(); String URI = ctx.getRequest().getRequestURI(); if (URI.contains("auth/code") || URI.contains("auth/token") || URI.contains("auth/refresh")) { shouldfilter = true; } return shouldfilter; } @Override public int filterOrder() { return 6; } @Override public String filterType() { return "pre"; } }
Мы используем фильтр типа pre для обработки запроса перед его передачей.
В методе фильтра run() мы добавляем параметры запроса для response_type , scope , client_id и redirect_uri – все, что нужно нашему Серверу авторизации, чтобы мы могли перейти на его страницу входа и отправить код.
Также обратите внимание на метод should Filter () . Мы фильтруем запросы только с упомянутыми 3 URI, другие не проходят через метод run .
5. Поместите код в файл cookie с помощью фильтра Zuul Post
То, что мы планируем здесь сделать, – это сохранить код в виде файла cookie, чтобы мы могли отправить его на сервер авторизации для получения маркера доступа. Код присутствует в качестве параметра запроса в URL-адресе запроса, на который Сервер авторизации перенаправляет нас после входа в систему.
Мы настроим постфильтр Zuul, чтобы извлечь этот код и установить его в файле cookie. Это не просто обычный файл cookie, а защищенный файл cookie HttpOnly с очень ограниченным путем ( /auth/token ) :
@Component public class CustomPostZuulFilter extends ZuulFilter { private ObjectMapper mapper = new ObjectMapper(); @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); try { Mapparams = ctx.getRequestQueryParams(); if (requestURI.contains("auth/redirect")) { Cookie cookie = new Cookie("code", params.get("code").get(0)); cookie.setHttpOnly(true); cookie.setPath(ctx.getRequest().getContextPath() + "/auth/token"); ctx.getResponse().addCookie(cookie); } } catch (Exception e) { logger.error("Error occured in zuul post filter", e); } return null; } @Override public boolean shouldFilter() { boolean shouldfilter = false; RequestContext ctx = RequestContext.getCurrentContext(); String URI = ctx.getRequest().getRequestURI(); if (URI.contains("auth/redirect") || URI.contains("auth/token") || URI.contains("auth/refresh")) { shouldfilter = true; } return shouldfilter; } @Override public int filterOrder() { return 10; } @Override public String filterType() { return "post"; } }
Чтобы добавить дополнительный уровень защиты от атак CSRF, мы добавим Файл cookie того же сайта заголовок ко всем нашим файлам cookie .
Для этого мы создадим класс конфигурации:
@Configuration public class SameSiteConfig implements WebMvcConfigurer { @Bean public TomcatContextCustomizer sameSiteCookiesConfig() { return context -> { final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor(); cookieProcessor.setSameSiteCookies(SameSiteCookies.STRICT.getValue()); context.setCookieProcessor(cookieProcessor); }; } }
Здесь мы устанавливаем атрибут в strict , чтобы любая передача файлов cookie между сайтами строго запрещалась.
6. Получите и используйте код из файла cookie
Теперь, когда у нас есть код в файле cookie, когда интерфейсное приложение Angular пытается запустить запрос токена, оно отправит запрос по адресу /auth/token , и поэтому браузер, конечно, отправит этот файл cookie.
Таким образом, теперь у нас будет еще одно условие в нашем pre фильтре в прокси-сервере, который извлечет код из файла cookie и отправит его вместе с другими параметрами формы для получения токена :
public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ... else if (requestURI.contains("auth/token"))) { try { String code = extractCookie(req, "code"); String formParams = String.format( "grant_type=%s&client_id=%s&client_secret=%s&redirect_uri=%s&code=%s", "authorization_code", CLIENT_ID, CLIENT_SECRET, REDIRECT_URL, code); byte[] bytes = formParams.getBytes("UTF-8"); ctx.setRequest(new CustomHttpServletRequest(req, bytes)); } catch (IOException e) { e.printStackTrace(); } } ... } private String extractCookie(HttpServletRequest req, String name) { Cookie[] cookies = req.getCookies(); if (cookies != null) { for (int i = 0; i < cookies.length; i++) { if (cookies[i].getName().equalsIgnoreCase(name)) { return cookies[i].getValue(); } } } return null; }
А вот наш |/CustomHttpServletRequest – используется для отправки тела запроса с необходимыми параметрами формы, преобразованными в байты :
public class CustomHttpServletRequest extends HttpServletRequestWrapper { private byte[] bytes; public CustomHttpServletRequest(HttpServletRequest request, byte[] bytes) { super(request); this.bytes = bytes; } @Override public ServletInputStream getInputStream() throws IOException { return new ServletInputStreamWrapper(bytes); } @Override public int getContentLength() { return bytes.length; } @Override public long getContentLengthLong() { return bytes.length; } @Override public String getMethod() { return "POST"; } }
Это даст нам маркер доступа с сервера авторизации в ответе. Далее мы посмотрим, как мы преобразуем ответ.
7. Поместите маркер обновления в файл cookie
Перейдем к забавным вещам.
То, что мы планируем здесь сделать, – это заставить клиента получить токен обновления в виде файла cookie.
Мы добавим в наш постфильтр Zuul, чтобы извлечь токен обновления из тела ответа JSON и установить его в файле cookie. Это снова защищенный файл cookie HttpOnly с очень ограниченным путем ( /auth/refresh ):
public Object run() { ... else if (requestURI.contains("auth/token") || requestURI.contains("auth/refresh")) { InputStream is = ctx.getResponseDataStream(); String responseBody = IOUtils.toString(is, "UTF-8"); if (responseBody.contains("refresh_token")) { MapresponseMap = mapper.readValue(responseBody, new TypeReference
Как мы видим, здесь мы добавили условие в наш постфильтр Zuul, чтобы прочитать ответ и извлечь токен обновления для маршрутов auth/token и auth/refresh . Мы делаем то же самое для этих двух, потому что сервер авторизации по существу отправляет одну и ту же полезную нагрузку при получении маркера доступа и маркера обновления.
Затем мы удалили refresh_token из ответа JSON, чтобы убедиться, что он никогда не будет доступен для внешнего интерфейса за пределами файла cookie.
Еще один момент, который следует отметить здесь, заключается в том, что мы установили максимальный возраст файла cookie на 30 дней, поскольку это соответствует времени истечения срока действия токена.
8. Получите и используйте токен обновления из файла cookie
Теперь, когда у нас есть токен обновления в файле cookie, когда интерфейсное приложение Angular пытается запустить обновление токена , оно отправит запрос в /auth/refresh , и поэтому браузер, конечно, отправит этот файл cookie.
Таким образом, теперь у нас будет еще одно условие в нашем pre фильтре в прокси – сервере, которое извлечет токен обновления из файла cookie и отправит его вперед в качестве параметра HTTP – чтобы запрос был действительным:
public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ... else if (requestURI.contains("auth/refresh"))) { try { String token = extractCookie(req, "token"); String formParams = String.format( "grant_type=%s&client_id=%s&client_secret=%s&refresh_token=%s", "refresh_token", CLIENT_ID, CLIENT_SECRET, token); byte[] bytes = formParams.getBytes("UTF-8"); ctx.setRequest(new CustomHttpServletRequest(req, bytes)); } catch (IOException e) { e.printStackTrace(); } } ... }
Это похоже на то, что мы сделали, когда впервые получили маркер доступа. Но обратите внимание, что тело формы отличается. Теперь мы отправляем grant_type из refresh_token вместо authorization_code вместе с токеном, который мы ранее сохраняли в файле cookie .
После получения ответа он снова проходит через то же преобразование в фильтре pre , что мы видели ранее в разделе 7.
9. Обновление маркера доступа из Angular
Наконец, давайте изменим наше простое интерфейсное приложение и фактически воспользуемся обновлением токена:
Вот наша функция обновить маркер доступа() :
refreshAccessToken() { let headers = new HttpHeaders({ 'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'}); this._http.post('auth/refresh', {}, {headers: headers }) .subscribe( data => this.saveToken(data), err => alert('Invalid Credentials') ); }
Обратите внимание, как мы просто используем существующую функцию saveToken () – и просто передаем ей разные входные данные.
Также обратите внимание, что мы не добавляем никаких параметров формы с помощью refresh_token сами – так как об этом позаботится фильтр Zuul .
10. Запустите передний конец
Поскольку наш интерфейсный угловой клиент теперь размещен в качестве загрузочного приложения, его запуск будет немного отличаться от предыдущего.
Первый шаг тот же самый. Нам нужно создать приложение :
mvn clean install
Это вызовет frontend-maven-плагин , определенный в вашем pom.xml для создания углового кода и копирования артефактов пользовательского интерфейса в папку target/classes/static . Этот процесс перезаписывает все остальное, что у нас есть в каталоге src/main/resources . Поэтому нам нужно убедиться и включить в процесс копирования любые необходимые ресурсы из этой папки, такие как application.yml .
На втором шаге нам нужно запустить наше SpringBootApplication class UIApplication . Наше клиентское приложение будет запущено и запущено на порту 8089, как указано в файле application.yml .
11. Заключение
В этом уроке OAuth2 мы узнали, как сохранить токен обновления в клиентском приложении Angular, как обновить токен доступа с истекшим сроком действия и как использовать прокси-сервер Zuul для всего этого.
Полную реализацию этого руководства можно найти на GitHub .